From a7cc909831436364a564aa466f488f245bcce910 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 23:02:14 +0000 Subject: [PATCH] Send refresh param on inbox load and manual refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend's GET /accounts/:id/threads already treats a present `refresh` query value as a signal to wake the account's IMAP/JMAP IDLE listener (threadsApi.ts), but nothing in the UI ever set it — the Refresh button and page load only re-read cached thread data. Now useThreadListQuery sends a fresh ISO timestamp as `refresh` on its first fetch (covers page load/reload) and again whenever requestRefresh() runs (the Refresh button and empty-state action). --- src/composables/useThreadQueries.ts | 26 ++++++++++++++++---- src/views/InboxView.vue | 4 +-- tests/component/InboxView.regression.test.ts | 4 ++- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/composables/useThreadQueries.ts b/src/composables/useThreadQueries.ts index d860843..d051a88 100644 --- a/src/composables/useThreadQueries.ts +++ b/src/composables/useThreadQueries.ts @@ -1,4 +1,4 @@ -import { computed } from 'vue' +import { computed, ref } from 'vue' import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/vue-query' import { useAccountStore } from '@/stores/account' import { api } from '@/lib/api' @@ -15,19 +15,35 @@ export function useThreadListQuery(status: () => ThreadStatus | undefined) { const accountStore = useAccountStore() const accountId = computed(() => accountStore.accountId) + // Sent as `refresh` on the query's very first fetch (covers a page load/reload) and again + // whenever requestRefresh() runs (the inbox's Refresh button) — the backend treats a present + // `refresh` value as a signal to wake the IMAP/JMAP IDLE listener for the account, on top of + // whatever TanStack Query itself decides to (re)fetch. Consumed after one fetch so routine + // refetches (pagination, window focus, mutation-triggered invalidation) don't keep re-sending it. + const pendingRefresh = ref(new Date().toISOString()) + const query = useInfiniteQuery({ queryKey: computed(() => queryKeys.threads.list(accountId.value!, status())), - queryFn: async ({ pageParam }) => - unwrap(await api.listThreads(accountId.value!, { + queryFn: async ({ pageParam }) => { + const refresh = pendingRefresh.value + pendingRefresh.value = undefined + return unwrap(await api.listThreads(accountId.value!, { status: status(), cursor: pageParam, limit: 50, - })), + ...(refresh ? { refresh } : {}), + })) + }, initialPageParam: undefined as string | undefined, getNextPageParam: (lastPage) => lastPage.pagination.cursor ?? undefined, enabled: computed(() => !!accountId.value), }) + function requestRefresh() { + pendingRefresh.value = new Date().toISOString() + return query.refetch() + } + const threads = computed(() => query.data.value?.pages.flatMap(p => p.threads).filter(t => { // Filter out sentinel/ghost threads: null lastSignalAt means signals were @@ -40,7 +56,7 @@ export function useThreadListQuery(status: () => ThreadStatus | undefined) { const activeCount = computed(() => threads.value.length) const hasMore = computed(() => query.hasNextPage?.value ?? false) - return { query, threads, activeCount, hasMore } + return { query, threads, activeCount, hasMore, requestRefresh } } export function useThreadDetailQuery(threadId: () => string | undefined) { diff --git a/src/views/InboxView.vue b/src/views/InboxView.vue index 54a98f9..6e46150 100644 --- a/src/views/InboxView.vue +++ b/src/views/InboxView.vue @@ -44,7 +44,7 @@ function statusFor(tab: TabKey): ThreadStatus | undefined { } // TanStack Query — thread list driven by current tab -const { query: threadListQuery, threads: allThreads, hasMore } = useThreadListQuery( +const { query: threadListQuery, threads: allThreads, hasMore, requestRefresh } = useThreadListQuery( () => statusFor(activeTab.value), ) @@ -79,7 +79,7 @@ async function fetchRecentSignals() { async function handleRefresh() { refreshing.value = true - await threadListQuery.refetch() + await requestRefresh() await fetchRecentSignals() lastRefreshedAt.value = new Date().toLocaleTimeString() refreshing.value = false diff --git a/tests/component/InboxView.regression.test.ts b/tests/component/InboxView.regression.test.ts index 1a22b82..3eda5e5 100644 --- a/tests/component/InboxView.regression.test.ts +++ b/tests/component/InboxView.regression.test.ts @@ -215,7 +215,9 @@ describe('InboxView — regression gate', () => { if (refreshBtn) { await refreshBtn.trigger('click') await flushPromises() - expect(api.listThreads).toHaveBeenCalled() + // The backend treats a present `refresh` value as a signal to wake the IMAP/JMAP IDLE + // listener for the account (see threadsApi.ts) — the button is a no-op without it. + expect(api.listThreads).toHaveBeenCalledWith('acc_1', expect.objectContaining({ refresh: expect.any(String) })) } }) })