Q142: a conversation is read only up to the message the reader actually saw
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87c297dd45
commit
b18eb9ad14
11 files changed
+167
-55
No files matched your search
@@ -220,22 +220,19 @@ export class ChatService {
|
||||
const employee = this.db.prepare("select id from employees where id = ? and org_id = ?")
|
||||
.get(input.employeeId, input.orgId) as { id: string } | undefined;
|
||||
if (!employee) throw new Error("Employee was not found.");
|
||||
// Read state is per-employee bookkeeping, not access control: the board
|
||||
// observes every conversation in its org (agent↔agent directs included),
|
||||
// so opening one clears its unread count even without membership.
|
||||
const latest = this.db.prepare(`
|
||||
select id from chat_messages
|
||||
where space_id = ? and org_id = ?
|
||||
order by created_at desc
|
||||
limit 1
|
||||
`).get(input.spaceId, input.orgId) as { id: string } | undefined;
|
||||
const now = new Date().toISOString();
|
||||
// Read state is per-employee bookkeeping, not access control: the board reads every conversation in its org.
|
||||
const seen = this.db.prepare("select id, created_at from chat_messages where id = ? and space_id = ? and org_id = ?")
|
||||
.get(input.upToMessageId, input.spaceId, input.orgId) as { id: string; created_at: string } | undefined;
|
||||
if (!seen) throw new Error("The message read is not in this conversation.");
|
||||
// Read up to what was seen, never past it; messages order by time then insertion, and a late mark never moves back.
|
||||
this.db.prepare(`
|
||||
insert into chat_read_state(org_id, employee_id, space_id, last_read_message_id, last_read_at)
|
||||
values (?, ?, ?, ?, ?)
|
||||
on conflict(employee_id, space_id)
|
||||
do update set last_read_message_id = excluded.last_read_message_id, last_read_at = excluded.last_read_at
|
||||
`).run(input.orgId, input.employeeId, input.spaceId, latest?.id ?? null, now);
|
||||
where (excluded.last_read_at, (select rowid from chat_messages where id = excluded.last_read_message_id))
|
||||
> (chat_read_state.last_read_at, coalesce((select rowid from chat_messages where id = chat_read_state.last_read_message_id), -1))
|
||||
`).run(input.orgId, input.employeeId, input.spaceId, seen.id, seen.created_at);
|
||||
}
|
||||
|
||||
editMessage(input: EditMessageInput): void {
|
||||
|
||||
@@ -96,11 +96,11 @@ export class SnapshotService {
|
||||
approvals: list(this.db, "approvals", "created_at desc"),
|
||||
chatSpaces: list(this.db, "chat_spaces", "created_at asc"),
|
||||
chatMemberships: list(this.db, "chat_memberships", "space_id asc, principal_type asc, principal_id asc"),
|
||||
chatMessages: recentList<AppSnapshot["chatMessages"][number]>(this.db, "chat_messages", "created_at desc", 500).reverse(),
|
||||
chatMessages: recentList<AppSnapshot["chatMessages"][number]>(this.db, "chat_messages", "created_at desc, rowid desc", 500).reverse(),
|
||||
// Every thread a loaded message belongs to, plus the most recently active ones, so no shown message loses its thread.
|
||||
chatThreads: all<Row>(this.db.prepare(`
|
||||
select * from chat_threads
|
||||
where id in (select thread_id from (select thread_id from chat_messages order by created_at desc limit 500))
|
||||
where id in (select thread_id from (select thread_id from chat_messages order by created_at desc, rowid desc limit 500))
|
||||
or id in (select id from chat_threads order by updated_at desc limit 200)
|
||||
order by updated_at desc
|
||||
`)).map((row) => camel(row) as unknown as AppSnapshot["chatThreads"][number]),
|
||||
|
||||
Reference in new issue
Block a user