fix(state): correct invite re-notify, status clear sync, soundboard resync (DP2/DP3/DP5)
- Invites: seed the notify baseline with the invite count at mount instead of 0, so a warm reload (allInvitesAtom populates synchronously from cache while SYNCING) no longer re-fires the toast+sound for pre-existing invites. Only a genuine increase notifies. - Status: the cross-device sync effect gated on a truthy presence.status, so a remote CLEAR never reset the input or localStorage[STATUS_MSG_KEY] and usePresenceUpdater.readStatus() re-sent the stale status. Now mirror an empty status as a clear (skipping offline/invisible, which carries an empty status_msg by design). - Soundboard: useState initializers ran once and never recomputed when the room/rooms arg changed. Add a resync effect keyed on the arg while keeping the live state-event update path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -52,7 +52,7 @@ import { ModalWide } from '../../../styles/Modal.css';
|
|||||||
import { createUploadAtom, UploadSuccess } from '../../../state/upload';
|
import { createUploadAtom, UploadSuccess } from '../../../state/upload';
|
||||||
import { CompactUploadCardRenderer } from '../../../components/upload-card';
|
import { CompactUploadCardRenderer } from '../../../components/upload-card';
|
||||||
import { useCapabilities } from '../../../hooks/useCapabilities';
|
import { useCapabilities } from '../../../hooks/useCapabilities';
|
||||||
import { useUserPresence } from '../../../hooks/useUserPresence';
|
import { Presence, useUserPresence } from '../../../hooks/useUserPresence';
|
||||||
import { ProfileDecoration } from './ProfileDecoration';
|
import { ProfileDecoration } from './ProfileDecoration';
|
||||||
import { EmojiBoard } from '../../../components/emoji-board';
|
import { EmojiBoard } from '../../../components/emoji-board';
|
||||||
|
|
||||||
@@ -365,13 +365,26 @@ function ProfileStatus() {
|
|||||||
|
|
||||||
// Sync input when another device changes the status.
|
// Sync input when another device changes the status.
|
||||||
// Skipped while the user has unsaved local edits to avoid clobbering
|
// Skipped while the user has unsaved local edits to avoid clobbering
|
||||||
// mid-flight input (e.g. an emoji being inserted).
|
// mid-flight input (e.g. an emoji being inserted), and while presence data has
|
||||||
|
// not loaded yet (presence === undefined is "no info", NOT a clear).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!statusDirtyRef.current && presence?.status) {
|
if (statusDirtyRef.current || !presence) return;
|
||||||
setStatusMsg(presence.status);
|
// An offline/invisible presence carries an empty status_msg by design (see
|
||||||
localStorage.setItem(STATUS_MSG_KEY(userId), presence.status);
|
// usePresenceUpdater.setOffline), so ignore it — reading it as a clear would
|
||||||
|
// wipe the saved status on every invisible toggle.
|
||||||
|
if (presence.presence === Presence.Offline) return;
|
||||||
|
const remoteStatus = presence.status ?? '';
|
||||||
|
if (remoteStatus) {
|
||||||
|
setStatusMsg(remoteStatus);
|
||||||
|
localStorage.setItem(STATUS_MSG_KEY(userId), remoteStatus);
|
||||||
|
} else {
|
||||||
|
// Another device CLEARED the status. Mirror it locally AND drop the stored
|
||||||
|
// value, otherwise usePresenceUpdater.readStatus() re-sends the stale
|
||||||
|
// message on the next presence heartbeat.
|
||||||
|
setStatusMsg('');
|
||||||
|
localStorage.removeItem(STATUS_MSG_KEY(userId));
|
||||||
}
|
}
|
||||||
}, [presence?.status, userId]);
|
}, [presence, userId]);
|
||||||
|
|
||||||
const [saveState, saveStatus] = useAsyncCallback(
|
const [saveState, saveStatus] = useAsyncCallback(
|
||||||
useCallback(
|
useCallback(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Room } from 'matrix-js-sdk';
|
import { Room } from 'matrix-js-sdk';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { AccountDataEvent } from '../../types/matrix/accountData';
|
import { AccountDataEvent } from '../../types/matrix/accountData';
|
||||||
import { StateEvent } from '../../types/matrix/room';
|
import { StateEvent } from '../../types/matrix/room';
|
||||||
import {
|
import {
|
||||||
@@ -79,6 +79,13 @@ export const useRoomSoundboardPack = (room: Room, stateKey: string): SoundboardP
|
|||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const [roomPack, setRoomPack] = useState(() => getRoomSoundboardPack(room, stateKey));
|
const [roomPack, setRoomPack] = useState(() => getRoomSoundboardPack(room, stateKey));
|
||||||
|
|
||||||
|
// Resync when the room/stateKey arg changes — the useState initializer only
|
||||||
|
// runs on mount, so without this a re-target (e.g. a different room) kept
|
||||||
|
// showing the first room's pack.
|
||||||
|
useEffect(() => {
|
||||||
|
setRoomPack(getRoomSoundboardPack(room, stateKey));
|
||||||
|
}, [room, stateKey]);
|
||||||
|
|
||||||
useStateEventCallback(
|
useStateEventCallback(
|
||||||
mx,
|
mx,
|
||||||
useCallback(
|
useCallback(
|
||||||
@@ -102,6 +109,11 @@ export const useRoomSoundboardPacks = (room: Room): SoundboardPack[] => {
|
|||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const [roomPacks, setRoomPacks] = useState(() => getRoomSoundboardPacks(room));
|
const [roomPacks, setRoomPacks] = useState(() => getRoomSoundboardPacks(room));
|
||||||
|
|
||||||
|
// Resync on room change (see useRoomSoundboardPack).
|
||||||
|
useEffect(() => {
|
||||||
|
setRoomPacks(getRoomSoundboardPacks(room));
|
||||||
|
}, [room]);
|
||||||
|
|
||||||
useStateEventCallback(
|
useStateEventCallback(
|
||||||
mx,
|
mx,
|
||||||
useCallback(
|
useCallback(
|
||||||
@@ -124,6 +136,12 @@ export const useRoomsSoundboardPacks = (rooms: Room[]): SoundboardPack[] => {
|
|||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const [roomPacks, setRoomPacks] = useState(() => rooms.flatMap(getRoomSoundboardPacks));
|
const [roomPacks, setRoomPacks] = useState(() => rooms.flatMap(getRoomSoundboardPacks));
|
||||||
|
|
||||||
|
// Resync when the room list changes (callers pass a memoized array, so this
|
||||||
|
// fires only on an actual change — see useRoomSoundboardPack).
|
||||||
|
useEffect(() => {
|
||||||
|
setRoomPacks(rooms.flatMap(getRoomSoundboardPacks));
|
||||||
|
}, [rooms]);
|
||||||
|
|
||||||
useStateEventCallback(
|
useStateEventCallback(
|
||||||
mx,
|
mx,
|
||||||
useCallback(
|
useCallback(
|
||||||
|
|||||||
@@ -134,7 +134,12 @@ function FaviconUpdater() {
|
|||||||
function InviteNotifications() {
|
function InviteNotifications() {
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
const invites = useAtomValue(allInvitesAtom);
|
const invites = useAtomValue(allInvitesAtom);
|
||||||
const perviousInviteLen = usePreviousValue(invites.length, 0);
|
// Seed the baseline with the invite count present at mount, NOT 0. allInvitesAtom
|
||||||
|
// populates synchronously from cache on a warm reload while sync is already
|
||||||
|
// SYNCING, so a 0 seed made every pre-existing invite look "new" and re-fired the
|
||||||
|
// toast+sound on each reload. Starting from the current length means only a
|
||||||
|
// genuine INCREASE (a newly-arrived invite) notifies.
|
||||||
|
const perviousInviteLen = usePreviousValue(invites.length, invites.length);
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
Reference in New Issue
Block a user