@wirecat/cli-core 0.380.0 → 0.382.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/dist/cli/bot/watch.js +4 -1
  3. package/dist/cli/index.d.ts +1 -1
  4. package/dist/cli/messenger/chat-answers.d.ts +2 -0
  5. package/dist/cli/messenger/chat-answers.js +2 -0
  6. package/dist/cli/messenger/chat-lists.d.ts +2 -0
  7. package/dist/cli/messenger/context.d.ts +2 -2
  8. package/dist/cli/messenger/context.js +2 -1
  9. package/dist/cli/messenger/conversation-answers.d.ts +1 -0
  10. package/dist/cli/messenger/message-answers.d.ts +1 -0
  11. package/dist/cli/messenger/message-items.js +1 -0
  12. package/dist/cli/messenger/message-writes.d.ts +1 -0
  13. package/dist/cli/messenger/serve-relay.d.ts +1 -0
  14. package/dist/cli/messenger/stored.d.ts +3 -1
  15. package/dist/cli/messenger/stored.js +24 -12
  16. package/dist/cli/messenger/watch-command.js +8 -0
  17. package/dist/domain/models.d.ts +52 -4
  18. package/dist/entities/chat.d.ts +4 -0
  19. package/dist/entities/chat.js +4 -0
  20. package/dist/entities/contact.d.ts +4 -0
  21. package/dist/entities/evidence.d.ts +8 -0
  22. package/dist/entities/index.d.ts +17 -0
  23. package/dist/entities/message.d.ts +2 -0
  24. package/dist/entities/message.js +2 -0
  25. package/dist/errors/reasons.js +1 -0
  26. package/dist/meetings/answers.d.ts +1 -0
  27. package/dist/meetings/import.js +1 -0
  28. package/dist/meetings/index.d.ts +1 -1
  29. package/dist/meetings/index.js +1 -1
  30. package/dist/meetings/item.d.ts +10 -0
  31. package/dist/meetings/item.js +34 -18
  32. package/dist/meetings/store.d.ts +6 -0
  33. package/dist/ports/messenger.d.ts +10 -3
  34. package/dist/ports/traits.d.ts +6 -0
  35. package/dist/resolve.d.ts +6 -0
  36. package/dist/resolve.js +13 -4
  37. package/dist/search/garbled.d.ts +2 -1
  38. package/dist/search/garbled.js +6 -1
  39. package/dist/services/chats.js +16 -2
  40. package/dist/services/deps.d.ts +2 -0
  41. package/dist/services/deps.js +2 -0
  42. package/dist/services/inbox.d.ts +1 -0
  43. package/dist/services/inbox.js +10 -3
  44. package/dist/store/keys.d.ts +1 -1
  45. package/dist/store/messages.d.ts +22 -3
  46. package/dist/store/messages.js +22 -3
  47. package/dist/store/sqlite/accounts.js +2 -1
  48. package/dist/store/sqlite/chat-rekey.js +18 -2
  49. package/dist/store/sqlite/chats.d.ts +2 -2
  50. package/dist/store/sqlite/chats.js +39 -9
  51. package/dist/store/sqlite/manifest.js +3 -0
  52. package/dist/store/sqlite/meeting-hit-query.js +18 -3
  53. package/dist/store/sqlite/meetings.js +56 -1
  54. package/dist/store/sqlite/messages.d.ts +19 -4
  55. package/dist/store/sqlite/messages.js +103 -5
  56. package/dist/store/sqlite/migrations.generated.js +23 -0
  57. package/dist/store/sqlite/open-store.js +16 -5
  58. package/dist/store/sqlite/reads.d.ts +50 -0
  59. package/dist/store/sqlite/reads.js +3 -1
  60. package/dist/store/sqlite/schema.d.ts +251 -0
  61. package/dist/store/sqlite/schema.js +28 -0
  62. package/dist/store/sqlite/search.d.ts +33 -0
  63. package/dist/store/sqlite/values.d.ts +4 -0
  64. package/dist/store/sqlite/values.js +3 -0
  65. package/package.json +1 -1
@@ -190,6 +190,7 @@ export declare const ENTITIES: {
190
190
  }, undefined>, undefined>, undefined>;
191
191
  readonly scheduled_for: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
192
192
  readonly starred: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
193
+ readonly delivery: v.OptionalSchema<v.PicklistSchema<["delivered", "read"], undefined>, undefined>;
193
194
  readonly email: v.OptionalSchema<v.StrictObjectSchema<{
194
195
  readonly subject: v.NullableSchema<v.StringSchema<undefined>, undefined>;
195
196
  readonly from: v.NullableSchema<v.StrictObjectSchema<{
@@ -370,6 +371,7 @@ export declare const ENTITIES: {
370
371
  }, undefined>, undefined>, undefined>;
371
372
  readonly scheduled_for: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
372
373
  readonly starred: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
374
+ readonly delivery: v.OptionalSchema<v.PicklistSchema<["delivered", "read"], undefined>, undefined>;
373
375
  readonly email: v.OptionalSchema<v.StrictObjectSchema<{
374
376
  readonly subject: v.NullableSchema<v.StringSchema<undefined>, undefined>;
375
377
  readonly from: v.NullableSchema<v.StrictObjectSchema<{
@@ -550,6 +552,7 @@ export declare const ENTITIES: {
550
552
  }, undefined>, undefined>, undefined>;
551
553
  readonly scheduled_for: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
552
554
  readonly starred: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
555
+ readonly delivery: v.OptionalSchema<v.PicklistSchema<["delivered", "read"], undefined>, undefined>;
553
556
  readonly email: v.OptionalSchema<v.StrictObjectSchema<{
554
557
  readonly subject: v.NullableSchema<v.StringSchema<undefined>, undefined>;
555
558
  readonly from: v.NullableSchema<v.StrictObjectSchema<{
@@ -1028,6 +1031,7 @@ export declare const ENTITIES: {
1028
1031
  }, undefined>, undefined>, undefined>;
1029
1032
  readonly scheduled_for: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
1030
1033
  readonly starred: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
1034
+ readonly delivery: v.OptionalSchema<v.PicklistSchema<["delivered", "read"], undefined>, undefined>;
1031
1035
  readonly email: v.OptionalSchema<v.StrictObjectSchema<{
1032
1036
  readonly subject: v.NullableSchema<v.StringSchema<undefined>, undefined>;
1033
1037
  readonly from: v.NullableSchema<v.StrictObjectSchema<{
@@ -1208,6 +1212,7 @@ export declare const ENTITIES: {
1208
1212
  }, undefined>, undefined>, undefined>;
1209
1213
  readonly scheduled_for: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
1210
1214
  readonly starred: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
1215
+ readonly delivery: v.OptionalSchema<v.PicklistSchema<["delivered", "read"], undefined>, undefined>;
1211
1216
  readonly email: v.OptionalSchema<v.StrictObjectSchema<{
1212
1217
  readonly subject: v.NullableSchema<v.StringSchema<undefined>, undefined>;
1213
1218
  readonly from: v.NullableSchema<v.StrictObjectSchema<{
@@ -1448,7 +1453,9 @@ export declare const ENTITIES: {
1448
1453
  readonly last_message_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
1449
1454
  readonly participants_count: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
1450
1455
  readonly muted: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
1456
+ readonly muted_until: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
1451
1457
  readonly archived: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
1458
+ readonly marked_unread: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
1452
1459
  readonly membership_state: v.OptionalSchema<v.PicklistSchema<["joined", "left", "public", "imported", "archived", "external"], undefined>, undefined>;
1453
1460
  readonly parent_chat_id: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
1454
1461
  readonly scope: v.OptionalSchema<v.PicklistSchema<["personal", "work"], undefined>, undefined>;
@@ -1527,7 +1534,9 @@ export declare const ENTITIES: {
1527
1534
  readonly last_message_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
1528
1535
  readonly participants_count: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
1529
1536
  readonly muted: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
1537
+ readonly muted_until: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
1530
1538
  readonly archived: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
1539
+ readonly marked_unread: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
1531
1540
  readonly membership_state: v.OptionalSchema<v.PicklistSchema<["joined", "left", "public", "imported", "archived", "external"], undefined>, undefined>;
1532
1541
  readonly parent_chat_id: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
1533
1542
  readonly scope: v.OptionalSchema<v.PicklistSchema<["personal", "work"], undefined>, undefined>;
@@ -1575,7 +1584,9 @@ export declare const ENTITIES: {
1575
1584
  readonly last_message_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
1576
1585
  readonly participants_count: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
1577
1586
  readonly muted: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
1587
+ readonly muted_until: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
1578
1588
  readonly archived: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
1589
+ readonly marked_unread: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
1579
1590
  readonly membership_state: v.OptionalSchema<v.PicklistSchema<["joined", "left", "public", "imported", "archived", "external"], undefined>, undefined>;
1580
1591
  readonly parent_chat_id: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
1581
1592
  readonly scope: v.OptionalSchema<v.PicklistSchema<["personal", "work"], undefined>, undefined>;
@@ -2032,6 +2043,7 @@ export declare const ENTITIES: {
2032
2043
  readonly duration_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
2033
2044
  readonly timezone: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
2034
2045
  readonly participants_count: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
2046
+ readonly issues: v.OptionalSchema<v.ArraySchema<v.PicklistSchema<readonly ["time_without_zone"], undefined>, undefined>, undefined>;
2035
2047
  readonly created_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
2036
2048
  readonly updated_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
2037
2049
  }, undefined>;
@@ -2049,6 +2061,7 @@ export declare const ENTITIES: {
2049
2061
  readonly duration_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
2050
2062
  readonly timezone: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
2051
2063
  readonly participants_count: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
2064
+ readonly issues: v.OptionalSchema<v.ArraySchema<v.PicklistSchema<readonly ["time_without_zone"], undefined>, undefined>, undefined>;
2052
2065
  readonly created_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
2053
2066
  readonly updated_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
2054
2067
  readonly event_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
@@ -3372,6 +3385,7 @@ export declare const ENTITIES: {
3372
3385
  }, undefined>, undefined>, undefined>;
3373
3386
  readonly scheduled_for: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
3374
3387
  readonly starred: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
3388
+ readonly delivery: v.OptionalSchema<v.PicklistSchema<["delivered", "read"], undefined>, undefined>;
3375
3389
  readonly email: v.OptionalSchema<v.StrictObjectSchema<{
3376
3390
  readonly subject: v.NullableSchema<v.StringSchema<undefined>, undefined>;
3377
3391
  readonly from: v.NullableSchema<v.StrictObjectSchema<{
@@ -3552,6 +3566,7 @@ export declare const ENTITIES: {
3552
3566
  }, undefined>, undefined>, undefined>;
3553
3567
  readonly scheduled_for: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
3554
3568
  readonly starred: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
3569
+ readonly delivery: v.OptionalSchema<v.PicklistSchema<["delivered", "read"], undefined>, undefined>;
3555
3570
  readonly email: v.OptionalSchema<v.StrictObjectSchema<{
3556
3571
  readonly subject: v.NullableSchema<v.StringSchema<undefined>, undefined>;
3557
3572
  readonly from: v.NullableSchema<v.StrictObjectSchema<{
@@ -3770,6 +3785,7 @@ export declare const ENTITIES: {
3770
3785
  }, undefined>, undefined>, undefined>;
3771
3786
  readonly scheduled_for: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
3772
3787
  readonly starred: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
3788
+ readonly delivery: v.OptionalSchema<v.PicklistSchema<["delivered", "read"], undefined>, undefined>;
3773
3789
  readonly email: v.OptionalSchema<v.StrictObjectSchema<{
3774
3790
  readonly subject: v.NullableSchema<v.StringSchema<undefined>, undefined>;
3775
3791
  readonly from: v.NullableSchema<v.StrictObjectSchema<{
@@ -3950,6 +3966,7 @@ export declare const ENTITIES: {
3950
3966
  }, undefined>, undefined>, undefined>;
3951
3967
  readonly scheduled_for: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
3952
3968
  readonly starred: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
3969
+ readonly delivery: v.OptionalSchema<v.PicklistSchema<["delivered", "read"], undefined>, undefined>;
3953
3970
  readonly email: v.OptionalSchema<v.StrictObjectSchema<{
3954
3971
  readonly subject: v.NullableSchema<v.StringSchema<undefined>, undefined>;
3955
3972
  readonly from: v.NullableSchema<v.StrictObjectSchema<{
@@ -151,6 +151,8 @@ export declare const message: v.StrictObjectSchema<{
151
151
  readonly scheduled_for: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
152
152
  /** The owner starred it; only where the messenger has stars, and only when set. */
153
153
  readonly starred: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
154
+ /** The owner's own message in a one-to-one chat: `delivered`, or `read` once opened; absent when no receipt came. */
155
+ readonly delivery: v.OptionalSchema<v.PicklistSchema<["delivered", "read"], undefined>, undefined>;
154
156
  readonly email: v.OptionalSchema<v.StrictObjectSchema<{
155
157
  readonly subject: v.NullableSchema<v.StringSchema<undefined>, undefined>;
156
158
  readonly from: v.NullableSchema<v.StrictObjectSchema<{
@@ -64,6 +64,8 @@ export const message = v.strictObject({
64
64
  scheduled_for: v.optional(time),
65
65
  /** The owner starred it; only where the messenger has stars, and only when set. */
66
66
  starred: v.optional(v.literal(true)),
67
+ /** The owner's own message in a one-to-one chat: `delivered`, or `read` once opened; absent when no receipt came. */
68
+ delivery: v.optional(v.picklist(["delivered", "read"])),
67
69
  email: v.optional(v.strictObject({
68
70
  subject: v.nullable(text),
69
71
  from: v.nullable(mailPerson),
@@ -27,6 +27,7 @@ const OTHERS = [
27
27
  validation("file.locked", "another process holds the file's lock"),
28
28
  ["profile.wrong-account", "authentication_error", "the signed-in source account does not match this profile"],
29
29
  validation("store.refused", "the local store refused the document text"),
30
+ ["store.not-writable", "permission_error", "the message store cannot be written from this process"],
30
31
  validation("catalog.scope-limit", "the selected catalog exceeds its entry limit"),
31
32
  validation("evidence.row-too-large", "one evidence row is over its size limit"),
32
33
  validation("index.not-ready", "the search index is still being built"),
@@ -77,6 +77,7 @@ export declare const meetingDetails: v.StrictObjectSchema<{
77
77
  readonly duration_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
78
78
  readonly timezone: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
79
79
  readonly participants_count: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
80
+ readonly issues: v.OptionalSchema<v.ArraySchema<v.PicklistSchema<readonly ["time_without_zone"], undefined>, undefined>, undefined>;
80
81
  readonly created_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
81
82
  readonly updated_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
82
83
  /** The stored event and series it belongs to, as `meetings_list` takes them. */
@@ -15,6 +15,7 @@ export const importFiles = async (files, store, now = Date.now()) => {
15
15
  participants: file.participants,
16
16
  transcripts: [await transcriptInput(lines, participants, file.source ?? "file", file.content, format)],
17
17
  now,
18
+ fromFile: true,
18
19
  });
19
20
  await linkEvent(store, details, now);
20
21
  report.meetings++;
@@ -8,7 +8,7 @@ export { type FindingEvidence, type FindingOutcome, findingHash, MEETING_FINDING
8
8
  export { type Finding, type FindingsInput, type FindingsReceipt, type FindingsRules, findingFingerprint, validateFindings, } from "./findings-rules.js";
9
9
  export { MEETING_HIT_QUERY, meetingWordsQuery, parseMeetingQuery } from "./hit-query.js";
10
10
  export { type ImportFile, importFiles } from "./import.js";
11
- export { MEETING_QUERY, type MeetingItem, type MeetingOwner, meeting, meetingItem } from "./item.js";
11
+ export { MEETING_ISSUES, MEETING_QUERY, type MeetingIssue, type MeetingItem, type MeetingOwner, meeting, meetingIssuesOf, meetingItem, } from "./item.js";
12
12
  export { createMeetingContext, MEETING_FACT_TYPES, type MeetingContextAction, type MeetingContextChange, type MeetingContextOperation, type MeetingContextPorts, type MeetingContextReceipt, validateMeetingContextChange, } from "./meeting-context.js";
13
13
  export { createMeetingManagement, dispatchMeetingMutation, type ManagedMeeting, type ManagedMeetingChange, type MeetingManagement, type MeetingManagementPorts, type MeetingMutationOptions, type MeetingMutationProposal, type MeetingMutationReceipt, type MeetingOperationOptions, type MeetingProviderContext, validateMeetingChange, } from "./meeting-management.js";
14
14
  export type { Attachment, ChatLine, Event, EventSeries, JsonValue, Meeting, MeetingSeries, Metadata, Occurrence, Participant, RecordTimes, SourceChatLine, SourceMeeting, Summary, SummaryContent, Transcript, TranscriptLine, TranscriptRow, } from "./model.js";
@@ -6,7 +6,7 @@ export { findingHash, MEETING_FINDINGS_KIND, meetingFindingFingerprint, validate
6
6
  export { findingFingerprint, validateFindings, } from "./findings-rules.js";
7
7
  export { MEETING_HIT_QUERY, meetingWordsQuery, parseMeetingQuery } from "./hit-query.js";
8
8
  export { importFiles } from "./import.js";
9
- export { MEETING_QUERY, meeting, meetingItem } from "./item.js";
9
+ export { MEETING_ISSUES, MEETING_QUERY, meeting, meetingIssuesOf, meetingItem, } from "./item.js";
10
10
  export { createMeetingContext, MEETING_FACT_TYPES, validateMeetingContextChange, } from "./meeting-context.js";
11
11
  export { createMeetingManagement, dispatchMeetingMutation, validateMeetingChange, } from "./meeting-management.js";
12
12
  export { normalizeMeetingParticipantQuery, validateMeetingParticipantSearch, } from "./participant-search.js";
@@ -1,5 +1,13 @@
1
1
  import * as v from "valibot";
2
2
  import type { Meeting } from "./model.js";
3
+ /**
4
+ * Why a stored meeting's facts may be off, kept so the data stays but a reader knows to doubt it.
5
+ * `time_without_zone`: the provider sent a time with no zone, and it was read in the computer's zone.
6
+ */
7
+ export declare const MEETING_ISSUES: readonly ["time_without_zone"];
8
+ export type MeetingIssue = (typeof MEETING_ISSUES)[number];
9
+ /** The issues a provider adapter put in `metadata.issues`, the known ones only, in their listed order. */
10
+ export declare const meetingIssuesOf: (metadata: Meeting["metadata"]) => MeetingIssue[];
3
11
  /** A stored meeting as every command and tool answers it; `src/entities/` registers it as `meeting`. */
4
12
  export declare const meeting: v.StrictObjectSchema<{
5
13
  readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
@@ -16,6 +24,8 @@ export declare const meeting: v.StrictObjectSchema<{
16
24
  readonly duration_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
17
25
  readonly timezone: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
18
26
  readonly participants_count: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
27
+ /** Present only when something about this meeting is doubtful. */
28
+ readonly issues: v.OptionalSchema<v.ArraySchema<v.PicklistSchema<readonly ["time_without_zone"], undefined>, undefined>, undefined>;
19
29
  readonly created_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
20
30
  readonly updated_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
21
31
  }, undefined>;
@@ -4,6 +4,16 @@ import * as v from "valibot";
4
4
  import { formatMeetingReference } from "./reference.js";
5
5
  const text = v.string();
6
6
  const count = v.pipe(v.number(), v.integer(), v.minValue(0));
7
+ /**
8
+ * Why a stored meeting's facts may be off, kept so the data stays but a reader knows to doubt it.
9
+ * `time_without_zone`: the provider sent a time with no zone, and it was read in the computer's zone.
10
+ */
11
+ export const MEETING_ISSUES = ["time_without_zone"];
12
+ /** The issues a provider adapter put in `metadata.issues`, the known ones only, in their listed order. */
13
+ export const meetingIssuesOf = (metadata) => {
14
+ const listed = metadata?.issues;
15
+ return Array.isArray(listed) ? MEETING_ISSUES.filter((issue) => listed.includes(issue)) : [];
16
+ };
7
17
  /** A stored meeting as every command and tool answers it; `src/entities/` registers it as `meeting`. */
8
18
  export const meeting = v.strictObject({
9
19
  ref: reference,
@@ -20,33 +30,39 @@ export const meeting = v.strictObject({
20
30
  duration_ms: v.nullable(count),
21
31
  timezone: v.optional(v.nullable(text)),
22
32
  participants_count: v.nullable(count),
33
+ /** Present only when something about this meeting is doubtful. */
34
+ issues: v.optional(v.array(v.picklist(MEETING_ISSUES))),
23
35
  created_at: isoTime,
24
36
  updated_at: isoTime,
25
37
  });
26
38
  const iso = (ms) => (ms === null ? null : new Date(ms).toISOString());
27
- export const meetingItem = (row, owner) => ({
28
- ref: formatMeetingReference({ type: "meeting", accountId: row.accountId, meetingId: row.id }),
29
- id: String(row.id),
30
- provider: owner.provider,
31
- account: owner.account,
32
- title: row.title,
33
- description: row.description,
34
- location: row.location,
35
- url: row.joinUrl,
36
- started_at: iso(row.startedAt),
37
- ended_at: iso(row.endedAt),
38
- duration_ms: row.durationMs,
39
- timezone: row.timezone,
40
- participants_count: row.participantsCount,
41
- created_at: new Date(row.createdAt).toISOString(),
42
- updated_at: new Date(row.updatedAt).toISOString(),
43
- });
39
+ export const meetingItem = (row, owner) => {
40
+ const issues = meetingIssuesOf(row.metadata);
41
+ return {
42
+ ref: formatMeetingReference({ type: "meeting", accountId: row.accountId, meetingId: row.id }),
43
+ id: String(row.id),
44
+ provider: owner.provider,
45
+ account: owner.account,
46
+ title: row.title,
47
+ description: row.description,
48
+ location: row.location,
49
+ url: row.joinUrl,
50
+ started_at: iso(row.startedAt),
51
+ ended_at: iso(row.endedAt),
52
+ duration_ms: row.durationMs,
53
+ timezone: row.timezone,
54
+ participants_count: row.participantsCount,
55
+ ...(issues.length ? { issues } : {}),
56
+ created_at: new Date(row.createdAt).toISOString(),
57
+ updated_at: new Date(row.updatedAt).toISOString(),
58
+ };
59
+ };
44
60
  /** `meetings list` fields: what the meetings table holds of the meeting itself. */
45
61
  export const MEETING_QUERY = {
46
62
  entity: "meeting",
47
63
  backend: "sql",
48
64
  fields: queryFieldsOf(meeting, {
49
- exclude: ["ref", "provider", "account", "description", "location", "url", "timezone"],
65
+ exclude: ["ref", "provider", "account", "description", "location", "url", "timezone", "issues"],
50
66
  examples: {
51
67
  id: "id:12",
52
68
  title: "title:*standup*",
@@ -36,6 +36,12 @@ export interface MeetingSave {
36
36
  summaries?: Omit<NewRecord<Summary>, "meetingId">[];
37
37
  attachments?: Omit<NewRecord<Attachment>, "attachableType" | "attachableId">[];
38
38
  now: number;
39
+ /**
40
+ * A copy of the occurrence read from a file, not from the provider. It joins the provider's record of the same
41
+ * occurrence — same series, start within 15 minutes — and only fills a missing transcript there; a provider
42
+ * record saved later takes over a record made from files. Transcripts from files have source `file`.
43
+ */
44
+ fromFile?: boolean;
39
45
  }
40
46
  export interface MeetingDetails {
41
47
  meeting: Meeting;
@@ -3,7 +3,7 @@ import type { CounterField, CounterObservations } from "../domain/counters.js";
3
3
  import type { HtmlFormatting, MarkdownFormatting, TextSpan } from "../domain/formatting.js";
4
4
  import type { Markup } from "../domain/markdown.js";
5
5
  import type { MessagePermalink } from "../domain/message-link.js";
6
- import type { Account, AccountSession, AdminRight, Button, CallRecord, Chat, ChatCard, ChatEvent, ChatEvents, ChatUpdate, Discussion, Folder, FolderChange, FolderRules, GroupCard, GroupChange, GroupMember, Id, InviteLink, InviteLinkChange, JoinRequest, LinkTarget, MediaKind, Member, Message, MessageEvent, MessageHit, OfficialChatStats, Page, PersonCard, PhoneBookEntry, Poll, PollVote, PrivacySettings, ProfileFacts, ProviderMetadata, SenderIdentity, Sticker, StickerSet, Topic, TopicChange, WindowedMessage } from "../domain/models.js";
6
+ import type { Account, AccountSession, AdminRight, Button, CallRecord, Chat, ChatCard, ChatEvent, ChatEvents, ChatUpdate, Discussion, Folder, FolderChange, FolderRules, GroupCard, GroupChange, GroupMember, Id, InviteLink, InviteLinkChange, JoinRequest, LinkTarget, MediaKind, Member, Message, MessageEvent, MessageHit, OfficialChatStats, Page, PersonCard, PersonReaction, PhoneBookEntry, Poll, PollVote, PrivacySettings, ProfileFacts, ProviderMetadata, SenderIdentity, Sticker, StickerSet, Topic, TopicChange, WindowedMessage } from "../domain/models.js";
7
7
  /** One attachment's bytes, fetched over the adapter's connection when `bytes` is read — so read it before the command closes. */
8
8
  export interface RemoteFile {
9
9
  kind: string;
@@ -331,6 +331,12 @@ export interface HistoryVote {
331
331
  messageId: Id;
332
332
  vote: PollVote;
333
333
  }
334
+ /** One person's reaction to message `messageId` of `chatId`, as `PersonReactionEvent` has it. */
335
+ export interface HistoryReaction {
336
+ chatId: Id;
337
+ messageId: Id;
338
+ reaction: PersonReaction;
339
+ }
334
340
  /** One change to who is in chat `chatId`, or to its title, as `ChatEventNotice` has it. */
335
341
  export interface HistoryChatEvent {
336
342
  chatId: Id;
@@ -344,8 +350,8 @@ export interface HistoryStar {
344
350
  }
345
351
  /**
346
352
  * What a messenger pushed in one go: any of chats, the people in them, past messages of any chats, votes in
347
- * their polls, the owner's stars, and changes to who is in them. A vote or a star is saved with its chat's messages,
348
- * after them, so a message in the same batch is held before its votes and its star.
353
+ * their polls, each person's reactions, the owner's stars, and changes to who is in them. A vote, a reaction or a star
354
+ * is saved with its chat's messages, after them, so a message in the same batch is held before them.
349
355
  * `rekeys` are chats the messenger now names by another id; they apply first, so the batch's messages under the
350
356
  * new id join the chat already stored under the old one.
351
357
  */
@@ -358,6 +364,7 @@ export interface HistoryBatch {
358
364
  people?: Member[];
359
365
  messages?: Message[];
360
366
  votes?: HistoryVote[];
367
+ reactions?: HistoryReaction[];
361
368
  stars?: HistoryStar[];
362
369
  events?: HistoryChatEvent[];
363
370
  }
@@ -72,6 +72,12 @@ export interface MessengerTraits {
72
72
  * adapter's `history` — `messages sync <chat>`. `--all` stays refused: there is no chat list to walk. No when unset.
73
73
  */
74
74
  historyOnDemand?: boolean;
75
+ /**
76
+ * Who keeps a chat's unread count. `messenger` when unset: the store holds what the messenger last said. `store`: the
77
+ * messenger says it only now and then (a history push, a read on the phone), so each message from someone else that
78
+ * `watch` or `listen` saves for the first time, sent after the last count, adds one.
79
+ */
80
+ unreadCounts?: "messenger" | "store";
75
81
  /**
76
82
  * Whether `search messages --backend` is offered: the adapter has `searchMessages`. `chat` when its server
77
83
  * searches one chat at a time, so only a query that names one chat asks it. No when unset.
package/dist/resolve.d.ts CHANGED
@@ -7,6 +7,12 @@ export interface PeopleLookup {
7
7
  }
8
8
  /** Shaped like a Telegram or MAX id. Another messenger's ids may be any string, so a miss here is not "not an id". */
9
9
  export declare const isId: (reference: string) => boolean;
10
+ /**
11
+ * A name as matching compares it. A copied emoji often carries a presentation mark (U+FE0F, U+FE0E) or a joiner
12
+ * (U+200D) the typed one lacks, and a letter may come composed or as letter plus accent; none of that is a different
13
+ * name.
14
+ */
15
+ export declare const foldName: (name: string) => string;
10
16
  /**
11
17
  * A name matched as a fragment — and **an ambiguous one is an error, not a guess**: sending to the
12
18
  * wrong conversation is not undoable, so the caller is shown the candidates and asked to be
package/dist/resolve.js CHANGED
@@ -1,6 +1,15 @@
1
1
  import { CliError, singleLine } from "@wirecat/cli-base";
2
2
  /** Shaped like a Telegram or MAX id. Another messenger's ids may be any string, so a miss here is not "not an id". */
3
3
  export const isId = (reference) => /^-?\d+$/.test(reference.trim());
4
+ /**
5
+ * A name as matching compares it. A copied emoji often carries a presentation mark (U+FE0F, U+FE0E) or a joiner
6
+ * (U+200D) the typed one lacks, and a letter may come composed or as letter plus accent; none of that is a different
7
+ * name.
8
+ */
9
+ export const foldName = (name) => name
10
+ .normalize("NFC")
11
+ .replace(/[\uFE0E\uFE0F\u200D]/g, "")
12
+ .toLowerCase();
4
13
  /**
5
14
  * A name matched as a fragment — and **an ambiguous one is an error, not a guess**: sending to the
6
15
  * wrong conversation is not undoable, so the caller is shown the candidates and asked to be
@@ -8,8 +17,8 @@ export const isId = (reference) => /^-?\d+$/.test(reference.trim());
8
17
  * "Мама" must not quietly take a send meant for "Мама Иванова".
9
18
  */
10
19
  export const pickChat = (reference, chats) => {
11
- const wanted = reference.trim().toLowerCase();
12
- const matches = chats.filter((chat) => chat.title?.toLowerCase().includes(wanted));
20
+ const wanted = foldName(reference.trim());
21
+ const matches = chats.filter((chat) => chat.title !== null && foldName(chat.title).includes(wanted));
13
22
  if (matches.length === 1 && matches[0])
14
23
  return matches[0];
15
24
  if (matches.length === 0)
@@ -27,9 +36,9 @@ export const pickPerson = (reference, people) => {
27
36
  return known;
28
37
  if (isId(trimmed))
29
38
  throw new CliError("not_found", `no person ${trimmed} in what this account has seen`);
30
- const wanted = trimmed.replace(/^@/, "").toLowerCase();
39
+ const wanted = foldName(trimmed.replace(/^@/, ""));
31
40
  const everyone = people.all().toSorted((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
32
- const fields = (person) => [person.alias, person.name, person.username].filter(isPresent).map((one) => one.toLowerCase());
41
+ const fields = (person) => [person.alias, person.name, person.username].filter(isPresent).map(foldName);
33
42
  const exact = everyone.filter((person) => fields(person).includes(wanted));
34
43
  const matches = exact.length > 0 ? exact : everyone.filter((person) => fields(person).some((one) => one.includes(wanted)));
35
44
  if (matches.length === 1 && matches[0])
@@ -13,7 +13,8 @@ export declare const plainMatch: (probe: GarbledProbe, word: string, stems: stri
13
13
  * stemmer cut differently: a stem one side extends counts only when the other side's word spells the extension
14
14
  * out — «циана» (stem `циа`) reaches «цианом» (`циан`), «устав» (`уста`) does not reach «установить» (`установ`).
15
15
  * `1` is a spelling recognition may have garbled, compared by stem: «рилторов» is three edits from «риелтор»
16
- * as a word but one as a stem.
16
+ * as a word but one as a stem; or a word recognition cut off, of four letters or more, that begins the query word
17
+ * («може» for «может»), as ztx matched.
17
18
  */
18
19
  export declare const garbledDistance: (probe: GarbledProbe, word: string, stems: string[]) => number | null;
19
20
  /**
@@ -34,7 +34,8 @@ export const plainMatch = (probe, word, stems) => word.startsWith(probe.literal)
34
34
  * stemmer cut differently: a stem one side extends counts only when the other side's word spells the extension
35
35
  * out — «циана» (stem `циа`) reaches «цианом» (`циан`), «устав» (`уста`) does not reach «установить» (`установ`).
36
36
  * `1` is a spelling recognition may have garbled, compared by stem: «рилторов» is three edits from «риелтор»
37
- * as a word but one as a stem.
37
+ * as a word but one as a stem; or a word recognition cut off, of four letters or more, that begins the query word
38
+ * («може» for «может»), as ztx matched.
38
39
  */
39
40
  export const garbledDistance = (probe, word, stems) => {
40
41
  if (probe.literal.length < SHORTEST_GARBLED || plainMatch(probe, word, stems))
@@ -59,6 +60,8 @@ export const garbledDistance = (probe, word, stems) => {
59
60
  if (distance <= 1)
60
61
  best = distance;
61
62
  }
63
+ if (best === null && stored.length >= SHORTEST_GARBLED && literal.startsWith(stored))
64
+ return 1;
62
65
  return best;
63
66
  };
64
67
  /** `editDistance` from `stem` to the nearest beginning of `word`, or `max + 1` once every beginning is further. */
@@ -88,6 +91,8 @@ const prefixDistance = (stem, word, max) => {
88
91
  */
89
92
  const worthStemming = (probe, word) => {
90
93
  const stored = plain(word);
94
+ if (stored.length >= SHORTEST_GARBLED && plain(probe.literal).startsWith(stored))
95
+ return true;
91
96
  return probe.stems.some((raw) => {
92
97
  const stem = plain(raw);
93
98
  if (stem.length >= 3 && stored.startsWith(stem))
@@ -9,7 +9,7 @@ import { codeOf, guardedWrite } from "../sends/guarded.js";
9
9
  import { newOperationId } from "../sends/send-id.js";
10
10
  import { CHAT_COLUMNS, queryFilter, } from "../store/store.js";
11
11
  import { chatStats } from "./chat-stats.js";
12
- import { accountStore, accountStoreIfOpen, fromStore, nothingStored } from "./deps.js";
12
+ import { accountStore, accountStoreIfOpen, fromStore, nothingStored, unlessBusy } from "./deps.js";
13
13
  import { memberEvents } from "./member-history.js";
14
14
  import { AUDIT_BUDGET, AUDIT_MIN_SCORE, AUDIT_PAGE, AUDIT_PAUSE_MS, auditMembers, } from "./members-audit.js";
15
15
  import { chatIdIn } from "./message-resolution.js";
@@ -249,7 +249,7 @@ export const chatsService = (deps) => ({
249
249
  const markTopicRead = capability(connection, "markTopicRead", "mark a forum topic read");
250
250
  act = (chatId) => markTopicRead(chatId, threadId, until);
251
251
  }
252
- const { id: chatId } = await connection.resolve(chat);
252
+ const { id: chatId, kind } = await connection.resolve(chat);
253
253
  const operationId = newOperationId();
254
254
  await guardedWrite(deps.guard, {
255
255
  operationId,
@@ -258,9 +258,23 @@ export const chatsService = (deps) => ({
258
258
  ...(until === undefined ? {} : { messageId: until }),
259
259
  ...(threadId === undefined ? {} : { threadId }),
260
260
  }, () => act(chatId));
261
+ if (deps.reads === "store" && threadId === undefined)
262
+ await readInStore(deps, chatId, kind, until);
261
263
  return { operationId, chatId, until: until ?? null, ...(threadId === undefined ? {} : { threadId }) };
262
264
  },
263
265
  });
266
+ /**
267
+ * Where `inbox` reads the store, the store learns of a read the owner made here at once: the messenger may never say.
268
+ * A read up to an older message from someone else leaves the count, which the store cannot split.
269
+ */
270
+ const readInStore = async (deps, chatId, kind, until) => {
271
+ const store = await accountStoreIfOpen(deps);
272
+ if (!store)
273
+ return;
274
+ const [newest] = (await store.messages.messages(chatId, { limit: 1, outgoing: false })).items;
275
+ if (newest !== undefined && (until === undefined || newest.id === until))
276
+ await unlessBusy(() => store.chats.saveChats([{ id: chatId, kind, unreadCount: 0, markedUnread: false }]));
277
+ };
264
278
  /** Each sender's first stored message in the chat; `undefined` when the store holds none of it. */
265
279
  const storedFacts = async (store, chatId) => {
266
280
  const chat = (await store.chats.chats({})).items.find((one) => one.id === chatId);
@@ -52,6 +52,8 @@ export declare const accountStore: (deps: Pick<ServiceDeps, "store" | "account">
52
52
  * `reason` says which, so every such read is found by searching for this name.
53
53
  */
54
54
  export declare const crossAccount: (deps: Pick<ServiceDeps, "store">, _reason: string) => Promise<MessageStore>;
55
+ /** A store write after the messenger already did the work: a store busy past its timeout leaves it unwritten. */
56
+ export declare const unlessBusy: <T>(work: () => Promise<T>) => Promise<T | undefined>;
55
57
  /** `accountStore`, or nothing when there is no store to read or the account is not known yet. */
56
58
  export declare const accountStoreIfOpen: (deps: ServiceDeps) => Promise<AccountStore | undefined>;
57
59
  /**
@@ -33,6 +33,8 @@ const unless = async (work, expected) => {
33
33
  throw error;
34
34
  }
35
35
  };
36
+ /** A store write after the messenger already did the work: a store busy past its timeout leaves it unwritten. */
37
+ export const unlessBusy = (work) => unless(work, storeAbsent);
36
38
  /** `accountStore`, or nothing when there is no store to read or the account is not known yet. */
37
39
  export const accountStoreIfOpen = async (deps) => {
38
40
  const held = await storeIfOpen(deps);
@@ -39,6 +39,7 @@ export declare const capped: (chats: Chat[], most: number) => {
39
39
  * Other people's unread messages, as the messenger counts them: for each chat with a count, its
40
40
  * newest that many. Reading marks nothing read, so the same messages come back until they are read
41
41
  * somewhere else — right for a person, wrong for a scheduled run, which is what `since` is for.
42
+ * A chat the owner marked unread is there with no count too, with its newest message from someone else.
42
43
  *
43
44
  * Muted and archived chats are left out unless `all`, or they mention the owner: on a busy account
44
45
  * they are most of what is unread, and they would take the history reads a person's chat needs.
@@ -15,8 +15,8 @@ export const REVIEW_DAYS = 3;
15
15
  export const UNANSWERED_HOURS = 24;
16
16
  export const reviewStart = (now = Date.now()) => now - REVIEW_DAYS * 86_400_000;
17
17
  export const byRecency = (chats) => chats.toSorted((a, b) => Date.parse(b.lastMessageAt ?? "") - Date.parse(a.lastMessageAt ?? ""));
18
- /** Muted or archived, and nothing in it mentions the owner or replies to them. */
19
- const isQuiet = (chat) => (chat.muted === true || chat.archived === true) && (chat.unreadMentions ?? 0) === 0;
18
+ /** Muted or archived, and nothing in it mentions the owner or replies to them; one the owner marked unread never is. */
19
+ const isQuiet = (chat) => (chat.muted === true || chat.archived === true) && (chat.unreadMentions ?? 0) === 0 && chat.markedUnread !== true;
20
20
  export const heard = (chats, all) => all
21
21
  ? { heard: chats, quiet: 0 }
22
22
  : { heard: chats.filter((chat) => !isQuiet(chat)), quiet: chats.filter(isQuiet).length };
@@ -75,17 +75,24 @@ export const capped = (chats, most) => ({
75
75
  * Other people's unread messages, as the messenger counts them: for each chat with a count, its
76
76
  * newest that many. Reading marks nothing read, so the same messages come back until they are read
77
77
  * somewhere else — right for a person, wrong for a scheduled run, which is what `since` is for.
78
+ * A chat the owner marked unread is there with no count too, with its newest message from someone else.
78
79
  *
79
80
  * Muted and archived chats are left out unless `all`, or they mention the owner: on a busy account
80
81
  * they are most of what is unread, and they would take the history reads a person's chat needs.
81
82
  */
82
83
  export const unreadIn = async (adapter, { limit, all = false, kinds }) => {
83
84
  const page = await everyChat(adapter);
84
- const { heard: waiting, quiet } = heard(byRecency(ofKinds(page.items, kinds).filter((chat) => (chat.unreadCount ?? 0) > 0)), all);
85
+ const { heard: waiting, quiet } = heard(byRecency(ofKinds(page.items, kinds).filter((chat) => (chat.unreadCount ?? 0) > 0 || chat.markedUnread === true)), all);
85
86
  const { read, skipped } = capped(waiting, INBOX_CHATS);
86
87
  const chats = [];
87
88
  for (const { id, title, kind, unreadCount } of read) {
88
89
  const count = unreadCount ?? 0;
90
+ if (count === 0) {
91
+ const newest = (await adapter.history(id, { limit })).items.findLast((message) => !message.outgoing);
92
+ if (newest)
93
+ chats.push({ id, title, kind, unreadCount, messages: [newest], more: false });
94
+ continue;
95
+ }
89
96
  const wanted = Math.min(count, limit);
90
97
  const { items } = await adapter.history(id, { limit: wanted });
91
98
  const theirs = items.slice(-wanted).filter((message) => !message.outgoing);
@@ -10,7 +10,7 @@ export declare const keysOver: (context: StoreContext) => {
10
10
  chatPkFor: (accountKey: number, chatId: Id) => number;
11
11
  answeredChatPk: (accountKey: number, chatId: Id, typed: Id) => number;
12
12
  identityPk: (accountKey: number, provider: Provider, nativeId: Id, name: string | null, facts?: Omit<PersonFacts, "id" | "name">) => number;
13
- upsertMessage: (key: AccountKey, accountKey: number, chatKey: number, message: Message, via: string, seenAt?: number) => void;
13
+ upsertMessage: (key: AccountKey, accountKey: number, chatKey: number, message: Message, via: string, seenAt?: number) => boolean;
14
14
  writeMembers: (key: AccountKey, accountKey: number, chatId: Id, memberIds: Id[]) => void;
15
15
  writeState: (accountKey: number, name: string, value: string) => void;
16
16
  };