@wirecat/cli-core 0.381.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.
- package/CHANGELOG.md +30 -1
- package/dist/cli/bot/watch.js +3 -0
- package/dist/cli/messenger/chat-answers.d.ts +2 -0
- package/dist/cli/messenger/chat-answers.js +2 -0
- package/dist/cli/messenger/chat-lists.d.ts +2 -0
- package/dist/cli/messenger/context.d.ts +2 -2
- package/dist/cli/messenger/context.js +2 -1
- package/dist/cli/messenger/conversation-answers.d.ts +1 -0
- package/dist/cli/messenger/message-answers.d.ts +1 -0
- package/dist/cli/messenger/message-items.js +1 -0
- package/dist/cli/messenger/message-writes.d.ts +1 -0
- package/dist/cli/messenger/serve-relay.d.ts +1 -0
- package/dist/cli/messenger/stored.d.ts +3 -1
- package/dist/cli/messenger/stored.js +12 -5
- package/dist/cli/messenger/watch-command.js +2 -0
- package/dist/domain/models.d.ts +31 -4
- package/dist/entities/chat.d.ts +4 -0
- package/dist/entities/chat.js +4 -0
- package/dist/entities/contact.d.ts +4 -0
- package/dist/entities/evidence.d.ts +8 -0
- package/dist/entities/index.d.ts +17 -0
- package/dist/entities/message.d.ts +2 -0
- package/dist/entities/message.js +2 -0
- package/dist/meetings/answers.d.ts +1 -0
- package/dist/meetings/index.d.ts +1 -1
- package/dist/meetings/index.js +1 -1
- package/dist/meetings/item.d.ts +10 -0
- package/dist/meetings/item.js +34 -18
- package/dist/ports/traits.d.ts +6 -0
- package/dist/services/chats.js +16 -2
- package/dist/services/deps.d.ts +2 -0
- package/dist/services/deps.js +2 -0
- package/dist/services/inbox.d.ts +1 -0
- package/dist/services/inbox.js +10 -3
- package/dist/store/keys.d.ts +1 -1
- package/dist/store/messages.d.ts +7 -1
- package/dist/store/messages.js +16 -3
- package/dist/store/sqlite/chat-rekey.js +9 -1
- package/dist/store/sqlite/chats.d.ts +2 -2
- package/dist/store/sqlite/chats.js +37 -8
- package/dist/store/sqlite/manifest.js +2 -0
- package/dist/store/sqlite/meeting-hit-query.js +18 -3
- package/dist/store/sqlite/messages.d.ts +9 -2
- package/dist/store/sqlite/messages.js +29 -2
- package/dist/store/sqlite/migrations.generated.js +16 -0
- package/dist/store/sqlite/reads.d.ts +50 -0
- package/dist/store/sqlite/reads.js +3 -1
- package/dist/store/sqlite/schema.d.ts +96 -0
- package/dist/store/sqlite/schema.js +6 -0
- package/dist/store/sqlite/search.d.ts +33 -0
- package/dist/store/sqlite/values.d.ts +4 -0
- package/dist/store/sqlite/values.js +3 -0
- package/package.json +1 -1
package/dist/meetings/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/meetings/index.js
CHANGED
|
@@ -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";
|
package/dist/meetings/item.d.ts
CHANGED
|
@@ -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>;
|
package/dist/meetings/item.js
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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*",
|
package/dist/ports/traits.d.ts
CHANGED
|
@@ -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/services/chats.js
CHANGED
|
@@ -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);
|
package/dist/services/deps.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/services/deps.js
CHANGED
|
@@ -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);
|
package/dist/services/inbox.d.ts
CHANGED
|
@@ -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.
|
package/dist/services/inbox.js
CHANGED
|
@@ -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);
|
package/dist/store/keys.d.ts
CHANGED
|
@@ -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) =>
|
|
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
|
};
|
package/dist/store/messages.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CounterObservations } from "../domain/counters.js";
|
|
2
|
-
import type { Id, Message, Page, PersonReaction, PollVote, Reactions, WindowedMessage } from "../domain/models.js";
|
|
2
|
+
import type { Delivery, Id, Message, Page, PersonReaction, PollVote, Reactions, WindowedMessage } from "../domain/models.js";
|
|
3
3
|
import { type DocumentLinkMessage } from "./sqlite/document-links.js";
|
|
4
4
|
import type { StoreContext } from "./sqlite/open.js";
|
|
5
5
|
import type { AccountKey, DeletionScope, IngestedVia, MessageFilter, SenderChatStats, StoredHit } from "./types.js";
|
|
@@ -15,6 +15,9 @@ export interface AccountMessages {
|
|
|
15
15
|
*
|
|
16
16
|
* `typedChatId` is the id the owner wrote to when the messenger answered `chatId`: a chat row made for `chatId`
|
|
17
17
|
* takes the typed row's title, kind and privacy, and is never more visible than it.
|
|
18
|
+
*
|
|
19
|
+
* `countUnread` raises the chat's unread count by each message from someone else stored for the first time and sent
|
|
20
|
+
* after the messenger's count was taken — for a messenger whose store counts (`MessengerTraits.unreadCounts`).
|
|
18
21
|
*/
|
|
19
22
|
saveMessages(key: AccountKey, chatId: Id, messages: Message[], options: {
|
|
20
23
|
via: IngestedVia;
|
|
@@ -32,6 +35,7 @@ export interface AccountMessages {
|
|
|
32
35
|
starred: boolean;
|
|
33
36
|
}[];
|
|
34
37
|
typedChatId?: Id;
|
|
38
|
+
countUnread?: boolean;
|
|
35
39
|
}): Promise<void>;
|
|
36
40
|
/** Oldest to newest, like a provider's history page. */
|
|
37
41
|
/**
|
|
@@ -108,6 +112,8 @@ export interface AccountMessages {
|
|
|
108
112
|
saveReaction(key: AccountKey, chatId: Id, messageId: Id, reaction: PersonReaction): Promise<void>;
|
|
109
113
|
/** The owner's star on a message, or its removal; answers whether the message is held at all. */
|
|
110
114
|
saveStar(key: AccountKey, chatId: Id, messageId: Id, starred: boolean): Promise<boolean>;
|
|
115
|
+
/** A receipt for the owner's own message; the state never moves back. Answers whether such a message is held. */
|
|
116
|
+
saveDelivery(key: AccountKey, chatId: Id, messageId: Id, delivery: Delivery): Promise<boolean>;
|
|
111
117
|
/** One voter's vote in the poll a message carries, unless the store holds a newer one of theirs; no answers is a take-back. */
|
|
112
118
|
saveVote(key: AccountKey, chatId: Id, messageId: Id, vote: PollVote): Promise<void>;
|
|
113
119
|
/** Each voter's newest vote in the poll a message carries, newest first. */
|
package/dist/store/messages.js
CHANGED
|
@@ -11,12 +11,14 @@ export const accountMessagesOver = (context) => {
|
|
|
11
11
|
const { database } = context;
|
|
12
12
|
const { accountPk, findAccountPk, findChatPk, chatKeyOf, chatPkFor, answeredChatPk, upsertMessage } = keysOver(context);
|
|
13
13
|
return {
|
|
14
|
-
saveMessages: async (key, chatId, messages, { via, seenAt, votes = [], reactions = [], stars = [], typedChatId }) => write(database, () => {
|
|
14
|
+
saveMessages: async (key, chatId, messages, { via, seenAt, votes = [], reactions = [], stars = [], typedChatId, countUnread = false }) => write(database, () => {
|
|
15
15
|
const accountKey = accountPk(key);
|
|
16
16
|
const chatKey = typedChatId === undefined ? chatPkFor(accountKey, chatId) : answeredChatPk(accountKey, chatId, typedChatId);
|
|
17
17
|
for (const message of messages) {
|
|
18
|
-
if (message.scheduledFor
|
|
19
|
-
|
|
18
|
+
if (message.scheduledFor !== undefined)
|
|
19
|
+
continue;
|
|
20
|
+
if (upsertMessage(key, accountKey, chatKey, message, via, seenAt) && countUnread)
|
|
21
|
+
messageWrites.countUnread(context, chatKey, message);
|
|
20
22
|
}
|
|
21
23
|
for (const { messageId, vote } of votes)
|
|
22
24
|
messageWrites.saveVote(context, accountKey, key.provider, chatKey, messageId, vote);
|
|
@@ -85,6 +87,17 @@ export const accountMessagesOver = (context) => {
|
|
|
85
87
|
});
|
|
86
88
|
return saved;
|
|
87
89
|
},
|
|
90
|
+
saveDelivery: async (key, chatId, messageId, delivery) => {
|
|
91
|
+
const accountKey = findAccountPk(key);
|
|
92
|
+
const chatKey = accountKey === undefined ? undefined : findChatPk(accountKey, chatId);
|
|
93
|
+
if (chatKey === undefined)
|
|
94
|
+
return false;
|
|
95
|
+
let saved = false;
|
|
96
|
+
write(database, () => {
|
|
97
|
+
saved = messageWrites.saveDelivery(context, chatKey, messageId, delivery);
|
|
98
|
+
});
|
|
99
|
+
return saved;
|
|
100
|
+
},
|
|
88
101
|
saveReaction: async (key, chatId, messageId, reaction) => write(database, () => {
|
|
89
102
|
const accountKey = accountPk(key);
|
|
90
103
|
messageWrites.saveReaction(context, accountKey, key.provider, chatPkFor(accountKey, chatId), messageId, reaction);
|
|
@@ -86,10 +86,13 @@ const absorb = (context, kept, gone) => {
|
|
|
86
86
|
const threads = database.prepare("UPDATE messages SET thread_root_id = ? WHERE thread_root_id = ?");
|
|
87
87
|
// Without a time on either star, the kept copy's word stands and the absorbed one only fills a gap.
|
|
88
88
|
const star = database.prepare("UPDATE messages SET starred = (SELECT starred FROM messages WHERE id = ?) WHERE id = ? AND starred IS NULL");
|
|
89
|
+
// A receipt only moves a message forward, so the copy that got further wins.
|
|
90
|
+
const delivery = database.prepare("UPDATE messages SET delivery = nullif(max(coalesce(delivery, 0), coalesce((SELECT delivery FROM messages WHERE id = ?), 0)), 0) WHERE id = ? AND outgoing = 1");
|
|
89
91
|
const links = ["message_id", "parent_id"].map((column) => database.prepare(`UPDATE OR IGNORE message_links SET ${column} = ? WHERE ${column} = ?`));
|
|
90
92
|
for (const pair of pairs) {
|
|
91
93
|
threads.run(pair.kept, pair.gone);
|
|
92
94
|
star.run(pair.gone, pair.kept);
|
|
95
|
+
delivery.run(pair.gone, pair.kept);
|
|
93
96
|
// Both copies become tombstones before anything moves, so only what a deletion keeps (the owner's links) moves.
|
|
94
97
|
if (pair.deleted) {
|
|
95
98
|
tombstone(context, pair.gone);
|
|
@@ -160,6 +163,10 @@ const privacyOf = (row) => ({
|
|
|
160
163
|
const MESSENGER_FIELDS = [
|
|
161
164
|
"title",
|
|
162
165
|
"unread_count",
|
|
166
|
+
"unread_counted_at",
|
|
167
|
+
"muted",
|
|
168
|
+
"archived",
|
|
169
|
+
"marked_unread",
|
|
163
170
|
"participants_count",
|
|
164
171
|
"metadata",
|
|
165
172
|
"username",
|
|
@@ -206,8 +213,9 @@ const moveChat = (context, key, accountKey, old, twin, from, to) => {
|
|
|
206
213
|
// Left only when both were: a complete chat list that named the new id before the re-key marked the old one.
|
|
207
214
|
`UPDATE chats SET ${fields}, kind = CASE WHEN kind = 'unknown' THEN ? ELSE kind END,
|
|
208
215
|
membership_state = CASE WHEN ? IS NULL THEN nullif(membership_state, 'left') ELSE coalesce(nullif(?, 'left'), membership_state) END,
|
|
216
|
+
muted_until = CASE WHEN ? IS NULL THEN muted_until ELSE ? END,
|
|
209
217
|
searchable = ?, scope = ?, last_message_at = ?, updated_at = ? WHERE id = ?`)
|
|
210
|
-
.run(...MESSENGER_FIELDS.map((field) => scalar(twin[field])), scalar(twin.kind), scalar(twin.membership_state), scalar(twin.membership_state), strict.searchable, strict.scope, latest(old.last_message_at, twin.last_message_at), context.now(), kept);
|
|
218
|
+
.run(...MESSENGER_FIELDS.map((field) => scalar(twin[field])), scalar(twin.kind), scalar(twin.membership_state), scalar(twin.membership_state), scalar(twin.muted), scalar(twin.muted_until), strict.searchable, strict.scope, latest(old.last_message_at, twin.last_message_at), context.now(), kept);
|
|
211
219
|
}
|
|
212
220
|
context.database.prepare("UPDATE chats SET external_id = ?, updated_at = ? WHERE id = ?").run(to, context.now(), kept);
|
|
213
221
|
renameText(context, key, accountKey, from, to);
|
|
@@ -61,7 +61,7 @@ export declare const leftChats: ({ orm }: StoreContext, accountKey: number) => {
|
|
|
61
61
|
};
|
|
62
62
|
/** These chats and everything under them, children before parents: the foreign keys are enforced. */
|
|
63
63
|
export declare const purgeChats: ({ orm }: StoreContext, pks: number[]) => void;
|
|
64
|
-
export declare const chatsWith: ({ orm }: StoreContext, accountKey: number, key: AccountKey, memberId: Id) => Chat[];
|
|
64
|
+
export declare const chatsWith: ({ orm, now }: StoreContext, accountKey: number, key: AccountKey, memberId: Id) => Chat[];
|
|
65
65
|
/**
|
|
66
66
|
* How the store answers a chat list's query. `title:` goes through the trigram index, so it matches part of a
|
|
67
67
|
* title, three characters or more, whatever the case.
|
|
@@ -96,7 +96,7 @@ export declare const CHAT_COLUMNS: {
|
|
|
96
96
|
};
|
|
97
97
|
/** `title:*book*` and `title:book` ask the same: the title holds "book". */
|
|
98
98
|
export declare const titlePart: (value: string) => string;
|
|
99
|
-
export declare const listChats: ({ orm }: StoreContext, accountKey: number, { limit, offset, ...filter }: {
|
|
99
|
+
export declare const listChats: ({ orm, now }: StoreContext, accountKey: number, { limit, offset, ...filter }: {
|
|
100
100
|
limit?: number;
|
|
101
101
|
offset?: number;
|
|
102
102
|
} & StoredChatFilter) => Page<Chat>;
|
|
@@ -137,6 +137,7 @@ export const upsertChat = (context, accountKey, chat) => {
|
|
|
137
137
|
participantsCount: chat.participantsCount ?? null,
|
|
138
138
|
metadata: mergedMetadata(stored?.metadata ?? null, chat.providerMetadata),
|
|
139
139
|
membershipState: chat.membershipState === "member" ? null : (chat.membershipState ?? null),
|
|
140
|
+
...stateOf(chat, now()),
|
|
140
141
|
createdAt: now(),
|
|
141
142
|
updatedAt: now(),
|
|
142
143
|
})
|
|
@@ -147,16 +148,33 @@ export const upsertChat = (context, accountKey, chat) => {
|
|
|
147
148
|
...(chat.parentChatId === undefined ? {} : { parentChatId: sql `excluded.parent_chat_id` }),
|
|
148
149
|
scope: sql `coalesce(excluded.scope, ${chats.scope})`,
|
|
149
150
|
...(chat.title === undefined ? {} : { title: sql `excluded.title` }),
|
|
150
|
-
...(chat.unreadCount === undefined
|
|
151
|
+
...(chat.unreadCount === undefined
|
|
152
|
+
? {}
|
|
153
|
+
: { unreadCount: sql `excluded.unread_count`, unreadCountedAt: sql `excluded.unread_counted_at` }),
|
|
151
154
|
...(chat.lastMessageAt === undefined ? {} : { lastMessageAt: sql `excluded.last_message_at` }),
|
|
152
155
|
...(chat.participantsCount === undefined ? {} : { participantsCount: sql `excluded.participants_count` }),
|
|
153
156
|
metadata: sql `excluded.metadata`,
|
|
154
157
|
...membershipSet(chat.membershipState),
|
|
158
|
+
...(chat.muted === undefined ? {} : { muted: sql `excluded.muted`, mutedUntil: sql `excluded.muted_until` }),
|
|
159
|
+
...(chat.archived === undefined ? {} : { archived: sql `excluded.archived` }),
|
|
160
|
+
...(chat.markedUnread === undefined ? {} : { markedUnread: sql `excluded.marked_unread` }),
|
|
155
161
|
updatedAt: sql `excluded.updated_at`,
|
|
156
162
|
},
|
|
157
163
|
})
|
|
158
164
|
.run();
|
|
159
165
|
};
|
|
166
|
+
const bit = (value) => (value === undefined ? null : value ? 1 : 0);
|
|
167
|
+
/**
|
|
168
|
+
* The count is taken as of the chat's newest message where the update names it, else as of now: a message the store
|
|
169
|
+
* saves later and sent after that point is one the count does not hold yet.
|
|
170
|
+
*/
|
|
171
|
+
const stateOf = (chat, at) => ({
|
|
172
|
+
unreadCountedAt: chat.unreadCount === undefined ? null : (toMs(chat.lastMessageAt) ?? at),
|
|
173
|
+
muted: bit(chat.muted),
|
|
174
|
+
mutedUntil: chat.muted === true ? toMs(chat.mutedUntil) : null,
|
|
175
|
+
archived: bit(chat.archived),
|
|
176
|
+
markedUnread: bit(chat.markedUnread),
|
|
177
|
+
});
|
|
160
178
|
const membershipSet = (state) => {
|
|
161
179
|
if (state === undefined)
|
|
162
180
|
return {};
|
|
@@ -286,7 +304,7 @@ export const purgeChats = ({ orm }, pks) => {
|
|
|
286
304
|
orm.delete(messages).where(inArray(messages.chatId, pks)).run();
|
|
287
305
|
orm.delete(chats).where(inArray(chats.id, pks)).run();
|
|
288
306
|
};
|
|
289
|
-
export const chatsWith = ({ orm }, accountKey, key, memberId) => {
|
|
307
|
+
export const chatsWith = ({ orm, now }, accountKey, key, memberId) => {
|
|
290
308
|
const rows = orm
|
|
291
309
|
.select({ chat: chats })
|
|
292
310
|
.from(chats)
|
|
@@ -300,7 +318,7 @@ export const chatsWith = ({ orm }, accountKey, key, memberId) => {
|
|
|
300
318
|
.orderBy(...byRecency)
|
|
301
319
|
.all()
|
|
302
320
|
.map(({ chat }) => chat);
|
|
303
|
-
return toChats(orm, rows);
|
|
321
|
+
return toChats(orm, rows, now());
|
|
304
322
|
};
|
|
305
323
|
const chatsWhere = (accountKey, { where, includeLeft }) => and(eq(chats.accountId, accountKey), includeLeft ? undefined : notLeft, where === undefined ? undefined : bound(where));
|
|
306
324
|
/**
|
|
@@ -324,7 +342,7 @@ export const CHAT_COLUMNS = {
|
|
|
324
342
|
};
|
|
325
343
|
/** `title:*book*` and `title:book` ask the same: the title holds "book". */
|
|
326
344
|
export const titlePart = (value) => value.replace(/^\*+|\*+$/g, "").trim();
|
|
327
|
-
export const listChats = ({ orm }, accountKey, { limit, offset = 0, ...filter }) => {
|
|
345
|
+
export const listChats = ({ orm, now }, accountKey, { limit, offset = 0, ...filter }) => {
|
|
328
346
|
const rows = orm
|
|
329
347
|
.select()
|
|
330
348
|
.from(chats)
|
|
@@ -333,10 +351,10 @@ export const listChats = ({ orm }, accountKey, { limit, offset = 0, ...filter })
|
|
|
333
351
|
.limit(limit === undefined ? -1 : limit + 1)
|
|
334
352
|
.offset(offset)
|
|
335
353
|
.all();
|
|
336
|
-
return { items: toChats(orm, rows.slice(0, limit)), hasMore: limit !== undefined && rows.length > limit };
|
|
354
|
+
return { items: toChats(orm, rows.slice(0, limit), now()), hasMore: limit !== undefined && rows.length > limit };
|
|
337
355
|
};
|
|
338
356
|
export const countChats = ({ orm }, accountKey, filter) => Number(orm.select({ n: sql `count(*)` }).from(chats).where(chatsWhere(accountKey, filter)).get()?.n);
|
|
339
|
-
const toChats = (orm, rows) => {
|
|
357
|
+
const toChats = (orm, rows, at) => {
|
|
340
358
|
const ids = [...new Set(rows.flatMap((row) => (row.parentChatId === null ? [] : [row.parentChatId])))];
|
|
341
359
|
const parents = new Map(ids.length === 0
|
|
342
360
|
? []
|
|
@@ -346,9 +364,10 @@ const toChats = (orm, rows) => {
|
|
|
346
364
|
.where(inArray(chats.id, ids))
|
|
347
365
|
.all()
|
|
348
366
|
.map((row) => [row.pk, row.externalId]));
|
|
349
|
-
return rows.map((row) => toChat(row, row.parentChatId === null ? undefined : parents.get(row.parentChatId)));
|
|
367
|
+
return rows.map((row) => toChat(row, at, row.parentChatId === null ? undefined : parents.get(row.parentChatId)));
|
|
350
368
|
};
|
|
351
|
-
const
|
|
369
|
+
const flag = (stored) => (stored === null ? undefined : stored === 1);
|
|
370
|
+
const toChat = (row, at, parentId) => ({
|
|
352
371
|
id: row.externalId,
|
|
353
372
|
title: row.title,
|
|
354
373
|
kind: row.kind,
|
|
@@ -361,7 +380,17 @@ const toChat = (row, parentId) => ({
|
|
|
361
380
|
membershipState: row.membershipState,
|
|
362
381
|
providerMetadata: parsed(row.metadata),
|
|
363
382
|
}),
|
|
383
|
+
...muteOf(row, at),
|
|
384
|
+
...present({ archived: flag(row.archived), markedUnread: flag(row.markedUnread) }),
|
|
364
385
|
});
|
|
386
|
+
// A mute is read against the clock, so one that ran out needs no write to end.
|
|
387
|
+
const muteOf = ({ muted, mutedUntil }, at) => {
|
|
388
|
+
if (muted === null)
|
|
389
|
+
return {};
|
|
390
|
+
if (muted !== 1 || (mutedUntil !== null && mutedUntil <= at))
|
|
391
|
+
return { muted: false };
|
|
392
|
+
return mutedUntil === null ? { muted: true } : { muted: true, mutedUntil: toIso(mutedUntil) };
|
|
393
|
+
};
|
|
365
394
|
const metadataChatPk = ({ database }, key, chatId) => {
|
|
366
395
|
const row = database
|
|
367
396
|
.prepare("SELECT c.id FROM chats c JOIN accounts a ON a.id=c.account_id WHERE a.provider=? AND a.external_id=? AND c.external_id=?")
|
|
@@ -32,6 +32,8 @@ export const MANIFEST = [
|
|
|
32
32
|
{ name: "20261019115025_version-25-chat-revisions", version: 25, minCompatible: 1 },
|
|
33
33
|
{ name: "20261019115027_version-25-chat-revisions-triggers", version: 25, minCompatible: 1 },
|
|
34
34
|
{ name: "20261021214318_version-26-message-reactions", version: 26, minCompatible: 1 },
|
|
35
|
+
{ name: "20261021221808_version-27-message-delivery", version: 27, minCompatible: 1 },
|
|
36
|
+
{ name: "20261021224625_version-28-chat-state", version: 28, minCompatible: 1 },
|
|
35
37
|
];
|
|
36
38
|
export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
|
|
37
39
|
const migrations = [];
|
|
@@ -21,7 +21,7 @@ const speakers = (database, accountId) => {
|
|
|
21
21
|
found.set(key, speaker);
|
|
22
22
|
};
|
|
23
23
|
const participants = database
|
|
24
|
-
.prepare(`SELECT p.id,p.identity_id,p.display_name,i.name AS identity_name,l.person_id,person.name AS person_name
|
|
24
|
+
.prepare(`SELECT p.id,p.identity_id,p.display_name,p.email,i.external_id AS identity_key,i.name AS identity_name,l.person_id,person.name AS person_name
|
|
25
25
|
FROM meeting_participants p JOIN meetings m ON m.id=p.meeting_id
|
|
26
26
|
LEFT JOIN identities i ON i.id=p.identity_id LEFT JOIN identity_links l ON l.identity_id=p.identity_id
|
|
27
27
|
LEFT JOIN persons person ON person.id=l.person_id WHERE m.account_id=? AND m.deleted_at IS NULL`)
|
|
@@ -34,11 +34,26 @@ const speakers = (database, accountId) => {
|
|
|
34
34
|
const others = personNames.all(Number(row.person_id)).map((one) => (one.name === null ? null : String(one.name)));
|
|
35
35
|
add(`person:${row.person_id}`, String(row.person_name ?? label), [...names, ...others], Number(row.id));
|
|
36
36
|
}
|
|
37
|
-
else if (row.identity_id !== null &&
|
|
38
|
-
|
|
37
|
+
else if (row.identity_id !== null &&
|
|
38
|
+
row.identity_id !== undefined &&
|
|
39
|
+
!String(row.identity_key).startsWith("name:"))
|
|
40
|
+
add(`identity:${row.identity_id}`, row.email ? `${label} <${row.email}>` : label, names, Number(row.id));
|
|
41
|
+
// A participant known only by name is keyed per meeting (`name:…@account/occurrence:n`); across meetings the name is all there is.
|
|
39
42
|
else
|
|
40
43
|
add(`name:${fold(label)}`, label, names, Number(row.id));
|
|
41
44
|
}
|
|
45
|
+
// A name heard in one meeting is the identified speaker of that name, when exactly one has it.
|
|
46
|
+
for (const [key, speaker] of found) {
|
|
47
|
+
if (!key.startsWith("name:"))
|
|
48
|
+
continue;
|
|
49
|
+
const named = [...found].filter(([other, one]) => !other.startsWith("name:") && one.names.has(key.slice(5)));
|
|
50
|
+
const [only] = named;
|
|
51
|
+
if (named.length !== 1 || !only)
|
|
52
|
+
continue;
|
|
53
|
+
for (const participant of speaker.participants)
|
|
54
|
+
only[1].participants.add(participant);
|
|
55
|
+
found.delete(key);
|
|
56
|
+
}
|
|
42
57
|
const unlinked = database
|
|
43
58
|
.prepare(`SELECT DISTINCT r.speaker_name AS name FROM meeting_transcript_rows r
|
|
44
59
|
JOIN meeting_transcripts t ON t.id=r.meeting_transcript_id JOIN meetings m ON m.id=t.meeting_id
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import type { Id, Message, PersonReaction, PollVote, Provider, Reactions } from "../../domain/models.js";
|
|
1
|
+
import type { Delivery, Id, Message, PersonReaction, PollVote, Provider, Reactions } from "../../domain/models.js";
|
|
2
2
|
import type { AccountKey, DeletionScope } from "../types.js";
|
|
3
3
|
import type { StoreContext } from "./open.js";
|
|
4
|
-
export declare const upsertMessage: (context: StoreContext, key: AccountKey, accountKey: number, chatKey: number, message: Message, via: string, seenAt?: number) =>
|
|
4
|
+
export declare const upsertMessage: (context: StoreContext, key: AccountKey, accountKey: number, chatKey: number, message: Message, via: string, seenAt?: number) => boolean;
|
|
5
|
+
/**
|
|
6
|
+
* One more unread for a message from someone else that the store first saw now, sent after the messenger's count was
|
|
7
|
+
* taken. A chat the messenger never gave a count for is left alone: the store would count its whole history.
|
|
8
|
+
*/
|
|
9
|
+
export declare const countUnread: ({ database }: StoreContext, chatKey: number, message: Message) => void;
|
|
5
10
|
/**
|
|
6
11
|
* The owner's ruling (NEED-393 A): a deleted message keeps its row, so a later sync cannot bring it
|
|
7
12
|
* back, and leaves no text — not in the row, the search copy, the edit history, a transcript or a vector.
|
|
@@ -11,6 +16,8 @@ export declare const tombstone: (context: StoreContext, pk: number) => number;
|
|
|
11
16
|
export declare const saveReactions: (context: StoreContext, chatKey: number, messageId: Id, reactions: Reactions) => boolean;
|
|
12
17
|
/** Answers whether the message is held; a deleted one keeps no star. */
|
|
13
18
|
export declare const saveStar: ({ orm }: StoreContext, chatKey: number, messageId: Id, starred: boolean) => boolean;
|
|
19
|
+
/** Answers whether the owner's own message is held. A receipt is for a message the owner sent, so another's keeps none. */
|
|
20
|
+
export declare const saveDelivery: ({ orm }: StoreContext, chatKey: number, messageId: Id, delivery: Delivery) => boolean;
|
|
14
21
|
export declare const markDeleted: (context: StoreContext, accountKey: number, messageIds: Id[], chatId: Id | undefined, among?: DeletionScope) => number;
|
|
15
22
|
/** A vote older than the one held for that voter is dropped, and so is any vote in a deleted poll. */
|
|
16
23
|
export declare const saveVote: (context: StoreContext, accountKey: number, provider: Provider, chatKey: number, messageId: Id, { person, answers, votedAt }: PollVote) => void;
|