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,