diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index d47c790a0c..bb4622b7da 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -209,6 +209,10 @@ limitations under the License. .mx_RoomView_searchResultsPanel { .mx_RoomView_messageListWrapper { justify-content: flex-start; + + > .mx_RoomView_MessageList > li > ol { + list-style-type: none; + } } a { diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 4fe5fd239a..ca34dffa7d 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -52,6 +52,7 @@ import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import EditorStateTransfer from "../../utils/EditorStateTransfer"; import { Action } from '../../dispatcher/actions'; import { getEventDisplayInfo } from "../../utils/EventUtils"; +import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = [EventType.Sticker, EventType.RoomMessage]; @@ -72,7 +73,7 @@ export function shouldFormContinuation( ): boolean { if (timelineRenderingType === TimelineRenderingType.ThreadsList) return false; // sanity check inputs - if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false; + if (!prevEvent?.sender || !mxEvent.sender) return false; // check if within the max continuation period if (mxEvent.getTs() - prevEvent.getTs() > CONTINUATION_MAX_INTERVAL) return false; @@ -208,7 +209,7 @@ export default class MessagePanel extends React.Component { // opaque readreceipt info for each userId; used by ReadReceiptMarker // to manage its animations - private readonly readReceiptMap: Record = {}; + private readonly readReceiptMap: { [userId: string]: IReadReceiptInfo } = {}; // Track read receipts by event ID. For each _shown_ event ID, we store // the list of read receipts to display: diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 5eb9ec65db..0386c3469e 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1783,6 +1783,13 @@ export class RoomView extends React.Component { }); }; + private get messagePanelClassNames(): string { + return classNames("mx_RoomView_messagePanel", { + mx_IRCLayout: this.state.layout === Layout.IRC, + mx_GroupLayout: this.state.layout === Layout.Group, + }); + } + render() { if (!this.state.room) { const loading = !this.state.matrixClientIsReady || this.state.roomLoading || this.state.peekLoading; @@ -2068,7 +2075,7 @@ export class RoomView extends React.Component { searchResultsPanel = ( @@ -2085,13 +2092,6 @@ export class RoomView extends React.Component { highlightedEventId = this.state.initialEventId; } - const messagePanelClassNames = classNames( - "mx_RoomView_messagePanel", - { - "mx_IRCLayout": this.state.layout == Layout.IRC, - "mx_GroupLayout": this.state.layout == Layout.Group, - }); - // console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); const messagePanel = ( { onUserScroll={this.onUserScroll} onReadMarkerUpdated={this.updateTopUnreadMessagesBar} showUrlPreview={this.state.showUrlPreview} - className={messagePanelClassNames} + className={this.messagePanelClassNames} membersLoaded={this.state.membersLoaded} permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)} resizeNotifier={this.props.resizeNotifier} diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 4b011acfeb..7644e7e759 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -55,7 +55,7 @@ import MemberAvatar from '../avatars/MemberAvatar'; import SenderProfile from '../messages/SenderProfile'; import MessageTimestamp from '../messages/MessageTimestamp'; import TooltipButton from '../elements/TooltipButton'; -import ReadReceiptMarker from "./ReadReceiptMarker"; +import ReadReceiptMarker, { IReadReceiptInfo } from "./ReadReceiptMarker"; import MessageActionBar from "../messages/MessageActionBar"; import ReactionsRow from '../messages/ReactionsRow'; import { getEventDisplayInfo } from '../../../utils/EventUtils'; @@ -262,8 +262,7 @@ interface IProps { // opaque readreceipt info for each userId; used by ReadReceiptMarker // to manage its animations. Should be an empty object when the room // first loads - // TODO: Proper typing for RR info - readReceiptMap?: any; + readReceiptMap?: { [userId: string]: IReadReceiptInfo }; // A function which is used to check if the parent panel is being // unmounted, to avoid unnecessary work. Should return true if we @@ -929,7 +928,7 @@ export default class EventTile extends React.Component { left = (hidden ? MAX_READ_AVATARS - 1 : i) * -receiptOffset; const userId = receipt.userId; - let readReceiptInfo; + let readReceiptInfo: IReadReceiptInfo; if (this.props.readReceiptMap) { readReceiptInfo = this.props.readReceiptMap[userId]; diff --git a/src/components/views/rooms/ReadReceiptMarker.tsx b/src/components/views/rooms/ReadReceiptMarker.tsx index fa8e428b37..c83d6e1881 100644 --- a/src/components/views/rooms/ReadReceiptMarker.tsx +++ b/src/components/views/rooms/ReadReceiptMarker.tsx @@ -26,6 +26,12 @@ import { toPx } from "../../../utils/units"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import MemberAvatar from '../avatars/MemberAvatar'; +export interface IReadReceiptInfo { + top?: number; + left?: number; + parent?: Element; +} + interface IProps { // the RoomMember to show the RR for member?: RoomMember; @@ -43,10 +49,8 @@ interface IProps { // don't animate this RR into position suppressAnimation?: boolean; - // an opaque object for storing information about this user's RR in - // this room - // TODO: proper typing for RR info - readReceiptInfo: any; + // an opaque object for storing information about this user's RR in this room + readReceiptInfo: IReadReceiptInfo; // A function which is used to check if the parent panel is being // unmounted, to avoid unnecessary work. Should return true if we diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index 3da4ad32f9..eea27fcec0 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -25,6 +25,8 @@ import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import DateSeparator from "../messages/DateSeparator"; import EventTile, { haveTileForEvent } from "./EventTile"; +import { shouldFormContinuation } from "../../structures/MessagePanel"; +import { wantsDateSeparator } from "../../../DateUtils"; interface IProps { // a matrix-js-sdk SearchResult containing the details of this result @@ -43,10 +45,10 @@ export default class SearchResultTile extends React.Component { public render() { const result = this.props.searchResult; - const mxEv = result.context.getEvent(); - const eventId = mxEv.getId(); + const resultEvent = result.context.getEvent(); + const eventId = resultEvent.getId(); - const ts1 = mxEv.getTs(); + const ts1 = resultEvent.getTs(); const ret = []; const layout = SettingsStore.getValue("layout"); const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps"); @@ -55,17 +57,46 @@ export default class SearchResultTile extends React.Component { const timeline = result.context.getTimeline(); for (let j = 0; j < timeline.length; j++) { - const ev = timeline[j]; + const mxEv = timeline[j]; let highlights; const contextual = (j != result.context.getOurEventIndex()); if (!contextual) { highlights = this.props.searchHighlights; } - if (haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline)) { + + if (haveTileForEvent(mxEv, this.context?.showHiddenEventsInTimeline)) { + // do we need a date separator since the last event? + const prevEv = timeline[j - 1]; + // is this a continuation of the previous message? + const continuation = prevEv && + !wantsDateSeparator(prevEv.getDate(), mxEv.getDate()) && + shouldFormContinuation( + prevEv, + mxEv, + this.context?.showHiddenEventsInTimeline, + TimelineRenderingType.Search, + ); + + let lastInSection = true; + const nextEv = timeline[j + 1]; + if (nextEv) { + const willWantDateSeparator = wantsDateSeparator(mxEv.getDate(), nextEv.getDate()); + lastInSection = ( + willWantDateSeparator || + mxEv.getSender() !== nextEv.getSender() || + !shouldFormContinuation( + mxEv, + nextEv, + this.context?.showHiddenEventsInTimeline, + TimelineRenderingType.Search, + ) + ); + } + ret.push( { alwaysShowTimestamps={alwaysShowTimestamps} enableFlair={enableFlair} timelineRenderingType={TimelineRenderingType.Search} + lastInSection={lastInSection} + continuation={continuation} />, ); } } - return
  • { ret }
  • ; + return
  • +
      { ret }
    +
  • ; } }