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
7 changes: 2 additions & 5 deletions src/app/features/room/message/Reactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Room } from 'matrix-js-sdk';
import { type Relations } from 'matrix-js-sdk/lib/models/relations';
import FocusTrap from 'focus-trap-react';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { factoryEventSentBy } from '../../../utils/matrix';
import { factoryEventSentBy, getReactionsByFirstOccurrence } from '../../../utils/matrix';
import { Reaction, ReactionTooltipMsg } from '../../../components/message';
import { useRelations } from '../../../hooks/useRelations';
import * as css from './styles.css';
Expand All @@ -37,10 +37,7 @@ export const Reactions = as<'div', ReactionsProps>(
const useAuthentication = useMediaAuthentication();
const [viewer, setViewer] = useState<boolean | string>(false);
const myUserId = mx.getUserId();
const reactions = useRelations(
relations,
useCallback((rel) => [...(rel.getSortedAnnotationsByKey() ?? [])], [])
);
const reactions = useRelations(relations, useCallback(getReactionsByFirstOccurrence, []));

const handleViewReaction: MouseEventHandler<HTMLButtonElement> = (evt) => {
evt.stopPropagation();
Expand Down
11 changes: 6 additions & 5 deletions src/app/features/room/reaction-viewer/ReactionViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import {
import { MatrixEvent, Room, RoomMember } from 'matrix-js-sdk';
import { Relations } from 'matrix-js-sdk/lib/models/relations';
import { getMemberDisplayName } from '../../../utils/room';
import { eventWithShortcode, getMxIdLocalPart } from '../../../utils/matrix';
import {
eventWithShortcode,
getMxIdLocalPart,
getReactionsByFirstOccurrence,
} from '../../../utils/matrix';
import * as css from './ReactionViewer.css';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useRelations } from '../../../hooks/useRelations';
Expand All @@ -39,10 +43,7 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
({ className, room, initialKey, relations, requestClose, ...props }, ref) => {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const reactions = useRelations(
relations,
useCallback((rel) => [...(rel.getSortedAnnotationsByKey() ?? [])], [])
);
const reactions = useRelations(relations, useCallback(getReactionsByFirstOccurrence, []));
const space = useSpaceOptionally();
const openProfile = useOpenUserRoomProfile();

Expand Down
19 changes: 19 additions & 0 deletions src/app/utils/matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
UploadResponse,
} from 'matrix-js-sdk';
import to from 'await-to-js';
import { type Relations } from 'matrix-js-sdk/lib/models/relations';
import { IImageInfo, IThumbnailContent, IVideoInfo } from '../../types/matrix/common';
import { AccountDataEvent } from '../../types/matrix/accountData';
import { getStateEvent } from './room';
Expand Down Expand Up @@ -173,6 +174,24 @@ export const uploadContent = async (

export const matrixEventByRecency = (m1: MatrixEvent, m2: MatrixEvent) => m2.getTs() - m1.getTs();

/**
* matrix-js-sdk's `Relations.getSortedAnnotationsByKey` re-sorts reaction
* groups by descending event count on every add/remove, so a pill's position
* shifts whenever its count changes. This instead orders groups by the
* timestamp of their first reaction, so a reaction keeps its position once
* it first appears regardless of later count changes.
*/
export const getReactionsByFirstOccurrence = (
relations: Relations
): [string | null, Set<MatrixEvent>][] => {
const annotations = [...(relations.getSortedAnnotationsByKey() ?? [])];
return annotations.sort((a, b) => {
const aFirstTs = Math.min(...Array.from(a[1], (ev) => ev.getTs()));
const bFirstTs = Math.min(...Array.from(b[1], (ev) => ev.getTs()));
return aFirstTs - bFirstTs;
});
};

export const factoryEventSentBy = (senderId: string) => (ev: MatrixEvent) =>
ev.getSender() === senderId;

Expand Down
Loading