Stoat: a read told soon after another is told when the wait is up, not dropped; the spoiler mark in the file tray; the probe; nobilis pointer

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
SalastilandClaude Sonnet 5.5 committed 2026-10-24 18:19:35 -04:00
1 parent 2a966c533c
commit ccf86fe5ec
4 files changed
+33 -4

No files matched your search

+1 -1
Submodule nobilis updated: 9cc8e8b4d2...9945800375.
+12
View File
@@ -259,6 +259,18 @@ try {
check('and bob’s alongside it, and taken away again, leave the count right', (await tally())?.count === 1 && (await tally())?.me === false, JSON.stringify(await tally()))
await as('bob', 'DELETE', `/channels/${channelId.General}/messages/${counted._id}`).catch(() => {})
// --- Reading here is reading there: what arrives in the channel on screen is told to Stoat as read, even two in a few seconds.
await page.call('Emulation.setFocusEmulationEnabled', { enabled: true })
await ev(`window.__mohoShots.store.selectBuffer(${JSON.stringify(general)})`)
await sleep(2500)
const lastRead = async () => (await as('alice', 'GET', '/sync/unreads')).find((u) => u._id.channel === channelId.General)?.last_id
const first = await as('bob', 'POST', `/channels/${channelId.General}/messages`, { content: 'read me once' })
await sleep(2000)
const second = await as('bob', 'POST', `/channels/${channelId.General}/messages`, { content: 'read me twice' })
check('a message that arrives in the open channel is told to Stoat as read, and a second soon after is too', !!(await until(async () => (await lastRead()) === second._id, 15000)), JSON.stringify([first._id, second._id, await lastRead()]))
await as('bob', 'DELETE', `/channels/${channelId.General}/messages/${first._id}`).catch(() => {})
await as('bob', 'DELETE', `/channels/${channelId.General}/messages/${second._id}`).catch(() => {})
// --- An emoji from a server this account is not in is still a picture.
const away = await as('bob', 'POST', '/servers/create', { name: `Elsewhere ${Date.now().toString(36).slice(-4)}` })
const awayServer = away.server?._id ?? away._id
+2 -2
View File
@@ -39,7 +39,7 @@ interface StagedAttachment {
preview?: string
/** In bytes, once it has been asked. */
size?: number | null
/** Sent hidden behind a spoiler: Discord and Steam. */
/** Sent hidden behind a spoiler: Discord, Steam, Telegram and Stoat. */
spoiler: boolean
}
@@ -1105,7 +1105,7 @@ export function Composer({ thread = false }: { thread?: boolean } = {}): JSX.Ele
{/* On the card's corner, and there only while it is pointed at
or focused - they would be a dozen buttons otherwise. */}
<div className="staged-actions">
{(service === 'discord' || service === 'steam' || service === 'telegram') && (
{(service === 'discord' || service === 'steam' || service === 'telegram' || service === 'stoat') && (
<button
type="button"
className={classes('staged-action', att.spoiler && 'on')}
+18 -1
View File
@@ -2734,11 +2734,28 @@ export class ChatStore {
ackIfReading(bufferId: string): void {
if (bufferId !== this.state.activeBufferId || !this.windowIsActive()) return
const now = Date.now()
if (now - (this.ackedAt.get(bufferId) ?? 0) < 5000) return
const wait = 5000 - (now - (this.ackedAt.get(bufferId) ?? 0))
if (wait > 0) {
// Too soon after the last: said when the few seconds are up instead of never. A message that arrived inside them is
// the newest, and without this it was left unread on the phone until another one came.
if (!this.ackLater.has(bufferId)) {
this.ackLater.set(
bufferId,
setTimeout(() => {
this.ackLater.delete(bufferId)
this.ackIfReading(bufferId)
}, wait + 50)
)
}
return
}
this.ackedAt.set(bufferId, now)
bestEffort(this.ackRead(bufferId), 'ack read')
}
/** A conversation's turn to be told is read that came too soon after the last, waiting for it. */
private ackLater = new Map<string, ReturnType<typeof setTimeout>>()
private windowIsActive(): boolean {
return document.visibilityState === 'visible' && document.hasFocus()
}