diff --git a/nobilis b/nobilis index ae2e818..1fe92d1 160000 --- a/nobilis +++ b/nobilis @@ -1 +1 @@ -Subproject commit ae2e8189b42596c57be39459c8c1114863f213ba +Subproject commit 1fe92d12fc4eae318942e46c70e672aaa073fd3b diff --git a/src/renderer/src/app.css b/src/renderer/src/app.css index 5a650f9..8aa0ee4 100644 --- a/src/renderer/src/app.css +++ b/src/renderer/src/app.css @@ -6676,6 +6676,33 @@ video.lightbox-media { color: var(--error); } +/* --- Jump to a date --------------------------------------------------- */ + +/* Under the search box, because it answers the same question by a different + route - and in an encrypted room, which the server cannot search, it is the + only route. */ +.search-date { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); +} + +.search-date input[type='date'] { + flex: 1; + min-width: 0; + border: none; + background: none; + color: var(--surface-text); + font: inherit; +} + +/* The picker's own button, which renders as a dark glyph on a dark field + without this. */ +.search-date input[type='date']::-webkit-calendar-picker-indicator { + filter: invert(0.8); + cursor: pointer; +} + /* --- Message source --------------------------------------------------- */ /* The event behind a message, for when what is on screen is wrong. Built like diff --git a/src/renderer/src/components/ConversationTools.tsx b/src/renderer/src/components/ConversationTools.tsx index 0a70da6..2068570 100644 --- a/src/renderer/src/components/ConversationTools.tsx +++ b/src/renderer/src/components/ConversationTools.tsx @@ -19,6 +19,13 @@ interface DiscordThread { messageCount?: number } +/** Today, as a `date` field spells it - local, not UTC. */ +function today(): string { + const d = new Date() + const pad = (n: number): string => String(n).padStart(2, '0') + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +} + /** What Discord's own search hands back. */ interface DiscordSearch { results: Message[] @@ -586,6 +593,9 @@ export function ConversationTools({ buffer }: { buffer: BufferEntry }): JSX.Elem const [jumping, setJumping] = useState(false) const [calling, setCalling] = useState(false) const [searchOpen, setSearchOpen] = useState(false) + /** The day somebody asked to read back to, as the date field spells it. */ + const [date, setDate] = useState('') + const [jumpingToDate, setJumpingToDate] = useState(false) /** This room, or every room this account is in. */ const [scope, setScope] = useState<'room' | 'account'>('room') const [roomNames, setRoomNames] = useState>({}) @@ -768,6 +778,47 @@ export function ConversationTools({ buffer }: { buffer: BufferEntry }): JSX.Elem setSearchOpen(false) } + /** + * Reads back to a particular day. + * + * The server is asked where that day is; everything after that is the path + * a search result already takes. Forwards from midnight, because a date + * means "that day" rather than "whatever came before it" - asking backwards + * lands on the last message of the day before. + * + * A day after everything in the room finds nothing forwards, which is the + * one case worth a second question: asking backwards from it lands on the + * last thing anybody said, which is the honest answer to "take me to + * Tuesday" in a room that went quiet in March. + */ + const jumpToDate = async (day: string): Promise => { + // Local midnight rather than UTC: somebody picking the 3rd means the 3rd + // where they are, and a UTC midnight is the 2nd for most of the Americas. + const midnight = new Date(`${day}T00:00:00`) + if (Number.isNaN(midnight.getTime())) return + const ts = Math.floor(midnight.getTime() / 1000) + + setJumpingToDate(true) + try { + let found = await window.moho + .rpc<{ eventId: string }>('matrixEventAtDate', { bufferId: buffer.id, ts, forwards: true }) + .catch(() => null) + if (!found) { + found = await window.moho + .rpc<{ eventId: string }>('matrixEventAtDate', { bufferId: buffer.id, ts, forwards: false }) + .catch(() => null) + } + if (!found) { + store.toast('info', 'Nothing was said in this room around then') + return + } + await jumpTo(found.eventId) + setDate('') + } finally { + setJumpingToDate(false) + } + } + const call = (): void => { if (inCall) { void store.leaveVoice(buffer.accountId) @@ -1043,6 +1094,37 @@ export function ConversationTools({ buffer }: { buffer: BufferEntry }): JSX.Elem )} + {/* Reading back to a particular day, which until now meant + scrolling to it. Beside the search because it answers the same + question - "where was that" - and because an encrypted room + cannot be searched at all, which leaves the date as the only + way back into it. + + Matrix only: it is the one protocol here whose server will say + where in a room a given day is. */} + {isMatrix && ( +
+ +
+ + { + setDate(e.target.value) + if (e.target.value) void jumpToDate(e.target.value) + }} + /> +
+
+ )} + {/* Why an encrypted room finds only what this window already had: the server holds ciphertext and cannot read it. Element says the same thing, and silence here reads as a broken search. */}