Skip to content
This repository was archived by the owner on Mar 9, 2026. It is now read-only.
2 changes: 1 addition & 1 deletion src/app/components/emoji-board/EmojiBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ export function EmojiBoard({
if (groups.length > 0) {
virtualizer.scrollToIndex(0, { align: 'start' });
}
}, [tab, virtualizer, groups]);
}, [tab, virtualizer, groups.length]);

return (
<FocusTrap
Expand Down
28 changes: 26 additions & 2 deletions src/app/features/common-settings/developer-tools/DevelopTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,23 @@ import { SequenceCardStyle } from '$features/common-settings/styles.css';
import { SendRoomEvent } from './SendRoomEvent';
import { StateEventEditor, StateEventInfo } from './StateEventEditor';

const formatSyncReason = (reason: string): string => {
if (reason === 'sliding_active') return 'Sliding Sync active';
if (reason === 'sliding_disabled_server') return 'Server-side sliding sync disabled';
if (reason === 'session_opt_out') return 'Session opt-in is off';
if (reason === 'missing_proxy') return 'Sliding proxy URL missing';
if (reason === 'cold_cache_bootstrap') return 'Cold-cache bootstrap (classic for this run)';
if (reason === 'probe_failed_fallback') return 'Sliding probe failed, using fallback';
return reason;
};

type DeveloperToolsProps = {
requestClose: () => void;
};
export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
const [developerTools, setDeveloperTools] = useSetting(settingsAtom, 'developerTools');
const mx = useMatrixClient();
const room = useRoom();

const roomState = useRoomState(room);
const accountData = useRoomAccountData(room);

Expand Down Expand Up @@ -124,7 +133,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
};
}, [mx, room, roomToUnread, allRooms, allInvites]);

const syncDiagnostics = useMemo(() => getClientSyncDiagnostics(mx), [mx]);
const syncDiagnostics = getClientSyncDiagnostics(mx);

const handleClose = useCallback(() => {
setOpenStateEvent(undefined);
Expand Down Expand Up @@ -412,6 +421,21 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
Sliding configured:{' '}
{syncDiagnostics.slidingConfigured ? 'yes' : 'no'}
</Text>
<Text size="T200">
Sliding server-enabled:{' '}
{syncDiagnostics.slidingEnabledOnServer ? 'yes' : 'no'}
</Text>
<Text size="T200">
Sliding session opt-in:{' '}
{syncDiagnostics.sessionOptIn ? 'yes' : 'no'}
</Text>
<Text size="T200">
Sliding requested:{' '}
{syncDiagnostics.slidingRequested ? 'yes' : 'no'}
</Text>
<Text size="T200">
Sync reason: {formatSyncReason(syncDiagnostics.reason)}
</Text>
<Text size="T200">
Client sync state: {syncDiagnostics.syncState ?? 'null'}
</Text>
Expand Down
15 changes: 6 additions & 9 deletions src/app/features/room/RoomInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -428,23 +428,20 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
if (replyDraft) {
content['m.relates_to'] = getReplyContent(replyDraft);
}
try {
await mx.sendMessage(roomId, content as any);
} catch (error) {
log.error('failed to send message', { roomId }, error);
return;
}

if (replyDraft) {
setReplyDraft(undefined);
}

resetEditor(editor);
resetEditorHistory(editor);

setInputKey((prev) => prev + 1);

sendTypingStatus(false);

try {
await mx.sendMessage(roomId, content as any);
} catch (error) {
log.error('failed to send message', { roomId }, error);
}
}, [mx, roomId, editor, replyDraft, sendTypingStatus, setReplyDraft, isMarkdown, commands]);

const handleKeyDown: KeyboardEventHandler = useCallback(
Expand Down
190 changes: 122 additions & 68 deletions src/app/features/room/RoomTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Fragment,
Dispatch,
MouseEventHandler,
ReactNode,
RefObject,
SetStateAction,
useCallback,
Expand All @@ -17,6 +18,7 @@ import {
EventTimeline,
EventTimelineSet,
EventTimelineSetHandlerMap,
EventStatus,
IContent,
IRoomTimelineData,
MatrixClient,
Expand Down Expand Up @@ -694,7 +696,7 @@ export function RoomTimeline({
// 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
if (atBottomRef.current) {
if (atBottomRef.current && atLiveEndRef.current) {
if (document.hasFocus() && (!unreadInfo || mEvt.getSender() === mx.getUserId())) {
// 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.
Expand Down Expand Up @@ -729,6 +731,24 @@ export function RoomTimeline({
)
);

useEffect(() => {
const handleLocalEchoUpdated: RoomEventHandlerMap[RoomEvent.LocalEchoUpdated] = (
_mEvent: MatrixEvent,
eventRoom: Room | undefined
) => {
if (eventRoom?.roomId !== room.roomId) return;
setTimeline((ct) => ({ ...ct }));
if (!unreadInfo) {
setUnreadInfo(getRoomUnreadInfo(room));
}
};

room.on(RoomEvent.LocalEchoUpdated, handleLocalEchoUpdated);
return () => {
room.removeListener(RoomEvent.LocalEchoUpdated, handleLocalEchoUpdated);
};
}, [room, unreadInfo, setTimeline, setUnreadInfo]);

const handleOpenEvent = useCallback(
async (
evtId: string,
Expand Down Expand Up @@ -1167,6 +1187,15 @@ export function RoomTimeline({
},
[mx, room]
);

const handleResend = useCallback(
(mEvent: MatrixEvent) => {
if (mEvent.getAssociatedStatus() !== EventStatus.NOT_SENT) return;
mx.resendEvent(mEvent, room).catch(() => undefined);
},
[mx, room]
);

const handleEdit = useCallback(
(editEvtId?: string) => {
if (editEvtId) {
Expand Down Expand Up @@ -1237,6 +1266,8 @@ export function RoomTimeline({
onReactionToggle={handleReactionToggle}
senderId={senderId}
senderDisplayName={senderDisplayName}
sendStatus={mEvent.getAssociatedStatus()}
onResend={handleResend}
onEditId={handleEdit}
activeReplyId={activeReplyId}
reply={
Expand Down Expand Up @@ -1322,6 +1353,8 @@ export function RoomTimeline({
senderId={senderId}
activeReplyId={activeReplyId}
senderDisplayName={senderDisplayName}
sendStatus={mEvent.getAssociatedStatus()}
onResend={handleResend}
reply={
replyEventId && (
<Reply
Expand Down Expand Up @@ -1438,6 +1471,8 @@ export function RoomTimeline({
senderId={senderId}
activeReplyId={activeReplyId}
senderDisplayName={senderDisplayName}
sendStatus={mEvent.getAssociatedStatus()}
onResend={handleResend}
reply={
replyEventId && (
<Reply
Expand Down Expand Up @@ -1768,6 +1803,7 @@ export function RoomTimeline({
let isPrevRendered = false;
let newDivider = false;
let dayDivider = false;
const timelineItems = getItems();
const eventRenderer = (item: number) => {
const [eventTimeline, baseIndex] = getTimelineAndBaseIndex(timeline.linkedTimelines, item);
if (!eventTimeline) return null;
Expand Down Expand Up @@ -1859,6 +1895,86 @@ export function RoomTimeline({
return eventJSX;
};

let backPaginationJSX: ReactNode | undefined;
if (canPaginateBack || !rangeAtStart) {
if (timelineItems.length === 0) {
backPaginationJSX =
messageLayout === MessageLayout.Compact ? (
<>
<MessageBase>
<CompactPlaceholder />
</MessageBase>
<MessageBase>
<CompactPlaceholder />
</MessageBase>
<MessageBase>
<CompactPlaceholder />
</MessageBase>
<MessageBase>
<CompactPlaceholder />
</MessageBase>
<MessageBase ref={observeBackAnchor}>
<CompactPlaceholder />
</MessageBase>
</>
) : (
<>
<MessageBase>
<DefaultPlaceholder />
</MessageBase>
<MessageBase>
<DefaultPlaceholder />
</MessageBase>
<MessageBase ref={observeBackAnchor}>
<DefaultPlaceholder />
</MessageBase>
</>
);
} else {
backPaginationJSX = <div ref={observeBackAnchor} style={{ height: 1 }} />;
}
}

let frontPaginationJSX: ReactNode | undefined;
if (!liveTimelineLinked || !rangeAtEnd) {
if (timelineItems.length === 0) {
frontPaginationJSX =
messageLayout === MessageLayout.Compact ? (
<>
<MessageBase ref={observeFrontAnchor}>
<CompactPlaceholder />
</MessageBase>
<MessageBase>
<CompactPlaceholder />
</MessageBase>
<MessageBase>
<CompactPlaceholder />
</MessageBase>
<MessageBase>
<CompactPlaceholder />
</MessageBase>
<MessageBase>
<CompactPlaceholder />
</MessageBase>
</>
) : (
<>
<MessageBase ref={observeFrontAnchor}>
<DefaultPlaceholder />
</MessageBase>
<MessageBase>
<DefaultPlaceholder />
</MessageBase>
<MessageBase>
<DefaultPlaceholder />
</MessageBase>
</>
);
} else {
frontPaginationJSX = <div ref={observeFrontAnchor} style={{ height: 1 }} />;
}
}

return (
<Box grow="Yes" style={{ position: 'relative' }}>
{unreadInfo?.readUptoEventId && !unreadInfo?.inLiveTimeline && (
Expand Down Expand Up @@ -1902,73 +2018,11 @@ export function RoomTimeline({
<RoomIntro room={room} />
</div>
)}
{(canPaginateBack || !rangeAtStart) &&
(messageLayout === MessageLayout.Compact ? (
<>
<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>
</>
) : (
<>
<MessageBase>
<DefaultPlaceholder key={getItems().length} />
</MessageBase>
<MessageBase>
<DefaultPlaceholder key={getItems().length} />
</MessageBase>
<MessageBase ref={observeBackAnchor}>
<DefaultPlaceholder key={getItems().length} />
</MessageBase>
</>
))}

{getItems().map(eventRenderer)}

{(!liveTimelineLinked || !rangeAtEnd) &&
(messageLayout === MessageLayout.Compact ? (
<>
<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>
</>
) : (
<>
<MessageBase ref={observeFrontAnchor}>
<DefaultPlaceholder key={getItems().length} />
</MessageBase>
<MessageBase>
<DefaultPlaceholder key={getItems().length} />
</MessageBase>
<MessageBase>
<DefaultPlaceholder key={getItems().length} />
</MessageBase>
</>
))}
{backPaginationJSX}

{timelineItems.map(eventRenderer)}

{frontPaginationJSX}
<span ref={atBottomAnchorRef} />
</Box>
</Scroll>
Expand Down
Loading
Loading