refactor(ui): reuse the store, media-query and deleted-tag rules the review found rebuilt, and name a loading pick
E32 review round 1 (F3–F6). - Channels' open-row store and the chat dock use lib/store's createStore instead of a hand-rolled listener set (the chat dock gets a test of its own). - The Plex backdrop reads prefers-reduced-motion through useMediaQuery; the query string is one constant (REDUCED_MOTION) for the hook and the carousel's one-off read. - A link-restored collection filter showed its raw rating_key in the picker while the titles loaded: usePlexCollectionTitles now says it is pending, the picker draws a placeholder (labelsPending, as the channel picker does) and so does the chip row. - The "deleted tag" rule was written twice with two i18n keys and had drifted: the channel manager showed NO chip for a pick while the tag list loaded, or ever after a failed load, while the pick still narrowed the table. One rule now (lib/tagPicks), one key (common.deletedTag); the manager's row shows every pick, as a placeholder while loading and as its id after a failure.
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
// A window can be opened from anywhere (a conversation's "pop out" button, or automatically
|
||||
// when a message arrives) and stays open across page navigation. The dock state (which chats
|
||||
// are open + their minimised state) is persisted per user so it survives a reload.
|
||||
import { useSyncExternalStore } from "react";
|
||||
import { LS } from "./storage";
|
||||
import { createStore } from "./store";
|
||||
|
||||
export interface DockChat {
|
||||
partnerId: number;
|
||||
@@ -16,9 +16,8 @@ export interface DockChat {
|
||||
type PartnerLike = { id: number; name: string; avatar_url: string | null };
|
||||
|
||||
const MAX_OPEN = 3; // keep the dock from overflowing the corner
|
||||
let chats: DockChat[] = [];
|
||||
const dock = createStore<DockChat[]>([]);
|
||||
let userId: number | null = null;
|
||||
const subs = new Set<() => void>();
|
||||
|
||||
function storageKey(): string | null {
|
||||
return userId != null ? LS.chatDock(userId) : null;
|
||||
@@ -29,16 +28,15 @@ function persist(): void {
|
||||
if (!k) return;
|
||||
try {
|
||||
// Don't persist the transient flash counter.
|
||||
localStorage.setItem(k, JSON.stringify(chats.map(({ flash, ...c }) => c)));
|
||||
localStorage.setItem(k, JSON.stringify(dock.get().map(({ flash, ...c }) => c)));
|
||||
} catch {
|
||||
/* ignore quota/serialization errors */
|
||||
}
|
||||
}
|
||||
|
||||
function emit(next: DockChat[]): void {
|
||||
chats = next;
|
||||
dock.set(next);
|
||||
persist();
|
||||
for (const s of subs) s();
|
||||
}
|
||||
|
||||
// Bind the dock to the signed-in user and restore their persisted windows (once per user).
|
||||
@@ -53,15 +51,17 @@ export function initDock(uid: number): void {
|
||||
} catch {
|
||||
restored = [];
|
||||
}
|
||||
chats = restored.slice(0, MAX_OPEN);
|
||||
for (const s of subs) s();
|
||||
dock.set(restored.slice(0, MAX_OPEN));
|
||||
}
|
||||
|
||||
export function openChat(partner: PartnerLike): void {
|
||||
const existing = chats.find((c) => c.partnerId === partner.id);
|
||||
const existing = dock.get().find((c) => c.partnerId === partner.id);
|
||||
if (existing) {
|
||||
// Re-opening focuses it: ensure expanded and move to the front.
|
||||
emit([{ ...existing, minimized: false }, ...chats.filter((c) => c.partnerId !== partner.id)]);
|
||||
emit([
|
||||
{ ...existing, minimized: false },
|
||||
...dock.get().filter((c) => c.partnerId !== partner.id),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
emit(
|
||||
@@ -73,7 +73,7 @@ export function openChat(partner: PartnerLike): void {
|
||||
minimized: false,
|
||||
flash: 0,
|
||||
},
|
||||
...chats,
|
||||
...dock.get(),
|
||||
].slice(0, MAX_OPEN),
|
||||
);
|
||||
}
|
||||
@@ -81,7 +81,7 @@ export function openChat(partner: PartnerLike): void {
|
||||
// An incoming message arrived from `partner`: open the window if it's closed, otherwise just
|
||||
// flash it (whether expanded or minimised — don't disturb the minimised state).
|
||||
export function notifyIncoming(partner: PartnerLike): void {
|
||||
const existing = chats.find((c) => c.partnerId === partner.id);
|
||||
const existing = dock.get().find((c) => c.partnerId === partner.id);
|
||||
if (!existing) {
|
||||
emit(
|
||||
[
|
||||
@@ -92,29 +92,22 @@ export function notifyIncoming(partner: PartnerLike): void {
|
||||
minimized: false,
|
||||
flash: 1,
|
||||
},
|
||||
...chats,
|
||||
...dock.get(),
|
||||
].slice(0, MAX_OPEN),
|
||||
);
|
||||
return;
|
||||
}
|
||||
emit(chats.map((c) => (c.partnerId === partner.id ? { ...c, flash: c.flash + 1 } : c)));
|
||||
emit(dock.get().map((c) => (c.partnerId === partner.id ? { ...c, flash: c.flash + 1 } : c)));
|
||||
}
|
||||
|
||||
export function closeChat(partnerId: number): void {
|
||||
emit(chats.filter((c) => c.partnerId !== partnerId));
|
||||
emit(dock.get().filter((c) => c.partnerId !== partnerId));
|
||||
}
|
||||
|
||||
export function toggleMinimize(partnerId: number): void {
|
||||
emit(chats.map((c) => (c.partnerId === partnerId ? { ...c, minimized: !c.minimized } : c)));
|
||||
emit(dock.get().map((c) => (c.partnerId === partnerId ? { ...c, minimized: !c.minimized } : c)));
|
||||
}
|
||||
|
||||
export function useDockChats(): DockChat[] {
|
||||
return useSyncExternalStore(
|
||||
(cb) => {
|
||||
subs.add(cb);
|
||||
return () => subs.delete(cb);
|
||||
},
|
||||
() => chats,
|
||||
() => chats,
|
||||
);
|
||||
return dock.useValue();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user