letagents 0.12.34 → 0.12.35
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/README.md +0 -2
- package/dist/mcp/server/runtime/messages.js +18 -1
- package/package.json +1 -1
- package/shared/code-block.css +193 -0
- package/shared/code-highlighting.d.mts +109 -0
- package/shared/code-highlighting.mjs +345 -0
- package/shared/codex-owner-isolation.mjs +1 -1
- package/shared/message-link-preview-store.d.mts +10 -0
- package/shared/message-link-preview-store.mjs +77 -0
- package/shared/message-link-previews.d.mts +16 -0
- package/shared/message-link-previews.mjs +70 -0
- package/shared/message-pin-store.d.mts +20 -0
- package/shared/message-pin-store.mjs +102 -0
- package/shared/message-pins.d.mts +25 -0
- package/shared/message-pins.mjs +16 -0
- package/shared/message-reaction-store.d.mts +51 -0
- package/shared/message-reaction-store.mjs +290 -0
- package/shared/message-reactions.d.mts +73 -0
- package/shared/message-reactions.mjs +130 -0
- package/shared/message-search-controller.d.mts +38 -0
- package/shared/message-search-controller.mjs +99 -0
- package/shared/message-search.d.mts +46 -0
- package/shared/message-search.mjs +114 -0
- package/shared/room-message-urls.d.mts +25 -0
- package/shared/room-message-urls.mjs +119 -0
- package/shared/room-notification-client.d.mts +18 -0
- package/shared/room-notification-client.mjs +140 -0
- package/shared/room-notification-preferences.d.mts +17 -0
- package/shared/room-notification-preferences.mjs +34 -0
- package/shared/room-resource-invalidation.d.mts +5 -0
- package/shared/room-resource-invalidation.mjs +6 -0
- package/shared/room-typing.d.mts +16 -0
- package/shared/room-typing.mjs +112 -0
- package/shared/room-unread-client.ts +128 -0
- package/shared/room-unread.d.mts +25 -0
- package/shared/room-unread.mjs +57 -0
- package/shared/room-unread.test.mjs +91 -0
- package/shared/rooms/RoomsDirectory.vue +1 -1
- package/shared/rooms/directory.ts +1 -0
- package/shared/slash-commands.d.mts +31 -0
- package/shared/slash-commands.mjs +48 -0
- package/shared/slash-commands.test.mjs +26 -0
- package/shared/ui/MessageReactionBar.vue +65 -0
- package/shared/ui/MessageReactionPicker.vue +291 -0
- package/shared/ui/MessageSearchResults.vue +106 -0
- package/shared/ui/PinnedMessages.vue +124 -0
- package/shared/ui/TypingIndicator.vue +22 -0
- package/shared/ui/message-reactions.css +254 -0
- package/shared/ui/message-search.css +186 -0
- package/shared/ui/useComposerSlashCommands.ts +92 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LinkPreviewReference, MessageLinkPreview, MessageLinkPreviewsResponse } from "./message-link-previews.mjs";
|
|
2
|
+
export function createMessageLinkPreviewStore(options: {
|
|
3
|
+
load(references: LinkPreviewReference[]): Promise<MessageLinkPreviewsResponse>;
|
|
4
|
+
onChange?(): void; refreshDelayMs?: number;
|
|
5
|
+
}): {
|
|
6
|
+
track(message: { id: string; references: LinkPreviewReference[] }): () => void;
|
|
7
|
+
get(messageId: string): MessageLinkPreview[];
|
|
8
|
+
refresh(): Promise<void>;
|
|
9
|
+
reset(): void;
|
|
10
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { LINK_PREVIEW_BATCH_LIMIT, linkPreviewKey } from "./message-link-previews.mjs";
|
|
2
|
+
|
|
3
|
+
/** Separate from messages: one bounded read chain for the references rendered in a room. */
|
|
4
|
+
export function createMessageLinkPreviewStore({ load, onChange, refreshDelayMs = 50 }) {
|
|
5
|
+
let epoch = 0;
|
|
6
|
+
let tracked = new Map();
|
|
7
|
+
let previews = new Map();
|
|
8
|
+
let timer = null;
|
|
9
|
+
let reading = null;
|
|
10
|
+
function schedule() {
|
|
11
|
+
if (timer !== null) return;
|
|
12
|
+
timer = setTimeout(() => { timer = null; void refresh(); }, refreshDelayMs);
|
|
13
|
+
}
|
|
14
|
+
function track(message) {
|
|
15
|
+
if (!message.references.length) return () => {};
|
|
16
|
+
const token = Symbol();
|
|
17
|
+
tracked.set(token, message);
|
|
18
|
+
onChange?.();
|
|
19
|
+
if (message.references.some((ref) => !previews.has(linkPreviewKey(ref)))) schedule();
|
|
20
|
+
return () => {
|
|
21
|
+
tracked.delete(token);
|
|
22
|
+
const used = new Set([...tracked.values()].flatMap((entry) => entry.references.map(linkPreviewKey)));
|
|
23
|
+
for (const key of previews.keys()) if (!used.has(key)) previews.delete(key);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function get(messageId) {
|
|
27
|
+
const message = [...tracked.values()].find((entry) => entry.id === messageId);
|
|
28
|
+
return (message?.references ?? []).flatMap((ref) => previews.get(linkPreviewKey(ref)) ?? []);
|
|
29
|
+
}
|
|
30
|
+
function refresh() {
|
|
31
|
+
if (timer !== null) clearTimeout(timer);
|
|
32
|
+
timer = null;
|
|
33
|
+
if (reading) { reading.again = true; return reading.promise; }
|
|
34
|
+
const startedIn = epoch;
|
|
35
|
+
const cycle = { again: false, promise: null };
|
|
36
|
+
reading = cycle;
|
|
37
|
+
cycle.promise = (async () => {
|
|
38
|
+
do {
|
|
39
|
+
cycle.again = false;
|
|
40
|
+
const unique = new Map([...tracked.values()].flatMap((entry) => entry.references.map((ref) => [linkPreviewKey(ref), ref])));
|
|
41
|
+
const references = [...unique.values()];
|
|
42
|
+
for (let i = 0; i < references.length; i += LINK_PREVIEW_BATCH_LIMIT) {
|
|
43
|
+
if (startedIn !== epoch) return;
|
|
44
|
+
const batch = references.slice(i, i + LINK_PREVIEW_BATCH_LIMIT);
|
|
45
|
+
try {
|
|
46
|
+
const result = await load(batch);
|
|
47
|
+
if (startedIn !== epoch) return;
|
|
48
|
+
const values = new Map(result.previews.map((preview) => [linkPreviewKey(preview), preview]));
|
|
49
|
+
let changed = false;
|
|
50
|
+
const used = new Set([...tracked.values()].flatMap((entry) => entry.references.map(linkPreviewKey)));
|
|
51
|
+
for (const ref of batch) {
|
|
52
|
+
const key = linkPreviewKey(ref);
|
|
53
|
+
if (!used.has(key)) continue;
|
|
54
|
+
const next = values.get(key) ?? null;
|
|
55
|
+
if (JSON.stringify(previews.get(key) ?? null) !== JSON.stringify(next)) changed = true;
|
|
56
|
+
previews.set(key, next);
|
|
57
|
+
}
|
|
58
|
+
if (changed) onChange?.();
|
|
59
|
+
} catch {
|
|
60
|
+
// Retry on the next stream update, reconnect or registration.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} while (cycle.again && startedIn === epoch);
|
|
64
|
+
})().finally(() => { if (reading === cycle) reading = null; });
|
|
65
|
+
return cycle.promise;
|
|
66
|
+
}
|
|
67
|
+
function reset() {
|
|
68
|
+
epoch++;
|
|
69
|
+
if (timer !== null) clearTimeout(timer);
|
|
70
|
+
timer = null;
|
|
71
|
+
reading = null;
|
|
72
|
+
tracked = new Map();
|
|
73
|
+
previews = new Map();
|
|
74
|
+
onChange?.();
|
|
75
|
+
}
|
|
76
|
+
return { track, get, refresh, reset };
|
|
77
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const LINK_PREVIEW_BATCH_LIMIT: 50;
|
|
2
|
+
export const LINK_PREVIEW_MESSAGE_LIMIT: 3;
|
|
3
|
+
export interface LinkPreviewReference { kind: "pull" | "issue"; number: number }
|
|
4
|
+
export interface GitHubLinkReference extends LinkPreviewReference { repository: string; url: string }
|
|
5
|
+
export interface MessageLinkPreview extends GitHubLinkReference { title: string; state: "merged" | "closed" | "draft" | "open" }
|
|
6
|
+
export interface MessageLinkPreviewsResponse { room_id: string; previews: MessageLinkPreview[]; available?: boolean }
|
|
7
|
+
export function parseGitHubLinkReference(value: unknown): GitHubLinkReference | null;
|
|
8
|
+
export function normalizePreviewRepository(value: unknown): string | null;
|
|
9
|
+
export function linkPreviewKey(reference: LinkPreviewReference): string;
|
|
10
|
+
export function parseLinkPreviewReferences(value: unknown): LinkPreviewReference[] | null;
|
|
11
|
+
export function eligibleLinkPreviewReferences(urls: readonly string[], repository: string | null): LinkPreviewReference[];
|
|
12
|
+
export function linkPreviewState(state: unknown, metadata?: { merged?: unknown; draft?: unknown } | null): MessageLinkPreview["state"] | null;
|
|
13
|
+
export function linkPreviewPresentation(preview: MessageLinkPreview): {
|
|
14
|
+
kind: "pull-request" | "issue"; kindLabel: string; tone: "emerald" | "slate" | "amber" | "violet";
|
|
15
|
+
statusLabel: string; headline: string; detail: null; repository: string; taskId: null; url: string; urlLabel: string;
|
|
16
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const LINK_PREVIEW_BATCH_LIMIT = 50;
|
|
2
|
+
export const LINK_PREVIEW_MESSAGE_LIMIT = 3;
|
|
3
|
+
|
|
4
|
+
export function parseGitHubLinkReference(value) {
|
|
5
|
+
if (typeof value !== "string" || value.length > 2048) return null;
|
|
6
|
+
try {
|
|
7
|
+
const url = new URL(value);
|
|
8
|
+
if (url.protocol !== "https:" || url.hostname !== "github.com" || url.port || url.username || url.password) return null;
|
|
9
|
+
const match = /^\/([\w.-]+)\/([\w.-]+)\/(pull|issues)\/([1-9]\d*)\/?$/.exec(url.pathname);
|
|
10
|
+
if (!match) return null;
|
|
11
|
+
const number = Number(match[4]);
|
|
12
|
+
if (!Number.isSafeInteger(number)) return null;
|
|
13
|
+
const repository = `${match[1]}/${match[2]}`.toLowerCase();
|
|
14
|
+
const kind = match[3] === "pull" ? "pull" : "issue";
|
|
15
|
+
return { repository, kind, number, url: `https://github.com/${repository}/${match[3]}/${number}` };
|
|
16
|
+
} catch { return null; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizePreviewRepository(value) {
|
|
20
|
+
if (typeof value !== "string") return null;
|
|
21
|
+
const name = value.replace(/^https:\/\//i, "").replace(/^github\.com\//i, "").replace(/\/$/, "");
|
|
22
|
+
return /^[\w.-]+\/[\w.-]+$/.test(name) ? name.toLowerCase() : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function linkPreviewKey(reference) { return `${reference.kind}:${reference.number}`; }
|
|
26
|
+
|
|
27
|
+
export function parseLinkPreviewReferences(value) {
|
|
28
|
+
if (!Array.isArray(value) || value.length > LINK_PREVIEW_BATCH_LIMIT) return null;
|
|
29
|
+
const references = new Map();
|
|
30
|
+
for (const ref of value) {
|
|
31
|
+
if (!ref || typeof ref !== "object" || Object.keys(ref).length !== 2
|
|
32
|
+
|| !["pull", "issue"].includes(ref.kind) || !Number.isSafeInteger(ref.number) || ref.number <= 0) return null;
|
|
33
|
+
references.set(linkPreviewKey(ref), { kind: ref.kind, number: ref.number });
|
|
34
|
+
}
|
|
35
|
+
return [...references.values()];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function eligibleLinkPreviewReferences(urls, repository) {
|
|
39
|
+
const repo = normalizePreviewRepository(repository);
|
|
40
|
+
if (!repo) return [];
|
|
41
|
+
const refs = new Map();
|
|
42
|
+
for (const url of urls) {
|
|
43
|
+
const ref = parseGitHubLinkReference(url);
|
|
44
|
+
if (!ref || ref.repository !== repo) continue;
|
|
45
|
+
refs.set(linkPreviewKey(ref), { kind: ref.kind, number: ref.number });
|
|
46
|
+
if (refs.size === LINK_PREVIEW_MESSAGE_LIMIT) break;
|
|
47
|
+
}
|
|
48
|
+
return [...refs.values()];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** State is a last-observed snapshot, never a request to GitHub. */
|
|
52
|
+
export function linkPreviewState(state, metadata) {
|
|
53
|
+
if (state === "merged" || metadata?.merged === true) return "merged";
|
|
54
|
+
if (state === "closed") return "closed";
|
|
55
|
+
if (state === "draft" || state === "open" && metadata?.draft === true) return "draft";
|
|
56
|
+
return state === "open" ? "open" : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function linkPreviewPresentation(preview) {
|
|
60
|
+
const pull = preview.kind === "pull";
|
|
61
|
+
return {
|
|
62
|
+
kind: pull ? "pull-request" : "issue",
|
|
63
|
+
kindLabel: pull ? "Pull request" : "Issue",
|
|
64
|
+
tone: preview.state === "merged" ? "emerald" : preview.state === "closed" ? "slate" : preview.state === "draft" ? "amber" : "violet",
|
|
65
|
+
statusLabel: preview.state,
|
|
66
|
+
headline: `#${preview.number} ${preview.title}`,
|
|
67
|
+
detail: null, repository: preview.repository, taskId: null,
|
|
68
|
+
url: preview.url, urlLabel: pull ? "Open pull request" : "Open issue",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MessagePin, MessagePinsResponse } from "./message-pins.mjs";
|
|
2
|
+
export interface MessagePinState {
|
|
3
|
+
pins: MessagePin[];
|
|
4
|
+
available: boolean;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
pending: string | null;
|
|
7
|
+
error: string | null;
|
|
8
|
+
}
|
|
9
|
+
export function createMessagePinStore<Context>(options: {
|
|
10
|
+
load(context: Context): Promise<MessagePinsResponse>;
|
|
11
|
+
mutate(context: Context, messageId: string, pinned: boolean): Promise<unknown>;
|
|
12
|
+
onChange?(state: MessagePinState): void;
|
|
13
|
+
onError?(message: string): void;
|
|
14
|
+
}): {
|
|
15
|
+
readonly state: MessagePinState;
|
|
16
|
+
reset(context?: Context | null): void;
|
|
17
|
+
refresh(): Promise<unknown>;
|
|
18
|
+
setPinned(messageId: string, pinned: boolean): Promise<unknown>;
|
|
19
|
+
dispose(): void;
|
|
20
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { isPinMessageId, MESSAGE_PIN_LIMIT } from "./message-pins.mjs";
|
|
2
|
+
|
|
3
|
+
/** One authoritative, bounded room list. All async work is fenced by context. */
|
|
4
|
+
export function createMessagePinStore(options) {
|
|
5
|
+
let epoch = 0;
|
|
6
|
+
let generation = 0;
|
|
7
|
+
let context = null;
|
|
8
|
+
let disposed = false;
|
|
9
|
+
let reading = null;
|
|
10
|
+
let queued = false;
|
|
11
|
+
let mutation = null;
|
|
12
|
+
let state = { pins: [], available: false, loading: false, pending: null, error: null };
|
|
13
|
+
const changed = () => options.onChange?.(state);
|
|
14
|
+
function reset(next = null) {
|
|
15
|
+
epoch++;
|
|
16
|
+
generation++;
|
|
17
|
+
context = next;
|
|
18
|
+
reading = null;
|
|
19
|
+
mutation = null;
|
|
20
|
+
queued = false;
|
|
21
|
+
state = { pins: [], available: false, loading: false, pending: null, error: null };
|
|
22
|
+
changed();
|
|
23
|
+
if (next && !disposed) void refresh();
|
|
24
|
+
}
|
|
25
|
+
async function refresh() {
|
|
26
|
+
if (!context || disposed) return;
|
|
27
|
+
if (reading || mutation) { queued = true; return reading ?? mutation; }
|
|
28
|
+
const ownEpoch = epoch;
|
|
29
|
+
const ownGeneration = generation;
|
|
30
|
+
const ownContext = context;
|
|
31
|
+
state = { ...state, loading: true };
|
|
32
|
+
changed();
|
|
33
|
+
const job = Promise.resolve().then(async () => {
|
|
34
|
+
if (ownEpoch !== epoch || disposed) return;
|
|
35
|
+
try {
|
|
36
|
+
const response = await options.load(ownContext);
|
|
37
|
+
if (ownEpoch !== epoch || ownGeneration !== generation || disposed) return;
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
const pins = (response.pins ?? []).filter((pin) => {
|
|
40
|
+
if (!isPinMessageId(pin.message_id) || seen.has(pin.message_id)) return false;
|
|
41
|
+
seen.add(pin.message_id);
|
|
42
|
+
return true;
|
|
43
|
+
}).slice(0, MESSAGE_PIN_LIMIT);
|
|
44
|
+
state = { ...state, pins, available: response.available !== false, error: null, pending: null };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (ownEpoch === epoch && ownGeneration === generation && !disposed) {
|
|
47
|
+
state = { ...state, pending: null, error: error instanceof Error ? error.message : "Pins could not be loaded." };
|
|
48
|
+
}
|
|
49
|
+
} finally {
|
|
50
|
+
if (ownEpoch === epoch && !disposed) {
|
|
51
|
+
reading = null;
|
|
52
|
+
state = { ...state, loading: false };
|
|
53
|
+
changed();
|
|
54
|
+
if (queued && !mutation) { queued = false; void refresh(); }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
reading = job;
|
|
59
|
+
return job;
|
|
60
|
+
}
|
|
61
|
+
async function setPinned(messageId, pinned) {
|
|
62
|
+
if (disposed || !context || !state.available || mutation || state.pending || !isPinMessageId(messageId)) return;
|
|
63
|
+
const ownEpoch = epoch;
|
|
64
|
+
const ownContext = context;
|
|
65
|
+
// A read started before this write is no longer authoritative.
|
|
66
|
+
generation++;
|
|
67
|
+
state = { ...state, pending: messageId, error: null };
|
|
68
|
+
changed();
|
|
69
|
+
const job = Promise.resolve().then(async () => {
|
|
70
|
+
if (ownEpoch !== epoch || disposed) return;
|
|
71
|
+
let succeeded = false;
|
|
72
|
+
try {
|
|
73
|
+
await options.mutate(ownContext, messageId, pinned);
|
|
74
|
+
succeeded = true;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (ownEpoch === epoch && !disposed) {
|
|
77
|
+
const message = error instanceof Error ? error.message : "Pins could not be saved.";
|
|
78
|
+
state = { ...state, error: message };
|
|
79
|
+
options.onError?.(message);
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
if (ownEpoch === epoch && !disposed) {
|
|
83
|
+
mutation = null;
|
|
84
|
+
state = { ...state, pending: succeeded ? messageId : null };
|
|
85
|
+
changed();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (ownEpoch === epoch && !disposed && (succeeded || queued)) {
|
|
89
|
+
// If an older read is still running it must finish before the repair.
|
|
90
|
+
queued = false;
|
|
91
|
+
await refresh();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
mutation = job;
|
|
95
|
+
return job;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
get state() { return state; },
|
|
99
|
+
reset, refresh, setPinned,
|
|
100
|
+
dispose() { disposed = true; reset(); },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const MESSAGE_PIN_LIMIT: 50;
|
|
2
|
+
export const MESSAGE_PIN_SNIPPET_LENGTH: 240;
|
|
3
|
+
export const MESSAGE_PIN_LIMIT_NOTICE: string;
|
|
4
|
+
export function isPinMessageId(value: unknown): value is string;
|
|
5
|
+
export function messagePinSnippet(text: string, displayText?: string | null): string;
|
|
6
|
+
export interface MessagePin {
|
|
7
|
+
message_id: string;
|
|
8
|
+
sender: string;
|
|
9
|
+
source: string | null;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
thread_root_id: string | null;
|
|
12
|
+
snippet: string;
|
|
13
|
+
pinned_at: string;
|
|
14
|
+
pinned_by: { login: string; name: string; avatar_url: string | null };
|
|
15
|
+
}
|
|
16
|
+
export interface MessagePinsResponse {
|
|
17
|
+
room_id: string;
|
|
18
|
+
pins: MessagePin[];
|
|
19
|
+
available?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface MessagePinMutationResponse {
|
|
22
|
+
room_id: string;
|
|
23
|
+
message_id: string;
|
|
24
|
+
changed: boolean;
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const MESSAGE_PIN_LIMIT = 50;
|
|
2
|
+
export const MESSAGE_PIN_SNIPPET_LENGTH = 240;
|
|
3
|
+
export const MESSAGE_PIN_LIMIT_NOTICE = "This room already has 50 pinned messages. Unpin a message first.";
|
|
4
|
+
|
|
5
|
+
export function isPinMessageId(value) {
|
|
6
|
+
return typeof value === "string" && /^msg_[1-9]\d*$/.test(value)
|
|
7
|
+
&& Number(value.slice(4)) <= 2147483647;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Match the clients' displayed-text fallback; return text, never HTML. */
|
|
11
|
+
export function messagePinSnippet(text, displayText) {
|
|
12
|
+
const chars = Array.from((displayText || text || "").replace(/\s+/gu, " ").trim());
|
|
13
|
+
return chars.length > MESSAGE_PIN_SNIPPET_LENGTH
|
|
14
|
+
? chars.slice(0, MESSAGE_PIN_SNIPPET_LENGTH - 1).join("") + "…"
|
|
15
|
+
: chars.join("");
|
|
16
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { MessageReaction, MessageReactor } from "./message-reactions.mjs";
|
|
2
|
+
|
|
3
|
+
export interface MessageReactionStoreOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Read the reactions of the messages from `firstMessageId` to `lastMessageId`
|
|
6
|
+
* (at most MESSAGE_REACTION_RANGE_MAX_SPAN apart). The answer may stop early
|
|
7
|
+
* and name the message to continue from.
|
|
8
|
+
*/
|
|
9
|
+
load(firstMessageId: string, lastMessageId: string): Promise<{
|
|
10
|
+
reactions: Record<string, unknown>;
|
|
11
|
+
viewer_reactions?: Record<string, unknown>;
|
|
12
|
+
next_first_message_id?: string | null;
|
|
13
|
+
}>;
|
|
14
|
+
/** Add (`reacted: true`) or remove the viewer's reaction; resolves with the message's reactions. */
|
|
15
|
+
mutate(messageId: string, emoji: string, reacted: boolean): Promise<{ reactions: unknown }>;
|
|
16
|
+
/** The signed-in person, or null when nobody can react. */
|
|
17
|
+
viewer(): MessageReactor | null;
|
|
18
|
+
onChange?(): void;
|
|
19
|
+
onError?(error: unknown, during: "load" | "mutate"): void;
|
|
20
|
+
/** How long newly tracked messages are gathered before one read confirms them. */
|
|
21
|
+
refreshDelayMs?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MessageReactionStore {
|
|
25
|
+
/** A stable empty list when the message has no reactions. */
|
|
26
|
+
get(messageId: string): readonly MessageReaction[];
|
|
27
|
+
viewerReacted(messageId: string, emoji: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* A rendered message registers itself; the returned function unregisters it.
|
|
30
|
+
* Its reactions start from what it carried and are confirmed by a read a
|
|
31
|
+
* moment after it is first tracked.
|
|
32
|
+
*/
|
|
33
|
+
track(message: { id: string; reactions?: unknown }): () => void;
|
|
34
|
+
/** Re-read the reactions of every message on screen. */
|
|
35
|
+
refresh(): Promise<void>;
|
|
36
|
+
/** Add the viewer's reaction, or remove it if it is already there. */
|
|
37
|
+
toggle(messageId: string, emoji: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Forget everything. With `keepTracked` the messages on screen stay
|
|
40
|
+
* registered (the signed-in person changed); without it they are dropped
|
|
41
|
+
* too (the room changed).
|
|
42
|
+
*/
|
|
43
|
+
reset(options?: { keepTracked?: boolean }): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The client-side state of a room's reactions, shared by the desktop and web
|
|
48
|
+
* apps: seeded from the messages on screen, confirmed and kept current by
|
|
49
|
+
* reading the server.
|
|
50
|
+
*/
|
|
51
|
+
export function createMessageReactionStore(options: MessageReactionStoreOptions): MessageReactionStore;
|