diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index d63faa989..3871c10bf 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -161,6 +161,7 @@ export function RoomTimeline({ ); const [incomingInlineImagesMaxHeight] = useSetting(settingsAtom, 'incomingInlineImagesMaxHeight'); const [hideMemberInReadOnly] = useSetting(settingsAtom, 'hideMembershipInReadOnly'); + const [messageGroupingThreshold] = useSetting(settingsAtom, 'messageGroupingThreshold'); const showUrlPreview = room.hasEncryptionStateEvent() ? encUrlPreview : urlPreview; const showClientUrlPreview = room.hasEncryptionStateEvent() @@ -784,6 +785,7 @@ export function RoomTimeline({ hideNickAvatarEvents, isReadOnly, hideMemberInReadOnly, + messageGroupingThreshold, }); processedEventsRef.current = processedEvents; 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 c492e6ff6..15fdc310c 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -168,6 +168,9 @@ export interface Settings { vcmsgSidebarWidth: number; widgetSidebarWidth: number; + // experimental + messageGroupingThreshold: number; + // furry stuff renderAnimals: boolean; @@ -297,6 +300,9 @@ export const defaultSettings: Settings = { threadRootHeight: 220, vcmsgSidebarWidth: 399, widgetSidebarWidth: 420, + + // experimental + messageGroupingThreshold: 2, // furry stuff renderAnimals: true,