107 lines
4.6 KiB
TypeScript
107 lines
4.6 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { EventStatus, MatrixEvent, Room, RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk';
|
|
import { color } from 'folds';
|
|
import { cssColorMXID } from '../../util/colorMXID';
|
|
import { useMatrixClient } from './useMatrixClient';
|
|
import { MessageDeliveryStatus, useMessageStatus } from './useMessageStatus';
|
|
|
|
export type DotColor = { color: string; opacity: number };
|
|
|
|
// Variant A + C decision tree (docs/plans/dm_1x1_redesign.md §6.5b):
|
|
// * mention-highlight (push-actions tweaks.highlight) → Warning gold, override
|
|
// * own NOT_SENT / CANCELLED → Critical, full
|
|
// * own otherwise → Primary, dim → full when peer reads
|
|
// * incoming → cssColorMXID hash, full → dim once we read
|
|
//
|
|
// Mention detection goes through the canonical `mx.getPushActionsForEvent`
|
|
// path (m.mentions, display-name match, configured keywords, room-mentions via
|
|
// power-level) — same as element-web. Don't roll our own mentionsMe() helper.
|
|
//
|
|
// `replacingEvent()` is consulted so a non-mention edit of a mention drops the
|
|
// gold dot and a mention added via edit lights one up.
|
|
//
|
|
// Read-state for incoming messages is reactive: we subscribe to RoomEvent.Receipt
|
|
// so the opacity flips when our own read receipt lands (e.g. another tab marks
|
|
// the room read). For own messages, useMessageStatus already wires this up via
|
|
// its internal effect.
|
|
//
|
|
// `hideReadReceipts` mirrors the legacy MessageStatus checkmark suppression:
|
|
// when the user opted into «Hide Typing & Read Receipts» we MUST NOT reveal
|
|
// peer-read state via the dot either, otherwise the privacy guarantee is
|
|
// asymmetric. With the flag on, own messages stop dimming once they leave the
|
|
// «sending» state — Sent and Read both render at full opacity.
|
|
export function useDotColor(
|
|
room: Room,
|
|
mEvent: MatrixEvent,
|
|
enabled = true,
|
|
hideReadReceipts = false
|
|
): DotColor {
|
|
const mx = useMatrixClient();
|
|
const myUserId = mx.getUserId() ?? '';
|
|
const senderId = mEvent.getSender() ?? '';
|
|
const isOwn = !!myUserId && senderId === myUserId;
|
|
const eventId = mEvent.getId();
|
|
|
|
const status = useMessageStatus(room, mEvent);
|
|
|
|
const [haveSeen, setHaveSeen] = useState<boolean>(() =>
|
|
enabled && !isOwn && !!eventId && !!myUserId ? room.hasUserReadEvent(myUserId, eventId) : false
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!enabled) return undefined;
|
|
if (isOwn || !eventId || !myUserId) return undefined;
|
|
setHaveSeen(room.hasUserReadEvent(myUserId, eventId));
|
|
const handler: RoomEventHandlerMap[RoomEvent.Receipt] = (_event, r) => {
|
|
if (r.roomId !== room.roomId) return;
|
|
setHaveSeen(room.hasUserReadEvent(myUserId, eventId));
|
|
};
|
|
room.on(RoomEvent.Receipt, handler);
|
|
return () => {
|
|
room.removeListener(RoomEvent.Receipt, handler);
|
|
};
|
|
}, [room, myUserId, eventId, isOwn, enabled]);
|
|
|
|
if (!enabled) {
|
|
return { color: color.Primary.Main, opacity: 1.0 };
|
|
}
|
|
|
|
const pushActions = mx.getPushActionsForEvent(mEvent.replacingEvent() ?? mEvent);
|
|
if (pushActions?.tweaks?.highlight) {
|
|
return { color: color.Warning.Main, opacity: 1.0 };
|
|
}
|
|
|
|
if (isOwn) {
|
|
if (mEvent.status === EventStatus.NOT_SENT || mEvent.status === EventStatus.CANCELLED) {
|
|
return { color: color.Critical.Main, opacity: 1.0 };
|
|
}
|
|
// Sending state always dims, regardless of hideReadReceipts — the in-flight
|
|
// signal is local to the sender and reveals nothing about the peer.
|
|
if (status === MessageDeliveryStatus.Sending) {
|
|
return { color: color.Primary.Main, opacity: 0.3 };
|
|
}
|
|
if (hideReadReceipts) {
|
|
// Privacy mode: don't differentiate Sent vs Read — both render full,
|
|
// matching the single-checkmark behaviour of MessageStatus.tsx when
|
|
// the same flag is on.
|
|
return { color: color.Primary.Main, opacity: 1.0 };
|
|
}
|
|
return {
|
|
color: color.Primary.Main,
|
|
// Stronger opacity contrast than the original 0.4: at 0.3 the dot still
|
|
// reads as Fleet-violet (own author colour) but is clearly less alive
|
|
// than the 1.0 read state. Author colour is preserved on purpose — the
|
|
// user explicitly asked NOT to grey-out / desaturate read dots, only to
|
|
// dim them through opacity.
|
|
opacity: status === MessageDeliveryStatus.Read ? 1.0 : 0.3,
|
|
};
|
|
}
|
|
|
|
return {
|
|
color: `var(${cssColorMXID(senderId)})`,
|
|
// Same logic for incoming dots: keep the author hash colour visible at
|
|
// all times so group-DM authorship stays scannable; only modulate the
|
|
// dot's brightness via opacity to mark «I've seen it» (0.3) vs fresh (1.0).
|
|
opacity: haveSeen ? 0.3 : 1.0,
|
|
};
|
|
}
|