package history import ( "context" "fmt" "time" ) // This file owns the per-session read cursor (design/16 §4.2, Android 协同批次 // D4/B3). There is a single owner account and no per-device identity at this // layer, so the cursor is session-scoped: reading on any one end marks the session // read for every end of the account. The cursor lives on chat_sessions.read_at in // the same millisecond unit as chat_messages.created_at, so the unread badge is a // plain `created_at > read_at` count (see listSessionsQuery). // MarkSessionRead advances the session's read cursor to before and returns the // resulting cursor as a Unix millisecond timestamp. A zero before means "now" — the // caller's "mark everything I have seen" case, which does not have to read the // clock itself. // // The cursor only ever moves forward: the UPDATE applies SQLite's two-argument // scalar MAX, so a stale or out-of-order request (two ends racing, a retried // request) can never un-read messages. A negative before therefore leaves the stored // value untouched rather than moving it backwards. // // An unknown session id returns ErrSessionNotFound, matching RenameSession/SetPinned // — isolated (group/stranger) sessions have no chat_sessions row and are not // addressable here. func (s *Store) MarkSessionRead(ctx context.Context, sessionID string, before time.Time) (int64, error) { ms := before.UnixMilli() if before.IsZero() { ms = time.Now().UnixMilli() } res, err := s.db.ExecContext(ctx, `UPDATE chat_sessions SET read_at = MAX(read_at, ?) WHERE id = ?`, ms, sessionID) if err != nil { return 0, fmt.Errorf("history: mark session read %s: %w", sessionID, err) } n, err := res.RowsAffected() if err != nil { return 0, fmt.Errorf("history: mark session read %s rows: %w", sessionID, err) } if n == 0 { return 0, fmt.Errorf("history: mark session read %s: %w", sessionID, ErrSessionNotFound) } var readAt int64 if err := s.db.QueryRowContext(ctx, `SELECT read_at FROM chat_sessions WHERE id = ?`, sessionID).Scan(&readAt); err != nil { return 0, fmt.Errorf("history: read session cursor %s: %w", sessionID, err) } return readAt, nil }