diff --git a/res/css/structures/_RoomView.pcss b/res/css/structures/_RoomView.pcss index 03f95020da1..1b7c998efe3 100644 --- a/res/css/structures/_RoomView.pcss +++ b/res/css/structures/_RoomView.pcss @@ -32,6 +32,7 @@ Please see LICENSE files in the repository root for full details. } .mx_MessageComposer { + position: relative; width: 100%; flex: 0 0 auto; margin-right: 2px; @@ -156,9 +157,6 @@ Please see LICENSE files in the repository root for full details. list-style-type: none; padding: var(--RoomView_MessageList-padding); /* mx_ProfileResizer depends on this value */ margin: 0; - /* needed as min-height is set to clientHeight in ScrollPanel - to prevent shrinking when WhoIsTypingTile is hidden */ - box-sizing: border-box; li { clear: both; diff --git a/res/css/views/right_panel/_TimelineCard.pcss b/res/css/views/right_panel/_TimelineCard.pcss index 14d33d4f99a..dd3a7e9bc26 100644 --- a/res/css/views/right_panel/_TimelineCard.pcss +++ b/res/css/views/right_panel/_TimelineCard.pcss @@ -158,14 +158,6 @@ Please see LICENSE files in the repository root for full details. } } - .mx_WhoIsTypingTile { - margin-left: -12px; /* undo padding on the message list */ - } - - .mx_WhoIsTypingTile_avatars { - flex-basis: 48px; /* 12 (padding on message list) + 36 (padding on event lines) */ - } - .mx_GenericEventListSummary_unstyledList, /* RR next to a message on the event list summary */ .mx_RoomView_MessageList { /* RR next to a message on the messsge list */ diff --git a/res/css/views/rooms/_WhoIsTypingTile.pcss b/res/css/views/rooms/_WhoIsTypingTile.pcss index eb604155c5d..3de1cb9c951 100644 --- a/res/css/views/rooms/_WhoIsTypingTile.pcss +++ b/res/css/views/rooms/_WhoIsTypingTile.pcss @@ -6,8 +6,14 @@ Please see LICENSE files in the repository root for full details. */ .mx_WhoIsTypingTile { - margin-left: -18px; /* offset padding from mx_RoomView_MessageList to center avatars */ - padding-top: 18px; + --padding-top: 9px; + position: absolute; + background: linear-gradient(to top, var(--cpd-color-bg-canvas-default) var(--padding-top), transparent); + left: 0; + right: 0; + top: calc((1lh + var(--padding-top)) * -1); + margin-left: 0; + padding-top: var(--padding-top); display: flex; align-items: center; } @@ -25,6 +31,7 @@ Please see LICENSE files in the repository root for full details. .mx_WhoIsTypingTile_avatars .mx_BaseAvatar { border: 1px solid $background; border-radius: 40px; + vertical-align: middle; } .mx_WhoIsTypingTile_remainingAvatarPlaceholder { @@ -45,6 +52,7 @@ Please see LICENSE files in the repository root for full details. .mx_WhoIsTypingTile_label { flex: 1; font: var(--cpd-font-body-md-semibold); + font-size: var(--cpd-font-size-body-sm); color: $roomtopic-color; } diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 30f9b474a64..b8e4308a52a 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -34,7 +34,6 @@ import EventTile, { import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; import defaultDispatcher from "../../dispatcher/dispatcher"; import type LegacyCallEventGrouper from "./LegacyCallEventGrouper"; -import WhoIsTypingTile from "../views/rooms/WhoIsTypingTile"; import ScrollPanel, { type IScrollState } from "./ScrollPanel"; import DateSeparator from "../views/messages/DateSeparator"; import TimelineSeparator, { SeparatorKind } from "../views/messages/TimelineSeparator"; @@ -188,7 +187,6 @@ interface IProps { interface IState { ghostReadMarkers: string[]; - showTypingNotifications: boolean; hideSender: boolean; } @@ -248,10 +246,8 @@ export default class MessagePanel extends React.Component { private unmounted = false; private readMarkerNode = createRef(); - private whoIsTyping = createRef(); public scrollPanel = createRef(); - private showTypingNotificationsWatcherRef?: string; private eventTiles: Record = {}; // A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination. @@ -264,7 +260,6 @@ export default class MessagePanel extends React.Component { // previous positions the read marker has been in, so we can // display 'ghost' read markers that are animating away ghostReadMarkers: [], - showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), hideSender: this.shouldHideSender(), }; @@ -276,11 +271,6 @@ export default class MessagePanel extends React.Component { public componentDidMount(): void { this.unmounted = false; - this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting( - "showTypingNotifications", - null, - this.onShowTypingNotificationsChange, - ); this.calculateRoomMembersCount(); this.props.room?.currentState.on(RoomStateEvent.Update, this.calculateRoomMembersCount); } @@ -288,7 +278,6 @@ export default class MessagePanel extends React.Component { public componentWillUnmount(): void { this.unmounted = true; this.props.room?.currentState.off(RoomStateEvent.Update, this.calculateRoomMembersCount); - SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); this.readReceiptMap = {}; this.resizeObserver.disconnect(); } @@ -335,12 +324,6 @@ export default class MessagePanel extends React.Component { }); }; - private onShowTypingNotificationsChange = (): void => { - this.setState({ - showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), - }); - }; - /* get the DOM node representing the given event */ public getNodeForEventId(eventId: string): HTMLElement | undefined { if (!this.eventTiles) { @@ -959,52 +942,6 @@ export default class MessagePanel extends React.Component { private resizeObserver = new ResizeObserver(this.onHeightChanged); - private onTypingShown = (): void => { - const scrollPanel = this.scrollPanel.current; - // this will make the timeline grow, so checkScroll - scrollPanel?.checkScroll(); - if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) { - scrollPanel.preventShrinking(); - } - }; - - private onTypingHidden = (): void => { - const scrollPanel = this.scrollPanel.current; - if (scrollPanel) { - // as hiding the typing notifications doesn't - // update the scrollPanel, we tell it to apply - // the shrinking prevention once the typing notifs are hidden - scrollPanel.updatePreventShrinking(); - // order is important here as checkScroll will scroll down to - // reveal added padding to balance the notifs disappearing. - scrollPanel.checkScroll(); - } - }; - - public updateTimelineMinHeight(): void { - const scrollPanel = this.scrollPanel.current; - - if (scrollPanel) { - const isAtBottom = scrollPanel.isAtBottom(); - const whoIsTyping = this.whoIsTyping.current; - const isTypingVisible = whoIsTyping && whoIsTyping.isVisible(); - // when messages get added to the timeline, - // but somebody else is still typing, - // update the min-height, so once the last - // person stops typing, no jumping occurs - if (isAtBottom && isTypingVisible) { - scrollPanel.preventShrinking(); - } - } - } - - public onTimelineReset(): void { - const scrollPanel = this.scrollPanel.current; - if (scrollPanel) { - scrollPanel.clearPreventShrinking(); - } - } - public render(): React.ReactNode { let topSpinner; let bottomSpinner; @@ -1025,22 +962,6 @@ export default class MessagePanel extends React.Component { const style = this.props.hidden ? { display: "none" } : {}; - let whoIsTyping; - if ( - this.props.room && - this.state.showTypingNotifications && - this.context.timelineRenderingType === TimelineRenderingType.Room - ) { - whoIsTyping = ( - - ); - } - let ircResizer: JSX.Element | undefined; if (this.props.layout == Layout.IRC) { ircResizer = ( @@ -1066,7 +987,6 @@ export default class MessagePanel extends React.Component { > {topSpinner} {this.getEventTiles()} - {whoIsTyping} {bottomSpinner} diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx index e206fc3c70c..28a8b3bdd87 100644 --- a/src/components/structures/ScrollPanel.tsx +++ b/src/components/structures/ScrollPanel.tsx @@ -144,11 +144,6 @@ export interface IScrollState { pixelOffset?: number; } -interface IPreventShrinkingState { - offsetFromBottom: number; - offsetNode: HTMLElement; -} - export default class ScrollPanel extends React.Component { // noinspection JSUnusedLocalSymbols public static defaultProps = { @@ -177,7 +172,6 @@ export default class ScrollPanel extends React.Component { // Is that next fill request scheduled because of a props update? private pendingFillDueToPropsUpdate = false; private scrollState!: IScrollState; - private preventShrinkingState: IPreventShrinkingState | null = null; private unfillDebouncer: number | null = null; private bottomGrowth!: number; private minListHeight!: number; @@ -206,7 +200,6 @@ export default class ScrollPanel extends React.Component { // // This will also re-check the fill state, in case the pagination was inadequate this.checkScroll(true); - this.updatePreventShrinking(); } public componentWillUnmount(): void { @@ -227,7 +220,6 @@ export default class ScrollPanel extends React.Component { debuglog("onScroll called past resize gate; scroll node top:", this.getScrollNode().scrollTop); this.scrollTimeout?.restart(); this.saveScrollState(); - this.updatePreventShrinking(); this.props.onScroll?.(ev); // noinspection JSIgnoredPromiseFromCall this.checkFillState(); @@ -236,10 +228,6 @@ export default class ScrollPanel extends React.Component { private onResize = (): void => { debuglog("onResize called"); this.checkScroll(); - // update preventShrinkingState if present - if (this.preventShrinkingState) { - this.preventShrinking(); - } }; // after an update to the contents of the panel, check that the scroll is @@ -849,86 +837,6 @@ export default class ScrollPanel extends React.Component { this.divScroll = divScroll; }; - /** - Mark the bottom offset of the last tile, so we can balance it out when - anything below it changes, by calling updatePreventShrinking, to keep - the same minimum bottom offset, effectively preventing the timeline to shrink. - */ - public preventShrinking = (): void => { - const messageList = this.itemlist.current; - const tiles = messageList?.children; - if (!tiles) { - return; - } - let lastTileNode; - for (let i = tiles.length - 1; i >= 0; i--) { - const node = tiles[i] as HTMLElement; - if (node.dataset.scrollTokens) { - lastTileNode = node; - break; - } - } - if (!lastTileNode) { - return; - } - this.clearPreventShrinking(); - const offsetFromBottom = messageList.clientHeight - (lastTileNode.offsetTop + lastTileNode.clientHeight); - this.preventShrinkingState = { - offsetFromBottom: offsetFromBottom, - offsetNode: lastTileNode, - }; - debuglog("prevent shrinking, last tile ", offsetFromBottom, "px from bottom"); - }; - - /** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */ - public clearPreventShrinking = (): void => { - const messageList = this.itemlist.current; - const balanceElement = messageList && messageList.parentElement; - if (balanceElement) balanceElement.style.removeProperty("paddingBottom"); - this.preventShrinkingState = null; - debuglog("prevent shrinking cleared"); - }; - - /** - update the container padding to balance - the bottom offset of the last tile since - preventShrinking was called. - Clears the prevent-shrinking state ones the offset - from the bottom of the marked tile grows larger than - what it was when marking. - */ - public updatePreventShrinking = (): void => { - if (this.preventShrinkingState && this.itemlist.current) { - const sn = this.getScrollNode(); - const scrollState = this.scrollState; - const messageList = this.itemlist.current; - const { offsetNode, offsetFromBottom } = this.preventShrinkingState; - // element used to set paddingBottom to balance the typing notifs disappearing - const balanceElement = messageList.parentElement; - // if the offsetNode got unmounted, clear - let shouldClear = !offsetNode.parentElement; - // also if 200px from bottom - if (!shouldClear && !scrollState.stuckAtBottom) { - const spaceBelowViewport = sn.scrollHeight - (sn.scrollTop + sn.clientHeight); - shouldClear = spaceBelowViewport >= 200; - } - // try updating if not clearing - if (!shouldClear) { - const currentOffset = messageList.clientHeight - (offsetNode.offsetTop + offsetNode.clientHeight); - const offsetDiff = offsetFromBottom - currentOffset; - if (offsetDiff > 0 && balanceElement) { - balanceElement.style.paddingBottom = `${offsetDiff}px`; - debuglog("update prevent shrinking ", offsetDiff, "px from bottom"); - } else if (offsetDiff < 0) { - shouldClear = true; - } - } - if (shouldClear) { - this.clearPreventShrinking(); - } - } - }; - public render(): ReactNode { // TODO: the classnames on the div and ol could do with being updated to // reflect the fact that we don't necessarily contain a list of messages. diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 665ca6f6778..e77ed1bc5c4 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -740,7 +740,6 @@ class TimelinePanel extends React.Component { } this.setState(updatedState as IState, () => { - this.messagePanel.current?.updateTimelineMinHeight(); if (callRMUpdated) { this.props.onReadMarkerUpdated?.(); } @@ -1447,8 +1446,6 @@ class TimelinePanel extends React.Component { const onLoaded = (): void => { if (this.unmounted) return; - // clear the timeline min-height when (re)loading the timeline - this.messagePanel.current?.onTimelineReset(); this.reloadEvents(); // If we switched away from the room while there were pending diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index c23bc7917db..d794f98d914 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -42,7 +42,7 @@ import { type ComposerInsertPayload } from "../../../dispatcher/payloads/Compose import { Action } from "../../../dispatcher/actions"; import type EditorModel from "../../../editor/model"; import UIStore, { UI_EVENTS } from "../../../stores/UIStore"; -import RoomContext from "../../../contexts/RoomContext"; +import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import { type SettingUpdatedPayload } from "../../../dispatcher/payloads/SettingUpdatedPayload"; import MessageComposerButtons from "./MessageComposerButtons"; import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton"; @@ -55,6 +55,7 @@ import { UIFeature } from "../../../settings/UIFeature"; import { formatTimeLeft } from "../../../DateUtils"; import RoomReplacedSvg from "../../../../res/img/room_replaced.svg"; import { HistoryVisibleBanner } from "../composer/HistoryVisibleBanner"; +import WhoIsTypingTile from "../../views/rooms/WhoIsTypingTile"; // The prefix used when persisting editor drafts to localstorage. export const WYSIWYG_EDITOR_STATE_STORAGE_PREFIX = "mx_wysiwyg_state_"; @@ -102,6 +103,7 @@ interface IState { isWysiwygLabEnabled: boolean; isRichTextEnabled: boolean; initialComposerContent: string; + showTypingNotifications: boolean; } type WysiwygComposerState = { @@ -114,8 +116,10 @@ export class MessageComposer extends React.Component { private dispatcherRef?: string; private messageComposerInput = createRef(); private voiceRecordingButton = createRef(); + private whoIsTyping = createRef(); private ref = createRef(); private instanceId: number; + private showTypingNotificationsWatcherRef?: string; private _voiceRecording?: VoiceMessageRecording; @@ -153,6 +157,7 @@ export class MessageComposer extends React.Component { isWysiwygLabEnabled: isWysiwygLabEnabled, isRichTextEnabled: isRichTextEnabled, initialComposerContent: initialComposerContent, + showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), }; this.instanceId = instanceCount++; @@ -246,6 +251,12 @@ export class MessageComposer extends React.Component { SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null); SettingsStore.monitorSetting("feature_wysiwyg_composer", null); + this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting( + "showTypingNotifications", + null, + this.onShowTypingNotificationsChange, + ); + this.dispatcherRef = dis.register(this.onAction); this.waitForOwnMember(); UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current!); @@ -327,12 +338,20 @@ export class MessageComposer extends React.Component { UIStore.instance.stopTrackingElementDimensions(`MessageComposer${this.instanceId}`); UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize); + SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); + window.removeEventListener("beforeunload", this.saveWysiwygEditorState); this.saveWysiwygEditorState(); // clean up our listeners by setting our cached recording to falsy (see internal setter) this.voiceRecording = undefined; } + private onShowTypingNotificationsChange = (): void => { + this.setState({ + showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), + }); + }; + private onTombstoneClick = (ev: ButtonEvent): void => { ev.preventDefault(); @@ -666,6 +685,15 @@ export class MessageComposer extends React.Component { const showSendButton = canSendMessages && (!this.state.isComposerEmpty || this.state.haveRecording); + let whoIsTyping; + if ( + this.props.room && + this.state.showTypingNotifications && + this.context.timelineRenderingType === TimelineRenderingType.Room + ) { + whoIsTyping = ; + } + const classes = classNames({ "mx_MessageComposer": true, "mx_MessageComposer--compact": this.props.compact, @@ -680,6 +708,7 @@ export class MessageComposer extends React.Component { canSendMessages={canSendMessages} threadId={threadId ?? null} /> + {whoIsTyping}
{ { } return ( -
  • +
    {this.renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit)}
    {typingString}
    -
  • +
    ); } }