@robono/messaging-react-native 0.5.0 → 0.7.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 +8 -0
- package/README.md +61 -5
- package/SBOM.spdx.json +411 -171
- package/dist/attachment-menu.d.ts +16 -0
- package/dist/attachment-menu.js +71 -0
- package/dist/expo-connected-calling.d.ts +32 -0
- package/dist/expo-connected-calling.js +41 -0
- package/dist/expo-extras.d.ts +18 -0
- package/dist/expo-extras.js +32 -0
- package/dist/expo-media.js +4 -4
- package/dist/expo-messenger.d.ts +34 -3
- package/dist/expo-messenger.js +27 -16
- package/dist/expo-notifications.d.ts +2 -1
- package/dist/expo-notifications.js +16 -5
- package/dist/expo-screen.js +10 -3
- package/dist/expo-tracking-task.d.ts +1 -0
- package/dist/expo-tracking-task.js +1 -0
- package/dist/extras-lifecycle.js +3 -3
- package/dist/extras.js +24 -4
- package/dist/header-calls.js +3 -2
- package/dist/inbox.js +28 -11
- package/dist/index.d.ts +6 -2
- package/dist/index.js +31 -36
- package/dist/maps-connection.d.ts +12 -0
- package/dist/maps-connection.js +42 -0
- package/dist/media-controller.d.ts +2 -2
- package/dist/media-controller.js +3 -3
- package/dist/message-menu.js +8 -4
- package/dist/popup.d.ts +25 -0
- package/dist/popup.js +71 -2
- package/dist/quick-send.js +3 -3
- package/dist/safety.js +3 -2
- package/dist/use-quote-gesture.d.ts +1 -1
- package/dist/use-quote-gesture.js +9 -3
- package/package.json +13 -4
package/dist/media-controller.js
CHANGED
|
@@ -168,7 +168,7 @@ export class MessagingMediaController {
|
|
|
168
168
|
})();
|
|
169
169
|
return result;
|
|
170
170
|
}
|
|
171
|
-
async pickMany(kind, maximum) {
|
|
171
|
+
async pickMany(kind, maximum, mixedLibrary = false) {
|
|
172
172
|
if (!this.options.stageLocal || !this.options.device.pickMany) {
|
|
173
173
|
const one = await this.pick(kind, undefined, 'attach');
|
|
174
174
|
return one ? [one] : [];
|
|
@@ -187,14 +187,14 @@ export class MessagingMediaController {
|
|
|
187
187
|
let files = [];
|
|
188
188
|
this.update({ phase: 'choosing', kind, file: null, error: null, progress: 0, retryUpload: false });
|
|
189
189
|
try {
|
|
190
|
-
files = await this.options.device.pickMany(kind, settings, abort.signal, limit);
|
|
190
|
+
files = await this.options.device.pickMany(kind, settings, abort.signal, limit, mixedLibrary);
|
|
191
191
|
if (!this.isCurrent(g) || conversationId !== this.options.getConversationId())
|
|
192
192
|
return [];
|
|
193
193
|
if (files.length > limit)
|
|
194
194
|
throw Error(`Choose up to ${limit} attachments.`);
|
|
195
195
|
const media = files.map(file => ({ assetId: this.options.createId(), mimeType: file.mimeType, fileName: file.fileName, byteSize: file.byteSize, ...(file.durationMs === undefined ? {} : { durationMs: file.durationMs }) }));
|
|
196
196
|
// Validate the whole selection before accepting any part of it.
|
|
197
|
-
media.forEach(item => validateMessageMedia(kind, item, this.options.getSettings()));
|
|
197
|
+
media.forEach(item => validateMessageMedia(mixedLibrary && kind !== 'document' ? (item.mimeType.startsWith('video/') ? 'video' : 'image') : kind, item, this.options.getSettings()));
|
|
198
198
|
media.forEach((item, index) => this.prepared.set(item.assetId, files[index]));
|
|
199
199
|
files = [];
|
|
200
200
|
return media;
|
package/dist/message-menu.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { messagingPopupBackdrop } from './popup.js';
|
|
2
|
+
import { messagingPopupBackdrop, MessagingMenuSurface } from './popup.js';
|
|
3
3
|
import { useEffect } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { Platform, Pressable, ScrollView, Text, View } from 'react-native';
|
|
5
5
|
import { MessageIcon } from './icons.js';
|
|
6
6
|
/** Anchor outside the selected bubble, never at its text's center. */
|
|
7
7
|
export function messageMenuPosition(x, y, width, height, screenWidth, screenHeight, mine) {
|
|
@@ -19,6 +19,10 @@ export function MessageMenu({ visible, position, screenHeight, mine, reactions,
|
|
|
19
19
|
e.stopPropagation();
|
|
20
20
|
onClose();
|
|
21
21
|
} }; document.addEventListener('keydown', key, true); return () => document.removeEventListener('keydown', key, true); }, [visible, onClose]);
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
return _jsx(MessagingMenuSurface, { visible: visible, onClose: onClose, onDismiss: onDismiss, children: rect => {
|
|
23
|
+
const menuWidth = Math.min(300, rect.width - 24), y = Math.max(12, Math.min(position.y - rect.top, rect.height - 12));
|
|
24
|
+
const local = { ...position, x: Math.max(12, Math.min(position.x - rect.left, rect.width - menuWidth - 12)), y, available: Math.max(44, (position.above ? y : rect.height - y) - 24) };
|
|
25
|
+
const card = _jsx(View, { testID: "message-action-card", style: { width: 218, maxWidth: '100%', maxHeight: Math.min(310, Math.max(43, local.available - (reactions ? 58 : 0))), borderRadius: 16, borderWidth: .5, borderColor: '#ffffff22', backgroundColor: '#151515', paddingVertical: 4, overflow: 'hidden', boxShadow: '0 8px 18px #0006', alignSelf: mine ? 'flex-end' : 'flex-start' }, children: _jsx(ScrollView, { showsVerticalScrollIndicator: false, bounces: false, keyboardShouldPersistTaps: "handled", children: children }) });
|
|
26
|
+
return _jsxs(View, { style: { flex: 1 }, children: [_jsx(Pressable, { accessibilityLabel: "Close message options", onPress: onClose, style: { position: 'absolute', inset: 0, backgroundColor: messagingPopupBackdrop } }), _jsxs(View, { testID: "message-context-menu", style: { position: 'absolute', left: local.x, ...(position.above ? { bottom: rect.height - local.y + 12 } : { top: local.y + 12 }), width: menuWidth, maxWidth: '94%', gap: 8 }, children: [position.above ? card : reactions, position.above ? reactions : card] })] });
|
|
27
|
+
} });
|
|
24
28
|
}
|
package/dist/popup.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
2
3
|
export interface MessagingPopupSurfaceProps {
|
|
3
4
|
children: React.ReactNode;
|
|
4
5
|
visible: boolean;
|
|
@@ -24,12 +25,35 @@ export declare function MessagingPopupHeader({ title, foreground, onClose, close
|
|
|
24
25
|
disabled?: boolean;
|
|
25
26
|
back?: boolean;
|
|
26
27
|
}): React.JSX.Element;
|
|
28
|
+
export declare function useMessagingPopupBounds(): {
|
|
29
|
+
root: React.RefObject<View | null>;
|
|
30
|
+
header: React.RefObject<View | null>;
|
|
31
|
+
revision: number;
|
|
32
|
+
changed: () => void;
|
|
33
|
+
} | null;
|
|
34
|
+
/** App-local overlay for anchored menus, without a browser-wide modal. */
|
|
35
|
+
export declare function MessagingPopupLayer({ children }: {
|
|
36
|
+
children: React.ReactNode;
|
|
37
|
+
}): React.JSX.Element | null;
|
|
27
38
|
export declare function MessagingPopupHost({ children }: {
|
|
28
39
|
children: React.ReactNode;
|
|
29
40
|
}): React.JSX.Element;
|
|
30
41
|
export declare function MessagingPopupHeaderBoundary({ children }: {
|
|
31
42
|
children: React.ReactNode;
|
|
32
43
|
}): React.JSX.Element;
|
|
44
|
+
type MenuRect = {
|
|
45
|
+
left: number;
|
|
46
|
+
top: number;
|
|
47
|
+
width: number;
|
|
48
|
+
height: number;
|
|
49
|
+
};
|
|
50
|
+
/** Anchored dropdowns use window coordinates on phones, app coordinates on web. */
|
|
51
|
+
export declare function MessagingMenuSurface({ visible, onClose, onDismiss, children }: {
|
|
52
|
+
visible: boolean;
|
|
53
|
+
onClose: () => void;
|
|
54
|
+
onDismiss?: () => void;
|
|
55
|
+
children: (rect: MenuRect) => React.ReactNode;
|
|
56
|
+
}): React.JSX.Element;
|
|
33
57
|
/** Content panels occupy the area below the persistent header; confirmations stay compact. */
|
|
34
58
|
export declare function MessagingPopup({ children, onClose, background, form, visible, onDismiss, expanded, bottomPadding, title, foreground, closeLabel, closeDisabled, contentWidth, headerBack, withinChat, compact }: {
|
|
35
59
|
compact?: boolean;
|
|
@@ -49,3 +73,4 @@ export declare function MessagingPopup({ children, onClose, background, form, vi
|
|
|
49
73
|
contentWidth?: number;
|
|
50
74
|
headerBack?: boolean;
|
|
51
75
|
}): React.JSX.Element;
|
|
76
|
+
export {};
|
package/dist/popup.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState, useCallback, useMemo } from 'react';
|
|
3
3
|
import { Keyboard, KeyboardAvoidingView, Modal, Platform, Pressable, SafeAreaView, Text, View, useWindowDimensions } from 'react-native';
|
|
4
4
|
import { MessageIcon } from './icons.js';
|
|
@@ -12,16 +12,85 @@ export function MessagingPopupHeader({ title, foreground, onClose, closeLabel =
|
|
|
12
12
|
}
|
|
13
13
|
// Measure the host and its persistent header, including embedded native layouts.
|
|
14
14
|
const PopupBounds = createContext(null);
|
|
15
|
+
const PopupLayer = createContext(null);
|
|
16
|
+
export function useMessagingPopupBounds() { return useContext(PopupBounds); }
|
|
17
|
+
/** App-local overlay for anchored menus, without a browser-wide modal. */
|
|
18
|
+
export function MessagingPopupLayer({ children }) {
|
|
19
|
+
const update = useContext(PopupLayer), owner = useRef(Symbol('menu')).current;
|
|
20
|
+
useLayoutEffect(() => { update?.(owner, children); return () => update?.(owner, null); }, [update, owner, children]);
|
|
21
|
+
return update ? null : _jsx(_Fragment, { children: children });
|
|
22
|
+
}
|
|
15
23
|
export function MessagingPopupHost({ children }) {
|
|
16
24
|
const root = useRef(null), header = useRef(null), [revision, setRevision] = useState(0);
|
|
25
|
+
const [layer, setLayer] = useState(null);
|
|
26
|
+
const update = useCallback((owner, node) => setLayer(current => node ? { owner, node } : current?.owner === owner ? null : current), []);
|
|
17
27
|
const changed = useCallback(() => setRevision(value => value + 1), []);
|
|
18
28
|
const value = useMemo(() => ({ root, header, revision, changed }), [revision, changed]);
|
|
19
|
-
return _jsx(PopupBounds.Provider, { value: value, children: _jsx(View, { ref: root, collapsable: false, onLayout: changed, style: { flex: 1, minHeight: 0 }, children: children }) });
|
|
29
|
+
return _jsx(PopupBounds.Provider, { value: value, children: _jsx(PopupLayer.Provider, { value: update, children: _jsxs(View, { ref: root, collapsable: false, onLayout: changed, style: { flex: 1, minHeight: 0 }, children: [children, layer?.node] }) }) });
|
|
20
30
|
}
|
|
21
31
|
export function MessagingPopupHeaderBoundary({ children }) {
|
|
22
32
|
const bounds = useContext(PopupBounds);
|
|
23
33
|
return _jsx(View, { ref: bounds?.header, collapsable: false, onLayout: bounds?.changed, children: children });
|
|
24
34
|
}
|
|
35
|
+
/** Anchored dropdowns use window coordinates on phones, app coordinates on web. */
|
|
36
|
+
export function MessagingMenuSurface({ visible, onClose, onDismiss, children }) {
|
|
37
|
+
const bounds = useContext(PopupBounds), viewport = useWindowDimensions(), panel = useRef(null), wasVisible = useRef(false), dismiss = useRef(onDismiss), close = useRef(onClose);
|
|
38
|
+
dismiss.current = onDismiss;
|
|
39
|
+
close.current = onClose;
|
|
40
|
+
const [rect, setRect] = useState(null);
|
|
41
|
+
useLayoutEffect(() => {
|
|
42
|
+
if (Platform.OS !== 'web' || !visible)
|
|
43
|
+
return;
|
|
44
|
+
let active = true;
|
|
45
|
+
const measure = () => bounds?.root.current?.measureInWindow((left, top, width, height) => { if (active)
|
|
46
|
+
setRect({ left, top, width, height }); });
|
|
47
|
+
measure();
|
|
48
|
+
const root = bounds?.root.current, observer = root ? new ResizeObserver(measure) : undefined;
|
|
49
|
+
if (root)
|
|
50
|
+
observer?.observe(root);
|
|
51
|
+
return () => { active = false; observer?.disconnect(); };
|
|
52
|
+
}, [visible, bounds, viewport.width, viewport.height]);
|
|
53
|
+
useEffect(() => { if (Platform.OS !== 'web')
|
|
54
|
+
return; if (wasVisible.current && !visible)
|
|
55
|
+
dismiss.current?.(); wasVisible.current = visible; }, [visible]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (Platform.OS !== 'web' || !visible || !rect)
|
|
58
|
+
return;
|
|
59
|
+
const previous = document.activeElement, frame = requestAnimationFrame(() => { panel.current?.querySelector('[role="button"],button')?.focus(); });
|
|
60
|
+
const key = (event) => {
|
|
61
|
+
if (event.key === 'Escape') {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
event.stopPropagation();
|
|
64
|
+
close.current();
|
|
65
|
+
}
|
|
66
|
+
if (event.key !== 'Tab')
|
|
67
|
+
return;
|
|
68
|
+
const root = panel.current;
|
|
69
|
+
if (!root)
|
|
70
|
+
return;
|
|
71
|
+
const controls = [...root.querySelectorAll('[role="button"],button,[tabindex="0"]')].filter(el => el.getClientRects().length && el.getAttribute('aria-disabled') !== 'true');
|
|
72
|
+
const first = controls[0], last = controls.at(-1);
|
|
73
|
+
if (!first) {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (event.shiftKey && (document.activeElement === first || !root.contains(document.activeElement))) {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
last?.focus();
|
|
80
|
+
}
|
|
81
|
+
else if (!event.shiftKey && (document.activeElement === last || !root.contains(document.activeElement))) {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
first.focus();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
document.addEventListener('keydown', key, true);
|
|
87
|
+
return () => { cancelAnimationFrame(frame); document.removeEventListener('keydown', key, true); if (previous?.isConnected)
|
|
88
|
+
previous.focus({ preventScroll: true }); };
|
|
89
|
+
}, [visible, !!rect]);
|
|
90
|
+
if (Platform.OS !== 'web')
|
|
91
|
+
return _jsx(Modal, { visible: visible, transparent: true, animationType: "none", onRequestClose: onClose, onDismiss: onDismiss, children: children({ left: 0, top: 0, width: viewport.width, height: viewport.height }) });
|
|
92
|
+
return _jsx(MessagingPopupLayer, { children: visible && rect ? _jsx(View, { ref: panel, testID: "messaging-menu-surface", style: { position: 'absolute', inset: 0, zIndex: 1000, overflow: 'hidden' }, children: children(rect) }) : null });
|
|
93
|
+
}
|
|
25
94
|
/** Content panels occupy the area below the persistent header; confirmations stay compact. */
|
|
26
95
|
export function MessagingPopup({ children, onClose, background, form = false, visible = true, onDismiss, expanded = true, bottomPadding, title, foreground, closeLabel, closeDisabled = false, contentWidth, headerBack = false, withinChat = false, compact = false }) {
|
|
27
96
|
const ChatSurface = useContext(MessagingPopupSurface), bounds = useContext(PopupBounds), viewport = useWindowDimensions();
|
package/dist/quick-send.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { MessagingPopupHeader, messagingPopupBackdrop } from './popup.js';
|
|
2
|
+
import { MessagingPopupHeader, messagingPopupBackdrop, MessagingMenuSurface } from './popup.js';
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { Platform, Pressable, ScrollView, Text, View, useWindowDimensions } from 'react-native';
|
|
5
5
|
import { quickMessageOptions, validQuickMessage } from '@robono/messaging';
|
|
6
6
|
import { MessageIcon, chatSurface } from './icons.js';
|
|
7
7
|
/** Messenger's empty-composer shortcut and anchored picker. Selecting never sends. */
|
|
@@ -34,5 +34,5 @@ export function QuickSendButton({ choice, empty, enabled, disabled, editing, sen
|
|
|
34
34
|
held.current = true;
|
|
35
35
|
onSchedule?.();
|
|
36
36
|
}
|
|
37
|
-
} } } : {}), style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center', opacity: unavailable ? .4 : 1 }, children: quick ? _jsx(Text, { style: { fontSize: 26, lineHeight: 32 }, children: emoji }) : _jsx(MessageIcon, { name: editing ? 'check' : 'send', size: 24, color: c.accent }) }), _jsx(
|
|
37
|
+
} } } : {}), style: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center', opacity: unavailable ? .4 : 1 }, children: quick ? _jsx(Text, { style: { fontSize: 26, lineHeight: 32 }, children: emoji }) : _jsx(MessageIcon, { name: editing ? 'check' : 'send', size: 24, color: c.accent }) }), _jsx(MessagingMenuSurface, { visible: anchor !== null, onClose: () => setAnchor(null), children: rect => _jsxs(View, { accessibilityViewIsModal: true, style: { flex: 1 }, children: [_jsx(Pressable, { accessibilityLabel: "Close quick messages", onPress: () => setAnchor(null), style: { position: 'absolute', inset: 0, backgroundColor: messagingPopupBackdrop } }), _jsxs(View, { style: { position: 'absolute', width: Math.min(304, rect.width - 24), maxHeight: Math.max(100, rect.height - 24), left: Math.max(12, Math.min((anchor?.x ?? rect.width) - rect.left - 304, rect.width - Math.min(304, rect.width - 24) - 12)), top: Math.max(12, Math.min((anchor?.y ?? rect.height) - rect.top - 282, rect.height - 312)), backgroundColor: chatSurface(c.background), borderRadius: 18, borderWidth: 1, borderColor: c.secondary + '40', padding: 8, boxShadow: '0 4px 18px #0005' }, children: [_jsx(MessagingPopupHeader, { title: "Quick message", foreground: c.foreground, closeLabel: "Close quick message picker", onClose: () => setAnchor(null) }), _jsx(ScrollView, { style: { maxHeight: Math.min(220, Math.max(44, rect.height - 90)) }, keyboardShouldPersistTaps: "handled", contentContainerStyle: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center' }, children: quickMessageOptions.map(item => _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: item.label, accessibilityState: { selected: emoji === item.emoji }, onPress: () => { held.current = false; setAnchor(null); onChoose(item.emoji); }, style: { width: 46, height: 44, alignItems: 'center', justifyContent: 'center', borderRadius: 10, backgroundColor: emoji === item.emoji ? c.accent + '20' : 'transparent' }, children: _jsx(Text, { style: { fontSize: 26, lineHeight: 32 }, children: item.emoji }) }, item.emoji)) })] })] }) })] });
|
|
38
38
|
}
|
package/dist/safety.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { MessagingMenuSurface } from './popup.js';
|
|
2
3
|
import { messagingPopupColors } from './popup.js';
|
|
3
4
|
import { useEffect, useRef, useState } from 'react';
|
|
4
5
|
import { MessagingPopup } from './popup.js';
|
|
5
6
|
import { MessageIcon } from './icons.js';
|
|
6
|
-
import {
|
|
7
|
+
import { Platform, Pressable, ScrollView, Text, TextInput, View, useWindowDimensions } from 'react-native';
|
|
7
8
|
/** Uses the authenticated messaging client's durable safety operations, or the host can keep its own UI. */
|
|
8
9
|
export function MessagingSafety({ client, state, appearance, name, messageId, onClose, onSearch, onDetails, onPins, onScheduled, onSaved, children, menuAnchor }) {
|
|
9
10
|
const [mode, setMode] = useState(messageId ? 'report' : 'menu');
|
|
@@ -61,7 +62,7 @@ export function MessagingSafety({ client, state, appearance, name, messageId, on
|
|
|
61
62
|
const choose = (next) => { afterClose.current = () => { setMode(next); setVisible(true); }; setVisible(false); };
|
|
62
63
|
if (mode === 'menu' && menuAnchor) {
|
|
63
64
|
const count = Number(!!onSearch) + Number(!!onDetails) + Number(!!onPins) + Number(!!onScheduled) + Number(!!children) + Number(!!safety?.canBlock) + Number(!!safety?.canReport), height = count * 44 + 8;
|
|
64
|
-
return _jsx(
|
|
65
|
+
return _jsx(MessagingMenuSurface, { visible: visible, onDismiss: dismissed, onClose: onClose, children: rect => _jsxs(View, { accessibilityViewIsModal: true, style: { flex: 1 }, children: [_jsx(Pressable, { accessibilityLabel: "Close contact options", onPress: onClose, style: { position: 'absolute', inset: 0 } }), _jsx(View, { testID: "messaging-contact-dropdown", style: { position: 'absolute', left: Math.max(12, Math.min(menuAnchor.x - rect.left, rect.width - Math.min(240, rect.width - 24) - 12)), top: Math.max(12, Math.min(menuAnchor.y - rect.top, rect.height - height - 12)), width: Math.min(240, rect.width - 24), maxHeight: rect.height - 24, borderRadius: 16, borderWidth: .5, borderColor: '#ffffff22', backgroundColor: '#151515', paddingVertical: 4, overflow: 'hidden', boxShadow: '0 8px 18px #0006' }, children: _jsxs(ScrollView, { bounces: false, keyboardShouldPersistTaps: "handled", children: [onDetails && menuRow(state.conversation?.kind === 'group' ? 'Group details' : 'Contact details', 'person', () => { afterClose.current = onDetails; setVisible(false); }), onPins && menuRow('Pinned messages', 'bookmark', () => { afterClose.current = onPins; setVisible(false); }), onScheduled && menuRow('Scheduled messages', 'send', () => { afterClose.current = onScheduled; setVisible(false); }), onSearch && menuRow('Search this chat', 'search', search), children && menuRow('Translation', 'translate', () => choose('translation')), safety?.canBlock && menuRow(blocked ? 'Unblock contact' : 'Block contact', 'shield', () => choose('block')), safety?.canReport && menuRow('Report', 'flag-outline', () => choose('report'))] }) })] }) });
|
|
65
66
|
}
|
|
66
67
|
return _jsx(MessagingPopup, { compact: mode === 'block' || mode === 'sent', withinChat: mode === 'translation', contentWidth: 640, visible: visible, onDismiss: dismissed, background: c.background, foreground: c.foreground, closeLabel: "Close contact options", closeDisabled: busy, title: mode === 'menu' ? name : mode === 'translation' ? 'Translation' : mode === 'block' ? `${blocked ? 'Unblock' : 'Block'} ${name}?` : mode === 'sent' ? 'Report sent' : messageId ? 'Report message' : 'Report conversation', onClose: () => { if (!busy)
|
|
67
68
|
onClose(); }, children: _jsxs(ScrollView, { keyboardShouldPersistTaps: "handled", style: { ...(mode === 'block' || mode === 'sent' ? { flexGrow: 0, flexShrink: 1 } : { flex: 1 }), minHeight: 0 }, contentContainerStyle: { gap: 20, paddingBottom: 12 }, children: [mode === 'menu' && _jsxs(_Fragment, { children: [onSearch && _jsx(View, { style: { backgroundColor: '#111111', borderRadius: 12, borderWidth: 1, borderColor: '#ffffff14', overflow: 'hidden' }, children: menuRow('Search this chat', 'search', search) }), children, (safety?.canBlock || safety?.canReport) && _jsxs(View, { style: { backgroundColor: '#111111', borderRadius: 12, borderWidth: 1, borderColor: '#ffffff14', overflow: 'hidden' }, children: [safety?.canBlock && menuRow(blocked ? 'Unblock contact' : 'Block contact', 'shield', () => setMode('block')), safety?.canReport && menuRow('Report', 'flag-outline', () => setMode('report'))] })] }), mode === 'translation' && children, mode === 'block' && _jsx(_Fragment, { children: _jsx(Text, { style: { fontSize: 14, lineHeight: 21, color: c.secondary }, children: blocked ? 'You will be able to message and call each other again. Messages attempted while blocked will not be delivered.' : 'This stops direct messages and calls between you. Your existing chat stays. They will not receive a notification that you blocked them. Shared group messages are not hidden.' }) }), mode === 'report' && _jsxs(_Fragment, { children: [_jsxs(Text, { style: { fontSize: 14, lineHeight: 21, color: c.secondary }, children: [messageId ? 'This message and your reason will be shared with this app’s moderation team.' : 'Up to five recent messages from other people in this conversation and your reason will be shared with this app’s moderation team.', " Report details are kept for up to 30 days. Reporting does not block this contact."] }), ['Spam', 'Harassment', 'Inappropriate content', 'Other'].map(value => _jsxs(Pressable, { accessibilityRole: "radio", accessibilityLabel: value, accessibilityState: { checked: category === value }, disabled: busy, onPress: () => setCategory(value), style: { minHeight: 40, flexDirection: 'row', alignItems: 'center', gap: 10 }, children: [_jsx(View, { style: { width: 18, height: 18, borderRadius: 9, borderWidth: 2, borderColor: c.accent, backgroundColor: category === value ? c.accent : 'transparent' } }), _jsx(Text, { style: { color: c.foreground, fontSize: 15 }, children: value })] }, value)), _jsx(TextInput, { accessibilityLabel: "Report details", placeholder: "Tell us what happened (optional)", placeholderTextColor: c.secondary, value: details, onChangeText: setDetails, multiline: true, maxLength: 1800, editable: !busy, style: { color: c.foreground, minHeight: 80, borderWidth: 1, borderColor: c.secondary + '60', borderRadius: 10, padding: 12, fontSize: 15 } })] }), mode === 'sent' && _jsx(Text, { style: { color: c.secondary, fontSize: 15, lineHeight: 21 }, children: "Your report was saved for review. The other person is not notified." }), !!error && _jsx(Text, { accessibilityRole: "alert", style: { color: c.foreground }, children: error }), mode !== 'menu' && mode !== 'translation' && _jsxs(View, { style: { flexDirection: 'row', gap: 8, marginTop: 8 }, children: [(mode === 'block' || mode === 'report') && button('Cancel', onClose, false, 'cancel'), mode === 'block' && button(blocked ? 'Unblock' : 'Block', () => void block(), false, blocked ? 'primary' : 'danger'), mode === 'report' && button(busy ? 'Sending…' : 'Send report', () => void report(), !category && !details.trim(), 'primary'), mode === 'sent' && button('Done', onClose, false, 'primary')] })] }) });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Animated } from 'react-native';
|
|
2
2
|
export declare const messageInfoRevealWidth = 176;
|
|
3
3
|
export declare const messageReplyRevealWidth = 104;
|
|
4
|
-
export declare function useQuoteGesture(enabled: boolean, outgoing: boolean, quote: (() => void) | undefined, details?: () => void): {
|
|
4
|
+
export declare function useQuoteGesture(enabled: boolean, outgoing: boolean, quote: (() => void) | undefined, details?: () => void, onQuoteReady?: () => void): {
|
|
5
5
|
offset: number | Animated.Value;
|
|
6
6
|
webRef: import("react").RefObject<HTMLElement | null> | undefined;
|
|
7
7
|
infoLoaded: boolean;
|
|
@@ -4,14 +4,15 @@ import { Animated, Easing, I18nManager, PanResponder, Platform } from 'react-nat
|
|
|
4
4
|
export const messageInfoRevealWidth = 176;
|
|
5
5
|
export const messageReplyRevealWidth = 104;
|
|
6
6
|
const threshold = 82;
|
|
7
|
-
export function useQuoteGesture(enabled, outgoing, quote, details) {
|
|
7
|
+
export function useQuoteGesture(enabled, outgoing, quote, details, onQuoteReady) {
|
|
8
8
|
const offset = useRef(new Animated.Value(0)).current;
|
|
9
9
|
const [webOffset, setWebOffset] = useState(0);
|
|
10
10
|
const [webDragging, setWebDragging] = useState(false);
|
|
11
11
|
const direction = (outgoing !== !!I18nManager.isRTL) ? -1 : 1;
|
|
12
12
|
const [infoLoaded, setInfoLoaded] = useState(false);
|
|
13
|
-
const
|
|
14
|
-
current
|
|
13
|
+
const quoteReady = useRef(false);
|
|
14
|
+
const current = useRef({ enabled, direction, quote, details, onQuoteReady });
|
|
15
|
+
current.current = { enabled, direction, quote, details, onQuoteReady };
|
|
15
16
|
const suppressUntil = useRef(0);
|
|
16
17
|
const reset = () => {
|
|
17
18
|
if (Platform.OS === 'web') {
|
|
@@ -29,6 +30,10 @@ export function useQuoteGesture(enabled, outgoing, quote, details) {
|
|
|
29
30
|
};
|
|
30
31
|
const move = (dx) => {
|
|
31
32
|
suppressUntil.current = Date.now() + 400;
|
|
33
|
+
const ready = !!current.current.quote && dx * current.current.direction >= threshold;
|
|
34
|
+
if (ready && !quoteReady.current)
|
|
35
|
+
current.current.onQuoteReady?.();
|
|
36
|
+
quoteReady.current = ready;
|
|
32
37
|
const info = dx * current.current.direction < 0;
|
|
33
38
|
if (info)
|
|
34
39
|
setInfoLoaded(true);
|
|
@@ -47,6 +52,7 @@ export function useQuoteGesture(enabled, outgoing, quote, details) {
|
|
|
47
52
|
current.current.quote?.();
|
|
48
53
|
else if (!cancelled && current.current.enabled && dx * current.current.direction <= -threshold)
|
|
49
54
|
current.current.details?.();
|
|
55
|
+
quoteReady.current = false;
|
|
50
56
|
reset();
|
|
51
57
|
};
|
|
52
58
|
const responder = useRef(PanResponder.create({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robono/messaging-react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Optional, composable direct and group messaging screens for Robono.",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"prepack": "npm run build && node ../../scripts/generate-sdk-file-sbom.mjs"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@robono/messaging": "^0.
|
|
56
|
+
"@robono/messaging": "^0.5.0",
|
|
57
57
|
"react": ">=18.2 <20",
|
|
58
58
|
"react-native": ">=0.76 <1",
|
|
59
59
|
"expo-audio": ">=55 <57",
|
|
@@ -65,14 +65,20 @@
|
|
|
65
65
|
"expo-crypto": ">=55 <57",
|
|
66
66
|
"react-native-safe-area-context": ">=5 <6",
|
|
67
67
|
"react-native-svg": ">=15 <16",
|
|
68
|
-
"@robono/calling-react-native": "^0.
|
|
68
|
+
"@robono/calling-react-native": "^0.5.0",
|
|
69
69
|
"expo-clipboard": ">=8 <57",
|
|
70
70
|
"expo-image-manipulator": ">=14 <57",
|
|
71
71
|
"expo-video-thumbnails": ">=14 <57",
|
|
72
72
|
"react-native-webview": ">=13 <14",
|
|
73
73
|
"expo-blur": ">=55 <57",
|
|
74
74
|
"expo-notifications": ">=55 <57",
|
|
75
|
-
"expo-secure-store": ">=55 <57"
|
|
75
|
+
"expo-secure-store": ">=55 <57",
|
|
76
|
+
"@robono/location-react-native": "^0.2.0",
|
|
77
|
+
"expo-location": ">=55 <57",
|
|
78
|
+
"expo-constants": ">=55 <57",
|
|
79
|
+
"@robono/location": "^0.2.0",
|
|
80
|
+
"expo-haptics": ">=55 <57",
|
|
81
|
+
"@robono/calling": "^0.4.0"
|
|
76
82
|
},
|
|
77
83
|
"license": "SEE LICENSE IN LICENSE",
|
|
78
84
|
"peerDependenciesMeta": {
|
|
@@ -123,6 +129,9 @@
|
|
|
123
129
|
},
|
|
124
130
|
"expo-secure-store": {
|
|
125
131
|
"optional": true
|
|
132
|
+
},
|
|
133
|
+
"expo-haptics": {
|
|
134
|
+
"optional": true
|
|
126
135
|
}
|
|
127
136
|
},
|
|
128
137
|
"author": {
|