Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/message-grouping.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

Add configurable message grouping time threshold setting.
29 changes: 21 additions & 8 deletions src/app/features/room/RoomTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => (
<Box
Expand Down Expand Up @@ -161,6 +179,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()
Expand Down Expand Up @@ -784,6 +803,7 @@ export function RoomTimeline({
hideNickAvatarEvents,
isReadOnly,
hideMemberInReadOnly,
messageGroupingThreshold,
});

processedEventsRef.current = processedEvents;
Expand All @@ -805,14 +825,7 @@ export function RoomTimeline({
const ref = onEditLastMessageRef;
ref.current = () => {
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]);
Expand Down
2 changes: 2 additions & 0 deletions src/app/features/room/ThreadDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export function ThreadDrawer({ room, threadRootId, onClose, overlay }: ThreadDra
const [autoplayStickers] = useSetting(settingsAtom, 'autoplayStickers');
const [autoplayEmojis] = useSetting(settingsAtom, 'autoplayEmojis');
const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
const [messageGroupingThreshold] = useSetting(settingsAtom, 'messageGroupingThreshold');
const [showTombstoneEvents] = useSetting(settingsAtom, 'showTombstoneEvents');
const [hideMemberInReadOnly] = useSetting(settingsAtom, 'hideMembershipInReadOnly');
const [showBundledPreview] = useSetting(settingsAtom, 'bundledPreview');
Expand Down Expand Up @@ -262,6 +263,7 @@ export function ThreadDrawer({ room, threadRootId, onClose, overlay }: ThreadDra
hideNickAvatarEvents: true,
isReadOnly,
hideMemberInReadOnly,
messageGroupingThreshold,
});

// When the thread's own timeline is empty (server-side threads not yet fetched,
Expand Down
2 changes: 2 additions & 0 deletions src/app/features/settings/experimental/Experimental.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -59,6 +60,7 @@ export function Experimental({ requestBack, requestClose }: Readonly<Experimenta
<br />
<Box direction="Column" gap="700">
<Sync />
<MessageGrouping />
<MSC4268HistoryShare />
<BandwidthSavingEmojis />
<PersonaToggle />
Expand Down
54 changes: 54 additions & 0 deletions src/app/features/settings/experimental/MessageGrouping.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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: 0, label: 'Off (no grouping)' },
{ 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' },
];
Comment on lines +8 to +15

export function MessageGrouping() {
const [threshold, setThreshold] = useSetting(settingsAtom, 'messageGroupingThreshold');

return (
<Box direction="Column" gap="100">
<Text size="L400">Message Grouping</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Group consecutive messages"
focusId="message-grouping-threshold"
description="Hide the sender header when the same person sends multiple messages within the chosen time window. Longer windows mean more messages are grouped together."
after={
<select
id="message-grouping-threshold"
value={threshold}
onChange={(e) => 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 }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
}
/>
</SequenceCard>
</Box>
);
}
11 changes: 10 additions & 1 deletion src/app/hooks/timeline/useProcessedTimeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -78,6 +84,7 @@ export function useProcessedTimeline({
isReadOnly,
hideMemberInReadOnly,
skipThreadFilter,
messageGroupingThreshold,
}: UseProcessedTimelineOptions): ProcessedEvent[] {
return useMemo(() => {
let prevEvent: MatrixEvent | undefined;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -211,5 +219,6 @@ export function useProcessedTimeline({
isReadOnly,
hideMemberInReadOnly,
skipThreadFilter,
messageGroupingThreshold,
]);
}
6 changes: 6 additions & 0 deletions src/app/state/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ export interface Settings {
vcmsgSidebarWidth: number;
widgetSidebarWidth: number;

// experimental
messageGroupingThreshold: number;

Comment on lines +173 to +175
// furry stuff
renderAnimals: boolean;

Expand Down Expand Up @@ -301,6 +304,9 @@ export const defaultSettings: Settings = {
threadRootHeight: 220,
vcmsgSidebarWidth: 399,
widgetSidebarWidth: 420,

// experimental
messageGroupingThreshold: 2,
// furry stuff
renderAnimals: true,

Expand Down
Loading