2024-05-31 19:49:46 +05:30
/* eslint-disable react/destructuring-assignment */
2023-10-06 13:44:06 +11:00
import React , {
Dispatch ,
MouseEventHandler ,
RefObject ,
SetStateAction ,
useCallback ,
useEffect ,
useLayoutEffect ,
useMemo ,
useRef ,
useState ,
} from 'react' ;
import {
Direction ,
EventTimeline ,
EventTimelineSet ,
EventTimelineSetHandlerMap ,
2024-08-15 16:52:32 +02:00
IContent ,
2023-10-06 13:44:06 +11:00
MatrixClient ,
MatrixEvent ,
Room ,
RoomEvent ,
RoomEventHandlerMap ,
} from 'matrix-js-sdk' ;
2024-05-31 19:49:46 +05:30
import { HTMLReactParserOptions } from 'html-react-parser' ;
2023-10-06 13:44:06 +11:00
import classNames from 'classnames' ;
import { ReactEditor } from 'slate-react' ;
import { Editor } from 'slate' ;
2026-03-07 18:03:32 +11:00
import { SessionMembershipData } from 'matrix-js-sdk/lib/matrixrtc/CallMembership' ;
2023-10-06 13:44:06 +11:00
import to from 'await-to-js' ;
2024-07-08 16:57:10 +05:30
import { useAtomValue , useSetAtom } from 'jotai' ;
2023-10-06 13:44:06 +11:00
import {
Badge ,
Box ,
Chip ,
ContainerColor ,
Icon ,
Icons ,
Line ,
Scroll ,
Text ,
as ,
color ,
config ,
toRem ,
} from 'folds' ;
2023-10-21 18:14:33 +11:00
import { isKeyHotkey } from 'is-hotkey' ;
2024-07-30 17:48:59 +05:30
import { Opts as LinkifyOpts } from 'linkifyjs' ;
2024-08-14 15:29:34 +02:00
import { useTranslation } from 'react-i18next' ;
2024-09-11 17:07:02 +10:00
import { eventWithShortcode , factoryEventSentBy , getMxIdLocalPart } from '../../utils/matrix' ;
2023-10-06 13:44:06 +11:00
import { useMatrixClient } from '../../hooks/useMatrixClient' ;
import { useVirtualPaginator , ItemRange } from '../../hooks/useVirtualPaginator' ;
import { useAlive } from '../../hooks/useAlive' ;
2023-10-14 16:08:43 +11:00
import { editableActiveElement , scrollToBottom } from '../../utils/dom' ;
2023-10-06 13:44:06 +11:00
import {
DefaultPlaceholder ,
CompactPlaceholder ,
Reply ,
MessageBase ,
MessageUnsupportedContent ,
Time ,
MessageNotDecryptedContent ,
2024-05-31 19:49:46 +05:30
RedactedContent ,
MSticker ,
2026-05-15 00:47:21 -04:00
PollContent ,
2024-05-31 19:49:46 +05:30
ImageContent ,
EventContent ,
2023-10-06 13:44:06 +11:00
} from '../../components/message' ;
2024-07-30 17:48:59 +05:30
import {
factoryRenderLinkifyWithMention ,
getReactCustomHtmlParser ,
LINKIFY_OPTS ,
makeMentionCustomProps ,
renderMatrixMention ,
} from '../../plugins/react-custom-html-parser' ;
2023-10-06 13:44:06 +11:00
import {
2023-10-14 16:08:43 +11:00
canEditEvent ,
2023-10-06 13:44:06 +11:00
decryptAllTimelineEvent ,
2023-10-14 16:08:43 +11:00
getEditedEvent ,
getEventReactions ,
getLatestEditableEvt ,
2023-10-06 13:44:06 +11:00
getMemberDisplayName ,
getReactionContent ,
2023-10-08 16:35:16 +11:00
isMembershipChanged ,
2023-10-23 21:43:07 +11:00
reactionOrEditEvent ,
2023-10-06 13:44:06 +11:00
} from '../../utils/room' ;
import { useSetting } from '../../state/hooks/settings' ;
2025-02-10 16:49:47 +11:00
import { MessageLayout , settingsAtom } from '../../state/settings' ;
2023-10-06 13:44:06 +11:00
import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer' ;
2024-05-31 19:49:46 +05:30
import { Reactions , Message , Event , EncryptedContent } from './message' ;
2026-05-15 18:56:17 -04:00
import { ReadPositionsContext } from './ReadPositionsContext' ;
import { useRoomReadPositions } from '../../hooks/useRoomReadPositions' ;
2023-10-06 13:44:06 +11:00
import { useMemberEventParser } from '../../hooks/useMemberEventParser' ;
import * as customHtmlCss from '../../styles/CustomHtml.css' ;
import { RoomIntro } from '../../components/room-intro' ;
import {
getIntersectionObserverEntry ,
useIntersectionObserver ,
} from '../../hooks/useIntersectionObserver' ;
2025-08-29 15:04:52 +05:30
import { markAsRead } from '../../utils/notifications' ;
2023-10-06 13:44:06 +11:00
import { useDebounce } from '../../hooks/useDebounce' ;
import { getResizeObserverEntry , useResizeObserver } from '../../hooks/useResizeObserver' ;
import * as css from './RoomTimeline.css' ;
import { inSameDay , minuteDifference , timeDayMonthYear , today , yesterday } from '../../utils/time' ;
2023-10-14 16:08:43 +11:00
import { createMentionElement , isEmptyEditor , moveCursor } from '../../components/editor' ;
2024-05-31 19:49:46 +05:30
import { roomIdToReplyDraftAtomFamily } from '../../state/room/roomInputDrafts' ;
2025-08-12 19:42:30 +05:30
import { usePowerLevelsContext } from '../../hooks/usePowerLevels' ;
2024-05-31 19:49:46 +05:30
import { GetContentCallback , MessageEvent , StateEvent } from '../../../types/matrix/room' ;
2023-10-14 16:08:43 +11:00
import { useKeyDown } from '../../hooks/useKeyDown' ;
2023-10-21 18:14:21 +11:00
import { useDocumentFocusChange } from '../../hooks/useDocumentFocusChange' ;
2024-05-31 19:49:46 +05:30
import { RenderMessageContent } from '../../components/RenderMessageContent' ;
import { Image } from '../../components/media' ;
import { ImageViewer } from '../../components/image-viewer' ;
2024-07-08 16:57:10 +05:30
import { roomToParentsAtom } from '../../state/room/roomToParents' ;
import { useRoomUnread } from '../../state/hooks/unread' ;
import { roomToUnreadAtom } from '../../state/room/roomToUnread' ;
2024-07-30 17:48:59 +05:30
import { useMentionClickHandler } from '../../hooks/useMentionClickHandler' ;
import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler' ;
import { useRoomNavigate } from '../../hooks/useRoomNavigate' ;
2024-09-09 18:45:20 +10:00
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication' ;
2025-02-28 18:47:23 +11:00
import { useIgnoredUsers } from '../../hooks/useIgnoredUsers' ;
2025-03-19 23:14:54 +11:00
import { useImagePackRooms } from '../../hooks/useImagePackRooms' ;
2025-03-23 22:09:29 +11:00
import { useIsDirectRoom } from '../../hooks/useRoom' ;
2025-08-09 17:46:10 +05:30
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile' ;
import { useSpaceOptionally } from '../../hooks/useSpace' ;
2025-08-12 19:42:30 +05:30
import { useRoomCreators } from '../../hooks/useRoomCreators' ;
import { useRoomPermissions } from '../../hooks/useRoomPermissions' ;
import { useAccessiblePowerTagColors , useGetMemberPowerTag } from '../../hooks/useMemberPowerTag' ;
import { useTheme } from '../../hooks/useTheme' ;
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag' ;
import { usePowerLevelTags } from '../../hooks/usePowerLevelTags' ;
2023-10-06 13:44:06 +11:00
const TimelineFloat = as < 'div' , css . TimelineFloatVariants > (
( { position , className , . . . props } , ref ) = > (
< Box
className = { classNames ( css . TimelineFloat ( { position } ) , className ) }
justifyContent = "Center"
alignItems = "Center"
gap = "200"
{ ...props }
ref = { ref }
/ >
)
) ;
const TimelineDivider = as < 'div' , { variant? : ContainerColor | 'Inherit' } > (
( { variant , children , . . . props } , ref ) = > (
< Box gap = "100" justifyContent = "Center" alignItems = "Center" { ...props } ref = { ref } >
< Line style = { { flexGrow : 1 } } variant = { variant } size = "300" / >
{ children }
< Line style = { { flexGrow : 1 } } variant = { variant } size = "300" / >
< / Box >
)
) ;
export const getLiveTimeline = ( room : Room ) : EventTimeline = >
room . getUnfilteredTimelineSet ( ) . getLiveTimeline ( ) ;
export const getEventTimeline = ( room : Room , eventId : string ) : EventTimeline | undefined = > {
const timelineSet = room . getUnfilteredTimelineSet ( ) ;
return timelineSet . getTimelineForEvent ( eventId ) ? ? undefined ;
} ;
export const getFirstLinkedTimeline = (
timeline : EventTimeline ,
direction : Direction
) : EventTimeline = > {
const linkedTm = timeline . getNeighbouringTimeline ( direction ) ;
if ( ! linkedTm ) return timeline ;
return getFirstLinkedTimeline ( linkedTm , direction ) ;
} ;
export const getLinkedTimelines = ( timeline : EventTimeline ) : EventTimeline [ ] = > {
const firstTimeline = getFirstLinkedTimeline ( timeline , Direction . Backward ) ;
2023-10-07 18:19:01 +11:00
const timelines : EventTimeline [ ] = [ ] ;
2023-10-06 13:44:06 +11:00
for (
let nextTimeline : EventTimeline | null = firstTimeline ;
nextTimeline ;
nextTimeline = nextTimeline . getNeighbouringTimeline ( Direction . Forward )
) {
timelines . push ( nextTimeline ) ;
}
return timelines ;
} ;
export const timelineToEventsCount = ( t : EventTimeline ) = > t . getEvents ( ) . length ;
export const getTimelinesEventsCount = ( timelines : EventTimeline [ ] ) : number = > {
const timelineEventCountReducer = ( count : number , tm : EventTimeline ) = >
count + timelineToEventsCount ( tm ) ;
return timelines . reduce ( timelineEventCountReducer , 0 ) ;
} ;
export const getTimelineAndBaseIndex = (
timelines : EventTimeline [ ] ,
index : number
) : [ EventTimeline | undefined , number ] = > {
let uptoTimelineLen = 0 ;
const timeline = timelines . find ( ( t ) = > {
uptoTimelineLen += t . getEvents ( ) . length ;
if ( index < uptoTimelineLen ) return true ;
return false ;
} ) ;
if ( ! timeline ) return [ undefined , 0 ] ;
return [ timeline , uptoTimelineLen - timeline . getEvents ( ) . length ] ;
} ;
export const getTimelineRelativeIndex = ( absoluteIndex : number , timelineBaseIndex : number ) = >
absoluteIndex - timelineBaseIndex ;
export const getTimelineEvent = ( timeline : EventTimeline , index : number ) : MatrixEvent | undefined = >
timeline . getEvents ( ) [ index ] ;
export const getEventIdAbsoluteIndex = (
timelines : EventTimeline [ ] ,
eventTimeline : EventTimeline ,
eventId : string
) : number | undefined = > {
const timelineIndex = timelines . findIndex ( ( t ) = > t === eventTimeline ) ;
if ( timelineIndex === - 1 ) return undefined ;
const eventIndex = eventTimeline . getEvents ( ) . findIndex ( ( evt ) = > evt . getId ( ) === eventId ) ;
if ( eventIndex === - 1 ) return undefined ;
const baseIndex = timelines
. slice ( 0 , timelineIndex )
. reduce ( ( accValue , timeline ) = > timeline . getEvents ( ) . length + accValue , 0 ) ;
return baseIndex + eventIndex ;
} ;
type RoomTimelineProps = {
room : Room ;
eventId? : string ;
roomInputRef : RefObject < HTMLElement > ;
editor : Editor ;
} ;
const PAGINATION_LIMIT = 80 ;
type Timeline = {
linkedTimelines : EventTimeline [ ] ;
range : ItemRange ;
} ;
const useEventTimelineLoader = (
mx : MatrixClient ,
room : Room ,
onLoad : ( eventId : string , linkedTimelines : EventTimeline [ ] , evtAbsIndex : number ) = > void ,
onError : ( err : Error | null ) = > void
) = > {
const loadEventTimeline = useCallback (
async ( eventId : string ) = > {
const [ err , replyEvtTimeline ] = await to (
mx . getEventTimeline ( room . getUnfilteredTimelineSet ( ) , eventId )
) ;
if ( ! replyEvtTimeline ) {
onError ( err ? ? null ) ;
return ;
}
const linkedTimelines = getLinkedTimelines ( replyEvtTimeline ) ;
const absIndex = getEventIdAbsoluteIndex ( linkedTimelines , replyEvtTimeline , eventId ) ;
if ( absIndex === undefined ) {
onError ( err ? ? null ) ;
return ;
}
onLoad ( eventId , linkedTimelines , absIndex ) ;
} ,
[ mx , room , onLoad , onError ]
) ;
return loadEventTimeline ;
} ;
const useTimelinePagination = (
mx : MatrixClient ,
timeline : Timeline ,
setTimeline : Dispatch < SetStateAction < Timeline > > ,
limit : number
) = > {
const timelineRef = useRef ( timeline ) ;
timelineRef . current = timeline ;
const alive = useAlive ( ) ;
const handleTimelinePagination = useMemo ( ( ) = > {
let fetching = false ;
const recalibratePagination = (
linkedTimelines : EventTimeline [ ] ,
timelinesEventsCount : number [ ] ,
backwards : boolean
) = > {
const topTimeline = linkedTimelines [ 0 ] ;
const timelineMatch = ( mt : EventTimeline ) = > ( t : EventTimeline ) = > t === mt ;
const newLTimelines = getLinkedTimelines ( topTimeline ) ;
const topTmIndex = newLTimelines . findIndex ( timelineMatch ( topTimeline ) ) ;
const topAddedTm = topTmIndex === - 1 ? [ ] : newLTimelines . slice ( 0 , topTmIndex ) ;
const topTmAddedEvt =
timelineToEventsCount ( newLTimelines [ topTmIndex ] ) - timelinesEventsCount [ 0 ] ;
const offsetRange = getTimelinesEventsCount ( topAddedTm ) + ( backwards ? topTmAddedEvt : 0 ) ;
setTimeline ( ( currentTimeline ) = > ( {
linkedTimelines : newLTimelines ,
range :
offsetRange > 0
? {
2024-09-11 17:07:02 +10:00
start : currentTimeline.range.start + offsetRange ,
end : currentTimeline.range.end + offsetRange ,
}
2023-10-06 13:44:06 +11:00
: { . . . currentTimeline . range } ,
} ) ) ;
} ;
return async ( backwards : boolean ) = > {
if ( fetching ) return ;
const { linkedTimelines : lTimelines } = timelineRef . current ;
const timelinesEventsCount = lTimelines . map ( timelineToEventsCount ) ;
const timelineToPaginate = backwards ? lTimelines [ 0 ] : lTimelines [ lTimelines . length - 1 ] ;
if ( ! timelineToPaginate ) return ;
const paginationToken = timelineToPaginate . getPaginationToken (
backwards ? Direction.Backward : Direction.Forward
) ;
if (
! paginationToken &&
getTimelinesEventsCount ( lTimelines ) !==
2024-09-11 17:07:02 +10:00
getTimelinesEventsCount ( getLinkedTimelines ( timelineToPaginate ) )
2023-10-06 13:44:06 +11:00
) {
recalibratePagination ( lTimelines , timelinesEventsCount , backwards ) ;
return ;
}
fetching = true ;
const [ err ] = await to (
mx . paginateEventTimeline ( timelineToPaginate , {
backwards ,
limit ,
} )
) ;
if ( err ) {
2026-05-20 21:11:38 -04:00
fetching = false ;
2023-10-06 13:44:06 +11:00
return ;
}
const fetchedTimeline =
timelineToPaginate . getNeighbouringTimeline (
backwards ? Direction.Backward : Direction.Forward
) ? ? timelineToPaginate ;
// Decrypt all event ahead of render cycle
2025-02-10 16:49:47 +11:00
const roomId = fetchedTimeline . getRoomId ( ) ;
const room = roomId ? mx . getRoom ( roomId ) : null ;
if ( room ? . hasEncryptionStateEvent ( ) ) {
2023-10-06 13:44:06 +11:00
await to ( decryptAllTimelineEvent ( mx , fetchedTimeline ) ) ;
}
fetching = false ;
if ( alive ( ) ) {
recalibratePagination ( lTimelines , timelinesEventsCount , backwards ) ;
}
} ;
} , [ mx , alive , setTimeline , limit ] ) ;
return handleTimelinePagination ;
} ;
const useLiveEventArrive = ( room : Room , onArrive : ( mEvent : MatrixEvent ) = > void ) = > {
useEffect ( ( ) = > {
const handleTimelineEvent : EventTimelineSetHandlerMap [ RoomEvent . Timeline ] = (
mEvent ,
eventRoom ,
toStartOfTimeline ,
removed ,
data
) = > {
if ( eventRoom ? . roomId !== room . roomId || ! data . liveEvent ) return ;
onArrive ( mEvent ) ;
} ;
const handleRedaction : RoomEventHandlerMap [ RoomEvent . Redaction ] = ( mEvent , eventRoom ) = > {
if ( eventRoom ? . roomId !== room . roomId ) return ;
onArrive ( mEvent ) ;
} ;
room . on ( RoomEvent . Timeline , handleTimelineEvent ) ;
room . on ( RoomEvent . Redaction , handleRedaction ) ;
return ( ) = > {
room . removeListener ( RoomEvent . Timeline , handleTimelineEvent ) ;
room . removeListener ( RoomEvent . Redaction , handleRedaction ) ;
} ;
} , [ room , onArrive ] ) ;
} ;
const useLiveTimelineRefresh = ( room : Room , onRefresh : ( ) = > void ) = > {
useEffect ( ( ) = > {
const handleTimelineRefresh : RoomEventHandlerMap [ RoomEvent . TimelineRefresh ] = ( r ) = > {
if ( r . roomId !== room . roomId ) return ;
onRefresh ( ) ;
} ;
room . on ( RoomEvent . TimelineRefresh , handleTimelineRefresh ) ;
return ( ) = > {
room . removeListener ( RoomEvent . TimelineRefresh , handleTimelineRefresh ) ;
} ;
} , [ room , onRefresh ] ) ;
} ;
const getInitialTimeline = ( room : Room ) = > {
const linkedTimelines = getLinkedTimelines ( getLiveTimeline ( room ) ) ;
const evLength = getTimelinesEventsCount ( linkedTimelines ) ;
return {
linkedTimelines ,
range : {
start : Math.max ( evLength - PAGINATION_LIMIT , 0 ) ,
end : evLength ,
} ,
} ;
} ;
const getEmptyTimeline = ( ) = > ( {
range : { start : 0 , end : 0 } ,
linkedTimelines : [ ] ,
} ) ;
const getRoomUnreadInfo = ( room : Room , scrollTo = false ) = > {
const readUptoEventId = room . getEventReadUpTo ( room . client . getUserId ( ) ? ? '' ) ;
if ( ! readUptoEventId ) return undefined ;
const evtTimeline = getEventTimeline ( room , readUptoEventId ) ;
const latestTimeline = evtTimeline && getFirstLinkedTimeline ( evtTimeline , Direction . Forward ) ;
return {
readUptoEventId ,
inLiveTimeline : latestTimeline === room . getLiveTimeline ( ) ,
scrollTo ,
} ;
} ;
2025-08-12 19:42:30 +05:30
export function RoomTimeline ( { room , eventId , roomInputRef , editor } : RoomTimelineProps ) {
2023-10-06 13:44:06 +11:00
const mx = useMatrixClient ( ) ;
2024-09-09 18:45:20 +10:00
const useAuthentication = useMediaAuthentication ( ) ;
2025-02-26 21:44:53 +11:00
const [ hideActivity ] = useSetting ( settingsAtom , 'hideActivity' ) ;
2023-10-06 13:44:06 +11:00
const [ messageLayout ] = useSetting ( settingsAtom , 'messageLayout' ) ;
2026-05-13 21:17:59 -04:00
const [ perMessageProfiles ] = useSetting ( settingsAtom , 'perMessageProfiles' ) ;
2023-10-06 13:44:06 +11:00
const [ messageSpacing ] = useSetting ( settingsAtom , 'messageSpacing' ) ;
2025-03-23 22:09:29 +11:00
const [ legacyUsernameColor ] = useSetting ( settingsAtom , 'legacyUsernameColor' ) ;
const direct = useIsDirectRoom ( ) ;
2026-05-15 18:56:17 -04:00
const readPositions = useRoomReadPositions ( room ) ;
2023-10-06 13:44:06 +11:00
const [ hideMembershipEvents ] = useSetting ( settingsAtom , 'hideMembershipEvents' ) ;
const [ hideNickAvatarEvents ] = useSetting ( settingsAtom , 'hideNickAvatarEvents' ) ;
const [ mediaAutoLoad ] = useSetting ( settingsAtom , 'mediaAutoLoad' ) ;
2023-10-30 07:14:58 +11:00
const [ urlPreview ] = useSetting ( settingsAtom , 'urlPreview' ) ;
const [ encUrlPreview ] = useSetting ( settingsAtom , 'encUrlPreview' ) ;
2025-02-10 16:49:47 +11:00
const showUrlPreview = room . hasEncryptionStateEvent ( ) ? encUrlPreview : urlPreview ;
2023-10-06 13:44:06 +11:00
const [ showHiddenEvents ] = useSetting ( settingsAtom , 'showHiddenEvents' ) ;
2025-06-28 12:35:59 +02:00
const [ showDeveloperTools ] = useSetting ( settingsAtom , 'developerTools' ) ;
2026-05-21 12:07:42 -04:00
const [ lotusTerminal ] = useSetting ( settingsAtom , 'lotusTerminal' ) ;
2025-02-28 18:47:23 +11:00
2025-07-27 15:13:00 +03:00
const [ hour24Clock ] = useSetting ( settingsAtom , 'hour24Clock' ) ;
const [ dateFormatString ] = useSetting ( settingsAtom , 'dateFormatString' ) ;
2025-02-28 18:47:23 +11:00
const ignoredUsersList = useIgnoredUsers ( ) ;
const ignoredUsersSet = useMemo ( ( ) = > new Set ( ignoredUsersList ) , [ ignoredUsersList ] ) ;
2023-10-06 13:44:06 +11:00
const setReplyDraft = useSetAtom ( roomIdToReplyDraftAtomFamily ( room . roomId ) ) ;
2024-05-31 19:49:46 +05:30
const powerLevels = usePowerLevelsContext ( ) ;
2025-08-12 19:42:30 +05:30
const creators = useRoomCreators ( room ) ;
2025-03-23 22:09:29 +11:00
2025-08-12 19:42:30 +05:30
const creatorsTag = useRoomCreatorsTag ( ) ;
const powerLevelTags = usePowerLevelTags ( room , powerLevels ) ;
const getMemberPowerTag = useGetMemberPowerTag ( room , creators , powerLevels ) ;
const theme = useTheme ( ) ;
const accessiblePowerTagColors = useAccessiblePowerTagColors (
theme . kind ,
creatorsTag ,
powerLevelTags
) ;
const permissions = useRoomPermissions ( creators , powerLevels ) ;
const canRedact = permissions . action ( 'redact' , mx . getSafeUserId ( ) ) ;
2026-02-12 06:27:17 +01:00
const canDeleteOwn = permissions . event ( MessageEvent . RoomRedaction , mx . getSafeUserId ( ) ) ;
2025-08-12 19:42:30 +05:30
const canSendReaction = permissions . event ( MessageEvent . Reaction , mx . getSafeUserId ( ) ) ;
const canPinEvent = permissions . stateEvent ( StateEvent . RoomPinnedEvents , mx . getSafeUserId ( ) ) ;
2023-10-14 16:08:43 +11:00
const [ editId , setEditId ] = useState < string > ( ) ;
2025-03-23 22:09:29 +11:00
2024-07-08 16:57:10 +05:30
const roomToParents = useAtomValue ( roomToParentsAtom ) ;
const unread = useRoomUnread ( room . roomId , roomToUnreadAtom ) ;
2024-07-30 17:48:59 +05:30
const { navigateRoom } = useRoomNavigate ( ) ;
const mentionClickHandler = useMentionClickHandler ( room . roomId ) ;
const spoilerClickHandler = useSpoilerClickHandler ( ) ;
2025-08-09 17:46:10 +05:30
const openUserRoomProfile = useOpenUserRoomProfile ( ) ;
const space = useSpaceOptionally ( ) ;
2023-10-06 13:44:06 +11:00
2025-03-19 23:14:54 +11:00
const imagePackRooms : Room [ ] = useImagePackRooms ( room . roomId , roomToParents ) ;
2023-10-06 13:44:06 +11:00
const [ unreadInfo , setUnreadInfo ] = useState ( ( ) = > getRoomUnreadInfo ( room , true ) ) ;
const readUptoEventIdRef = useRef < string > ( ) ;
if ( unreadInfo ) {
readUptoEventIdRef . current = unreadInfo . readUptoEventId ;
}
const atBottomAnchorRef = useRef < HTMLElement > ( null ) ;
2023-10-19 17:40:01 +11:00
const [ atBottom , setAtBottom ] = useState < boolean > ( true ) ;
2023-10-06 13:44:06 +11:00
const atBottomRef = useRef ( atBottom ) ;
atBottomRef . current = atBottom ;
const scrollRef = useRef < HTMLDivElement > ( null ) ;
const scrollToBottomRef = useRef ( {
count : 0 ,
smooth : true ,
} ) ;
2024-05-31 19:49:46 +05:30
const [ focusItem , setFocusItem ] = useState <
| {
2024-09-11 17:07:02 +10:00
index : number ;
scrollTo : boolean ;
highlight : boolean ;
}
2024-05-31 19:49:46 +05:30
| undefined
> ( ) ;
2023-10-06 13:44:06 +11:00
const alive = useAlive ( ) ;
2024-07-30 17:48:59 +05:30
const linkifyOpts = useMemo < LinkifyOpts > (
( ) = > ( {
. . . LINKIFY_OPTS ,
render : factoryRenderLinkifyWithMention ( ( href ) = >
renderMatrixMention ( mx , room . roomId , href , makeMentionCustomProps ( mentionClickHandler ) )
) ,
} ) ,
[ mx , room , mentionClickHandler ]
) ;
2023-10-06 13:44:06 +11:00
const htmlReactParserOptions = useMemo < HTMLReactParserOptions > (
( ) = >
2024-07-30 17:48:59 +05:30
getReactCustomHtmlParser ( mx , room . roomId , {
linkifyOpts ,
2024-09-07 21:45:55 +08:00
useAuthentication ,
2024-07-30 17:48:59 +05:30
handleSpoilerClick : spoilerClickHandler ,
handleMentionClick : mentionClickHandler ,
2023-10-06 13:44:06 +11:00
} ) ,
2024-09-07 21:45:55 +08:00
[ mx , room , linkifyOpts , spoilerClickHandler , mentionClickHandler , useAuthentication ]
2023-10-06 13:44:06 +11:00
) ;
const parseMemberEvent = useMemberEventParser ( ) ;
const [ timeline , setTimeline ] = useState < Timeline > ( ( ) = >
eventId ? getEmptyTimeline ( ) : getInitialTimeline ( room )
) ;
2026-05-20 21:39:35 -04:00
const timelineRef = React . useRef ( timeline ) ;
timelineRef . current = timeline ;
2023-10-06 13:44:06 +11:00
const eventsLength = getTimelinesEventsCount ( timeline . linkedTimelines ) ;
2026-05-20 21:54:33 -04:00
// Perf-5: precompute base offsets once per linkedTimelines change instead of O(N× T) scan
const timelineSegments = useMemo < Array < [ number , number , EventTimeline ] > > ( ( ) = > {
let base = 0 ;
return timeline . linkedTimelines . map ( ( t ) = > {
const len = t . getEvents ( ) . length ;
const seg : [ number , number , EventTimeline ] = [ base , len , t ] ;
base += len ;
return seg ;
} ) ;
} , [ timeline . linkedTimelines ] ) ;
2023-10-06 13:44:06 +11:00
const liveTimelineLinked =
timeline . linkedTimelines [ timeline . linkedTimelines . length - 1 ] === getLiveTimeline ( room ) ;
const canPaginateBack =
typeof timeline . linkedTimelines [ 0 ] ? . getPaginationToken ( Direction . Backward ) === 'string' ;
const rangeAtStart = timeline . range . start === 0 ;
const rangeAtEnd = timeline . range . end === eventsLength ;
2023-10-19 17:40:01 +11:00
const atLiveEndRef = useRef ( liveTimelineLinked && rangeAtEnd ) ;
atLiveEndRef . current = liveTimelineLinked && rangeAtEnd ;
2023-10-06 13:44:06 +11:00
const handleTimelinePagination = useTimelinePagination (
mx ,
timeline ,
setTimeline ,
PAGINATION_LIMIT
) ;
const getScrollElement = useCallback ( ( ) = > scrollRef . current , [ ] ) ;
2023-10-14 16:08:43 +11:00
const { getItems , scrollToItem , scrollToElement , observeBackAnchor , observeFrontAnchor } =
useVirtualPaginator ( {
count : eventsLength ,
limit : PAGINATION_LIMIT ,
range : timeline.range ,
onRangeChange : useCallback ( ( r ) = > setTimeline ( ( cs ) = > ( { . . . cs , range : r } ) ) , [ ] ) ,
getScrollElement ,
getItemElement : useCallback (
( index : number ) = >
( scrollRef . current ? . querySelector ( ` [data-message-item=" ${ index } "] ` ) as HTMLElement ) ? ?
undefined ,
[ ]
) ,
onEnd : handleTimelinePagination ,
} ) ;
2023-10-06 13:44:06 +11:00
const loadEventTimeline = useEventTimelineLoader (
mx ,
room ,
useCallback (
( evtId , lTimelines , evtAbsIndex ) = > {
if ( ! alive ( ) ) return ;
const evLength = getTimelinesEventsCount ( lTimelines ) ;
2024-05-31 19:49:46 +05:30
setFocusItem ( {
2023-10-06 13:44:06 +11:00
index : evtAbsIndex ,
scrollTo : true ,
2023-10-26 16:21:55 +11:00
highlight : evtId !== readUptoEventIdRef . current ,
2024-05-31 19:49:46 +05:30
} ) ;
2023-10-06 13:44:06 +11:00
setTimeline ( {
linkedTimelines : lTimelines ,
range : {
start : Math.max ( evtAbsIndex - PAGINATION_LIMIT , 0 ) ,
end : Math.min ( evtAbsIndex + PAGINATION_LIMIT , evLength ) ,
} ,
} ) ;
} ,
2023-10-26 16:21:55 +11:00
[ alive ]
2023-10-06 13:44:06 +11:00
) ,
useCallback ( ( ) = > {
if ( ! alive ( ) ) return ;
setTimeline ( getInitialTimeline ( room ) ) ;
scrollToBottomRef . current . count += 1 ;
scrollToBottomRef . current . smooth = false ;
} , [ alive , room ] )
) ;
useLiveEventArrive (
room ,
useCallback (
( mEvt : MatrixEvent ) = > {
2023-10-19 17:40:01 +11:00
// if user is at bottom of timeline
// keep paginating timeline and conditionally mark as read
// otherwise we update timeline without paginating
// so timeline can be updated with evt like: edits, reactions etc
2023-10-21 18:14:21 +11:00
if ( atBottomRef . current ) {
if ( document . hasFocus ( ) && ( ! unreadInfo || mEvt . getSender ( ) === mx . getUserId ( ) ) ) {
2025-02-21 19:18:02 +11:00
// Check if the document is in focus (user is actively viewing the app),
// and either there are no unread messages or the latest message is from the current user.
// If either condition is met, trigger the markAsRead function to send a read receipt.
2026-05-20 21:11:38 -04:00
const _roomId = mEvt . getRoomId ( ) ;
if ( _roomId ) requestAnimationFrame ( ( ) = > markAsRead ( mx , _roomId , hideActivity ) ) ;
2023-10-06 13:44:06 +11:00
}
2025-02-21 19:18:02 +11:00
if ( ! document . hasFocus ( ) && ! unreadInfo ) {
2023-10-23 21:42:27 +11:00
setUnreadInfo ( getRoomUnreadInfo ( room ) ) ;
2023-10-21 18:14:21 +11:00
}
2025-02-21 19:18:02 +11:00
scrollToBottomRef . current . count += 1 ;
scrollToBottomRef . current . smooth = true ;
2023-10-06 13:44:06 +11:00
setTimeline ( ( ct ) = > ( {
. . . ct ,
range : {
start : ct.range.start + 1 ,
end : ct.range.end + 1 ,
} ,
} ) ) ;
return ;
}
setTimeline ( ( ct ) = > ( { . . . ct } ) ) ;
if ( ! unreadInfo ) {
setUnreadInfo ( getRoomUnreadInfo ( room ) ) ;
}
} ,
2025-02-26 21:44:53 +11:00
[ mx , room , unreadInfo , hideActivity ]
2023-10-06 13:44:06 +11:00
)
) ;
2025-02-21 19:18:02 +11:00
const handleOpenEvent = useCallback (
async (
evtId : string ,
highlight = true ,
onScroll : ( ( scrolled : boolean ) = > void ) | undefined = undefined
) = > {
const evtTimeline = getEventTimeline ( room , evtId ) ;
const absoluteIndex =
2026-05-20 21:39:35 -04:00
evtTimeline && getEventIdAbsoluteIndex ( timelineRef . current . linkedTimelines , evtTimeline , evtId ) ;
2025-02-21 19:18:02 +11:00
if ( typeof absoluteIndex === 'number' ) {
const scrolled = scrollToItem ( absoluteIndex , {
behavior : 'smooth' ,
align : 'center' ,
stopInView : true ,
} ) ;
if ( onScroll ) onScroll ( scrolled ) ;
setFocusItem ( {
index : absoluteIndex ,
scrollTo : false ,
highlight ,
} ) ;
} else {
setTimeline ( getEmptyTimeline ( ) ) ;
loadEventTimeline ( evtId ) ;
}
} ,
2026-05-20 21:39:35 -04:00
[ room , scrollToItem , loadEventTimeline ]
2025-02-21 19:18:02 +11:00
) ;
2023-10-06 13:44:06 +11:00
useLiveTimelineRefresh (
room ,
useCallback ( ( ) = > {
if ( liveTimelineLinked ) {
setTimeline ( getInitialTimeline ( room ) ) ;
}
} , [ room , liveTimelineLinked ] )
) ;
// Stay at bottom when room editor resize
useResizeObserver (
2023-10-19 17:40:01 +11:00
useMemo ( ( ) = > {
let mounted = false ;
return ( entries ) = > {
if ( ! mounted ) {
// skip initial mounting call
mounted = true ;
return ;
}
2023-10-06 13:44:06 +11:00
if ( ! roomInputRef . current ) return ;
const editorBaseEntry = getResizeObserverEntry ( roomInputRef . current , entries ) ;
const scrollElement = getScrollElement ( ) ;
if ( ! editorBaseEntry || ! scrollElement ) return ;
if ( atBottomRef . current ) {
scrollToBottom ( scrollElement ) ;
}
2023-10-19 17:40:01 +11:00
} ;
} , [ getScrollElement , roomInputRef ] ) ,
2023-10-06 13:44:06 +11:00
useCallback ( ( ) = > roomInputRef . current , [ roomInputRef ] )
) ;
2023-10-19 17:40:01 +11:00
const tryAutoMarkAsRead = useCallback ( ( ) = > {
2025-02-21 19:18:02 +11:00
const readUptoEventId = readUptoEventIdRef . current ;
if ( ! readUptoEventId ) {
2025-02-26 21:44:53 +11:00
requestAnimationFrame ( ( ) = > markAsRead ( mx , room . roomId , hideActivity ) ) ;
2023-10-19 17:40:01 +11:00
return ;
}
2025-02-21 19:18:02 +11:00
const evtTimeline = getEventTimeline ( room , readUptoEventId ) ;
2023-10-19 17:40:01 +11:00
const latestTimeline = evtTimeline && getFirstLinkedTimeline ( evtTimeline , Direction . Forward ) ;
if ( latestTimeline === room . getLiveTimeline ( ) ) {
2025-02-26 21:44:53 +11:00
requestAnimationFrame ( ( ) = > markAsRead ( mx , room . roomId , hideActivity ) ) ;
2023-10-19 17:40:01 +11:00
}
2025-02-26 21:44:53 +11:00
} , [ mx , room , hideActivity ] ) ;
2023-10-19 17:40:01 +11:00
2023-10-08 00:09:43 +11:00
const debounceSetAtBottom = useDebounce (
useCallback ( ( entry : IntersectionObserverEntry ) = > {
if ( ! entry . isIntersecting ) setAtBottom ( false ) ;
} , [ ] ) ,
{ wait : 1000 }
) ;
2023-10-06 13:44:06 +11:00
useIntersectionObserver (
2023-10-08 00:09:43 +11:00
useCallback (
( entries ) = > {
const target = atBottomAnchorRef . current ;
if ( ! target ) return ;
const targetEntry = getIntersectionObserverEntry ( target , entries ) ;
if ( targetEntry ) debounceSetAtBottom ( targetEntry ) ;
2023-10-19 17:40:01 +11:00
if ( targetEntry ? . isIntersecting && atLiveEndRef . current ) {
setAtBottom ( true ) ;
2025-02-21 19:18:02 +11:00
if ( document . hasFocus ( ) ) {
tryAutoMarkAsRead ( ) ;
}
2023-10-19 17:40:01 +11:00
}
2023-10-08 00:09:43 +11:00
} ,
2023-10-19 17:40:01 +11:00
[ debounceSetAtBottom , tryAutoMarkAsRead ]
2023-10-08 00:09:43 +11:00
) ,
useCallback (
2023-10-06 13:44:06 +11:00
( ) = > ( {
root : getScrollElement ( ) ,
rootMargin : '100px' ,
} ) ,
[ getScrollElement ]
) ,
useCallback ( ( ) = > atBottomAnchorRef . current , [ ] )
) ;
2023-10-21 18:14:21 +11:00
useDocumentFocusChange (
useCallback (
( inFocus ) = > {
if ( inFocus && atBottomRef . current ) {
2025-02-21 19:18:02 +11:00
if ( unreadInfo ? . inLiveTimeline ) {
handleOpenEvent ( unreadInfo . readUptoEventId , false , ( scrolled ) = > {
// the unread event is already in view
// so, try mark as read;
if ( ! scrolled ) {
tryAutoMarkAsRead ( ) ;
}
} ) ;
return ;
}
2023-10-21 18:14:21 +11:00
tryAutoMarkAsRead ( ) ;
}
} ,
2025-02-21 19:18:02 +11:00
[ tryAutoMarkAsRead , unreadInfo , handleOpenEvent ]
2023-10-21 18:14:21 +11:00
)
) ;
2023-10-14 16:08:43 +11:00
// Handle up arrow edit
useKeyDown (
window ,
useCallback (
( evt ) = > {
if (
2023-10-21 18:14:33 +11:00
isKeyHotkey ( 'arrowup' , evt ) &&
2023-10-14 16:08:43 +11:00
editableActiveElement ( ) &&
document . activeElement ? . getAttribute ( 'data-editable-name' ) === 'RoomInput' &&
isEmptyEditor ( editor )
) {
const editableEvt = getLatestEditableEvt ( room . getLiveTimeline ( ) , ( mEvt ) = >
canEditEvent ( mx , mEvt )
) ;
const editableEvtId = editableEvt ? . getId ( ) ;
if ( ! editableEvtId ) return ;
setEditId ( editableEvtId ) ;
2024-09-11 17:07:02 +10:00
evt . preventDefault ( ) ;
2023-10-14 16:08:43 +11:00
}
} ,
[ mx , room , editor ]
)
) ;
2023-10-06 13:44:06 +11:00
useEffect ( ( ) = > {
if ( eventId ) {
setTimeline ( getEmptyTimeline ( ) ) ;
loadEventTimeline ( eventId ) ;
}
} , [ eventId , loadEventTimeline ] ) ;
// Scroll to bottom on initial timeline load
useLayoutEffect ( ( ) = > {
const scrollEl = scrollRef . current ;
2023-10-19 17:40:01 +11:00
if ( scrollEl ) {
scrollToBottom ( scrollEl ) ;
}
2023-10-06 13:44:06 +11:00
} , [ ] ) ;
2023-10-19 17:40:01 +11:00
// if live timeline is linked and unreadInfo change
// Scroll to last read message
2023-10-06 13:44:06 +11:00
useLayoutEffect ( ( ) = > {
const { readUptoEventId , inLiveTimeline , scrollTo } = unreadInfo ? ? { } ;
if ( readUptoEventId && inLiveTimeline && scrollTo ) {
const linkedTimelines = getLinkedTimelines ( getLiveTimeline ( room ) ) ;
const evtTimeline = getEventTimeline ( room , readUptoEventId ) ;
const absoluteIndex =
evtTimeline && getEventIdAbsoluteIndex ( linkedTimelines , evtTimeline , readUptoEventId ) ;
2026-05-20 21:11:38 -04:00
if ( typeof absoluteIndex === 'number' ) {
2023-10-06 13:44:06 +11:00
scrollToItem ( absoluteIndex , {
behavior : 'instant' ,
align : 'start' ,
stopInView : true ,
} ) ;
2023-10-19 17:40:01 +11:00
}
2023-10-06 13:44:06 +11:00
}
} , [ room , unreadInfo , scrollToItem ] ) ;
// scroll to focused message
useLayoutEffect ( ( ) = > {
2024-05-31 19:49:46 +05:30
if ( focusItem && focusItem . scrollTo ) {
scrollToItem ( focusItem . index , {
2023-10-06 13:44:06 +11:00
behavior : 'instant' ,
align : 'center' ,
stopInView : true ,
} ) ;
}
2026-05-20 21:39:35 -04:00
const timer = setTimeout ( ( ) = > {
2024-05-31 19:49:46 +05:30
if ( ! alive ( ) ) return ;
setFocusItem ( ( currentItem ) = > {
if ( currentItem === focusItem ) return undefined ;
return currentItem ;
} ) ;
} , 2000 ) ;
2026-05-20 21:39:35 -04:00
return ( ) = > clearTimeout ( timer ) ;
2024-05-31 19:49:46 +05:30
} , [ alive , focusItem , scrollToItem ] ) ;
2023-10-06 13:44:06 +11:00
// scroll to bottom of timeline
const scrollToBottomCount = scrollToBottomRef . current . count ;
useLayoutEffect ( ( ) = > {
if ( scrollToBottomCount > 0 ) {
const scrollEl = scrollRef . current ;
if ( scrollEl )
scrollToBottom ( scrollEl , scrollToBottomRef . current . smooth ? 'smooth' : 'instant' ) ;
}
} , [ scrollToBottomCount ] ) ;
2023-10-19 17:40:01 +11:00
// Remove unreadInfo on mark as read
2023-10-06 13:44:06 +11:00
useEffect ( ( ) = > {
2024-07-08 16:57:10 +05:30
if ( ! unread ) {
2023-10-19 17:40:01 +11:00
setUnreadInfo ( undefined ) ;
2024-07-08 16:57:10 +05:30
}
} , [ unread ] ) ;
2023-10-06 13:44:06 +11:00
2023-10-14 16:08:43 +11:00
// scroll out of view msg editor in view.
useEffect ( ( ) = > {
if ( editId ) {
const editMsgElement =
( scrollRef . current ? . querySelector ( ` [data-message-id=" ${ editId } "] ` ) as HTMLElement ) ? ?
undefined ;
if ( editMsgElement ) {
scrollToElement ( editMsgElement , {
align : 'center' ,
behavior : 'smooth' ,
stopInView : true ,
} ) ;
}
}
} , [ scrollToElement , editId ] ) ;
2023-10-06 13:44:06 +11:00
const handleJumpToLatest = ( ) = > {
2024-07-30 17:48:59 +05:30
if ( eventId ) {
navigateRoom ( room . roomId , undefined , { replace : true } ) ;
}
2023-10-06 13:44:06 +11:00
setTimeline ( getInitialTimeline ( room ) ) ;
scrollToBottomRef . current . count += 1 ;
scrollToBottomRef . current . smooth = false ;
} ;
const handleJumpToUnread = ( ) = > {
if ( unreadInfo ? . readUptoEventId ) {
setTimeline ( getEmptyTimeline ( ) ) ;
loadEventTimeline ( unreadInfo . readUptoEventId ) ;
}
} ;
const handleMarkAsRead = ( ) = > {
2025-02-26 21:44:53 +11:00
markAsRead ( mx , room . roomId , hideActivity ) ;
2023-10-06 13:44:06 +11:00
} ;
2024-08-15 16:52:32 +02:00
const handleOpenReply : MouseEventHandler = useCallback (
2023-10-06 13:44:06 +11:00
async ( evt ) = > {
2024-08-15 16:52:32 +02:00
const targetId = evt . currentTarget . getAttribute ( 'data-event-id' ) ;
if ( ! targetId ) return ;
2025-02-21 19:18:02 +11:00
handleOpenEvent ( targetId ) ;
2023-10-06 13:44:06 +11:00
} ,
2025-02-21 19:18:02 +11:00
[ handleOpenEvent ]
2023-10-06 13:44:06 +11:00
) ;
const handleUserClick : MouseEventHandler < HTMLButtonElement > = useCallback (
( evt ) = > {
evt . preventDefault ( ) ;
evt . stopPropagation ( ) ;
const userId = evt . currentTarget . getAttribute ( 'data-user-id' ) ;
if ( ! userId ) {
console . warn ( 'Button should have "data-user-id" attribute!' ) ;
return ;
}
2025-08-09 17:46:10 +05:30
openUserRoomProfile (
room . roomId ,
space ? . roomId ,
userId ,
evt . currentTarget . getBoundingClientRect ( )
) ;
2023-10-06 13:44:06 +11:00
} ,
2025-08-09 17:46:10 +05:30
[ room , space , openUserRoomProfile ]
2023-10-06 13:44:06 +11:00
) ;
const handleUsernameClick : MouseEventHandler < HTMLButtonElement > = useCallback (
( evt ) = > {
evt . preventDefault ( ) ;
const userId = evt . currentTarget . getAttribute ( 'data-user-id' ) ;
if ( ! userId ) {
console . warn ( 'Button should have "data-user-id" attribute!' ) ;
return ;
}
const name = getMemberDisplayName ( room , userId ) ? ? getMxIdLocalPart ( userId ) ? ? userId ;
editor . insertNode (
createMentionElement (
userId ,
name . startsWith ( '@' ) ? name : ` @ ${ name } ` ,
userId === mx . getUserId ( )
)
) ;
ReactEditor . focus ( editor ) ;
moveCursor ( editor ) ;
} ,
[ mx , room , editor ]
) ;
const handleReplyClick : MouseEventHandler < HTMLButtonElement > = useCallback (
2025-07-23 16:17:17 +01:00
( evt , startThread = false ) = > {
2023-10-06 13:44:06 +11:00
const replyId = evt . currentTarget . getAttribute ( 'data-event-id' ) ;
if ( ! replyId ) {
console . warn ( 'Button should have "data-event-id" attribute!' ) ;
return ;
}
const replyEvt = room . findEventById ( replyId ) ;
if ( ! replyEvt ) return ;
const editedReply = getEditedEvent ( replyId , replyEvt , room . getUnfilteredTimelineSet ( ) ) ;
2024-08-15 16:52:32 +02:00
const content : IContent = editedReply ? . getContent ( ) [ 'm.new_content' ] ? ? replyEvt . getContent ( ) ;
const { body , formatted_body : formattedBody } = content ;
2025-07-23 16:17:17 +01:00
const { 'm.relates_to' : relation } = startThread
? { 'm.relates_to' : { rel_type : 'm.thread' , event_id : replyId } }
: replyEvt . getWireContent ( ) ;
2023-10-06 13:44:06 +11:00
const senderId = replyEvt . getSender ( ) ;
if ( senderId && typeof body === 'string' ) {
setReplyDraft ( {
userId : senderId ,
eventId : replyId ,
body ,
formattedBody ,
2024-08-15 16:52:32 +02:00
relation ,
2023-10-06 13:44:06 +11:00
} ) ;
setTimeout ( ( ) = > ReactEditor . focus ( editor ) , 100 ) ;
}
} ,
[ room , setReplyDraft , editor ]
) ;
const handleReactionToggle = useCallback (
( targetEventId : string , key : string , shortcode? : string ) = > {
const relations = getEventReactions ( room . getUnfilteredTimelineSet ( ) , targetEventId ) ;
const allReactions = relations ? . getSortedAnnotationsByKey ( ) ? ? [ ] ;
const [ , reactionsSet ] = allReactions . find ( ( [ k ] ) = > k === key ) ? ? [ ] ;
const reactions = reactionsSet ? Array . from ( reactionsSet ) : [ ] ;
const myReaction = reactions . find ( factoryEventSentBy ( mx . getUserId ( ) ! ) ) ;
if ( myReaction && ! ! myReaction ? . isRelation ( ) ) {
mx . redactEvent ( room . roomId , myReaction . getId ( ) ! ) ;
return ;
}
const rShortcode =
shortcode ||
( reactions . find ( eventWithShortcode ) ? . getContent ( ) . shortcode as string | undefined ) ;
mx . sendEvent (
room . roomId ,
2025-08-12 19:42:30 +05:30
MessageEvent . Reaction as any ,
2023-10-06 13:44:06 +11:00
getReactionContent ( targetEventId , key , rShortcode )
) ;
} ,
[ mx , room ]
) ;
2023-10-14 16:08:43 +11:00
const handleEdit = useCallback (
( editEvtId? : string ) = > {
if ( editEvtId ) {
setEditId ( editEvtId ) ;
return ;
}
setEditId ( undefined ) ;
ReactEditor . focus ( editor ) ;
} ,
[ editor ]
) ;
2024-08-14 15:29:34 +02:00
const { t } = useTranslation ( ) ;
2023-10-06 13:44:06 +11:00
2024-05-31 19:49:46 +05:30
const renderMatrixEvent = useMatrixEventRenderer <
[ string , MatrixEvent , number , EventTimelineSet , boolean ]
> (
{
[ MessageEvent . RoomMessage ] : ( mEventId , mEvent , item , timelineSet , collapse ) = > {
const reactionRelations = getEventReactions ( timelineSet , mEventId ) ;
const reactions = reactionRelations && reactionRelations . getSortedAnnotationsByKey ( ) ;
const hasReactions = reactions && reactions . length > 0 ;
2024-08-15 16:52:32 +02:00
const { replyEventId , threadRootId } = mEvent ;
2024-05-31 19:49:46 +05:30
const highlighted = focusItem ? . index === item && focusItem . highlight ;
const editedEvent = getEditedEvent ( mEventId , mEvent , timelineSet ) ;
const getContent = ( ( ) = >
editedEvent ? . getContent ( ) [ 'm.new_content' ] ? ? mEvent . getContent ( ) ) as GetContentCallback ;
const senderId = mEvent . getSender ( ) ? ? '' ;
const senderDisplayName =
getMemberDisplayName ( room , senderId ) ? ? getMxIdLocalPart ( senderId ) ? ? senderId ;
2023-10-26 16:21:55 +11:00
2024-05-31 19:49:46 +05:30
return (
< Message
key = { mEvent . getId ( ) }
data-message-item = { item }
data-message-id = { mEventId }
room = { room }
mEvent = { mEvent }
messageSpacing = { messageSpacing }
messageLayout = { messageLayout }
collapse = { collapse }
highlight = { highlighted }
edit = { editId === mEventId }
2026-02-12 11:40:11 +01:00
canDelete = { canRedact || ( canDeleteOwn && mEvent . getSender ( ) === mx . getUserId ( ) ) }
2024-05-31 19:49:46 +05:30
canSendReaction = { canSendReaction }
2024-12-16 21:55:15 +11:00
canPinEvent = { canPinEvent }
2024-05-31 19:49:46 +05:30
imagePackRooms = { imagePackRooms }
relations = { hasReactions ? reactionRelations : undefined }
onUserClick = { handleUserClick }
onUsernameClick = { handleUsernameClick }
onReplyClick = { handleReplyClick }
onReactionToggle = { handleReactionToggle }
onEditId = { handleEdit }
reply = {
replyEventId && (
< Reply
room = { room }
timelineSet = { timelineSet }
2024-08-15 16:52:32 +02:00
replyEventId = { replyEventId }
threadRootId = { threadRootId }
2024-05-31 19:49:46 +05:30
onClick = { handleOpenReply }
2025-08-12 19:42:30 +05:30
getMemberPowerTag = { getMemberPowerTag }
accessibleTagColors = { accessiblePowerTagColors }
2025-03-23 22:09:29 +11:00
legacyUsernameColor = { legacyUsernameColor || direct }
2024-05-31 19:49:46 +05:30
/ >
)
}
reactions = {
reactionRelations && (
< Reactions
style = { { marginTop : config.space.S200 } }
room = { room }
relations = { reactionRelations }
mEventId = { mEventId }
canSendReaction = { canSendReaction }
onReactionToggle = { handleReactionToggle }
/ >
)
}
2025-02-26 21:44:53 +11:00
hideReadReceipts = { hideActivity }
2025-06-28 12:35:59 +02:00
showDeveloperTools = { showDeveloperTools }
2025-08-12 19:42:30 +05:30
memberPowerTag = { getMemberPowerTag ( senderId ) }
accessibleTagColors = { accessiblePowerTagColors }
2025-03-23 22:09:29 +11:00
legacyUsernameColor = { legacyUsernameColor || direct }
2025-07-27 15:13:00 +03:00
hour24Clock = { hour24Clock }
dateFormatString = { dateFormatString }
2026-05-21 12:07:42 -04:00
lotusTerminal = { ! ! lotusTerminal }
2023-10-30 07:14:58 +11:00
>
2024-05-31 19:49:46 +05:30
{ mEvent . isRedacted ( ) ? (
2026-05-15 14:39:16 -04:00
< RedactedContent reason = { mEvent . getUnsigned ( ) . redacted_because ? . content . reason } / >
2024-05-31 19:49:46 +05:30
) : (
< RenderMessageContent
displayName = { senderDisplayName }
msgType = { mEvent . getContent ( ) . msgtype ? ? '' }
ts = { mEvent . getTs ( ) }
edited = { ! ! editedEvent }
getContent = { getContent }
mediaAutoLoad = { mediaAutoLoad }
urlPreview = { showUrlPreview }
htmlReactParserOptions = { htmlReactParserOptions }
2024-07-30 17:48:59 +05:30
linkifyOpts = { linkifyOpts }
2025-02-10 16:49:47 +11:00
outlineAttachment = { messageLayout === MessageLayout . Bubble }
2024-05-31 19:49:46 +05:30
/ >
) }
< / Message >
) ;
} ,
[ MessageEvent . RoomMessageEncrypted ] : ( mEventId , mEvent , item , timelineSet , collapse ) = > {
const reactionRelations = getEventReactions ( timelineSet , mEventId ) ;
const reactions = reactionRelations && reactionRelations . getSortedAnnotationsByKey ( ) ;
const hasReactions = reactions && reactions . length > 0 ;
2024-08-15 16:52:32 +02:00
const { replyEventId , threadRootId } = mEvent ;
2024-05-31 19:49:46 +05:30
const highlighted = focusItem ? . index === item && focusItem . highlight ;
2023-10-30 07:14:58 +11:00
2024-05-31 19:49:46 +05:30
return (
< Message
key = { mEvent . getId ( ) }
data-message-item = { item }
data-message-id = { mEventId }
room = { room }
mEvent = { mEvent }
messageSpacing = { messageSpacing }
messageLayout = { messageLayout }
collapse = { collapse }
highlight = { highlighted }
edit = { editId === mEventId }
2026-02-12 11:40:11 +01:00
canDelete = { canRedact || ( canDeleteOwn && mEvent . getSender ( ) === mx . getUserId ( ) ) }
2024-05-31 19:49:46 +05:30
canSendReaction = { canSendReaction }
2024-12-16 21:55:15 +11:00
canPinEvent = { canPinEvent }
2024-05-31 19:49:46 +05:30
imagePackRooms = { imagePackRooms }
relations = { hasReactions ? reactionRelations : undefined }
onUserClick = { handleUserClick }
onUsernameClick = { handleUsernameClick }
onReplyClick = { handleReplyClick }
onReactionToggle = { handleReactionToggle }
onEditId = { handleEdit }
reply = {
replyEventId && (
< Reply
room = { room }
timelineSet = { timelineSet }
2024-08-15 16:52:32 +02:00
replyEventId = { replyEventId }
threadRootId = { threadRootId }
2024-05-31 19:49:46 +05:30
onClick = { handleOpenReply }
2025-08-12 19:42:30 +05:30
getMemberPowerTag = { getMemberPowerTag }
accessibleTagColors = { accessiblePowerTagColors }
2025-03-23 22:09:29 +11:00
legacyUsernameColor = { legacyUsernameColor || direct }
2024-05-31 19:49:46 +05:30
/ >
)
}
reactions = {
reactionRelations && (
< Reactions
style = { { marginTop : config.space.S200 } }
room = { room }
relations = { reactionRelations }
mEventId = { mEventId }
canSendReaction = { canSendReaction }
onReactionToggle = { handleReactionToggle }
/ >
)
}
2025-02-26 21:44:53 +11:00
hideReadReceipts = { hideActivity }
2025-06-28 12:35:59 +02:00
showDeveloperTools = { showDeveloperTools }
2025-08-12 19:42:30 +05:30
memberPowerTag = { getMemberPowerTag ( mEvent . getSender ( ) ? ? '' ) }
accessibleTagColors = { accessiblePowerTagColors }
2025-03-23 22:09:29 +11:00
legacyUsernameColor = { legacyUsernameColor || direct }
2025-07-27 15:13:00 +03:00
hour24Clock = { hour24Clock }
dateFormatString = { dateFormatString }
2024-05-31 19:49:46 +05:30
>
< EncryptedContent mEvent = { mEvent } >
{ ( ) = > {
if ( mEvent . isRedacted ( ) ) return < RedactedContent / > ;
if ( mEvent . getType ( ) === MessageEvent . Sticker )
return (
< MSticker
content = { mEvent . getContent ( ) }
renderImageContent = { ( props ) = > (
< ImageContent
{ ...props }
autoPlay = { mediaAutoLoad }
renderImage = { ( p ) = > < Image { ...p } loading = "lazy" / > }
renderViewer = { ( p ) = > < ImageViewer { ...p } / > }
/ >
) }
/ >
) ;
if ( mEvent . getType ( ) === MessageEvent . RoomMessage ) {
const editedEvent = getEditedEvent ( mEventId , mEvent , timelineSet ) ;
const getContent = ( ( ) = >
editedEvent ? . getContent ( ) [ 'm.new_content' ] ? ?
mEvent . getContent ( ) ) as GetContentCallback ;
const senderId = mEvent . getSender ( ) ? ? '' ;
const senderDisplayName =
getMemberDisplayName ( room , senderId ) ? ? getMxIdLocalPart ( senderId ) ? ? senderId ;
return (
< RenderMessageContent
displayName = { senderDisplayName }
msgType = { mEvent . getContent ( ) . msgtype ? ? '' }
ts = { mEvent . getTs ( ) }
edited = { ! ! editedEvent }
getContent = { getContent }
mediaAutoLoad = { mediaAutoLoad }
urlPreview = { showUrlPreview }
htmlReactParserOptions = { htmlReactParserOptions }
2024-07-30 17:48:59 +05:30
linkifyOpts = { linkifyOpts }
2025-02-10 16:49:47 +11:00
outlineAttachment = { messageLayout === MessageLayout . Bubble }
2024-05-31 19:49:46 +05:30
/ >
) ;
}
2026-05-15 00:47:21 -04:00
if (
mEvent . getType ( ) === 'm.poll.start' ||
mEvent . getType ( ) === 'org.matrix.msc3381.poll.start'
)
2026-05-15 13:37:03 -04:00
return < PollContent content = { mEvent . getContent ( ) } roomId = { room . roomId } eventId = { mEvent . getId ( ) ? ? undefined } / > ;
2024-05-31 19:49:46 +05:30
if ( mEvent . getType ( ) === MessageEvent . RoomMessageEncrypted )
return (
< Text >
< MessageNotDecryptedContent / >
< / Text >
) ;
return (
< Text >
< MessageUnsupportedContent / >
< / Text >
) ;
} }
< / EncryptedContent >
< / Message >
) ;
} ,
[ MessageEvent . Sticker ] : ( mEventId , mEvent , item , timelineSet , collapse ) = > {
const reactionRelations = getEventReactions ( timelineSet , mEventId ) ;
const reactions = reactionRelations && reactionRelations . getSortedAnnotationsByKey ( ) ;
const hasReactions = reactions && reactions . length > 0 ;
const highlighted = focusItem ? . index === item && focusItem . highlight ;
2023-10-30 07:14:58 +11:00
2024-05-31 19:49:46 +05:30
return (
< Message
key = { mEvent . getId ( ) }
data-message-item = { item }
data-message-id = { mEventId }
room = { room }
mEvent = { mEvent }
messageSpacing = { messageSpacing }
messageLayout = { messageLayout }
collapse = { collapse }
highlight = { highlighted }
2026-02-12 11:40:11 +01:00
canDelete = { canRedact || ( canDeleteOwn && mEvent . getSender ( ) === mx . getUserId ( ) ) }
2024-05-31 19:49:46 +05:30
canSendReaction = { canSendReaction }
2024-12-16 21:55:15 +11:00
canPinEvent = { canPinEvent }
2024-05-31 19:49:46 +05:30
imagePackRooms = { imagePackRooms }
relations = { hasReactions ? reactionRelations : undefined }
onUserClick = { handleUserClick }
onUsernameClick = { handleUsernameClick }
onReplyClick = { handleReplyClick }
onReactionToggle = { handleReactionToggle }
reactions = {
reactionRelations && (
< Reactions
style = { { marginTop : config.space.S200 } }
room = { room }
relations = { reactionRelations }
mEventId = { mEventId }
canSendReaction = { canSendReaction }
onReactionToggle = { handleReactionToggle }
/ >
)
}
2025-02-26 21:44:53 +11:00
hideReadReceipts = { hideActivity }
2025-06-28 12:35:59 +02:00
showDeveloperTools = { showDeveloperTools }
2025-08-12 19:42:30 +05:30
memberPowerTag = { getMemberPowerTag ( mEvent . getSender ( ) ? ? '' ) }
accessibleTagColors = { accessiblePowerTagColors }
2025-03-23 22:09:29 +11:00
legacyUsernameColor = { legacyUsernameColor || direct }
2025-07-27 15:13:00 +03:00
hour24Clock = { hour24Clock }
dateFormatString = { dateFormatString }
2023-10-30 07:14:58 +11:00
>
2024-05-31 19:49:46 +05:30
{ mEvent . isRedacted ( ) ? (
< RedactedContent reason = { mEvent . getUnsigned ( ) . redacted_because ? . content . reason } / >
) : (
< MSticker
content = { mEvent . getContent ( ) }
renderImageContent = { ( props ) = > (
< ImageContent
{ ...props }
autoPlay = { mediaAutoLoad }
renderImage = { ( p ) = > < Image { ...p } loading = "lazy" / > }
renderViewer = { ( p ) = > < ImageViewer { ...p } / > }
/ >
) }
/ >
) }
< / Message >
) ;
} ,
2026-05-15 00:47:21 -04:00
'org.matrix.msc3381.poll.start' : ( mEventId , mEvent , item , timelineSet , collapse ) = > {
const reactionRelations = getEventReactions ( timelineSet , mEventId ) ;
const reactions = reactionRelations && reactionRelations . getSortedAnnotationsByKey ( ) ;
const hasReactions = reactions && reactions . length > 0 ;
const highlighted = focusItem ? . index === item && focusItem . highlight ;
return (
< Message
key = { mEvent . getId ( ) }
data-message-item = { item }
data-message-id = { mEventId }
room = { room }
mEvent = { mEvent }
messageSpacing = { messageSpacing }
messageLayout = { messageLayout }
collapse = { collapse }
highlight = { highlighted }
canDelete = { canRedact || ( canDeleteOwn && mEvent . getSender ( ) === mx . getUserId ( ) ) }
canSendReaction = { canSendReaction }
canPinEvent = { canPinEvent }
imagePackRooms = { imagePackRooms }
relations = { hasReactions ? reactionRelations : undefined }
onUserClick = { handleUserClick }
onUsernameClick = { handleUsernameClick }
onReplyClick = { handleReplyClick }
onReactionToggle = { handleReactionToggle }
reactions = {
reactionRelations && (
< Reactions
style = { { marginTop : config.space.S200 } }
room = { room }
relations = { reactionRelations }
mEventId = { mEventId }
canSendReaction = { canSendReaction }
onReactionToggle = { handleReactionToggle }
/ >
)
}
hideReadReceipts = { hideActivity }
showDeveloperTools = { showDeveloperTools }
memberPowerTag = { getMemberPowerTag ( mEvent . getSender ( ) ? ? '' ) }
accessibleTagColors = { accessiblePowerTagColors }
legacyUsernameColor = { legacyUsernameColor || direct }
hour24Clock = { hour24Clock }
dateFormatString = { dateFormatString }
>
{ mEvent . isRedacted ( ) ? (
< RedactedContent reason = { mEvent . getUnsigned ( ) . redacted_because ? . content . reason } / >
) : (
2026-05-15 13:37:03 -04:00
< PollContent content = { mEvent . getContent ( ) } roomId = { room . roomId } eventId = { mEvent . getId ( ) ? ? undefined } / >
2026-05-15 00:47:21 -04:00
) }
< / Message >
) ;
} ,
'm.poll.start' : ( mEventId , mEvent , item , timelineSet , collapse ) = > {
const reactionRelations = getEventReactions ( timelineSet , mEventId ) ;
const reactions = reactionRelations && reactionRelations . getSortedAnnotationsByKey ( ) ;
const hasReactions = reactions && reactions . length > 0 ;
const highlighted = focusItem ? . index === item && focusItem . highlight ;
return (
< Message
key = { mEvent . getId ( ) }
data-message-item = { item }
data-message-id = { mEventId }
room = { room }
mEvent = { mEvent }
messageSpacing = { messageSpacing }
messageLayout = { messageLayout }
collapse = { collapse }
highlight = { highlighted }
canDelete = { canRedact || ( canDeleteOwn && mEvent . getSender ( ) === mx . getUserId ( ) ) }
canSendReaction = { canSendReaction }
canPinEvent = { canPinEvent }
imagePackRooms = { imagePackRooms }
relations = { hasReactions ? reactionRelations : undefined }
onUserClick = { handleUserClick }
onUsernameClick = { handleUsernameClick }
onReplyClick = { handleReplyClick }
onReactionToggle = { handleReactionToggle }
reactions = {
reactionRelations && (
< Reactions
style = { { marginTop : config.space.S200 } }
room = { room }
relations = { reactionRelations }
mEventId = { mEventId }
canSendReaction = { canSendReaction }
onReactionToggle = { handleReactionToggle }
/ >
)
}
hideReadReceipts = { hideActivity }
showDeveloperTools = { showDeveloperTools }
memberPowerTag = { getMemberPowerTag ( mEvent . getSender ( ) ? ? '' ) }
accessibleTagColors = { accessiblePowerTagColors }
legacyUsernameColor = { legacyUsernameColor || direct }
hour24Clock = { hour24Clock }
dateFormatString = { dateFormatString }
>
{ mEvent . isRedacted ( ) ? (
< RedactedContent reason = { mEvent . getUnsigned ( ) . redacted_because ? . content . reason } / >
) : (
2026-05-15 13:37:03 -04:00
< PollContent content = { mEvent . getContent ( ) } roomId = { room . roomId } eventId = { mEvent . getId ( ) ? ? undefined } / >
2026-05-15 00:47:21 -04:00
) }
< / Message >
) ;
} ,
2024-05-31 19:49:46 +05:30
[ StateEvent . RoomMember ] : ( mEventId , mEvent , item ) = > {
const membershipChanged = isMembershipChanged ( mEvent ) ;
if ( membershipChanged && hideMembershipEvents ) return null ;
if ( ! membershipChanged && hideNickAvatarEvents ) return null ;
2023-10-06 13:44:06 +11:00
2024-05-31 19:49:46 +05:30
const highlighted = focusItem ? . index === item && focusItem . highlight ;
const parsed = parseMemberEvent ( mEvent ) ;
2023-10-30 07:14:58 +11:00
2025-02-10 16:49:47 +11:00
const timeJSX = (
2025-07-27 15:13:00 +03:00
< Time
ts = { mEvent . getTs ( ) }
compact = { messageLayout === MessageLayout . Compact }
hour24Clock = { hour24Clock }
dateFormatString = { dateFormatString }
/ >
2025-02-10 16:49:47 +11:00
) ;
2023-10-06 13:44:06 +11:00
2024-05-31 19:49:46 +05:30
return (
< Event
key = { mEvent . getId ( ) }
data-message-item = { item }
data-message-id = { mEventId }
room = { room }
mEvent = { mEvent }
highlight = { highlighted }
messageSpacing = { messageSpacing }
canDelete = { canRedact || mEvent . getSender ( ) === mx . getUserId ( ) }
2025-02-26 21:44:53 +11:00
hideReadReceipts = { hideActivity }
2025-06-28 12:35:59 +02:00
showDeveloperTools = { showDeveloperTools }
2023-10-06 13:44:06 +11:00
>
2024-05-31 19:49:46 +05:30
< EventContent
messageLayout = { messageLayout }
time = { timeJSX }
iconSrc = { parsed . icon }
content = {
< Box grow = "Yes" direction = "Column" >
< Text size = "T300" priority = "300" >
{ parsed . body }
< / Text >
< / Box >
}
2023-10-06 13:44:06 +11:00
/ >
2024-05-31 19:49:46 +05:30
< / Event >
) ;
} ,
[ StateEvent . RoomName ] : ( mEventId , mEvent , item ) = > {
const highlighted = focusItem ? . index === item && focusItem . highlight ;
const senderId = mEvent . getSender ( ) ? ? '' ;
const senderName = getMemberDisplayName ( room , senderId ) || getMxIdLocalPart ( senderId ) ;
2023-10-06 13:44:06 +11:00
2025-02-10 16:49:47 +11:00
const timeJSX = (
2025-07-27 15:13:00 +03:00
< Time
ts = { mEvent . getTs ( ) }
compact = { messageLayout === MessageLayout . Compact }
hour24Clock = { hour24Clock }
dateFormatString = { dateFormatString }
/ >
2025-02-10 16:49:47 +11:00
) ;
2023-10-06 13:44:06 +11:00
2024-05-31 19:49:46 +05:30
return (
< Event
key = { mEvent . getId ( ) }
data-message-item = { item }
data-message-id = { mEventId }
room = { room }
mEvent = { mEvent }
highlight = { highlighted }
messageSpacing = { messageSpacing }
canDelete = { canRedact || mEvent . getSender ( ) === mx . getUserId ( ) }
2025-02-26 21:44:53 +11:00
hideReadReceipts = { hideActivity }
2025-06-28 12:35:59 +02:00
showDeveloperTools = { showDeveloperTools }
2023-10-06 13:44:06 +11:00
>
2024-05-31 19:49:46 +05:30
< EventContent
messageLayout = { messageLayout }
time = { timeJSX }
iconSrc = { Icons . Hash }
content = {
< Box grow = "Yes" direction = "Column" >
< Text size = "T300" priority = "300" >
< b > { senderName } < / b >
2024-08-14 15:29:34 +02:00
{ t ( 'Organisms.RoomCommon.changed_room_name' ) }
2024-05-31 19:49:46 +05:30
< / Text >
< / Box >
}
2023-10-06 13:44:06 +11:00
/ >
2024-05-31 19:49:46 +05:30
< / Event >
) ;
} ,
[ StateEvent . RoomTopic ] : ( mEventId , mEvent , item ) = > {
const highlighted = focusItem ? . index === item && focusItem . highlight ;
const senderId = mEvent . getSender ( ) ? ? '' ;
const senderName = getMemberDisplayName ( room , senderId ) || getMxIdLocalPart ( senderId ) ;
2023-10-06 13:44:06 +11:00
2025-02-10 16:49:47 +11:00
const timeJSX = (
2025-07-27 15:13:00 +03:00
< Time
ts = { mEvent . getTs ( ) }
compact = { messageLayout === MessageLayout . Compact }
hour24Clock = { hour24Clock }
dateFormatString = { dateFormatString }
/ >
2025-02-10 16:49:47 +11:00
) ;
2023-10-06 13:44:06 +11:00
return (
2024-05-31 19:49:46 +05:30
< Event
key = { mEvent . getId ( ) }
data-message-item = { item }
data-message-id = { mEventId }
room = { room }
mEvent = { mEvent }
highlight = { highlighted }
messageSpacing = { messageSpacing }
canDelete = { canRedact || mEvent . getSender ( ) === mx . getUserId ( ) }
2025-02-26 21:44:53 +11:00
hideReadReceipts = { hideActivity }
2025-06-28 12:35:59 +02:00
showDeveloperTools = { showDeveloperTools }
2024-05-31 19:49:46 +05:30
>
< EventContent
messageLayout = { messageLayout }
time = { timeJSX }
iconSrc = { Icons . Hash }
content = {
< Box grow = "Yes" direction = "Column" >
< Text size = "T300" priority = "300" >
< b > { senderName } < / b >
{ ' changed room topic' }
< / Text >
< / Box >
}
/ >
< / Event >
2023-10-06 13:44:06 +11:00
) ;
2024-05-31 19:49:46 +05:30
} ,
[ StateEvent . RoomAvatar ] : ( mEventId , mEvent , item ) = > {
const highlighted = focusItem ? . index === item && focusItem . highlight ;
const senderId = mEvent . getSender ( ) ? ? '' ;
const senderName = getMemberDisplayName ( room , senderId ) || getMxIdLocalPart ( senderId ) ;
2023-10-06 13:44:06 +11:00
2025-02-10 16:49:47 +11:00
const timeJSX = (
2025-07-27 15:13:00 +03:00
< Time
ts = { mEvent . getTs ( ) }
compact = { messageLayout === MessageLayout . Compact }
hour24Clock = { hour24Clock }
dateFormatString = { dateFormatString }
/ >
2025-02-10 16:49:47 +11:00
) ;
2023-10-06 13:44:06 +11:00
2024-05-31 19:49:46 +05:30
return (
< Event
key = { mEvent . getId ( ) }
data-message-item = { item }
data-message-id = { mEventId }
room = { room }
mEvent = { mEvent }
highlight = { highlighted }
messageSpacing = { messageSpacing }
canDelete = { canRedact || mEvent . getSender ( ) === mx . getUserId ( ) }
2025-02-26 21:44:53 +11:00
hideReadReceipts = { hideActivity }
2025-06-28 12:35:59 +02:00
showDeveloperTools = { showDeveloperTools }
2024-05-31 19:49:46 +05:30
>
< EventContent
messageLayout = { messageLayout }
time = { timeJSX }
iconSrc = { Icons . Hash }
content = {
< Box grow = "Yes" direction = "Column" >
< Text size = "T300" priority = "300" >
< b > { senderName } < / b >
{ ' changed room avatar' }
2023-10-06 13:44:06 +11:00
< / Text >
2024-05-31 19:49:46 +05:30
< / Box >
}
/ >
< / Event >
) ;
} ,
2026-05-19 16:45:02 -04:00
[ StateEvent . RoomEncryption ] : ( mEventId , mEvent , item ) = > {
const highlighted = focusItem ? . index === item && focusItem . highlight ;
const senderId = mEvent . getSender ( ) ? ? '' ;
const senderName = getMemberDisplayName ( room , senderId ) || getMxIdLocalPart ( senderId ) ;
const timeJSX = (
< Time ts = { mEvent . getTs ( ) } compact = { messageLayout === MessageLayout . Compact } hour24Clock = { hour24Clock } dateFormatString = { dateFormatString } / >
) ;
return (
< Event key = { mEvent . getId ( ) } data-message-item = { item } data-message-id = { mEventId } room = { room } mEvent = { mEvent } highlight = { highlighted } messageSpacing = { messageSpacing } canDelete = { canRedact || mEvent . getSender ( ) === mx . getUserId ( ) } hideReadReceipts = { hideActivity } showDeveloperTools = { showDeveloperTools } >
< EventContent messageLayout = { messageLayout } time = { timeJSX } iconSrc = { Icons . Lock }
content = { < Box grow = "Yes" direction = "Column" > < Text size = "T300" priority = "300" > < b > { senderName } < / b > { ' enabled end-to-end encryption' } < / Text > < / Box > }
/ >
< / Event >
) ;
} ,
[ StateEvent . RoomJoinRules ] : ( mEventId , mEvent , item ) = > {
const highlighted = focusItem ? . index === item && focusItem . highlight ;
const senderId = mEvent . getSender ( ) ? ? '' ;
const senderName = getMemberDisplayName ( room , senderId ) || getMxIdLocalPart ( senderId ) ;
const joinRule = mEvent . getContent < { join_rule? : string } > ( ) . join_rule ? ? 'unknown' ;
const ruleLabel : Record < string , string > = { public : 'public' , invite : 'invite-only' , knock : 'knock' , restricted : 'restricted' } ;
const timeJSX = (
< Time ts = { mEvent . getTs ( ) } compact = { messageLayout === MessageLayout . Compact } hour24Clock = { hour24Clock } dateFormatString = { dateFormatString } / >
) ;
return (
< Event key = { mEvent . getId ( ) } data-message-item = { item } data-message-id = { mEventId } room = { room } mEvent = { mEvent } highlight = { highlighted } messageSpacing = { messageSpacing } canDelete = { canRedact || mEvent . getSender ( ) === mx . getUserId ( ) } hideReadReceipts = { hideActivity } showDeveloperTools = { showDeveloperTools } >
< EventContent messageLayout = { messageLayout } time = { timeJSX } iconSrc = { Icons . Settings }
content = { < Box grow = "Yes" direction = "Column" > < Text size = "T300" priority = "300" > < b > { senderName } < / b > { ` set room join rule to ${ ruleLabel [ joinRule ] ? ? joinRule } ` } < / Text > < / Box > }
/ >
< / Event >
) ;
} ,
[ StateEvent . RoomGuestAccess ] : ( mEventId , mEvent , item ) = > {
const highlighted = focusItem ? . index === item && focusItem . highlight ;
const senderId = mEvent . getSender ( ) ? ? '' ;
const senderName = getMemberDisplayName ( room , senderId ) || getMxIdLocalPart ( senderId ) ;
const access = mEvent . getContent < { guest_access? : string } > ( ) . guest_access ? ? 'unknown' ;
const timeJSX = (
< Time ts = { mEvent . getTs ( ) } compact = { messageLayout === MessageLayout . Compact } hour24Clock = { hour24Clock } dateFormatString = { dateFormatString } / >
) ;
return (
< Event key = { mEvent . getId ( ) } data-message-item = { item } data-message-id = { mEventId } room = { room } mEvent = { mEvent } highlight = { highlighted } messageSpacing = { messageSpacing } canDelete = { canRedact || mEvent . getSender ( ) === mx . getUserId ( ) } hideReadReceipts = { hideActivity } showDeveloperTools = { showDeveloperTools } >
< EventContent messageLayout = { messageLayout } time = { timeJSX } iconSrc = { Icons . Settings }
content = { < Box grow = "Yes" direction = "Column" > < Text size = "T300" priority = "300" > < b > { senderName } < / b > { access === 'can_join' ? ' allowed guest access' : ' disabled guest access' } < / Text > < / Box > }
/ >
< / Event >
) ;
} ,
[ StateEvent . RoomCanonicalAlias ] : ( mEventId , mEvent , item ) = > {
const highlighted = focusItem ? . index === item && focusItem . highlight ;
const senderId = mEvent . getSender ( ) ? ? '' ;
const senderName = getMemberDisplayName ( room , senderId ) || getMxIdLocalPart ( senderId ) ;
const alias = mEvent . getContent < { alias? : string } > ( ) . alias ;
const timeJSX = (
< Time ts = { mEvent . getTs ( ) } compact = { messageLayout === MessageLayout . Compact } hour24Clock = { hour24Clock } dateFormatString = { dateFormatString } / >
) ;
return (
< Event key = { mEvent . getId ( ) } data-message-item = { item } data-message-id = { mEventId } room = { room } mEvent = { mEvent } highlight = { highlighted } messageSpacing = { messageSpacing } canDelete = { canRedact || mEvent . getSender ( ) === mx . getUserId ( ) } hideReadReceipts = { hideActivity } showDeveloperTools = { showDeveloperTools } >
< EventContent messageLayout = { messageLayout } time = { timeJSX } iconSrc = { Icons . Hash }
content = { < Box grow = "Yes" direction = "Column" > < Text size = "T300" priority = "300" > < b > { senderName } < / b > { alias ? ` set room address to ${ alias } ` : ' removed room address' } < / Text > < / Box > }
/ >
< / Event >
) ;
} ,
2026-03-07 18:03:32 +11:00
[ StateEvent . GroupCallMemberPrefix ] : ( mEventId , mEvent , item ) = > {
const highlighted = focusItem ? . index === item && focusItem . highlight ;
const senderId = mEvent . getSender ( ) ? ? '' ;
const senderName = getMemberDisplayName ( room , senderId ) || getMxIdLocalPart ( senderId ) ;
2026-03-10 22:45:26 +11:00
const content = mEvent . getContent < SessionMembershipData > ( ) ;
const prevContent = mEvent . getPrevContent ( ) ;
const callJoined = content . application ;
if ( callJoined && 'application' in prevContent ) {
return null ;
}
2026-03-07 18:03:32 +11:00
const timeJSX = (
< Time
ts = { mEvent . getTs ( ) }
compact = { messageLayout === MessageLayout . Compact }
hour24Clock = { hour24Clock }
dateFormatString = { dateFormatString }
/ >
) ;
return (
< Event
key = { mEvent . getId ( ) }
data-message-item = { item }
data-message-id = { mEventId }
room = { room }
mEvent = { mEvent }
highlight = { highlighted }
messageSpacing = { messageSpacing }
canDelete = { canRedact || mEvent . getSender ( ) === mx . getUserId ( ) }
hideReadReceipts = { hideActivity }
showDeveloperTools = { showDeveloperTools }
>
< EventContent
messageLayout = { messageLayout }
time = { timeJSX }
iconSrc = { callJoined ? Icons.Phone : Icons.PhoneDown }
content = {
< Box grow = "Yes" direction = "Column" >
< Text size = "T300" priority = "300" >
< b > { senderName } < / b >
{ callJoined ? ' joined the call' : ' ended the call' }
< / Text >
< / Box >
}
/ >
< / Event >
) ;
} ,
2023-10-06 13:44:06 +11:00
} ,
2024-05-31 19:49:46 +05:30
( mEventId , mEvent , item ) = > {
2023-10-06 13:44:06 +11:00
if ( ! showHiddenEvents ) return null ;
2024-05-31 19:49:46 +05:30
const highlighted = focusItem ? . index === item && focusItem . highlight ;
2023-10-06 13:44:06 +11:00
const senderId = mEvent . getSender ( ) ? ? '' ;
const senderName = getMemberDisplayName ( room , senderId ) || getMxIdLocalPart ( senderId ) ;
2025-02-10 16:49:47 +11:00
const timeJSX = (
2025-07-27 15:13:00 +03:00
< Time
ts = { mEvent . getTs ( ) }
compact = { messageLayout === MessageLayout . Compact }
hour24Clock = { hour24Clock }
dateFormatString = { dateFormatString }
/ >
2025-02-10 16:49:47 +11:00
) ;
2023-10-06 13:44:06 +11:00
return (
< Event
key = { mEvent . getId ( ) }
data-message-item = { item }
2023-10-14 16:08:43 +11:00
data-message-id = { mEventId }
2023-10-06 13:44:06 +11:00
room = { room }
mEvent = { mEvent }
highlight = { highlighted }
messageSpacing = { messageSpacing }
canDelete = { canRedact || mEvent . getSender ( ) === mx . getUserId ( ) }
2025-02-26 21:44:53 +11:00
hideReadReceipts = { hideActivity }
2025-06-28 12:35:59 +02:00
showDeveloperTools = { showDeveloperTools }
2023-10-06 13:44:06 +11:00
>
< EventContent
messageLayout = { messageLayout }
time = { timeJSX }
iconSrc = { Icons . Code }
content = {
< Box grow = "Yes" direction = "Column" >
< Text size = "T300" priority = "300" >
< b > { senderName } < / b >
{ ' sent ' }
< code className = { customHtmlCss . Code } > { mEvent . getType ( ) } < / code >
{ ' state event' }
< / Text >
< / Box >
}
/ >
< / Event >
) ;
} ,
2024-05-31 19:49:46 +05:30
( mEventId , mEvent , item ) = > {
2023-10-06 13:44:06 +11:00
if ( ! showHiddenEvents ) return null ;
if ( Object . keys ( mEvent . getContent ( ) ) . length === 0 ) return null ;
if ( mEvent . getRelation ( ) ) return null ;
if ( mEvent . isRedaction ( ) ) return null ;
2024-05-31 19:49:46 +05:30
const highlighted = focusItem ? . index === item && focusItem . highlight ;
2023-10-06 13:44:06 +11:00
const senderId = mEvent . getSender ( ) ? ? '' ;
const senderName = getMemberDisplayName ( room , senderId ) || getMxIdLocalPart ( senderId ) ;
2025-02-10 16:49:47 +11:00
const timeJSX = (
2025-07-27 15:13:00 +03:00
< Time
ts = { mEvent . getTs ( ) }
compact = { messageLayout === MessageLayout . Compact }
hour24Clock = { hour24Clock }
dateFormatString = { dateFormatString }
/ >
2025-02-10 16:49:47 +11:00
) ;
2023-10-06 13:44:06 +11:00
return (
< Event
key = { mEvent . getId ( ) }
data-message-item = { item }
2023-10-14 16:08:43 +11:00
data-message-id = { mEventId }
2023-10-06 13:44:06 +11:00
room = { room }
mEvent = { mEvent }
highlight = { highlighted }
messageSpacing = { messageSpacing }
canDelete = { canRedact || mEvent . getSender ( ) === mx . getUserId ( ) }
2025-02-26 21:44:53 +11:00
hideReadReceipts = { hideActivity }
2025-06-28 12:35:59 +02:00
showDeveloperTools = { showDeveloperTools }
2023-10-06 13:44:06 +11:00
>
< EventContent
messageLayout = { messageLayout }
time = { timeJSX }
iconSrc = { Icons . Code }
content = {
< Box grow = "Yes" direction = "Column" >
< Text size = "T300" priority = "300" >
< b > { senderName } < / b >
{ ' sent ' }
< code className = { customHtmlCss . Code } > { mEvent . getType ( ) } < / code >
{ ' event' }
< / Text >
< / Box >
}
/ >
< / Event >
) ;
2024-05-31 19:49:46 +05:30
}
) ;
2023-10-06 13:44:06 +11:00
let prevEvent : MatrixEvent | undefined ;
let isPrevRendered = false ;
let newDivider = false ;
let dayDivider = false ;
const eventRenderer = ( item : number ) = > {
2026-05-20 21:54:33 -04:00
// Perf-5: O(T) → O(log T) via precomputed segments
let eventTimeline : EventTimeline | undefined ;
let baseIndex = 0 ;
{
let lo = 0 ;
let hi = timelineSegments . length - 1 ;
while ( lo <= hi ) {
const mid = ( lo + hi ) >>> 1 ;
const [ base , len ] = timelineSegments [ mid ] ;
if ( item < base ) { hi = mid - 1 ; }
else if ( item >= base + len ) { lo = mid + 1 ; }
else { eventTimeline = timelineSegments [ mid ] [ 2 ] ; baseIndex = base ; break ; }
}
}
2023-10-06 13:44:06 +11:00
if ( ! eventTimeline ) return null ;
const timelineSet = eventTimeline ? . getTimelineSet ( ) ;
const mEvent = getTimelineEvent ( eventTimeline , getTimelineRelativeIndex ( item , baseIndex ) ) ;
const mEventId = mEvent ? . getId ( ) ;
if ( ! mEvent || ! mEventId ) return null ;
2025-02-28 18:47:23 +11:00
const eventSender = mEvent . getSender ( ) ;
if ( eventSender && ignoredUsersSet . has ( eventSender ) ) {
return null ;
}
2025-03-01 18:48:11 +11:00
if ( mEvent . isRedacted ( ) && ! showHiddenEvents ) {
2026-05-15 00:47:21 -04:00
const t = mEvent . getType ( ) ;
if (
t !== MessageEvent . RoomMessage &&
t !== MessageEvent . RoomMessageEncrypted &&
t !== MessageEvent . Sticker
) {
return null ;
}
2025-03-01 18:48:11 +11:00
}
2025-02-28 18:47:23 +11:00
2023-10-06 13:44:06 +11:00
if ( ! newDivider && readUptoEventIdRef . current ) {
newDivider = prevEvent ? . getId ( ) === readUptoEventIdRef . current ;
}
if ( ! dayDivider ) {
dayDivider = prevEvent ? ! inSameDay ( prevEvent . getTs ( ) , mEvent . getTs ( ) ) : false ;
}
const collapsed =
2026-05-13 21:17:59 -04:00
! perMessageProfiles &&
2023-10-06 13:44:06 +11:00
isPrevRendered &&
! dayDivider &&
2025-02-28 18:47:23 +11:00
( ! newDivider || eventSender === mx . getUserId ( ) ) &&
2023-10-06 13:44:06 +11:00
prevEvent !== undefined &&
2025-02-28 18:47:23 +11:00
prevEvent . getSender ( ) === eventSender &&
2023-10-06 13:44:06 +11:00
prevEvent . getType ( ) === mEvent . getType ( ) &&
minuteDifference ( prevEvent . getTs ( ) , mEvent . getTs ( ) ) < 2 ;
2023-10-23 21:43:07 +11:00
const eventJSX = reactionOrEditEvent ( mEvent )
2023-10-06 13:44:06 +11:00
? null
2024-05-31 19:49:46 +05:30
: renderMatrixEvent (
2024-09-11 17:07:02 +10:00
mEvent . getType ( ) ,
typeof mEvent . getStateKey ( ) === 'string' ,
mEventId ,
mEvent ,
item ,
timelineSet ,
collapsed
) ;
2023-10-06 13:44:06 +11:00
prevEvent = mEvent ;
isPrevRendered = ! ! eventJSX ;
const newDividerJSX =
2025-02-28 18:47:23 +11:00
newDivider && eventJSX && eventSender !== mx . getUserId ( ) ? (
2023-10-06 13:44:06 +11:00
< MessageBase space = { messageSpacing } >
< TimelineDivider style = { { color : color.Success.Main } } variant = "Inherit" >
< Badge as = "span" size = "500" variant = "Success" fill = "Solid" radii = "300" >
< Text size = "L400" > New Messages < / Text >
< / Badge >
< / TimelineDivider >
< / MessageBase >
) : null ;
const dayDividerJSX =
dayDivider && eventJSX ? (
< MessageBase space = { messageSpacing } >
< TimelineDivider variant = "Surface" >
< Badge as = "span" size = "500" variant = "Secondary" fill = "None" radii = "300" >
< Text size = "L400" >
{ ( ( ) = > {
if ( today ( mEvent . getTs ( ) ) ) return 'Today' ;
if ( yesterday ( mEvent . getTs ( ) ) ) return 'Yesterday' ;
return timeDayMonthYear ( mEvent . getTs ( ) ) ;
} ) ( ) }
< / Text >
< / Badge >
< / TimelineDivider >
< / MessageBase >
) : null ;
if ( eventJSX && ( newDividerJSX || dayDividerJSX ) ) {
if ( newDividerJSX ) newDivider = false ;
if ( dayDividerJSX ) dayDivider = false ;
return (
< React.Fragment key = { mEventId } >
{ newDividerJSX }
{ dayDividerJSX }
{ eventJSX }
< / React.Fragment >
) ;
}
return eventJSX ;
} ;
return (
2026-05-15 18:56:17 -04:00
< ReadPositionsContext.Provider value = { readPositions } >
2024-05-31 19:49:46 +05:30
< Box grow = "Yes" style = { { position : 'relative' } } >
2023-10-06 13:44:06 +11:00
{ unreadInfo ? . readUptoEventId && ! unreadInfo ? . inLiveTimeline && (
< TimelineFloat position = "Top" >
< Chip
variant = "Primary"
radii = "Pill"
outlined
before = { < Icon size = "50" src = { Icons . MessageUnread } / > }
onClick = { handleJumpToUnread }
>
< Text size = "L400" > Jump to Unread < / Text >
< / Chip >
< Chip
variant = "SurfaceVariant"
radii = "Pill"
outlined
before = { < Icon size = "50" src = { Icons . CheckTwice } / > }
onClick = { handleMarkAsRead }
>
< Text size = "L400" > Mark as Read < / Text >
< / Chip >
< / TimelineFloat >
) }
< Scroll ref = { scrollRef } visibility = "Hover" >
< Box
direction = "Column"
justifyContent = "End"
style = { { minHeight : '100%' , padding : ` ${ config . space . S600 } 0 ` } }
>
{ ! canPaginateBack && rangeAtStart && getItems ( ) . length > 0 && (
< div
style = { {
2024-09-11 17:07:02 +10:00
padding : ` ${ config . space . S700 } ${ config . space . S400 } ${ config . space . S600 } ${
2025-02-10 16:49:47 +11:00
messageLayout === MessageLayout . Compact ? config.space.S400 : toRem ( 64 )
2024-09-11 17:07:02 +10:00
} ` ,
2023-10-06 13:44:06 +11:00
} }
>
< RoomIntro room = { room } / >
< / div >
) }
{ ( canPaginateBack || ! rangeAtStart ) &&
2025-02-10 16:49:47 +11:00
( messageLayout === MessageLayout . Compact ? (
2023-10-06 13:44:06 +11:00
< >
2024-12-16 21:55:15 +11:00
< MessageBase >
< CompactPlaceholder key = { getItems ( ) . length } / >
< / MessageBase >
< MessageBase >
< CompactPlaceholder key = { getItems ( ) . length } / >
< / MessageBase >
< MessageBase >
< CompactPlaceholder key = { getItems ( ) . length } / >
< / MessageBase >
< MessageBase >
< CompactPlaceholder key = { getItems ( ) . length } / >
< / MessageBase >
< MessageBase ref = { observeBackAnchor } >
< CompactPlaceholder key = { getItems ( ) . length } / >
< / MessageBase >
2023-10-06 13:44:06 +11:00
< / >
) : (
< >
2024-12-16 21:55:15 +11:00
< MessageBase >
< DefaultPlaceholder key = { getItems ( ) . length } / >
< / MessageBase >
< MessageBase >
< DefaultPlaceholder key = { getItems ( ) . length } / >
< / MessageBase >
< MessageBase ref = { observeBackAnchor } >
< DefaultPlaceholder key = { getItems ( ) . length } / >
< / MessageBase >
2023-10-06 13:44:06 +11:00
< / >
) ) }
{ getItems ( ) . map ( eventRenderer ) }
{ ( ! liveTimelineLinked || ! rangeAtEnd ) &&
2025-02-10 16:49:47 +11:00
( messageLayout === MessageLayout . Compact ? (
2023-10-06 13:44:06 +11:00
< >
2024-12-16 21:55:15 +11:00
< MessageBase ref = { observeFrontAnchor } >
< CompactPlaceholder key = { getItems ( ) . length } / >
< / MessageBase >
< MessageBase >
< CompactPlaceholder key = { getItems ( ) . length } / >
< / MessageBase >
< MessageBase >
< CompactPlaceholder key = { getItems ( ) . length } / >
< / MessageBase >
< MessageBase >
< CompactPlaceholder key = { getItems ( ) . length } / >
< / MessageBase >
< MessageBase >
< CompactPlaceholder key = { getItems ( ) . length } / >
< / MessageBase >
2023-10-06 13:44:06 +11:00
< / >
) : (
< >
2024-12-16 21:55:15 +11:00
< MessageBase ref = { observeFrontAnchor } >
< DefaultPlaceholder key = { getItems ( ) . length } / >
< / MessageBase >
< MessageBase >
< DefaultPlaceholder key = { getItems ( ) . length } / >
< / MessageBase >
< MessageBase >
< DefaultPlaceholder key = { getItems ( ) . length } / >
< / MessageBase >
2023-10-06 13:44:06 +11:00
< / >
) ) }
< span ref = { atBottomAnchorRef } / >
< / Box >
< / Scroll >
2023-10-19 17:40:01 +11:00
{ ! atBottom && (
2023-10-06 13:44:06 +11:00
< TimelineFloat position = "Bottom" >
< Chip
variant = "SurfaceVariant"
radii = "Pill"
outlined
before = { < Icon size = "50" src = { Icons . ArrowBottom } / > }
onClick = { handleJumpToLatest }
>
< Text size = "L400" > Jump to Latest < / Text >
< / Chip >
< / TimelineFloat >
) }
< / Box >
2026-05-15 18:56:17 -04:00
< / ReadPositionsContext.Provider >
2023-10-06 13:44:06 +11:00
) ;
}