diff --git a/.changeset/message-grouping.md b/.changeset/message-grouping.md new file mode 100644 index 000000000..c38516112 --- /dev/null +++ b/.changeset/message-grouping.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Add configurable message grouping time threshold setting. diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 92f82061b..6d056baa4 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -86,6 +86,24 @@ import { import { useTimelineEventRenderer } from '$hooks/timeline/useTimelineEventRenderer'; import * as css from './RoomTimeline.css'; +function findLastOwnEditableProcessedEvent( + events: ProcessedEvent[], + myUserId: string | null | undefined +): ProcessedEvent | undefined { + for (let i = events.length - 1; i >= 0; i -= 1) { + const event = events[i]; + if (!event) continue; + if ( + event.mEvent.getSender() === myUserId && + event.mEvent.getEffectiveEvent()?.type === 'm.room.message' && + !event.mEvent.isRedacted() + ) { + return event; + } + } + return undefined; +} + const TimelineFloat = as<'div', css.TimelineFloatVariants>( ({ position, className, ...props }, ref) => ( { const myUserId = mx.getUserId(); - const found = [...processedEventsRef.current] - .toReversed() - .find( - (e) => - e.mEvent.getSender() === myUserId && - e.mEvent.getType() === 'm.room.message' && - !e.mEvent.isRedacted() - ); + const found = findLastOwnEditableProcessedEvent(processedEventsRef.current, myUserId); if (found?.mEvent.getId()) actions.handleEdit(found.mEvent.getId()); }; }, [onEditLastMessageRef, mx, actions]); diff --git a/src/app/features/settings/experimental/Experimental.tsx b/src/app/features/settings/experimental/Experimental.tsx index 330412185..6ffded609 100644 --- a/src/app/features/settings/experimental/Experimental.tsx +++ b/src/app/features/settings/experimental/Experimental.tsx @@ -10,6 +10,7 @@ import { Sync } from '../general'; import { SettingsSectionPage } from '../SettingsSectionPage'; import { BandwidthSavingEmojis } from './BandwithSavingEmojis'; import { MSC4268HistoryShare } from './MSC4268HistoryShare'; +import { MessageGrouping } from './MessageGrouping'; function PersonaToggle() { const [showPersonaSetting, setShowPersonaSetting] = useSetting( @@ -59,6 +60,7 @@ export function Experimental({ requestBack, requestClose }: Readonly + diff --git a/src/app/features/settings/experimental/MessageGrouping.tsx b/src/app/features/settings/experimental/MessageGrouping.tsx new file mode 100644 index 000000000..95ce8139f --- /dev/null +++ b/src/app/features/settings/experimental/MessageGrouping.tsx @@ -0,0 +1,53 @@ +import { Box, Text, config } from 'folds'; +import { settingsAtom } from '$state/settings'; +import { useSetting } from '$state/hooks/settings'; +import { SequenceCardStyle } from '$features/common-settings/styles.css'; +import { SettingTile } from '$components/setting-tile'; +import { SequenceCard } from '$components/sequence-card'; + +const THRESHOLD_OPTIONS: { value: number; label: string }[] = [ + { value: 2, label: '2 min (default)' }, + { value: 5, label: '5 min' }, + { value: 15, label: '15 min (Discord-style)' }, + { value: 30, label: '30 min' }, + { value: 60, label: '60 min' }, +]; + +export function MessageGrouping() { + const [threshold, setThreshold] = useSetting(settingsAtom, 'messageGroupingThreshold'); + + return ( + + Message Grouping + + setThreshold(Number(e.target.value))} + style={{ + background: 'var(--bg-surface)', + color: 'var(--tc-surface-high)', + border: '1px solid var(--bg-surface-border)', + borderRadius: config.radii.R300, + padding: `${config.space.S100} ${config.space.S200}`, + fontSize: config.fontSize.T300, + cursor: 'pointer', + }} + > + {THRESHOLD_OPTIONS.map(({ value, label }) => ( + + ))} + + } + /> + + + ); +} diff --git a/src/app/hooks/timeline/useProcessedTimeline.ts b/src/app/hooks/timeline/useProcessedTimeline.ts index 9609dafc0..c517ba911 100644 --- a/src/app/hooks/timeline/useProcessedTimeline.ts +++ b/src/app/hooks/timeline/useProcessedTimeline.ts @@ -26,6 +26,12 @@ export interface UseProcessedTimelineOptions { * where every reply legitimately has `threadRootId` set to the root. */ skipThreadFilter?: boolean; + /** + * Minutes of inactivity before a new message from the same sender gets a + * full user header. Defaults to 2 (the original behaviour). Set higher + * (e.g. 15) for Discord-style compact grouping. + */ + messageGroupingThreshold?: number; } export interface ProcessedEvent { @@ -78,6 +84,7 @@ export function useProcessedTimeline({ isReadOnly, hideMemberInReadOnly, skipThreadFilter, + messageGroupingThreshold = 2, }: UseProcessedTimelineOptions): ProcessedEvent[] { return useMemo(() => { let prevEvent: MatrixEvent | undefined; @@ -157,7 +164,8 @@ export function useProcessedTimeline({ let collapsed = false; if (isPrevRendered && !dayDivider && prevEvent !== undefined) { if (isMessageEvent) { - const withinTimeThreshold = minuteDifference(prevEvent.getTs(), mEvent.getTs()) < 2; + const withinTimeThreshold = + minuteDifference(prevEvent.getTs(), mEvent.getTs()) < messageGroupingThreshold; const senderMatch = prevEvent.getSender() === eventSender; const typeMatch = normalizeMessageType(prevEvent.getType()) === normalizeMessageType(type); @@ -211,5 +219,6 @@ export function useProcessedTimeline({ isReadOnly, hideMemberInReadOnly, skipThreadFilter, + messageGroupingThreshold, ]); } diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index b1b744c1f..41562f40f 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -170,6 +170,9 @@ export interface Settings { vcmsgSidebarWidth: number; widgetSidebarWidth: number; + // experimental + messageGroupingThreshold: number; + // furry stuff renderAnimals: boolean; @@ -301,6 +304,9 @@ export const defaultSettings: Settings = { threadRootHeight: 220, vcmsgSidebarWidth: 399, widgetSidebarWidth: 420, + + // experimental + messageGroupingThreshold: 2, // furry stuff renderAnimals: true,