From e4cc54a1b58db4d202b4f57dc6025b033f88a2b9 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Sat, 25 Apr 2026 01:40:22 +0200 Subject: [PATCH] revert(orbit): drop maxPending cap feature, keep suggestion mute The pending counter desynced from the actual approval list (state.queue holds approved items as history, so the count never decreased after a host approve). The host-pushed pendingApprovalCount workaround didn't hold up under live testing either, so we're rolling the whole cap feature back rather than ship something flaky. What's gone: - OrbitSettings.maxPending + state.pendingApprovalCount - cap branch in applyOutboxSnapshotsToState (now back to mute-only) - maxPending number input in settings popover - pending counter chip in OrbitQueueHead - 'cap-reached' branch in evaluateOrbitSuggestGate / OrbitSuggestGateReason - cap-related toasts in ContextMenu / useOrbitSongRowBehavior - cap-related i18n keys (suggestBlockedCap, settingMaxPending*, pendingCounter*) - cap CSS (.orbit-queue-head__pending, .orbit-settings-pop__number) What stays: per-guest suggestion mute (works correctly) and everything that fed into both features (OrbitState.suggestionBlocked, setOrbitSuggestionBlocked, evaluateOrbitSuggestGate, the participants popover Mic/MicOff toggle, the suggestBlockedMuted toast). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/api/orbit.ts | 17 ---------- src/components/ContextMenu.tsx | 34 +++++-------------- src/components/OrbitQueueHead.tsx | 20 +---------- src/components/OrbitSettingsPopover.tsx | 21 ------------ src/hooks/useOrbitHost.ts | 20 +---------- src/hooks/useOrbitSongRowBehavior.ts | 6 ---- src/locales/de.ts | 5 --- src/locales/en.ts | 5 --- src/locales/es.ts | 5 --- src/locales/fr.ts | 5 --- src/locales/nb.ts | 5 --- src/locales/nl.ts | 5 --- src/locales/ru.ts | 5 --- src/locales/zh.ts | 5 --- src/styles/components.css | 36 -------------------- src/utils/orbit.ts | 44 +++---------------------- 16 files changed, 14 insertions(+), 224 deletions(-) diff --git a/src/api/orbit.ts b/src/api/orbit.ts index 45eb3079..f98612ec 100644 --- a/src/api/orbit.ts +++ b/src/api/orbit.ts @@ -106,14 +106,6 @@ export interface OrbitState { * before they reach the approval list. Symmetric — host can re-enable. */ suggestionBlocked?: string[]; - /** - * Authoritative count of suggestions actually waiting on host action right - * now (`state.queue` minus host-authored, merged, declined). The host - * rewrites it every tick — guests can't compute it themselves because the - * merged/declined sets live in the host's local store. Older clients that - * don't write the field fall back to a `state.queue` count in the UI. - */ - pendingApprovalCount?: number; } /** @@ -135,13 +127,6 @@ export interface OrbitSettings { * field fall back to 15 via `effectiveShuffleIntervalMs`. */ shuffleIntervalMin?: OrbitShuffleIntervalMin; - /** - * Cap on simultaneously-pending guest suggestions. 0 = unlimited. - * When the cap is reached, the host's outbox sweep drops new suggestions - * until existing ones are approved or declined; the guest UI surfaces - * the count so users know to wait. - */ - maxPending?: number; } export const ORBIT_DEFAULT_SETTINGS: OrbitSettings = { @@ -149,8 +134,6 @@ export const ORBIT_DEFAULT_SETTINGS: OrbitSettings = { autoApprove: false, autoShuffle: true, shuffleIntervalMin: 15, - // 0 = unlimited; host opts in via the session-settings popover. - maxPending: 0, }; /** What the guest's outbox-playlist comment holds (heartbeat only, for now). */ diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index a8aae593..af6cf655 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -1452,27 +1452,18 @@ export default function ContextMenu() { {t('contextMenu.addToQueue')} {orbitRole === 'guest' && (() => { - const gate = evaluateOrbitSuggestGate(); - const muted = gate.reason === 'muted'; - const capReached = gate.reason === 'cap-reached'; - const disabled = muted || capReached; - const tooltip = muted - ? t('orbit.suggestBlockedMuted') - : capReached ? t('orbit.suggestBlockedCap') : ''; + const muted = evaluateOrbitSuggestGate().reason === 'muted'; return (
handleAction(() => { - if (muted) { showToast(t('orbit.suggestBlockedMuted'), 3500, 'error'); return; } - if (capReached) { showToast(t('orbit.suggestBlockedCap'), 3500, 'info'); return; } + if (muted) { showToast(t('orbit.suggestBlockedMuted'), 3500, 'error'); return; } suggestOrbitTrack(song.id) .then(() => showToast(t('orbit.ctxSuggestedToast'), 2200, 'info')) .catch(err => { if (err instanceof OrbitSuggestBlockedError && err.reason === 'muted') { showToast(t('orbit.suggestBlockedMuted'), 3500, 'error'); - } else if (err instanceof OrbitSuggestBlockedError && err.reason === 'cap-reached') { - showToast(t('orbit.suggestBlockedCap'), 3500, 'info'); } else { showToast(t('orbit.ctxSuggestFailed'), 3000, 'error'); } @@ -1625,27 +1616,18 @@ export default function ContextMenu() { {t('contextMenu.addToQueue')}
{orbitRole === 'guest' && (() => { - const gate = evaluateOrbitSuggestGate(); - const muted = gate.reason === 'muted'; - const capReached = gate.reason === 'cap-reached'; - const disabled = muted || capReached; - const tooltip = muted - ? t('orbit.suggestBlockedMuted') - : capReached ? t('orbit.suggestBlockedCap') : ''; + const muted = evaluateOrbitSuggestGate().reason === 'muted'; return (
handleAction(() => { - if (muted) { showToast(t('orbit.suggestBlockedMuted'), 3500, 'error'); return; } - if (capReached) { showToast(t('orbit.suggestBlockedCap'), 3500, 'info'); return; } + if (muted) { showToast(t('orbit.suggestBlockedMuted'), 3500, 'error'); return; } suggestOrbitTrack(song.id) .then(() => showToast(t('orbit.ctxSuggestedToast'), 2200, 'info')) .catch(err => { if (err instanceof OrbitSuggestBlockedError && err.reason === 'muted') { showToast(t('orbit.suggestBlockedMuted'), 3500, 'error'); - } else if (err instanceof OrbitSuggestBlockedError && err.reason === 'cap-reached') { - showToast(t('orbit.suggestBlockedCap'), 3500, 'info'); } else { showToast(t('orbit.ctxSuggestFailed'), 3000, 'error'); } diff --git a/src/components/OrbitQueueHead.tsx b/src/components/OrbitQueueHead.tsx index 2229832c..da8f6a85 100644 --- a/src/components/OrbitQueueHead.tsx +++ b/src/components/OrbitQueueHead.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Users, Wifi, WifiOff, Inbox } from 'lucide-react'; +import { Users, Wifi, WifiOff } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useOrbitStore } from '../store/orbitStore'; import type { OrbitState } from '../api/orbit'; @@ -36,15 +36,6 @@ export default function OrbitQueueHead({ state }: Props) { const names = [state.host, ...state.participants.map(p => p.user)]; const showPresence = role === 'guest' && state.positionAt > 0; const hostAway = showPresence && (nowMs - state.positionAt) > HOST_AWAY_THRESHOLD_MS; - const cap = state.settings?.maxPending ?? 0; - // Authoritative count comes from the host's own tick (state.pendingApprovalCount). - // Older clients that predate that field fall back to the raw queue length - // — that one over-counts because merged/declined items stay in state.queue - // as history, but it's the best a non-host client can do. - const pendingCount = cap > 0 - ? (state.pendingApprovalCount - ?? state.queue.filter(q => q.addedBy !== state.host).length) - : 0; return (
@@ -63,15 +54,6 @@ export default function OrbitQueueHead({ state }: Props) {
{names.join(', ')} - {cap > 0 && ( - = cap ? ' is-full' : ''}`} - data-tooltip={t('orbit.pendingCounterTooltip')} - > - - {t('orbit.pendingCounter', { count: pendingCount, max: cap })} - - )}
); diff --git a/src/components/OrbitSettingsPopover.tsx b/src/components/OrbitSettingsPopover.tsx index 9f00f321..2070052f 100644 --- a/src/components/OrbitSettingsPopover.tsx +++ b/src/components/OrbitSettingsPopover.tsx @@ -82,27 +82,6 @@ export default function OrbitSettingsPopover({ anchorRef, onClose }: Props) { - -
{t('orbit.settingShuffleInterval')}
diff --git a/src/hooks/useOrbitHost.ts b/src/hooks/useOrbitHost.ts index 5a97a613..4a2c0daa 100644 --- a/src/hooks/useOrbitHost.ts +++ b/src/hooks/useOrbitHost.ts @@ -87,12 +87,7 @@ export function useOrbitHost(): void { let afterSweep = base; try { const snaps = await sweepGuestOutboxes(base.sid, base.host); - // Hand the cap-check the host's local merged/declined sets so it can - // tell which `state.queue` items are still actually awaiting approval. - afterSweep = applyOutboxSnapshotsToState(base, snaps, Date.now(), { - mergedKeys: new Set(store.mergedSuggestionKeys), - declinedKeys: new Set(store.declinedSuggestionKeys), - }); + afterSweep = applyOutboxSnapshotsToState(base, snaps); } catch { /* best-effort; keep old participants and queue */ } // 2) Merge newly-suggested items into the host's local play queue so @@ -130,24 +125,11 @@ export function useOrbitHost(): void { trackId: t.id, addedBy: suggesterByTrack.get(t.id) ?? base.host, })); - // Authoritative pending count — same predicate the approval list uses - // (state.queue minus host-authored, minus merged, minus declined). We - // recompute fresh each tick from the *current* store sets so an approve - // / decline that happened mid-sweep is reflected on the very next push. - const mergedNow = new Set(useOrbitStore.getState().mergedSuggestionKeys); - const declinedNow = new Set(useOrbitStore.getState().declinedSuggestionKeys); - const pendingApprovalCount = afterShuffle.queue.filter(q => - q.addedBy !== afterShuffle.host - && !mergedNow.has(suggestionKey(q)) - && !declinedNow.has(suggestionKey(q)) - ).length; - const next: OrbitState = { ...afterShuffle, ...snapshotPlayerPatch(base.host), playQueue, playQueueTotal: upcoming.length, - pendingApprovalCount, }; // 5) Commit locally + push remote. diff --git a/src/hooks/useOrbitSongRowBehavior.ts b/src/hooks/useOrbitSongRowBehavior.ts index df6cb26a..c439feb1 100644 --- a/src/hooks/useOrbitSongRowBehavior.ts +++ b/src/hooks/useOrbitSongRowBehavior.ts @@ -53,17 +53,11 @@ export function useOrbitSongRowBehavior() { showToast(t('orbit.suggestBlockedMuted'), 3500, 'error'); return; } - if (!gate.allowed && gate.reason === 'cap-reached') { - showToast(t('orbit.suggestBlockedCap'), 3500, 'info'); - return; - } suggestOrbitTrack(songId) .then(() => showToast(t('orbit.ctxSuggestedToast'), 2200, 'info')) .catch(err => { if (err instanceof OrbitSuggestBlockedError && err.reason === 'muted') { showToast(t('orbit.suggestBlockedMuted'), 3500, 'error'); - } else if (err instanceof OrbitSuggestBlockedError && err.reason === 'cap-reached') { - showToast(t('orbit.suggestBlockedCap'), 3500, 'info'); } else { showToast(t('orbit.ctxSuggestFailed'), 3000, 'error'); } diff --git a/src/locales/de.ts b/src/locales/de.ts index 368c2e4b..075e22dc 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -1609,12 +1609,7 @@ export const deTranslation = { settingAutoShuffleHint: 'Periodischer Fisher-Yates-Shuffle der kommenden Warteschlange. Aus: Reihenfolge wird nur verändert, wenn du selbst sortierst.', settingShuffleInterval: 'Alle', settingShuffleIntervalHint: 'Wie oft die kommende Warteschlange während der Session neu gemischt wird.', - settingMaxPending: 'Max offene Vorschläge', - settingMaxPendingHint: 'Begrenzt, wie viele Gast-Vorschläge gleichzeitig auf Bestätigung warten dürfen. 0 = unbegrenzt.', - pendingCounter: '{{count}} / {{max}} offen', - pendingCounterTooltip: 'Wartende Gast-Vorschläge / Limit', suggestBlockedMuted: 'Der Host hat dich für diese Session gesperrt — keine Vorschläge möglich.', - suggestBlockedCap: 'Approval-Liste ist voll — warte bis der Host welche durchwinkt.', settingShuffleIntervalValue_one: '{{count}} Min.', settingShuffleIntervalValue_other: '{{count}} Min.', settingShuffleNow: 'Jetzt mischen', diff --git a/src/locales/en.ts b/src/locales/en.ts index 3fb4a6f5..04ea2f07 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1613,12 +1613,7 @@ export const enTranslation = { settingAutoShuffleHint: "Periodic Fisher–Yates shuffle of the upcoming queue. Off: order only changes when you rearrange it.", settingShuffleInterval: 'Reshuffle every', settingShuffleIntervalHint: 'How often the upcoming queue gets reshuffled while the session runs.', - settingMaxPending: 'Max pending suggestions', - settingMaxPendingHint: 'Cap on how many guest suggestions can wait for approval. 0 = unlimited.', - pendingCounter: '{{count}} / {{max}} pending', - pendingCounterTooltip: 'Pending guest suggestions / cap', suggestBlockedMuted: 'The host muted you for this session — suggestions are off.', - suggestBlockedCap: 'Approval queue is full — wait for the host to clear some.', settingShuffleIntervalValue_one: '{{count}} min', settingShuffleIntervalValue_other: '{{count}} min', settingShuffleNow: 'Shuffle now', diff --git a/src/locales/es.ts b/src/locales/es.ts index 02a0dda9..9f64eb61 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -1596,12 +1596,7 @@ export const esTranslation = { settingAutoShuffleHint: 'Mezclado Fisher-Yates periódico de la cola próxima. Desactivado: el orden solo cambia cuando lo reorganizas.', settingShuffleInterval: 'Remezclar cada', settingShuffleIntervalHint: 'Con qué frecuencia se remezcla la cola próxima durante la sesión.', - settingMaxPending: 'Máx. sugerencias pendientes', - settingMaxPendingHint: 'Límite de sugerencias de invitados que pueden esperar aprobación a la vez. 0 = ilimitado.', - pendingCounter: '{{count}} / {{max}} pendientes', - pendingCounterTooltip: 'Sugerencias de invitados pendientes / límite', suggestBlockedMuted: 'El anfitrión te silenció en esta sesión — sugerencias desactivadas.', - suggestBlockedCap: 'La cola de aprobación está llena — espera a que el anfitrión despeje algunas.', settingShuffleIntervalValue_one: '{{count}} min', settingShuffleIntervalValue_other: '{{count}} min', settingShuffleNow: 'Mezclar ahora', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 931bbd0d..301c2d8c 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -1591,12 +1591,7 @@ export const frTranslation = { settingAutoShuffleHint: 'Mélange Fisher-Yates périodique de la file à venir. Désactivé : l\'ordre ne change que lorsque tu le réorganises.', settingShuffleInterval: 'Remélanger toutes les', settingShuffleIntervalHint: 'Fréquence de remélange de la file pendant la session.', - settingMaxPending: 'Max suggestions en attente', - settingMaxPendingHint: 'Limite le nombre de suggestions invitées qui attendent ton approbation. 0 = illimité.', - pendingCounter: '{{count}} / {{max}} en attente', - pendingCounterTooltip: 'Suggestions invitées en attente / limite', suggestBlockedMuted: 'L\'hôte t\'a coupé le micro pour cette session — suggestions désactivées.', - suggestBlockedCap: 'La file d\'approbation est pleine — attends que l\'hôte en traite.', settingShuffleIntervalValue_one: '{{count}} min', settingShuffleIntervalValue_other: '{{count}} min', settingShuffleNow: 'Mélanger maintenant', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index 47911b5a..961d2316 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -1590,12 +1590,7 @@ export const nbTranslation = { settingAutoShuffleHint: 'Periodisk Fisher-Yates-stokking av kommende kø. Av: rekkefølgen endres bare når du omorganiserer den.', settingShuffleInterval: 'Stokk på nytt hver', settingShuffleIntervalHint: 'Hvor ofte kommende kø stokkes på nytt mens økten kjører.', - settingMaxPending: 'Maks ventende forslag', - settingMaxPendingHint: 'Hvor mange gjesteforslag som samtidig kan vente på godkjenning. 0 = ubegrenset.', - pendingCounter: '{{count}} / {{max}} venter', - pendingCounterTooltip: 'Ventende gjesteforslag / grense', suggestBlockedMuted: 'Verten har blokkert deg for denne økten — forslag er av.', - suggestBlockedCap: 'Godkjenningskøen er full — vent til verten tar unna noen.', settingShuffleIntervalValue_one: '{{count}} min', settingShuffleIntervalValue_other: '{{count}} min', settingShuffleNow: 'Stokk nå', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 76686010..ef394551 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -1590,12 +1590,7 @@ export const nlTranslation = { settingAutoShuffleHint: 'Periodieke Fisher-Yates-shuffle van de komende wachtrij. Uit: de volgorde verandert alleen wanneer je zelf herschikt.', settingShuffleInterval: 'Opnieuw shufflen elke', settingShuffleIntervalHint: 'Hoe vaak de komende wachtrij tijdens de sessie opnieuw wordt geshuffled.', - settingMaxPending: 'Max openstaande voorstellen', - settingMaxPendingHint: 'Hoeveel gastvoorstellen tegelijk op goedkeuring mogen wachten. 0 = onbeperkt.', - pendingCounter: '{{count}} / {{max}} open', - pendingCounterTooltip: 'Wachtende gastvoorstellen / limiet', suggestBlockedMuted: 'De host heeft je voor deze sessie geblokkeerd — voorstellen uitgeschakeld.', - suggestBlockedCap: 'De goedkeuringslijst is vol — wacht tot de host er een paar afhandelt.', settingShuffleIntervalValue_one: '{{count}} min', settingShuffleIntervalValue_other: '{{count}} min', settingShuffleNow: 'Nu shufflen', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index e0ec42d7..28dbb2c6 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -1670,12 +1670,7 @@ export const ruTranslation = { settingAutoShuffleHint: 'Периодическое Fisher-Yates перемешивание предстоящей очереди. Выкл.: порядок меняется только при ручной перестановке.', settingShuffleInterval: 'Перемешивать каждые', settingShuffleIntervalHint: 'Как часто предстоящая очередь перемешивается во время сессии.', - settingMaxPending: 'Макс. ожидающих заявок', - settingMaxPendingHint: 'Сколько гостевых предложений могут одновременно ждать одобрения. 0 = без лимита.', - pendingCounter: '{{count}} / {{max}} в ожидании', - pendingCounterTooltip: 'Ожидающие гостевые заявки / лимит', suggestBlockedMuted: 'Хост отключил тебе предложение треков на этой сессии.', - suggestBlockedCap: 'Очередь одобрений переполнена — подожди, пока хост её разгребёт.', settingShuffleIntervalValue_one: '{{count}} мин', settingShuffleIntervalValue_few: '{{count}} мин', settingShuffleIntervalValue_many: '{{count}} мин', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 94a49e35..1b054188 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -1583,12 +1583,7 @@ export const zhTranslation = { settingAutoShuffleHint: '即将播放队列的定期 Fisher-Yates 洗牌。关闭:顺序仅在你手动重新排列时改变。', settingShuffleInterval: '洗牌间隔', settingShuffleIntervalHint: '会话运行时即将播放队列的重新洗牌频率。', - settingMaxPending: '最大待审投歌数', - settingMaxPendingHint: '同时等待审核的访客投歌上限。0 = 不限制。', - pendingCounter: '{{count}} / {{max}} 待审', - pendingCounterTooltip: '等待中的访客投歌 / 上限', suggestBlockedMuted: '主持人已禁止你在本场会话投歌。', - suggestBlockedCap: '审核队列已满,等主持人处理一些再继续。', settingShuffleIntervalValue_one: '{{count}} 分钟', settingShuffleIntervalValue_other: '{{count}} 分钟', settingShuffleNow: '立即洗牌', diff --git a/src/styles/components.css b/src/styles/components.css index 3c59482f..e03eedfc 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -12388,22 +12388,6 @@ html[data-psy-native-hidden="true"] *::after { .orbit-settings-pop__row--stacked:hover { background: transparent; } -.orbit-settings-pop__number { - width: 64px; - padding: 4px 6px; - border-radius: 6px; - background: var(--ctp-surface0, rgba(127, 127, 127, 0.12)); - border: 1px solid color-mix(in srgb, var(--text-secondary) 18%, transparent); - color: var(--text-primary); - font-size: 12px; - font-variant-numeric: tabular-nums; - text-align: right; - flex-shrink: 0; -} -.orbit-settings-pop__number:focus { - outline: none; - border-color: var(--accent); -} .orbit-settings-pop__preset-group { display: flex; @@ -12540,26 +12524,6 @@ html[data-psy-native-hidden="true"] *::after { white-space: nowrap; min-width: 0; } -.orbit-queue-head__pending { - display: inline-flex; - align-items: center; - gap: 3px; - margin-left: auto; - padding: 1px 6px; - border-radius: 9px; - background: color-mix(in srgb, var(--accent) 12%, transparent); - border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent); - color: var(--text-secondary); - font-size: 10px; - font-variant-numeric: tabular-nums; - flex-shrink: 0; -} -.orbit-queue-head__pending.is-full { - color: var(--ctp-red, #f38ba8); - background: color-mix(in srgb, var(--ctp-red, #f38ba8) 14%, transparent); - border-color: color-mix(in srgb, var(--ctp-red, #f38ba8) 40%, transparent); -} -.orbit-queue-head__pending svg { color: inherit; } .orbit-guest-queue__current { position: relative; diff --git a/src/utils/orbit.ts b/src/utils/orbit.ts index 128c3e17..29b0d060 100644 --- a/src/utils/orbit.ts +++ b/src/utils/orbit.ts @@ -458,37 +458,20 @@ export async function leaveOrbitSession(): Promise { */ /** Why a guest's suggestion would be blocked, in priority order. `null` means * the suggestion can proceed. */ -export type OrbitSuggestGateReason = 'not-guest' | 'muted' | 'cap-reached' | null; +export type OrbitSuggestGateReason = 'not-guest' | 'muted' | null; /** * Evaluate whether the local guest is allowed to send a new suggestion right * now — used by both the UI (to disable buttons / show toasts) and * {@link suggestOrbitTrack} as a defensive check. - * - * Mute check uses the host-pushed `state.suggestionBlocked` list. Cap check - * is approximate from the guest's POV: it counts every non-host entry in - * `state.queue` plus the local `pendingSuggestions` list that hasn't been - * reconciled yet. We can't subtract host-side merged/declined sets — the - * guest never sees them — so the count is conservative (over-, not under-). */ export function evaluateOrbitSuggestGate(): { allowed: boolean; reason: OrbitSuggestGateReason } { - const { role, state, pendingSuggestions } = useOrbitStore.getState(); + const { role, state } = useOrbitStore.getState(); if (role !== 'guest' || !state) return { allowed: false, reason: 'not-guest' }; const username = useAuthStore.getState().getActiveServer()?.username ?? ''; if (state.suggestionBlocked?.includes(username)) { return { allowed: false, reason: 'muted' }; } - const cap = state.settings?.maxPending ?? 0; - if (cap > 0) { - // Prefer the host-pushed authoritative count when present; fall back to a - // raw queue scan for older hosts. Add the local guest's pending list on - // top — those are tracks the host hasn't swept in yet. - const knownPending = state.pendingApprovalCount - ?? state.queue.filter(q => q.addedBy !== state.host).length; - if (knownPending + pendingSuggestions.length >= cap) { - return { allowed: false, reason: 'cap-reached' }; - } - } return { allowed: true, reason: null }; } @@ -949,7 +932,6 @@ export function applyOutboxSnapshotsToState( state: OrbitState, snapshots: OutboxSnapshot[], nowMs: number = Date.now(), - opts?: { mergedKeys?: ReadonlySet; declinedKeys?: ReadonlySet }, ): OrbitState { // ── Queue additions ── // Guest outboxes are append-only from the host's POV — the host reads the @@ -965,35 +947,17 @@ export function applyOutboxSnapshotsToState( existingKeys.add(`${state.currentTrack.addedBy} ${state.currentTrack.trackId}`); } - // Compute remaining slots under the host's pending-approval cap. Items the - // host has merged or declined have left the approval queue and don't - // occupy a slot any more — without those sets we conservatively count - // every non-host queue entry as pending. Plus drop any new suggestion - // from a user the host has muted. + // Drop any new suggestion from a user the host has muted before the + // dedupe scan — they shouldn't count against the queue at all. const blocked = new Set(state.suggestionBlocked ?? []); - const cap = state.settings?.maxPending ?? 0; - const merged = opts?.mergedKeys ?? new Set(); - const declined = opts?.declinedKeys ?? new Set(); - const pendingCount = cap > 0 - ? state.queue.filter(q => - q.addedBy !== state.host - && !merged.has(suggestionKey(q)) - && !declined.has(suggestionKey(q)) - ).length - : 0; - let remaining = cap > 0 ? Math.max(0, cap - pendingCount) : Infinity; - const newItems: OrbitQueueItem[] = []; - outer: for (const snap of snapshots) { if (blocked.has(snap.user)) continue; for (const trackId of snap.trackIds) { - if (remaining <= 0) break outer; const key = `${snap.user} ${trackId}`; if (existingKeys.has(key)) continue; existingKeys.add(key); newItems.push({ trackId, addedBy: snap.user, addedAt: nowMs }); - remaining--; } }