diff --git a/src/app/features/message-search/MessageSearch.tsx b/src/app/features/message-search/MessageSearch.tsx index 7f790e3b0..13e2c525c 100644 --- a/src/app/features/message-search/MessageSearch.tsx +++ b/src/app/features/message-search/MessageSearch.tsx @@ -33,6 +33,8 @@ const useSearchPathSearchParams = (searchParams: URLSearchParams): _SearchPathSe order: searchParams.get('order') ?? undefined, rooms: searchParams.get('rooms') ?? undefined, senders: searchParams.get('senders') ?? undefined, + fromTs: searchParams.get('fromTs') ?? undefined, + toTs: searchParams.get('toTs') ?? undefined, }), [searchParams], ); @@ -193,6 +195,8 @@ export function MessageSearch({ order: searchPathSearchParams.order ?? SearchOrderBy.Recent, rooms: searchParamRooms ?? defaultRooms, senders: searchParamsSenders ?? senders, + fromTs: searchPathSearchParams.fromTs ? Number(searchPathSearchParams.fromTs) : undefined, + toTs: searchPathSearchParams.toTs ? Number(searchPathSearchParams.toTs) : undefined, }; }, [searchPathSearchParams, searchParamRooms, searchParamsSenders, rooms, senders]); @@ -235,6 +239,8 @@ export function MessageSearch({ msgSearchParams.order, msgSearchParams.rooms, msgSearchParams.senders, + msgSearchParams.fromTs, + msgSearchParams.toTs, ], queryFn: ({ pageParam }) => searchMessages(pageParam), initialPageParam: '', @@ -329,6 +335,20 @@ export function MessageSearch({ [searchParamsSenders, handleSelectedSendersChange], ); + const handleDateRangeChange = useCallback( + (fromTs?: number, toTs?: number) => { + setSearchParams((prevParams) => { + const p = new URLSearchParams(prevParams); + p.delete('fromTs'); + p.delete('toTs'); + if (fromTs) p.append('fromTs', String(fromTs)); + if (toTs) p.append('toTs', String(toTs)); + return p; + }); + }, + [setSearchParams], + ); + const lastVItem = vItems[vItems.length - 1]; const lastVItemIndex: number | undefined = lastVItem?.index; const lastGroupIndex = groups.length - 1; @@ -378,6 +398,9 @@ export function MessageSearch({ onOrderChange={handleOrderChange} selectedSenders={searchParamsSenders} onSelectedSendersChange={handleSelectedSendersChange} + fromTs={msgSearchParams.fromTs} + toTs={msgSearchParams.toTs} + onDateRangeChange={handleDateRangeChange} /> diff --git a/src/app/features/message-search/SearchFilters.tsx b/src/app/features/message-search/SearchFilters.tsx index c16f362cc..ffdcf321f 100644 --- a/src/app/features/message-search/SearchFilters.tsx +++ b/src/app/features/message-search/SearchFilters.tsx @@ -326,6 +326,123 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto ); } +type DateRangeButtonProps = { + fromTs?: number; + toTs?: number; + onChange: (fromTs?: number, toTs?: number) => void; +}; +function DateRangeButton({ fromTs, toTs, onChange }: DateRangeButtonProps) { + const [menuAnchor, setMenuAnchor] = useState(); + const toISODate = (ts: number) => new Date(ts).toISOString().split('T')[0]; + const fromDate = fromTs ? toISODate(fromTs) : ''; + const toDate = toTs ? toISODate(toTs) : ''; + + const handleFrom = (val: string) => { + onChange(val ? new Date(val).setHours(0, 0, 0, 0) : undefined, toTs); + }; + const handleTo = (val: string) => { + onChange(fromTs, val ? new Date(val).setHours(23, 59, 59, 999) : undefined); + }; + + const hasRange = !!fromTs || !!toTs; + const label = hasRange + ? [fromTs && toISODate(fromTs), toTs && toISODate(toTs)].filter(Boolean).join(' – ') + : 'Date range'; + + return ( + setMenuAnchor(undefined), + clickOutsideDeactivates: true, + }} + > + + + + From + handleFrom(e.target.value)} + style={{ + background: 'var(--bg-surface-variant)', + border: '1px solid var(--border-surface-variant)', + borderRadius: config.radii.R300, + color: 'inherit', + fontSize: '0.82rem', + padding: `${config.space.S100} ${config.space.S200}`, + }} + /> + + + To + handleTo(e.target.value)} + style={{ + background: 'var(--bg-surface-variant)', + border: '1px solid var(--border-surface-variant)', + borderRadius: config.radii.R300, + color: 'inherit', + fontSize: '0.82rem', + padding: `${config.space.S100} ${config.space.S200}`, + }} + /> + + {hasRange && ( + + )} + + + + } + > + } + after={ + hasRange ? ( + { + e.stopPropagation(); + onChange(undefined, undefined); + }} + /> + ) : undefined + } + onClick={(e: React.MouseEvent) => + setMenuAnchor(e.currentTarget.getBoundingClientRect()) + } + > + {label} + + + ); +} + type SearchFiltersProps = { defaultRoomsFilterName: string; allowGlobal?: boolean; @@ -338,6 +455,9 @@ type SearchFiltersProps = { onOrderChange: (order?: string) => void; selectedSenders?: string[]; onSelectedSendersChange: (senders?: string[]) => void; + fromTs?: number; + toTs?: number; + onDateRangeChange: (fromTs?: number, toTs?: number) => void; }; export function SearchFilters({ defaultRoomsFilterName, @@ -351,6 +471,9 @@ export function SearchFilters({ onOrderChange, selectedSenders, onSelectedSendersChange, + fromTs, + toTs, + onDateRangeChange, }: SearchFiltersProps) { const mx = useMatrixClient(); @@ -433,6 +556,7 @@ export function SearchFilters({ ); })} + diff --git a/src/app/features/message-search/useMessageSearch.ts b/src/app/features/message-search/useMessageSearch.ts index 048188665..a9cb66990 100644 --- a/src/app/features/message-search/useMessageSearch.ts +++ b/src/app/features/message-search/useMessageSearch.ts @@ -68,10 +68,12 @@ export type MessageSearchParams = { order?: string; rooms?: string[]; senders?: string[]; + fromTs?: number; + toTs?: number; }; export const useMessageSearch = (params: MessageSearchParams) => { const mx = useMatrixClient(); - const { term, order, rooms, senders } = params; + const { term, order, rooms, senders, fromTs, toTs } = params; const searchMessages = useCallback( async (nextBatch?: string) => { @@ -90,11 +92,15 @@ export const useMessageSearch = (params: MessageSearchParams) => { after_limit: 0, include_profile: false, }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any filter: { limit, rooms, senders, - }, + // from_ts / to_ts are valid Matrix spec fields not yet in SDK types + ...(fromTs !== undefined && { from_ts: fromTs }), + ...(toTs !== undefined && { to_ts: toTs }), + } as any, include_state: false, order_by: order as SearchOrderBy.Recent, search_term: term, diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 4c6c8d32f..06284a39b 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -314,16 +314,31 @@ export const RoomInput = forwardRef( didRestoreDraft.current = true; if (msgDraft.length > 0) { Transforms.insertFragment(editor, msgDraft); + } else { + // Jotai draft is empty (page reload) — try localStorage fallback + try { + const stored = localStorage.getItem(`draft-msg-${roomId}`); + if (stored) { + const nodes = JSON.parse(stored); + if (Array.isArray(nodes) && nodes.length > 0) { + Transforms.insertFragment(editor, nodes); + } + } + } catch { + // Ignore malformed stored draft + } } - }, [editor, msgDraft]); + }, [editor, msgDraft, roomId]); useEffect( () => () => { if (!isEmptyEditor(editor)) { const parsedDraft = JSON.parse(JSON.stringify(editor.children)); setMsgDraft(parsedDraft); + localStorage.setItem(`draft-msg-${roomId}`, JSON.stringify(parsedDraft)); } else { setMsgDraft([]); + localStorage.removeItem(`draft-msg-${roomId}`); } resetEditor(editor); resetEditorHistory(editor); @@ -463,6 +478,7 @@ export const RoomInput = forwardRef( mx.sendMessage(roomId, content as any); resetEditor(editor); resetEditorHistory(editor); + localStorage.removeItem(`draft-msg-${roomId}`); setReplyDraft(undefined); sendTypingStatus(false); }, [mx, roomId, editor, replyDraft, sendTypingStatus, setReplyDraft, isMarkdown, commands]); diff --git a/src/app/pages/client/ClientNonUIFeatures.tsx b/src/app/pages/client/ClientNonUIFeatures.tsx index 0ba611006..8e0f33008 100644 --- a/src/app/pages/client/ClientNonUIFeatures.tsx +++ b/src/app/pages/client/ClientNonUIFeatures.tsx @@ -56,22 +56,26 @@ function FaviconUpdater() { const roomToUnread = useAtomValue(roomToUnreadAtom); useEffect(() => { - let notification = false; - let highlight = false; + let totalNotif = 0; + let totalHighlight = 0; roomToUnread.forEach((unread) => { - if (unread.total > 0) { - notification = true; - } - if (unread.highlight > 0) { - highlight = true; - } + totalNotif += unread.total; + totalHighlight += unread.highlight; }); - if (notification) { - setFavicon(highlight ? LogoHighlightSVG : LogoUnreadSVG); + if (totalNotif > 0) { + setFavicon(totalHighlight > 0 ? LogoHighlightSVG : LogoUnreadSVG); } else { setFavicon(LogoSVG); } + + if (totalHighlight > 0) { + document.title = `(${totalHighlight}) Lotus Chat`; + } else if (totalNotif > 0) { + document.title = `· Lotus Chat`; + } else { + document.title = 'Lotus Chat'; + } }, [roomToUnread]); return null; diff --git a/src/app/pages/paths.ts b/src/app/pages/paths.ts index 9cf4bfb96..a12f59a60 100644 --- a/src/app/pages/paths.ts +++ b/src/app/pages/paths.ts @@ -33,6 +33,8 @@ export type _SearchPathSearchParams = { order?: string; rooms?: string; senders?: string; + fromTs?: string; + toTs?: string; }; export const _SEARCH_PATH = 'search/';