@robono/messaging-react-native 0.1.0 → 0.3.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 +65 -0
- package/README.md +160 -8
- package/SBOM.spdx.json +1985 -209
- package/dist/account.d.ts +15 -0
- package/dist/account.js +27 -0
- package/dist/call-message.js +7 -1
- package/dist/calling.js +5 -9
- package/dist/conversation-details.d.ts +24 -0
- package/dist/conversation-details.js +83 -0
- package/dist/emoji-text.d.ts +2 -0
- package/dist/emoji-text.js +8 -0
- package/dist/expo-download.d.ts +3 -0
- package/dist/expo-download.js +43 -0
- package/dist/expo-history.d.ts +3 -0
- package/dist/expo-history.js +59 -0
- package/dist/expo-media.d.ts +1 -0
- package/dist/expo-media.js +69 -43
- package/dist/expo-messenger.d.ts +35 -0
- package/dist/expo-messenger.js +39 -0
- package/dist/expo-notifications.d.ts +31 -0
- package/dist/expo-notifications.js +92 -0
- package/dist/expo-outbox.d.ts +13 -0
- package/dist/expo-outbox.js +82 -0
- package/dist/expo-privacy.d.ts +4 -0
- package/dist/expo-privacy.js +7 -0
- package/dist/expo-saved.d.ts +24 -0
- package/dist/expo-saved.js +76 -0
- package/dist/expo-screen.d.ts +42 -0
- package/dist/expo-screen.js +557 -0
- package/dist/expo-social.d.ts +3 -0
- package/dist/expo-social.js +21 -0
- package/dist/expo-thumbnails.d.ts +9 -0
- package/dist/expo-thumbnails.js +68 -0
- package/dist/expo.d.ts +2 -36
- package/dist/expo.js +2 -429
- package/dist/extras-lifecycle.d.ts +2 -0
- package/dist/extras-lifecycle.js +27 -0
- package/dist/extras.d.ts +49 -0
- package/dist/extras.js +49 -0
- package/dist/forward.d.ts +13 -0
- package/dist/forward.js +62 -0
- package/dist/group-editor.d.ts +18 -0
- package/dist/group-editor.js +98 -0
- package/dist/header-calls.d.ts +18 -0
- package/dist/header-calls.js +45 -0
- package/dist/icons.d.ts +1 -1
- package/dist/inbox-search.d.ts +11 -0
- package/dist/inbox-search.js +42 -0
- package/dist/inbox.d.ts +19 -0
- package/dist/inbox.js +52 -0
- package/dist/index.d.ts +76 -3
- package/dist/index.js +648 -167
- package/dist/media-controller.d.ts +21 -0
- package/dist/media-controller.js +157 -3
- package/dist/media-readiness.d.ts +1 -1
- package/dist/media-ui.d.ts +27 -5
- package/dist/media-ui.js +95 -29
- package/dist/message-links.d.ts +5 -0
- package/dist/message-links.js +30 -0
- package/dist/message-menu.d.ts +28 -0
- package/dist/message-menu.js +24 -0
- package/dist/message-order.d.ts +7 -0
- package/dist/message-order.js +27 -0
- package/dist/messenger-video-view.d.ts +8 -0
- package/dist/messenger-video-view.js +46 -0
- package/dist/native-outbox.d.ts +55 -0
- package/dist/native-outbox.js +53 -0
- package/dist/photo-gesture.d.ts +20 -0
- package/dist/photo-gesture.js +52 -0
- package/dist/pollVoteQueue.d.ts +3 -0
- package/dist/pollVoteQueue.js +29 -0
- package/dist/polls.d.ts +7 -2
- package/dist/polls.js +89 -20
- package/dist/popup.d.ts +51 -0
- package/dist/popup.js +73 -0
- package/dist/quick-send.d.ts +15 -0
- package/dist/quick-send.js +26 -0
- package/dist/recent-media.js +1 -0
- package/dist/safety.d.ts +10 -1
- package/dist/safety.js +29 -8
- package/dist/saved.d.ts +10 -0
- package/dist/saved.js +51 -0
- package/dist/search.d.ts +9 -0
- package/dist/search.js +59 -0
- package/dist/social.d.ts +25 -0
- package/dist/social.js +31 -0
- package/dist/translation-settings.d.ts +12 -0
- package/dist/translation-settings.js +16 -0
- package/dist/use-quote-gesture.d.ts +5 -1
- package/dist/use-quote-gesture.js +35 -17
- package/dist/visual/RobonoIcon.js +2 -1
- package/dist/visual/WeatherArtwork.d.ts +9 -0
- package/dist/visual/WeatherArtwork.js +43 -0
- package/dist/visual/weather-artwork.d.ts +17 -0
- package/dist/visual/weather-artwork.js +86 -0
- package/dist/visual/weather-conditions.d.ts +5 -0
- package/dist/visual/weather-conditions.js +2 -0
- package/dist/voice-composer.js +2 -2
- package/dist/voice-waveform.d.ts +2 -0
- package/dist/voice-waveform.js +21 -0
- package/package.json +41 -5
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Only adjacent parts of the same send share a rail. Never join across a
|
|
2
|
+
* missing part, another sender/conversation, or a date separator. */
|
|
3
|
+
export function continuesAttachmentSend(before, after) {
|
|
4
|
+
if (!before || !after || before.deleted || after.deleted)
|
|
5
|
+
return false;
|
|
6
|
+
const a = before.attachmentBatch, b = after.attachmentBatch;
|
|
7
|
+
return !!a && !!b && a.id === b.id && a.count === b.count && a.index + 1 === b.index
|
|
8
|
+
&& before.senderId === after.senderId && before.conversationId === after.conversationId
|
|
9
|
+
&& new Date(before.createdAt).toDateString() === new Date(after.createdAt).toDateString();
|
|
10
|
+
}
|
|
11
|
+
/** Upload completion order must not reorder the attachments selected together.
|
|
12
|
+
* Transport state remains in server sequence order for reconnect cursors. */
|
|
13
|
+
export function orderChatMessages(messages) {
|
|
14
|
+
const first = new Map();
|
|
15
|
+
const key = (m) => m.attachmentBatch ? JSON.stringify([m.senderId, m.attachmentBatch.id]) : null;
|
|
16
|
+
for (let i = 0; i < messages.length; i++) {
|
|
17
|
+
const k = key(messages[i]);
|
|
18
|
+
if (k && !first.has(k))
|
|
19
|
+
first.set(k, i);
|
|
20
|
+
}
|
|
21
|
+
return messages.map((message, index) => ({ message, index })).sort((a, b) => {
|
|
22
|
+
const ak = key(a.message), bk = key(b.message);
|
|
23
|
+
if (ak && ak === bk)
|
|
24
|
+
return a.message.attachmentBatch.index - b.message.attachmentBatch.index || a.index - b.index;
|
|
25
|
+
return (ak ? first.get(ak) : a.index) - (bk ? first.get(bk) : b.index);
|
|
26
|
+
}).map(item => item.message);
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type VideoViewProps, type VideoPlayer } from 'expo-video';
|
|
3
|
+
/** Shared by Robono Messenger and the SDK. A decoded frame includes the camera's
|
|
4
|
+
* rotation; the encoded track's natural width/height does not necessarily do so. */
|
|
5
|
+
export declare function MessengerVideoView({ player, posterUri, ...props }: Omit<VideoViewProps, 'player' | 'contentFit' | 'fullscreenOptions'> & {
|
|
6
|
+
player: VideoPlayer;
|
|
7
|
+
posterUri?: string | null;
|
|
8
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Image } from 'react-native';
|
|
4
|
+
import { VideoView } from 'expo-video';
|
|
5
|
+
/** Shared by Robono Messenger and the SDK. A decoded frame includes the camera's
|
|
6
|
+
* rotation; the encoded track's natural width/height does not necessarily do so. */
|
|
7
|
+
export function MessengerVideoView({ player, posterUri, ...props }) {
|
|
8
|
+
const [orientation, setOrientation] = useState('default');
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
let alive = true, pending = false, resolved = false;
|
|
11
|
+
setOrientation('default');
|
|
12
|
+
const accept = (width, height) => { if (!alive || width <= 0 || height <= 0)
|
|
13
|
+
return; resolved = true; setOrientation(width > height ? 'landscape' : 'portrait'); };
|
|
14
|
+
const decode = async () => {
|
|
15
|
+
if (!alive || pending || resolved || player.status !== 'readyToPlay' || typeof player.generateThumbnailsAsync !== 'function')
|
|
16
|
+
return;
|
|
17
|
+
pending = true;
|
|
18
|
+
try {
|
|
19
|
+
const frames = await player.generateThumbnailsAsync(0, { maxWidth: 96, maxHeight: 96 });
|
|
20
|
+
try {
|
|
21
|
+
const frame = frames[0];
|
|
22
|
+
if (frame)
|
|
23
|
+
accept(frame.width, frame.height);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
for (const frame of frames)
|
|
27
|
+
frame.release();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch { /* Keep the platform default if this source cannot provide a frame. */ }
|
|
31
|
+
finally {
|
|
32
|
+
pending = false;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
if (posterUri)
|
|
36
|
+
Image.getSize(posterUri, accept, () => { void decode(); });
|
|
37
|
+
else
|
|
38
|
+
void decode();
|
|
39
|
+
const status = player.addListener('statusChange', () => { if (!posterUri)
|
|
40
|
+
void decode(); });
|
|
41
|
+
const source = player.addListener('sourceLoad', () => { if (!posterUri)
|
|
42
|
+
void decode(); });
|
|
43
|
+
return () => { alive = false; status.remove(); source.remove(); };
|
|
44
|
+
}, [player, posterUri]);
|
|
45
|
+
return _jsx(VideoView, { nativeControls: true, ...props, player: player, contentFit: "contain", fullscreenOptions: { enable: true, orientation } });
|
|
46
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { TranslationPreference, MessageContent, MessageMedia, MessagingOutbox, SavedOutgoingMessage } from '@robono/messaging';
|
|
2
|
+
import type { LocalMessagingMedia } from './media-controller.js';
|
|
3
|
+
export interface NativeDraftFiles {
|
|
4
|
+
read(scope: string, conversation: string): Promise<NativeDraftState>;
|
|
5
|
+
write(scope: string, conversation: string, state: NativeDraftState): Promise<void>;
|
|
6
|
+
copy(scope: string, conversation: string, id: string, source: LocalMessagingMedia): Promise<string>;
|
|
7
|
+
file(scope: string, conversation: string, id: string): Promise<string | null>;
|
|
8
|
+
remove(scope: string, conversation: string, id: string): Promise<void>;
|
|
9
|
+
clear(scope: string, conversation?: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface NativeDraftState {
|
|
12
|
+
translationPreference?: TranslationPreference;
|
|
13
|
+
quickMessage?: string;
|
|
14
|
+
draft: string;
|
|
15
|
+
messages: Array<SavedOutgoingMessage & {
|
|
16
|
+
fileId?: string;
|
|
17
|
+
}>;
|
|
18
|
+
files: Record<string, {
|
|
19
|
+
kind: MessageContent['kind'];
|
|
20
|
+
media: MessageMedia;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export declare const emptyNativeDraft: () => NativeDraftState;
|
|
24
|
+
/** Durable outgoing content only, isolated by endpoint, namespace and signed-in user. */
|
|
25
|
+
export declare class NativeMessagingOutbox implements MessagingOutbox {
|
|
26
|
+
private files;
|
|
27
|
+
private generation;
|
|
28
|
+
private scope;
|
|
29
|
+
constructor(endpoint: string, userId: string, files: NativeDraftFiles, namespace?: string);
|
|
30
|
+
private serial;
|
|
31
|
+
load(cid: string): Promise<{
|
|
32
|
+
draft: string;
|
|
33
|
+
messages: {
|
|
34
|
+
clientMessageId: string;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
content: MessageContent;
|
|
37
|
+
needsPreparation?: boolean;
|
|
38
|
+
}[];
|
|
39
|
+
attachments: {
|
|
40
|
+
kind: "text" | "video" | "image" | "document" | "poll" | "voice";
|
|
41
|
+
media: MessageMedia;
|
|
42
|
+
}[];
|
|
43
|
+
quickMessage?: string | undefined;
|
|
44
|
+
translationPreference?: TranslationPreference | undefined;
|
|
45
|
+
}>;
|
|
46
|
+
put(cid: string, message: SavedOutgoingMessage): Promise<void>;
|
|
47
|
+
remove(cid: string, id: string): Promise<void>;
|
|
48
|
+
draft(cid: string, text: string): Promise<void>;
|
|
49
|
+
translationPreference(cid: string, value: TranslationPreference): Promise<void>;
|
|
50
|
+
quickMessage(cid: string, emoji: string): Promise<void>;
|
|
51
|
+
clear(cid?: string): Promise<void>;
|
|
52
|
+
saveFile(cid: string, media: MessageMedia, file: LocalMessagingMedia, kind: MessageContent['kind']): Promise<void>;
|
|
53
|
+
file(cid: string, id: string): Promise<LocalMessagingMedia | undefined>;
|
|
54
|
+
releaseFile(cid: string, id: string): Promise<void>;
|
|
55
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const emptyNativeDraft = () => ({ draft: '', messages: [], files: {} });
|
|
2
|
+
const queues = new Map();
|
|
3
|
+
const epochs = new Map();
|
|
4
|
+
/** Durable outgoing content only, isolated by endpoint, namespace and signed-in user. */
|
|
5
|
+
export class NativeMessagingOutbox {
|
|
6
|
+
files;
|
|
7
|
+
generation = 0;
|
|
8
|
+
scope;
|
|
9
|
+
constructor(endpoint, userId, files, namespace = 'default') {
|
|
10
|
+
this.files = files;
|
|
11
|
+
this.scope = JSON.stringify([endpoint, namespace, userId]);
|
|
12
|
+
}
|
|
13
|
+
serial(work) { const task = (queues.get(this.scope) ?? Promise.resolve()).catch(() => { }).then(work); queues.set(this.scope, task); void task.finally(() => { if (queues.get(this.scope) === task)
|
|
14
|
+
queues.delete(this.scope); }).catch(() => { }); return task; }
|
|
15
|
+
load(cid) { return this.serial(async () => { const saved = await this.files.read(this.scope, cid), used = new Set(saved.messages.map(m => m.fileId)); return { ...(saved.translationPreference ? { translationPreference: saved.translationPreference } : {}), ...(saved.quickMessage ? { quickMessage: saved.quickMessage } : {}), draft: saved.draft, messages: saved.messages.map(({ fileId, ...message }) => message), attachments: Object.entries(saved.files).filter(([id]) => !used.has(id)).map(([, f]) => ({ kind: f.kind, media: f.media })) }; }); }
|
|
16
|
+
put(cid, message) { return this.serial(async () => { const state = await this.files.read(this.scope, cid), previous = state.messages.find(m => m.clientMessageId === message.clientMessageId); state.messages = state.messages.filter(m => m.clientMessageId !== message.clientMessageId); state.messages.push({ ...message, fileId: message.needsPreparation ? message.content.media?.assetId : previous?.fileId }); await this.files.write(this.scope, cid, state); }); }
|
|
17
|
+
remove(cid, id) { return this.serial(async () => { const state = await this.files.read(this.scope, cid), fileId = state.messages.find(m => m.clientMessageId === id)?.fileId; state.messages = state.messages.filter(m => m.clientMessageId !== id); if (fileId && !state.messages.some(m => m.fileId === fileId)) {
|
|
18
|
+
delete state.files[fileId];
|
|
19
|
+
await this.files.write(this.scope, cid, state);
|
|
20
|
+
await this.files.remove(this.scope, cid, fileId);
|
|
21
|
+
}
|
|
22
|
+
else
|
|
23
|
+
await this.files.write(this.scope, cid, state); }); }
|
|
24
|
+
draft(cid, text) { return this.serial(async () => { const state = await this.files.read(this.scope, cid); state.draft = text.slice(0, 10000); await this.files.write(this.scope, cid, state); }); }
|
|
25
|
+
translationPreference(cid, value) { return this.serial(async () => { const state = await this.files.read(this.scope, cid); state.translationPreference = value; await this.files.write(this.scope, cid, state); }); }
|
|
26
|
+
quickMessage(cid, emoji) { return this.serial(async () => { const state = await this.files.read(this.scope, cid); state.quickMessage = emoji; await this.files.write(this.scope, cid, state); }); }
|
|
27
|
+
clear(cid) { this.generation++; epochs.set(this.scope, (epochs.get(this.scope) ?? 0) + 1); return this.serial(() => this.files.clear(this.scope, cid)); }
|
|
28
|
+
saveFile(cid, media, file, kind) {
|
|
29
|
+
const generation = this.generation, epoch = epochs.get(this.scope) ?? 0;
|
|
30
|
+
const current = () => generation === this.generation && epoch === (epochs.get(this.scope) ?? 0);
|
|
31
|
+
return this.serial(async () => {
|
|
32
|
+
if (!current())
|
|
33
|
+
throw Error('The chat closed before the attachment was saved.');
|
|
34
|
+
await this.files.copy(this.scope, cid, media.assetId, file);
|
|
35
|
+
try {
|
|
36
|
+
if (!current())
|
|
37
|
+
throw Error('The chat closed before the attachment was saved.');
|
|
38
|
+
const state = await this.files.read(this.scope, cid);
|
|
39
|
+
state.files[media.assetId] = { kind, media };
|
|
40
|
+
await this.files.write(this.scope, cid, state);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
await this.files.remove(this.scope, cid, media.assetId).catch(() => { });
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
file(cid, id) { return this.serial(async () => { const saved = (await this.files.read(this.scope, cid)).files[id]; if (!saved)
|
|
49
|
+
return undefined; const uri = await this.files.file(this.scope, cid, id); if (!uri)
|
|
50
|
+
throw Error('This saved attachment is missing. Remove it and choose it again.'); return { ...saved.media, uri }; }); }
|
|
51
|
+
releaseFile(cid, id) { return this.serial(async () => { const state = await this.files.read(this.scope, cid); if (state.messages.some(m => m.fileId === id))
|
|
52
|
+
return; delete state.files[id]; await this.files.write(this.scope, cid, state); await this.files.remove(this.scope, cid, id); }); }
|
|
53
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Messenger photo transforms, shared by native touches and browser pointers. */
|
|
2
|
+
export type PhotoPoint = {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
};
|
|
6
|
+
export type PhotoTransform = {
|
|
7
|
+
zoom: number;
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function createPhotoGesture(changed: (value: PhotoTransform) => void): {
|
|
12
|
+
readonly value: PhotoTransform;
|
|
13
|
+
layout(w: number, h: number): void;
|
|
14
|
+
image(w: number, h: number): void;
|
|
15
|
+
reset(): void;
|
|
16
|
+
zoom(zoom: number): void;
|
|
17
|
+
begin(points: PhotoPoint[]): void;
|
|
18
|
+
move(points: PhotoPoint[]): void;
|
|
19
|
+
end(points: PhotoPoint[], cancelled?: boolean): boolean;
|
|
20
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
|
|
2
|
+
const center = (p) => ({ x: p.reduce((n, p) => n + p.x, 0) / p.length, y: p.reduce((n, p) => n + p.y, 0) / p.length });
|
|
3
|
+
const distance = (p) => p.length > 1 ? Math.hypot(p[0].x - p[1].x, p[0].y - p[1].y) : 0;
|
|
4
|
+
export function createPhotoGesture(changed) {
|
|
5
|
+
let value = { zoom: 1, x: 0, y: 0 }, width = 1, height = 1, imageWidth = 1, imageHeight = 1;
|
|
6
|
+
let start = null;
|
|
7
|
+
let tap = false, started = 0;
|
|
8
|
+
function apply(zoom, x, y) {
|
|
9
|
+
zoom = clamp(zoom, 1, 5);
|
|
10
|
+
const fit = Math.min(width / imageWidth, height / imageHeight);
|
|
11
|
+
const maxX = Math.max(0, (imageWidth * fit * zoom - width) / 2), maxY = Math.max(0, (imageHeight * fit * zoom - height) / 2);
|
|
12
|
+
value = { zoom, x: clamp(x, -maxX, maxX), y: clamp(y, -maxY, maxY) };
|
|
13
|
+
changed(value);
|
|
14
|
+
}
|
|
15
|
+
function rebase(points) { start = points.length ? { value: { ...value }, center: center(points), distance: distance(points), count: points.length } : null; }
|
|
16
|
+
return {
|
|
17
|
+
get value() { return value; },
|
|
18
|
+
layout(w, h) { width = Math.max(1, w); height = Math.max(1, h); apply(value.zoom, value.x, value.y); },
|
|
19
|
+
image(w, h) { imageWidth = Math.max(1, w); imageHeight = Math.max(1, h); apply(value.zoom, value.x, value.y); },
|
|
20
|
+
reset() { tap = false; start = null; apply(1, 0, 0); },
|
|
21
|
+
zoom(zoom) { tap = false; apply(zoom, 0, 0); },
|
|
22
|
+
begin(points) { if (!start) {
|
|
23
|
+
tap = points.length === 1;
|
|
24
|
+
started = Date.now();
|
|
25
|
+
}
|
|
26
|
+
else
|
|
27
|
+
tap = false; rebase(points); },
|
|
28
|
+
move(points) {
|
|
29
|
+
if (!points.length)
|
|
30
|
+
return;
|
|
31
|
+
if (!start || start.count !== points.length) {
|
|
32
|
+
tap = false;
|
|
33
|
+
rebase(points);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const at = center(points), dx = at.x - start.center.x, dy = at.y - start.center.y;
|
|
37
|
+
if (points.length > 1 || Math.abs(dx) > 8 || Math.abs(dy) > 8)
|
|
38
|
+
tap = false;
|
|
39
|
+
const zoom = points.length > 1 && start.distance > 0 ? start.value.zoom * distance(points) / start.distance : start.value.zoom;
|
|
40
|
+
apply(zoom, start.value.x + dx, start.value.y + dy);
|
|
41
|
+
},
|
|
42
|
+
end(points, cancelled = false) {
|
|
43
|
+
const tapped = !cancelled && !points.length && tap && Date.now() - started < 260;
|
|
44
|
+
if (points.length)
|
|
45
|
+
tap = false;
|
|
46
|
+
rebase(points);
|
|
47
|
+
if (!points.length)
|
|
48
|
+
tap = false;
|
|
49
|
+
return tapped;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Show the latest selection immediately, but serialize complete selections so
|
|
2
|
+
* an older, slower request can never overwrite a newer vote on the server. */
|
|
3
|
+
export declare function createPollVoteQueue(save: (choices: number[]) => Promise<void>, pending: (choices: number[] | null) => void, failed: () => void): (choices: number[]) => Promise<void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Show the latest selection immediately, but serialize complete selections so
|
|
2
|
+
* an older, slower request can never overwrite a newer vote on the server. */
|
|
3
|
+
export function createPollVoteQueue(save, pending, failed) {
|
|
4
|
+
let running = false, next;
|
|
5
|
+
return async (choices) => {
|
|
6
|
+
next = [...choices];
|
|
7
|
+
pending(next);
|
|
8
|
+
if (running)
|
|
9
|
+
return;
|
|
10
|
+
running = true;
|
|
11
|
+
try {
|
|
12
|
+
while (next) {
|
|
13
|
+
const current = next;
|
|
14
|
+
next = undefined;
|
|
15
|
+
try {
|
|
16
|
+
await save(current);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
if (!next)
|
|
20
|
+
failed();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
running = false;
|
|
26
|
+
pending(null);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
package/dist/polls.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { type MessagingAppearance, type MessagingMessage, type MessagePoll } from '@robono/messaging';
|
|
3
3
|
/** Optional reusable poll content. Votes are submitted as a complete selection, not a toggle request. */
|
|
4
|
-
export declare function PollMessage({ message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, background, foreground }: {
|
|
4
|
+
export declare function PollMessage({ message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, background, foreground, renderDetails, detailsOpen, onDetailsOpenChange, onLongPress }: {
|
|
5
|
+
onLongPress?: () => void;
|
|
6
|
+
detailsOpen?: boolean;
|
|
7
|
+
onDetailsOpenChange?: (open: boolean) => void;
|
|
8
|
+
renderDetails?: (content: React.ReactNode, onClose: () => void) => React.ReactNode;
|
|
5
9
|
message: MessagingMessage;
|
|
6
10
|
userId: string;
|
|
7
11
|
ink: string;
|
|
@@ -13,7 +17,8 @@ export declare function PollMessage({ message, userId, ink, accent, enabled, onV
|
|
|
13
17
|
background?: string;
|
|
14
18
|
foreground?: string;
|
|
15
19
|
}): React.JSX.Element | null;
|
|
16
|
-
export declare function PollComposer({ appearance, onSend, onCancel, retrySafe }: {
|
|
20
|
+
export declare function PollComposer({ appearance, onSend, onCancel, retrySafe, yesNo }: {
|
|
21
|
+
yesNo?: boolean;
|
|
17
22
|
retrySafe?: boolean;
|
|
18
23
|
appearance: MessagingAppearance;
|
|
19
24
|
onSend: (poll: MessagePoll) => Promise<void>;
|
package/dist/polls.js
CHANGED
|
@@ -1,36 +1,100 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { messagingPopupColors } from './popup.js';
|
|
3
|
+
import { ExtraPopup } from './extras.js';
|
|
4
|
+
import { MessageIcon } from './icons.js';
|
|
5
|
+
import { useRef, useState } from 'react';
|
|
6
|
+
import { createPollVoteQueue } from './pollVoteQueue';
|
|
7
|
+
import { ActivityIndicator, Pressable, ScrollView, Switch, Text, TextInput, View } from 'react-native';
|
|
5
8
|
import { validatePoll } from '@robono/messaging';
|
|
9
|
+
const PollArea = View;
|
|
10
|
+
import { MessagingPopup as PollModal } from './popup.js';
|
|
6
11
|
/** Optional reusable poll content. Votes are submitted as a complete selection, not a toggle request. */
|
|
7
|
-
export function PollMessage({ message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, background = '#101010', foreground = '#fff' }) {
|
|
8
|
-
const [busy, setBusy] = useState(false), [error, setError] = useState(''), [
|
|
12
|
+
export function PollMessage({ message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, background = '#101010', foreground = '#fff', renderDetails, detailsOpen, onDetailsOpenChange, onLongPress }) {
|
|
13
|
+
const [busy, setBusy] = useState(false), [error, setError] = useState(''), [localShowVotes, setLocalShowVotes] = useState(false);
|
|
14
|
+
const showVotes = detailsOpen ?? localShowVotes;
|
|
15
|
+
const setShowVotes = (open) => { setLocalShowVotes(open); onDetailsOpenChange?.(open); };
|
|
16
|
+
const [pending, setPending] = useState(null);
|
|
17
|
+
const current = useRef({ onVote });
|
|
18
|
+
current.current = { onVote };
|
|
19
|
+
const selection = useRef([]);
|
|
20
|
+
const queue = useRef(null);
|
|
21
|
+
if (!queue.current)
|
|
22
|
+
queue.current = createPollVoteQueue(choices => current.current.onVote(choices), choices => { if (choices)
|
|
23
|
+
selection.current = choices; setPending(choices); }, () => setError('The poll could not be updated. Try again.'));
|
|
24
|
+
const actionBusy = useRef(false);
|
|
9
25
|
const poll = message.poll;
|
|
10
26
|
if (!poll)
|
|
11
27
|
return null;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
28
|
+
if (poll.kind === 'yes_no')
|
|
29
|
+
return _jsx(YesNoQuestion, { message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, renderDetails, detailsOpen, onDetailsOpenChange, onLongPress, background, foreground });
|
|
30
|
+
const savedVotes = message.pollState?.votes ?? [], selected = pending ?? savedVotes.find(v => v.userId === userId)?.options ?? [], closed = !!message.pollState?.closed;
|
|
31
|
+
selection.current = selected;
|
|
32
|
+
const votes = pending === null ? savedVotes : [...savedVotes.filter(v => v.userId !== userId), ...(pending.length ? [{ userId, options: pending }] : [])];
|
|
33
|
+
async function act(work) { if (actionBusy.current)
|
|
34
|
+
return; actionBusy.current = true; setBusy(true); setError(''); try {
|
|
15
35
|
await work();
|
|
16
36
|
}
|
|
17
37
|
catch {
|
|
18
38
|
setError('The poll could not be updated. Try again.');
|
|
19
39
|
}
|
|
20
40
|
finally {
|
|
41
|
+
actionBusy.current = false;
|
|
21
42
|
setBusy(false);
|
|
22
43
|
} }
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
44
|
+
const details = _jsxs(View, { style: { gap: 14 }, children: [_jsxs(View, { style: { gap: 4 }, children: [_jsx(Text, { style: { fontSize: 17, lineHeight: 23, fontWeight: '600', color: messagingPopupColors.foreground }, children: poll.question }), _jsxs(Text, { style: { fontSize: 12, lineHeight: 17, color: messagingPopupColors.foreground, opacity: .65 }, children: [votes.length, " ", votes.length === 1 ? 'person voted' : 'people voted', " \u00B7 ", closed ? 'Closed' : poll.multiple ? 'Multiple answers' : 'One answer per person'] })] }), _jsx(View, { style: { gap: 14 }, children: poll.options.map((option, index) => {
|
|
45
|
+
const people = votes.filter(v => v.options.includes(index));
|
|
46
|
+
const names = people.map(v => v.userId === userId ? 'You' : resolveVoterName?.(v.userId) || 'Chat member').join(', ');
|
|
47
|
+
return _jsxs(View, { style: { gap: 5 }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'baseline', gap: 12 }, children: [_jsx(Text, { style: { flex: 1, color: messagingPopupColors.foreground, fontSize: 15, lineHeight: 21, fontWeight: '600' }, children: option }), _jsx(Text, { style: { color: messagingPopupColors.foreground, opacity: .65, fontSize: 13 }, children: people.length })] }), _jsxs(View, { style: { height: 4, borderRadius: 2, overflow: 'hidden' }, children: [_jsx(View, { style: { position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, backgroundColor: foreground, opacity: .12 } }), _jsx(View, { style: { height: 4, width: `${votes.length ? people.length / votes.length * 100 : 0}%`, backgroundColor: accent } })] }), !!names && _jsx(Text, { style: { color: messagingPopupColors.foreground, opacity: .7, fontSize: 13, lineHeight: 18 }, children: names })] }, index);
|
|
48
|
+
}) }), message.senderId === userId && !closed && enabled && _jsx(Pressable, { accessibilityRole: "button", disabled: busy || pending !== null, onPress: () => void act(onClose), style: { minHeight: 36, alignSelf: 'flex-end', justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent, fontSize: 14 }, children: "End poll" }) }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: messagingPopupColors.foreground, fontSize: 13, lineHeight: 18 }, children: error })] });
|
|
49
|
+
return _jsxs(_Fragment, { children: [_jsxs(Pressable, { accessible: false, onLongPress: onLongPress, onPress: event => { event.stopPropagation(); setShowVotes(true); }, accessibilityLabel: `Poll: ${poll.question}. Open votes`, style: { width: 270, maxWidth: '100%', gap: 0, padding: 0 }, children: [_jsxs(Text, { accessibilityRole: "button", onLongPress: onLongPress, onPress: () => setShowVotes(true), style: { fontSize: 17, lineHeight: 23, fontWeight: '600', color: ink }, accessibilityHint: closed ? 'Poll closed' : poll.multiple ? 'Choose one or more answers' : 'Choose one answer', children: [poll.question, closed ? ' · Closed' : ''] }), poll.options.map((label, index) => {
|
|
50
|
+
const count = votes.filter(v => v.options.includes(index)).length, checked = selected.includes(index);
|
|
51
|
+
return _jsxs(View, { style: { minHeight: 36, paddingVertical: 3, gap: 2 }, children: [_jsxs(View, { style: { flexDirection: 'row', gap: 10, alignItems: 'center' }, children: [_jsx(Pressable, { accessibilityRole: poll.multiple ? 'checkbox' : 'radio', accessibilityLabel: `${label}, ${count} ${count === 1 ? 'vote' : 'votes'}`, "aria-checked": checked, accessibilityState: { checked, disabled: !enabled || closed || busy, busy: pending !== null }, hitSlop: 4, disabled: !enabled || closed || busy, onPress: event => { event.stopPropagation(); if (!enabled || closed || busy || actionBusy.current)
|
|
52
|
+
return; const previous = selection.current; setError(''); void queue.current(previous.includes(index) ? previous.filter(x => x !== index) : poll.multiple ? [...previous, index] : [index]); }, style: { width: 20, height: 20, borderRadius: poll.multiple ? 4 : 10, borderWidth: 2, borderColor: checked ? accent : ink, alignItems: 'center', justifyContent: 'center' }, children: checked && _jsx(View, { style: { width: 10, height: 10, borderRadius: poll.multiple ? 2 : 5, backgroundColor: accent } }) }), _jsx(Text, { style: { flex: 1, color: ink, fontSize: 16 }, children: label }), _jsx(Text, { style: { color: ink, fontSize: 13 }, children: count })] }), _jsx(View, { pointerEvents: "none", style: { height: 3, marginLeft: 30, backgroundColor: ink + '20', borderRadius: 2, overflow: 'hidden' }, children: _jsx(View, { style: { height: 3, width: `${votes.length ? count / votes.length * 100 : 0}%`, backgroundColor: accent } }) })] }, index);
|
|
53
|
+
}), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: ink }, children: error })] }), showVotes && (renderDetails ? renderDetails(details, () => setShowVotes(false)) : _jsx(ExtraPopup, { title: "Votes", background: background, foreground: foreground, onClose: () => setShowVotes(false), children: _jsx(ScrollView, { style: { flexShrink: 1 }, children: details }) }))] });
|
|
27
54
|
}
|
|
28
|
-
|
|
55
|
+
/** Recipient-only decision. Keep the attempted answer on an uncertain failure so
|
|
56
|
+
* retry cannot silently switch an answer the server may already have accepted. */
|
|
57
|
+
function YesNoQuestion({ message, userId, ink, accent, enabled, onVote, onClose, resolveVoterName, renderDetails, detailsOpen, onDetailsOpenChange, onLongPress, background = '#111111', foreground = '#fff' }) {
|
|
58
|
+
const [attempt, setAttempt] = useState(null), [busy, setBusy] = useState(false), [error, setError] = useState(''), [localOpen, setLocalOpen] = useState(false);
|
|
59
|
+
const inFlight = useRef(false), firstChoice = useRef(null);
|
|
60
|
+
const poll = message.poll, answer = message.pollState?.votes.find(v => v.userId !== message.senderId)?.options[0];
|
|
61
|
+
const closed = !!message.pollState?.closed, chosen = answer ?? attempt, mine = message.senderId === userId;
|
|
62
|
+
const open = detailsOpen ?? localOpen;
|
|
63
|
+
const setOpen = (value) => { setLocalOpen(value); onDetailsOpenChange?.(value); };
|
|
64
|
+
async function choose(index) {
|
|
65
|
+
if (inFlight.current || !enabled || mine || closed)
|
|
66
|
+
return;
|
|
67
|
+
if (firstChoice.current !== null && firstChoice.current !== index)
|
|
68
|
+
return;
|
|
69
|
+
firstChoice.current = index;
|
|
70
|
+
inFlight.current = true;
|
|
71
|
+
setAttempt(index);
|
|
72
|
+
setBusy(true);
|
|
73
|
+
setError('');
|
|
74
|
+
setOpen(false);
|
|
75
|
+
try {
|
|
76
|
+
await onVote([index]);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
setError('Answer not confirmed. Tap to retry.');
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
inFlight.current = false;
|
|
83
|
+
setBusy(false);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const respondent = message.pollState?.votes.find(v => v.userId !== message.senderId)?.userId;
|
|
87
|
+
const details = _jsxs(View, { style: { gap: 12 }, children: [_jsx(Text, { style: { color: messagingPopupColors.foreground, fontSize: 17, lineHeight: 23, fontWeight: '600' }, children: poll.question }), _jsx(Text, { style: { color: messagingPopupColors.foreground, fontSize: 15, lineHeight: 21 }, children: answer !== undefined ? `${respondent === userId ? 'You' : resolveVoterName?.(respondent) || 'Recipient'} answered ${poll.options[answer]}.` : closed ? 'Closed without an answer.' : attempt !== null ? 'Confirming your answer…' : 'Waiting for an answer.' }), mine && !closed && enabled && _jsx(Pressable, { accessibilityRole: "button", onPress: () => { if (inFlight.current)
|
|
88
|
+
return; inFlight.current = true; void onClose().catch(() => setError('Could not close the question. Try again.')).finally(() => { inFlight.current = false; }); }, style: { minHeight: 36, alignSelf: 'flex-end', justifyContent: 'center' }, children: _jsx(Text, { style: { color: accent }, children: "End question" }) })] });
|
|
89
|
+
return _jsxs(_Fragment, { children: [_jsxs(Pressable, { accessible: false, onLongPress: onLongPress, onPress: e => { e.stopPropagation(); setOpen(true); }, testID: "yes-no-question", style: { maxWidth: 270, minWidth: 160 }, children: [_jsxs(View, { style: { flexDirection: 'row', alignItems: 'flex-start', gap: 6 }, children: [_jsx(View, { style: { paddingTop: 3 }, children: _jsx(MessageIcon, { name: "poll", color: accent, size: 16 }) }), _jsx(Text, { accessibilityRole: "button", onLongPress: onLongPress, onPress: () => setOpen(true), style: { flexShrink: 1, fontSize: 16, lineHeight: 22, fontWeight: '600', color: ink }, children: poll.question })] }), _jsx(View, { style: { flexDirection: 'row', gap: 20, marginTop: 2 }, children: poll.options.map((label, index) => _jsxs(Pressable, { accessibilityRole: "radio", accessibilityLabel: label, accessibilityState: { checked: chosen === index, disabled: !enabled || mine || closed || attempt !== null, busy }, disabled: !enabled || mine || closed || attempt !== null, onPress: e => { e.stopPropagation(); void choose(index); }, hitSlop: 4, style: { flexDirection: 'row', alignItems: 'center', gap: 8, minHeight: 32 }, children: [_jsx(View, { style: { width: 20, height: 20, borderRadius: 10, borderWidth: 2, borderColor: chosen === index ? accent : ink, alignItems: 'center', justifyContent: 'center', opacity: chosen === undefined && mine ? .5 : 1 }, children: chosen === index && _jsx(View, { style: { width: 10, height: 10, borderRadius: 5, backgroundColor: accent } }) }), _jsx(Text, { style: { color: ink, fontSize: 16, fontWeight: chosen === index ? '600' : '400' }, children: label })] }, index)) }), !!error && !closed && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Retry answer", disabled: busy || closed || attempt === null, onPress: e => { e.stopPropagation(); if (attempt !== null)
|
|
90
|
+
void choose(attempt); }, children: _jsx(Text, { accessibilityRole: "alert", style: { color: ink, fontSize: 13 }, children: error }) })] }), open && (renderDetails ? renderDetails(details, () => setOpen(false)) : _jsx(ExtraPopup, { title: "Answer", background: background, foreground: foreground, onClose: () => setOpen(false), children: _jsx(ScrollView, { style: { flexShrink: 1 }, children: details }) }))] });
|
|
91
|
+
}
|
|
92
|
+
export function PollComposer({ appearance, onSend, onCancel, retrySafe = false, yesNo = false }) {
|
|
29
93
|
const [question, setQuestion] = useState(''), [options, setOptions] = useState(['', '']), [multiple, setMultiple] = useState(false), [busy, setBusy] = useState(false), [error, setError] = useState(''), [submitted, setSubmitted] = useState(false);
|
|
30
|
-
const c = appearance.colors;
|
|
94
|
+
const c = { ...appearance.colors, ...messagingPopupColors };
|
|
31
95
|
async function send() { if (busy || submitted)
|
|
32
96
|
return; let poll; try {
|
|
33
|
-
poll = validatePoll({ question, options, multiple });
|
|
97
|
+
poll = validatePoll(yesNo ? { question, options: ['Yes', 'No'], multiple: false, kind: 'yes_no' } : { question, options, multiple });
|
|
34
98
|
}
|
|
35
99
|
catch (e) {
|
|
36
100
|
setError(e.message);
|
|
@@ -47,8 +111,13 @@ export function PollComposer({ appearance, onSend, onCancel, retrySafe = false }
|
|
|
47
111
|
finally {
|
|
48
112
|
setBusy(false);
|
|
49
113
|
} }
|
|
50
|
-
const inputStyle = { fontSize: 17, color: c.foreground, minHeight: 48,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
114
|
+
const inputStyle = { fontSize: 17, color: c.foreground, minHeight: 48, borderWidth: 1, borderColor: '#ffffff20', borderRadius: 10, backgroundColor: '#111111', padding: 12 };
|
|
115
|
+
let ready = false;
|
|
116
|
+
try {
|
|
117
|
+
validatePoll(yesNo ? { question, options: ['Yes', 'No'], multiple: false, kind: 'yes_no' } : { question, options, multiple });
|
|
118
|
+
ready = true;
|
|
119
|
+
}
|
|
120
|
+
catch { }
|
|
121
|
+
return _jsx(PollModal, { contentWidth: 640, form: true, title: yesNo ? 'Yes/No question' : 'New poll', foreground: c.foreground, closeDisabled: busy, background: c.background, onClose: () => { if (!busy)
|
|
122
|
+
onCancel(); }, children: _jsxs(PollArea, { style: { flex: 1, minHeight: 0, backgroundColor: c.background }, children: [_jsx(View, { style: { flexShrink: 1 }, children: _jsxs(ScrollView, { keyboardShouldPersistTaps: "handled", keyboardDismissMode: "on-drag", contentContainerStyle: { padding: 0, paddingBottom: 12, gap: 8 }, children: [_jsx(Text, { style: { color: c.secondary, fontSize: 13, fontWeight: '600', marginTop: 4 }, children: "Question" }), _jsx(TextInput, { accessibilityLabel: "Poll question", placeholder: "Ask a question", placeholderTextColor: c.secondary, maxLength: 300, multiline: true, value: question, onChangeText: setQuestion, editable: !busy, style: inputStyle }), !yesNo && _jsxs(_Fragment, { children: [_jsx(Text, { style: { color: c.secondary, fontSize: 13, fontWeight: '600', marginTop: 12 }, children: "Answers" }), options.map((option, i) => _jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 8 }, children: [_jsx(TextInput, { accessibilityLabel: `Option ${i + 1}`, placeholder: "Add an option", placeholderTextColor: c.secondary, maxLength: 100, value: option, onChangeText: v => setOptions(items => items.map((x, n) => n === i ? v : x)), editable: !busy, style: { ...inputStyle, flex: 1 } }), options.length > 2 && _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: `Remove option ${i + 1}`, disabled: busy, onPress: () => setOptions(items => items.filter((_, n) => n !== i)), style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.secondary, fontSize: 26 }, children: "\u00D7" }) })] }, i)), options.length < 12 && _jsx(Pressable, { accessibilityRole: "button", disabled: busy, onPress: () => setOptions(items => [...items, '']), style: { minHeight: 44, justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.accent, fontSize: 16 }, children: "Add option" }) }), _jsxs(View, { style: { minHeight: 48, flexDirection: 'row', gap: 12, alignItems: 'center', marginTop: 8 }, children: [_jsx(Text, { style: { color: c.foreground, fontSize: 16, flex: 1 }, children: "Allow multiple answers" }), _jsx(Switch, { accessibilityLabel: "Allow multiple answers", disabled: busy, value: multiple, onValueChange: setMultiple, trackColor: { true: c.accent } })] }), _jsx(Text, { style: { color: c.secondary, fontSize: 13, lineHeight: 19 }, children: "Everyone in the chat can see the votes." })] }), yesNo && _jsx(Text, { style: { color: c.secondary, fontSize: 13, lineHeight: 19, marginTop: 8 }, children: "Their first Yes or No answer is final." }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground, marginTop: 8 }, children: error })] }) }), _jsxs(View, { style: { flexDirection: 'row', gap: 8, marginTop: 8 }, children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Cancel", disabled: busy, onPress: onCancel, style: { flex: 1, minHeight: 44, borderRadius: 12, backgroundColor: c.secondary + '20', alignItems: 'center', justifyContent: 'center' }, children: _jsx(Text, { style: { color: c.foreground, fontSize: 16, fontWeight: '600' }, children: "Cancel" }) }), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: yesNo ? "Send question" : "Send poll", disabled: busy || submitted || !ready, onPress: () => void send(), style: { flex: 1, minHeight: 44, borderRadius: 12, backgroundColor: c.accent, opacity: (busy || submitted || !ready) ? 0.45 : 1, alignItems: 'center', justifyContent: 'center' }, children: busy ? _jsx(ActivityIndicator, { color: "#fff" }) : _jsx(Text, { style: { color: '#fff', fontSize: 16, fontWeight: '600' }, children: "Send" }) })] })] }) });
|
|
54
123
|
}
|
package/dist/popup.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface MessagingPopupSurfaceProps {
|
|
3
|
+
children: React.ReactNode;
|
|
4
|
+
visible: boolean;
|
|
5
|
+
onDismiss?: () => void;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
title?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Browser hosts can keep settings dialogs inside their embedded chat. */
|
|
10
|
+
export declare const MessagingPopupSurface: React.Context<React.ComponentType<MessagingPopupSurfaceProps> | null>;
|
|
11
|
+
export declare const messagingPopupBackdrop = "#000000d9";
|
|
12
|
+
export declare const messagingPopupColors: {
|
|
13
|
+
background: string;
|
|
14
|
+
foreground: string;
|
|
15
|
+
secondary: string;
|
|
16
|
+
incoming: string;
|
|
17
|
+
};
|
|
18
|
+
/** One title/close row for forms, menus and expanded content. */
|
|
19
|
+
export declare function MessagingPopupHeader({ title, foreground, onClose, closeLabel, disabled, back }: {
|
|
20
|
+
title: string;
|
|
21
|
+
foreground: string;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
closeLabel?: string;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
back?: boolean;
|
|
26
|
+
}): React.JSX.Element;
|
|
27
|
+
export declare function MessagingPopupHost({ children }: {
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
}): React.JSX.Element;
|
|
30
|
+
export declare function MessagingPopupHeaderBoundary({ children }: {
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
}): React.JSX.Element;
|
|
33
|
+
/** Content panels occupy the area below the persistent header; confirmations stay compact. */
|
|
34
|
+
export declare function MessagingPopup({ children, onClose, background, form, visible, onDismiss, expanded, bottomPadding, title, foreground, closeLabel, closeDisabled, contentWidth, headerBack, withinChat, compact }: {
|
|
35
|
+
compact?: boolean;
|
|
36
|
+
withinChat?: boolean;
|
|
37
|
+
children: React.ReactNode;
|
|
38
|
+
onClose: () => void;
|
|
39
|
+
background: string;
|
|
40
|
+
form?: boolean;
|
|
41
|
+
visible?: boolean;
|
|
42
|
+
onDismiss?: () => void;
|
|
43
|
+
expanded?: boolean;
|
|
44
|
+
bottomPadding?: number;
|
|
45
|
+
title?: string;
|
|
46
|
+
foreground?: string;
|
|
47
|
+
closeLabel?: string;
|
|
48
|
+
closeDisabled?: boolean;
|
|
49
|
+
contentWidth?: number;
|
|
50
|
+
headerBack?: boolean;
|
|
51
|
+
}): React.JSX.Element;
|