From 46cc8c2002f8503f08cfd5efdfa0afca6899c940 Mon Sep 17 00:00:00 2001 From: iamdoubz Date: Fri, 23 Oct 2026 13:55:46 -0500 Subject: [PATCH 1/2] voicemail: search messages by transcript or caller Filters the already-loaded mailbox in the browser (GET /api/me/voicemail isn't paged). --- apps/web/src/pages/user/VoicemailPage.tsx | 28 ++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/apps/web/src/pages/user/VoicemailPage.tsx b/apps/web/src/pages/user/VoicemailPage.tsx index 8a771a9..3cc743a 100644 --- a/apps/web/src/pages/user/VoicemailPage.tsx +++ b/apps/web/src/pages/user/VoicemailPage.tsx @@ -1,16 +1,21 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -// My voicemail (05 §4): messages (play marks read, download, delete) and greetings. +// My voicemail (05 §4): messages (play marks read, download, delete, search) and greetings. import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useRef, useState } from 'react'; import { formatNumber, type GreetingKind, type VoicemailDto } from '@selfphoneng/shared'; import { meApi } from '../../api/client.ts'; import { ConfirmDialog } from '../../components/Dialog.tsx'; -import { Alert, Button } from '../../components/ui.tsx'; +import { Alert, Button, inputClass } from '../../components/ui.tsx'; export const mmss = (s: number) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`; export const callerOf = (m: VoicemailDto) => m.callerName ?? (m.callerNumber ? formatNumber(m.callerNumber) : 'Unknown caller'); export const isNew = (m: VoicemailDto) => m.folder !== 'Old'; +/** Words in the transcript, caller name or number, ignoring case. */ +export const matches = (m: VoicemailDto, q: string) => + [m.transcript, m.callerName, m.callerNumber].some((f) => + f?.toLowerCase().includes(q.trim().toLowerCase()), + ); export function VoicemailPage() { return ( @@ -25,6 +30,10 @@ function Messages() { const qc = useQueryClient(); const list = useQuery({ queryKey: ['me', 'voicemail'], queryFn: meApi.voicemail }); const [deleting, setDeleting] = useState(null); + const [q, setQ] = useState(''); + // ponytail: the whole mailbox is already loaded, so search filters it here; move to the API if + // mailboxes ever get paged. + const shown = q.trim() ? list.data?.filter((m) => matches(m, q)) : list.data; const refresh = () => qc.invalidateQueries({ queryKey: ['me', 'voicemail'] }); const markRead = useMutation({ mutationFn: (id: string) => meApi.moveVoicemail(id, 'Old'), @@ -44,9 +53,22 @@ function Messages() { Voicemail {list.error && {list.error.message}} + {!!list.data?.length && ( + setQ(e.target.value)} + /> + )} {list.data?.length === 0 &&

No messages.

} + {!!list.data?.length && shown?.length === 0 && ( +

No message mentions that.

+ )}