feat(sessions): show a pending-question indicator on session rows
Adds a per-session pending-question badge to sidebar rows, driven by the live directory-store question state through a dedicated per-session subscription channel so unrelated streaming never re-renders rows. Collapsed parent rows roll up pending questions of hidden descendants from their owning directory stores without bootstrapping them. Question state is cloned on session delete/archive so badges clear when sessions disappear. Adds the questionChangeCallbacks sync performance counter, i18n keys for all locales, and unit tests for the subscription channel and scope selection. Fixes #2634
This commit is contained in:
@@ -14,8 +14,10 @@ export type DirectoryStore = State & {
|
||||
replace: (next: State) => void
|
||||
}
|
||||
|
||||
type PermissionSubscriber = () => void
|
||||
const permissionSubscribersByStore = new WeakMap<StoreApi<DirectoryStore>, Map<string, Set<PermissionSubscriber>>>()
|
||||
type BlockingRequestSubscriber = () => void
|
||||
type BlockingRequestSubscribers = WeakMap<StoreApi<DirectoryStore>, Map<string, Set<BlockingRequestSubscriber>>>
|
||||
const permissionSubscribersByStore: BlockingRequestSubscribers = new WeakMap()
|
||||
const questionSubscribersByStore: BlockingRequestSubscribers = new WeakMap()
|
||||
|
||||
type SessionMessageChange = {
|
||||
messagesChanged: boolean
|
||||
@@ -72,12 +74,42 @@ export function markDirectorySessionPartChanged(
|
||||
export function subscribeDirectoryPermission(
|
||||
store: StoreApi<DirectoryStore>,
|
||||
sessionID: string,
|
||||
listener: PermissionSubscriber,
|
||||
listener: BlockingRequestSubscriber,
|
||||
): () => void {
|
||||
let bySession = permissionSubscribersByStore.get(store)
|
||||
return subscribeBlockingRequest(permissionSubscribersByStore, store, sessionID, listener)
|
||||
}
|
||||
|
||||
export function subscribeDirectoryQuestion(
|
||||
store: StoreApi<DirectoryStore>,
|
||||
sessionID: string,
|
||||
listener: BlockingRequestSubscriber,
|
||||
): () => void {
|
||||
return subscribeBlockingRequest(questionSubscribersByStore, store, sessionID, listener)
|
||||
}
|
||||
|
||||
export function subscribeDirectoryQuestions(
|
||||
store: StoreApi<DirectoryStore>,
|
||||
sessionIDs: readonly string[],
|
||||
listener: BlockingRequestSubscriber,
|
||||
): () => void {
|
||||
const unsubscribers = [...new Set(sessionIDs.filter(Boolean))].map((sessionID) => (
|
||||
subscribeBlockingRequest(questionSubscribersByStore, store, sessionID, listener)
|
||||
))
|
||||
return () => {
|
||||
for (const unsubscribe of unsubscribers) unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeBlockingRequest(
|
||||
subscribersByStore: BlockingRequestSubscribers,
|
||||
store: StoreApi<DirectoryStore>,
|
||||
sessionID: string,
|
||||
listener: BlockingRequestSubscriber,
|
||||
): () => void {
|
||||
let bySession = subscribersByStore.get(store)
|
||||
if (!bySession) {
|
||||
bySession = new Map()
|
||||
permissionSubscribersByStore.set(store, bySession)
|
||||
subscribersByStore.set(store, bySession)
|
||||
}
|
||||
let listeners = bySession.get(sessionID)
|
||||
if (!listeners) {
|
||||
@@ -88,24 +120,28 @@ export function subscribeDirectoryPermission(
|
||||
return () => {
|
||||
listeners?.delete(listener)
|
||||
if (listeners?.size === 0) bySession?.delete(sessionID)
|
||||
if (bySession?.size === 0) permissionSubscribersByStore.delete(store)
|
||||
if (bySession?.size === 0) subscribersByStore.delete(store)
|
||||
}
|
||||
}
|
||||
|
||||
const notifyChangedPermissions = (
|
||||
const notifyChangedBlockingRequests = <T,>(
|
||||
subscribersByStore: BlockingRequestSubscribers,
|
||||
counter: "permissionChangeCallbacks" | "questionChangeCallbacks",
|
||||
store: StoreApi<DirectoryStore>,
|
||||
current: State["permission"],
|
||||
previous: State["permission"],
|
||||
current: Record<string, T>,
|
||||
previous: Record<string, T>,
|
||||
): void => {
|
||||
if (current === previous) return
|
||||
const subscribers = permissionSubscribersByStore.get(store)
|
||||
const subscribers = subscribersByStore.get(store)
|
||||
if (!subscribers || subscribers.size === 0) return
|
||||
const changedListeners = new Set<BlockingRequestSubscriber>()
|
||||
for (const [sessionID, listeners] of subscribers) {
|
||||
if (current[sessionID] === previous[sessionID]) continue
|
||||
for (const listener of listeners) {
|
||||
countSyncPerformance("permissionChangeCallbacks")
|
||||
listener()
|
||||
}
|
||||
for (const listener of listeners) changedListeners.add(listener)
|
||||
}
|
||||
for (const listener of changedListeners) {
|
||||
countSyncPerformance(counter)
|
||||
listener()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +275,8 @@ function createDirectoryStore(directory: string): StoreApi<DirectoryStore> {
|
||||
if (state.projectMeta !== prev.projectMeta) persistProjectMeta(directory, state.projectMeta)
|
||||
if (state.icon !== prev.icon) persistIcon(directory, state.icon)
|
||||
if (state.session !== prev.session) persistSessions(directory, state.session)
|
||||
notifyChangedPermissions(store, state.permission, prev.permission)
|
||||
notifyChangedBlockingRequests(permissionSubscribersByStore, "permissionChangeCallbacks", store, state.permission, prev.permission)
|
||||
notifyChangedBlockingRequests(questionSubscribersByStore, "questionChangeCallbacks", store, state.question, prev.question)
|
||||
notifyChangedSessionMessages(store, state, prev)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user