@joinskips/brand 6.0.0 → 7.0.0-develop.132
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/chat/attachments.d.ts +5 -3
- package/chat/components/Bubble.d.ts +3 -2
- package/chat/components/Bubble.js +0 -2
- package/chat/components/Conversation.js +5 -5
- package/chat/components/Markdown.d.ts +9 -2
- package/chat/components/Markdown.js +7 -5
- package/chat/components/Welcome.d.ts +22 -16
- package/chat/components/Welcome.js +26 -12
- package/chat/components/parts.d.ts +9 -6
- package/chat/index.d.ts +1 -0
- package/chat/index.js +1 -0
- package/chat/thread.d.ts +7 -4
- package/chat/typing.js +14 -6
- package/manifest.json +1 -1
- package/package.json +1 -1
package/chat/attachments.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ export declare const ATTACHMENT_TYPES: string[];
|
|
|
2
2
|
export declare const MAX_ATTACHMENT_BYTES: number;
|
|
3
3
|
/** A picked file's type: the picker's own, else the one its name says, else none. */
|
|
4
4
|
export declare function attachmentType(name: string, given?: string | null): string;
|
|
5
|
-
/**
|
|
6
|
-
export
|
|
5
|
+
/** What attachmentProblem reads of a picked file. */
|
|
6
|
+
export interface FileFacts {
|
|
7
7
|
type: string;
|
|
8
8
|
size: number;
|
|
9
|
-
}
|
|
9
|
+
}
|
|
10
|
+
/** Why a picked file cannot be sent, or null when it can. */
|
|
11
|
+
export declare function attachmentProblem(file: FileFacts): 'type' | 'size' | null;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Text, View } from 'react-native';
|
|
3
3
|
import { Markdown } from "./Markdown.js";
|
|
4
|
-
// The member's line in the accent and exactly as typed, Skips' on a card with its Markdown drawn, each with its
|
|
5
|
-
// tail corner tighter.
|
|
6
4
|
export function Bubble({ mine = false, text }) {
|
|
7
5
|
if (mine) {
|
|
8
6
|
return (_jsx(Text, { className: "rounded-[20px] rounded-br-[7px] bg-accent px-4 py-2.5 font-ui text-md text-accent-ink", children: text }));
|
|
@@ -10,7 +10,6 @@ const AVATAR_SIZE = 32;
|
|
|
10
10
|
export const PHOTO_RETRIES = 5;
|
|
11
11
|
export const PHOTO_RETRY_MS = 1_500;
|
|
12
12
|
const MIN_BOTTOM_SPACE = 12;
|
|
13
|
-
// React Native text inherits no font from the screen, so each line carries it.
|
|
14
13
|
function Line({ className, ...props }) {
|
|
15
14
|
return _jsx(Text, { className: `font-ui ${className}`, ...props });
|
|
16
15
|
}
|
|
@@ -71,11 +70,14 @@ function ThreadRow({ item, words, onRetry, ...props }) {
|
|
|
71
70
|
const sentAt = Math.max(...messages.map((m) => m.sentAt));
|
|
72
71
|
return (_jsxs(View, { className: `max-w-[82%] gap-1 ${mine ? 'items-end self-end' : 'items-start self-start'}`, children: [messages.map((message, index) => (_jsxs(View, { className: "flex-row items-end gap-2", children: [mine ? null : index === messages.length - 1 ? (_jsx(SenderAvatar, { sender: sender, name: name, skipMark: props.skipMark })) : (_jsx(View, { className: "w-8" })), _jsxs(View, { className: `shrink gap-2 ${mine ? 'items-end' : 'items-start'}`, children: [message.attachments.map((attachment) => (_jsx(Attachment, { attachment: attachment, words: words, Icon: props.Icon }, attachment.url))), message.text ? _jsx(Bubble, { mine: mine, text: message.text }) : null] })] }, message.id))), _jsx(Line, { className: `px-1 text-xs text-ink-secondary ${mine ? '' : 'ml-10'}`, children: `${name} · ${words.messageTime(sentAt)}` })] }));
|
|
73
72
|
}
|
|
73
|
+
// A failed line is a button to send it again, and its note is announced.
|
|
74
|
+
const BUTTON_ROLE = 'button';
|
|
75
|
+
const ALERT_ROLE = 'alert';
|
|
74
76
|
function Unsent({ message, words, onRetry, Icon, }) {
|
|
75
77
|
const failed = message.delivery === 'failed';
|
|
76
|
-
return (_jsxs(Pressable, { ...(failed ? { role:
|
|
78
|
+
return (_jsxs(Pressable, { ...(failed ? { role: BUTTON_ROLE } : {}), disabled: !failed, onPress: () => {
|
|
77
79
|
onRetry(message.id);
|
|
78
|
-
}, className: `max-w-[82%] items-end gap-1 self-end ${failed ? '' : 'opacity-70'}`, children: [message.file ? _jsx(PickedPreview, { picked: message.file, words: words, Icon: Icon }) : null, message.text ? _jsx(Bubble, { mine: true, text: message.text }) : null, _jsx(Line, { ...(failed ? { role:
|
|
80
|
+
}, className: `max-w-[82%] items-end gap-1 self-end ${failed ? '' : 'opacity-70'}`, children: [message.file ? _jsx(PickedPreview, { picked: message.file, words: words, Icon: Icon }) : null, message.text ? _jsx(Bubble, { mine: true, text: message.text }) : null, _jsx(Line, { ...(failed ? { role: ALERT_ROLE } : {}), className: `px-1 text-xs ${failed ? 'text-error-ink' : 'text-ink-secondary'}`, children: `${words.you} · ${failed ? words.notSent : words.sending}` })] }));
|
|
79
81
|
}
|
|
80
82
|
// Skip's mark, or a care team member's photo, or their initial when they have none or it does not load.
|
|
81
83
|
function SenderAvatar({ sender, name, skipMark, }) {
|
|
@@ -87,12 +89,10 @@ function SenderAvatar({ sender, name, skipMark, }) {
|
|
|
87
89
|
setFailed(true);
|
|
88
90
|
}, style: { width: AVATAR_SIZE, height: AVATAR_SIZE } })) : (_jsx(Line, { className: "text-sm font-semibold text-ink", children: name.trim().charAt(0).toUpperCase() })) }));
|
|
89
91
|
}
|
|
90
|
-
// Brand's attachment card, with what kind of file it is under the name.
|
|
91
92
|
function FileCard({ name, detail, Icon }) {
|
|
92
93
|
const secondary = useColor('ink-secondary');
|
|
93
94
|
return (_jsxs(View, { className: "w-80 max-w-full flex-row items-center gap-3 rounded-control border border-line-subtle bg-surface p-3", children: [_jsx(Icon, { name: "file", color: secondary, size: 20 }), _jsxs(View, { className: "flex-1", children: [_jsx(Line, { className: "text-sm text-ink", numberOfLines: 1, children: name }), _jsx(Line, { className: "text-xs text-ink-secondary", children: detail })] })] }));
|
|
94
95
|
}
|
|
95
|
-
// A picked file on its way: the photo itself, or a PDF's card with its size.
|
|
96
96
|
function PickedPreview({ picked, words, Icon }) {
|
|
97
97
|
if (isPhoto(picked)) {
|
|
98
98
|
return (_jsx(Image, { source: { uri: picked.uri }, accessibilityLabel: `${words.photo} ${picked.name}`, resizeMode: "cover", className: "h-48 w-48 rounded-control bg-muted" }));
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
interface NewTab {
|
|
2
|
+
target: string;
|
|
3
|
+
rel: string;
|
|
4
|
+
}
|
|
5
|
+
export declare const NEW_TAB: NewTab;
|
|
6
|
+
export interface MarkdownProps {
|
|
2
7
|
text: string;
|
|
3
|
-
}
|
|
8
|
+
}
|
|
9
|
+
export declare function Markdown({ text }: MarkdownProps): import("react").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Linking, Text, View } from 'react-native';
|
|
2
|
+
import { Linking, Platform, Text, View } from 'react-native';
|
|
3
3
|
import { readMarkdown } from "../markdown.js";
|
|
4
|
-
//
|
|
4
|
+
// On the web a link opens in a new tab, as the website's own links do, telling the other site nothing.
|
|
5
|
+
export const NEW_TAB = { target: '_blank', rel: 'noopener noreferrer nofollow' };
|
|
5
6
|
function Inlines({ nodes }) {
|
|
6
7
|
return nodes.map((node, i) => _jsx(InlineNode, { node: node }, `${String(i)}-${node.type}`));
|
|
7
8
|
}
|
|
@@ -20,9 +21,10 @@ function InlineNode({ node }) {
|
|
|
20
21
|
case 'del':
|
|
21
22
|
return (_jsx(Text, { className: "line-through", children: _jsx(Inlines, { nodes: node.children }) }));
|
|
22
23
|
case 'link':
|
|
23
|
-
return (_jsx(Text, { role: "link", className: "underline",
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
return (_jsx(Text, { role: "link", className: "underline", ...Platform.select({
|
|
25
|
+
web: { href: node.href, hrefAttrs: NEW_TAB },
|
|
26
|
+
default: { onPress: Linking.openURL.bind(Linking, node.href) },
|
|
27
|
+
}), children: _jsx(Inlines, { nodes: node.children }) }));
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
function Blocks({ blocks }) {
|
|
@@ -20,14 +20,14 @@ export interface WelcomeWords {
|
|
|
20
20
|
codeRanOut: string;
|
|
21
21
|
typing: string;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
export declare function WelcomeChoice({ words, onLogIn, onContinueHere, }: {
|
|
23
|
+
export interface WelcomeChoiceProps {
|
|
25
24
|
words: WelcomeWords;
|
|
26
25
|
onLogIn: () => void;
|
|
27
26
|
onContinueHere: () => void;
|
|
28
|
-
}
|
|
29
|
-
/** The
|
|
30
|
-
export declare function
|
|
27
|
+
}
|
|
28
|
+
/** The choice under Skip's welcome: log in as a member, or go on here as someone new. */
|
|
29
|
+
export declare function WelcomeChoice({ words, onLogIn, onContinueHere, }: WelcomeChoiceProps): import("react").JSX.Element;
|
|
30
|
+
export interface ChoicesProps {
|
|
31
31
|
items: ChoiceItem[];
|
|
32
32
|
question: string;
|
|
33
33
|
sending: string | null;
|
|
@@ -35,24 +35,28 @@ export declare function Choices({ items, question, sending, holding, failed, wor
|
|
|
35
35
|
failed: boolean;
|
|
36
36
|
words: WelcomeWords;
|
|
37
37
|
onChoose: (answer: ChoiceItem) => void;
|
|
38
|
-
}
|
|
39
|
-
/** The
|
|
40
|
-
export declare function
|
|
38
|
+
}
|
|
39
|
+
/** The answers to Skip's question, all drawn alike. One answer only: while one is on its way, none can be pressed. */
|
|
40
|
+
export declare function Choices({ items, question, sending, holding, failed, words, onChoose, }: ChoicesProps): import("react").JSX.Element;
|
|
41
|
+
export interface EmailStepProps {
|
|
41
42
|
words: WelcomeWords;
|
|
42
43
|
onSubmit: (email: string) => void;
|
|
43
44
|
initial?: string;
|
|
44
|
-
}
|
|
45
|
-
/** The
|
|
46
|
-
export declare function
|
|
45
|
+
}
|
|
46
|
+
/** The email asked for after Continue here, and again for a visitor changing it. */
|
|
47
|
+
export declare function EmailStep({ words, onSubmit, initial, }: EmailStepProps): import("react").JSX.Element;
|
|
48
|
+
export interface NameStepProps {
|
|
47
49
|
words: WelcomeWords;
|
|
48
50
|
onSubmit: (name: string) => void;
|
|
49
51
|
onChangeEmail: () => void;
|
|
50
|
-
}
|
|
52
|
+
}
|
|
53
|
+
/** The name asked of a visitor the Skips App does not know, with the way back to change a mistyped email. */
|
|
54
|
+
export declare function NameStep({ words, onSubmit, onChangeEmail, }: NameStepProps): import("react").JSX.Element;
|
|
51
55
|
/**
|
|
52
56
|
* The code emailed to the visitor, one digit to a box over one real field, so typing, deleting, a paste and the
|
|
53
57
|
* phone's own code suggestion work as in any field. The chat mounts it afresh for each code that goes out.
|
|
54
58
|
*/
|
|
55
|
-
export
|
|
59
|
+
export interface CodeStepProps {
|
|
56
60
|
words: WelcomeWords;
|
|
57
61
|
length: number;
|
|
58
62
|
noMatch: boolean;
|
|
@@ -61,12 +65,14 @@ export declare function CodeStep({ words, length, noMatch, ranOut, resendAfterMs
|
|
|
61
65
|
onSubmit: (code: string) => void;
|
|
62
66
|
onResend: () => void;
|
|
63
67
|
onChangeEmail: () => void;
|
|
64
|
-
}
|
|
68
|
+
}
|
|
69
|
+
export declare function CodeStep({ words, length, noMatch, ranOut, resendAfterMs, onSubmit, onResend, onChangeEmail, }: CodeStepProps): import("react").JSX.Element;
|
|
65
70
|
export declare const DOT_REST_OPACITY = 0.35;
|
|
66
71
|
export declare const DOT_RISE = 3;
|
|
67
72
|
export declare const DOT_STILL_OPACITY = 0.6;
|
|
68
73
|
/** Skip writing a reply: three dots rising in turn in Skip's bubble beside Skip's mark, announced as a status. */
|
|
69
|
-
export
|
|
74
|
+
export interface SkipTypingProps {
|
|
70
75
|
label: string;
|
|
71
76
|
skipMark: ImageSourcePropType;
|
|
72
|
-
}
|
|
77
|
+
}
|
|
78
|
+
export declare function SkipTyping({ label, skipMark }: SkipTypingProps): import("react").JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect,
|
|
2
|
+
import { useEffect, useId, useState } from 'react';
|
|
3
3
|
import { AccessibilityInfo, ActivityIndicator, Animated, Image, Pressable, Text, TextInput, View } from 'react-native';
|
|
4
4
|
import { DOT_CYCLE_MS } from "../skipTyping.js";
|
|
5
5
|
import { codeDigits, completesCode, EMAIL_MAX_LENGTH, givenEmail, givenName, NAME_MAX_LENGTH, RESEND_AFTER_MS, resendClock, } from "../welcome.js";
|
|
@@ -23,10 +23,16 @@ export function Choices({ items, question, sending, holding = false, failed, wor
|
|
|
23
23
|
onChoose(item);
|
|
24
24
|
} }, item.value))) }), failed ? (_jsx(Text, { role: "alert", className: "font-ui text-sm text-error-ink", children: words.choiceNotSent })) : null] }));
|
|
25
25
|
}
|
|
26
|
+
// The error under a field, as the field's description. React Native's types leave aria-describedby out, and
|
|
27
|
+
// react-native-web reads it.
|
|
28
|
+
function describedBy(errorId) {
|
|
29
|
+
return errorId === undefined ? {} : { 'aria-describedby': errorId };
|
|
30
|
+
}
|
|
26
31
|
// One thing asked of a new visitor: a field in focus and its button. What is typed is checked here and handed on.
|
|
27
32
|
function StepField({ label, error, submitLabel, input, usable, onSubmit, initial = '', aside }) {
|
|
28
33
|
const [typed, setTyped] = useState(initial);
|
|
29
34
|
const [refused, setRefused] = useState(false);
|
|
35
|
+
const errorId = useId();
|
|
30
36
|
const placeholder = useColor('ink-placeholder');
|
|
31
37
|
const submit = () => {
|
|
32
38
|
const value = usable(typed);
|
|
@@ -34,7 +40,7 @@ function StepField({ label, error, submitLabel, input, usable, onSubmit, initial
|
|
|
34
40
|
if (value !== null)
|
|
35
41
|
onSubmit(value);
|
|
36
42
|
};
|
|
37
|
-
return (_jsxs(View, { className: "w-full max-w-[360px] gap-2 self-center", children: [aside ? (_jsx(View, { className: "self-center", children: _jsx(Pill, { label: aside.label, look: "flat", onPress: aside.onPress }) })) : null, _jsx(Text, { className: "font-ui text-sm font-medium text-ink", children: label }), _jsx(TextInput, { autoFocus: true, value: typed, onChangeText: setTyped, onSubmitEditing: submit, accessibilityLabel: label, "aria-invalid": refused, placeholderTextColor: placeholder, className: `h-control rounded-control border bg-surface px-3 font-ui text-md text-ink ${refused ? 'border-error-ink' : 'border-line-control'}`, ...input }), refused ? (_jsx(Text, { role: "alert", className: "font-ui text-sm text-error-ink", children: error })) : null, _jsx(View, { className: "self-center", children: _jsx(Pill, { label: submitLabel, onPress: submit }) })] }));
|
|
43
|
+
return (_jsxs(View, { className: "w-full max-w-[360px] gap-2 self-center", children: [aside ? (_jsx(View, { className: "self-center", children: _jsx(Pill, { label: aside.label, look: "flat", onPress: aside.onPress }) })) : null, _jsx(Text, { className: "font-ui text-sm font-medium text-ink", children: label }), _jsx(TextInput, { autoFocus: true, value: typed, onChangeText: setTyped, onSubmitEditing: submit, accessibilityLabel: label, "aria-invalid": refused, ...describedBy(refused ? errorId : undefined), placeholderTextColor: placeholder, className: `h-control rounded-control border bg-surface px-3 font-ui text-md text-ink ${refused ? 'border-error-ink' : 'border-line-control'}`, ...input }), refused ? (_jsx(Text, { nativeID: errorId, role: "alert", className: "font-ui text-sm text-error-ink", children: error })) : null, _jsx(View, { className: "self-center", children: _jsx(Pill, { label: submitLabel, onPress: submit }) })] }));
|
|
38
44
|
}
|
|
39
45
|
/** The email asked for after Continue here, and again for a visitor changing it. */
|
|
40
46
|
export function EmailStep({ words, onSubmit, initial, }) {
|
|
@@ -45,7 +51,6 @@ export function NameStep({ words, onSubmit, onChangeEmail, }) {
|
|
|
45
51
|
return (_jsx(StepField, { label: words.nameLabel, error: words.nameError, submitLabel: words.nameSubmit, input: { autoComplete: 'name', maxLength: NAME_MAX_LENGTH, autoCapitalize: 'words' }, usable: givenName, onSubmit: onSubmit, aside: { label: words.changeEmail, onPress: onChangeEmail } }));
|
|
46
52
|
}
|
|
47
53
|
const TICK_MS = 250;
|
|
48
|
-
// Send new code, held back until the wait is over, and the wait beside it.
|
|
49
54
|
function NewCode({ waitMs, words, onResend }) {
|
|
50
55
|
const [secondsLeft, setSecondsLeft] = useState(Math.ceil(waitMs / 1000));
|
|
51
56
|
useEffect(() => {
|
|
@@ -62,19 +67,26 @@ function NewCode({ waitMs, words, onResend }) {
|
|
|
62
67
|
}, [waitMs]);
|
|
63
68
|
return (_jsxs(_Fragment, { children: [_jsx(Pill, { label: words.resend, look: "flat", disabled: secondsLeft > 0, onPress: onResend }), secondsLeft > 0 ? (_jsx(Text, { role: "timer", accessibilityLabel: words.resendWait, className: "font-ui text-sm text-ink-secondary", children: resendClock(secondsLeft) })) : null] }));
|
|
64
69
|
}
|
|
65
|
-
/**
|
|
66
|
-
* The code emailed to the visitor, one digit to a box over one real field, so typing, deleting, a paste and the
|
|
67
|
-
* phone's own code suggestion work as in any field. The chat mounts it afresh for each code that goes out.
|
|
68
|
-
*/
|
|
69
70
|
export function CodeStep({ words, length, noMatch, ranOut, resendAfterMs = RESEND_AFTER_MS, onSubmit, onResend, onChangeEmail, }) {
|
|
70
71
|
const [code, setCode] = useState('');
|
|
72
|
+
const [focused, setFocused] = useState(false);
|
|
73
|
+
const errorId = useId();
|
|
71
74
|
const error = ranOut ? words.codeRanOut : noMatch ? words.codeNoMatch : null;
|
|
72
|
-
|
|
75
|
+
// The box the next digit goes in, or the last once the code is whole.
|
|
76
|
+
const nextBox = Math.min(code.length, length - 1);
|
|
77
|
+
return (_jsxs(View, { className: "w-full max-w-[360px] items-center gap-3 self-center", children: [_jsx(Text, { className: "font-ui text-sm font-medium text-ink", children: words.codeLabel }), _jsxs(View, { className: "relative", children: [_jsx(View, { "aria-hidden": true, className: "flex-row gap-2", children: Array.from({ length }, (_, box) => (_jsx(View, { className: `h-control w-11 items-center justify-center rounded-control border ${focused && box === nextBox ? 'border-focus' : error ? 'border-error-ink' : 'border-line-control'}`, children: _jsx(Text, { className: "font-ui text-xl font-medium text-ink", children: code[box] ?? '' }) }, box))) }), _jsx(TextInput, { autoFocus: true, value: code, onChangeText: (typed) => {
|
|
73
78
|
const digits = codeDigits(typed, length);
|
|
74
79
|
setCode(digits);
|
|
75
80
|
if (completesCode(digits, code, length))
|
|
76
81
|
onSubmit(digits);
|
|
77
|
-
},
|
|
82
|
+
}, onFocus: () => {
|
|
83
|
+
setFocused(true);
|
|
84
|
+
}, onBlur: () => {
|
|
85
|
+
setFocused(false);
|
|
86
|
+
}, accessibilityLabel: words.codeLabel, "aria-invalid": error !== null, ...describedBy(error === null ? undefined : errorId), inputMode: "numeric", autoComplete: "one-time-code", textContentType: "oneTimeCode", caretHidden: true,
|
|
87
|
+
// Unseen, not just clear: the boxes draw the digits and the focus, and a checker would read clear text as
|
|
88
|
+
// text with no contrast.
|
|
89
|
+
className: "absolute inset-0 opacity-0" })] }), error ? (_jsx(Text, { nativeID: errorId, role: "alert", className: "text-center font-ui text-sm text-error-ink", children: error })) : null, _jsxs(View, { className: "flex-row flex-wrap items-center justify-center gap-2", children: [_jsx(Pill, { label: words.changeEmail, look: "flat", onPress: onChangeEmail }), _jsx(NewCode, { waitMs: resendAfterMs, words: words, onResend: onResend }), _jsx(Pill, { label: words.codeSubmit, disabled: code.length < length, onPress: () => {
|
|
78
90
|
onSubmit(code);
|
|
79
91
|
} })] })] }));
|
|
80
92
|
}
|
|
@@ -89,7 +101,9 @@ export const DOT_STILL_OPACITY = 0.6;
|
|
|
89
101
|
function useReducedMotion() {
|
|
90
102
|
const [reduced, setReduced] = useState(null);
|
|
91
103
|
useEffect(() => {
|
|
92
|
-
void
|
|
104
|
+
void (async () => {
|
|
105
|
+
setReduced(await AccessibilityInfo.isReduceMotionEnabled());
|
|
106
|
+
})();
|
|
93
107
|
const change = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduced);
|
|
94
108
|
return () => {
|
|
95
109
|
change.remove();
|
|
@@ -97,12 +111,12 @@ function useReducedMotion() {
|
|
|
97
111
|
}, []);
|
|
98
112
|
return reduced;
|
|
99
113
|
}
|
|
100
|
-
/** Skip writing a reply: three dots rising in turn in Skip's bubble beside Skip's mark, announced as a status. */
|
|
101
114
|
export function SkipTyping({ label, skipMark }) {
|
|
102
115
|
const dotColor = useColor('ink-secondary');
|
|
103
116
|
const reduced = useReducedMotion();
|
|
104
117
|
const still = reduced === true;
|
|
105
|
-
|
|
118
|
+
// Made once and kept, so render reads no ref.
|
|
119
|
+
const [rise] = useState(() => DOTS.map(() => new Animated.Value(0)));
|
|
106
120
|
useEffect(() => {
|
|
107
121
|
if (reduced !== false)
|
|
108
122
|
return undefined;
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { ComponentType } from 'react';
|
|
2
2
|
/** The icons the chat draws, from the brand's interface icons, which each app already draws. */
|
|
3
3
|
export type ChatIconName = 'attach' | 'arrow-right' | 'file' | 'close';
|
|
4
|
-
export
|
|
4
|
+
export interface ChatIconProps {
|
|
5
5
|
name: ChatIconName;
|
|
6
6
|
color: string;
|
|
7
7
|
size: number;
|
|
8
|
-
}
|
|
8
|
+
}
|
|
9
|
+
export type ChatIcon = ComponentType<ChatIconProps>;
|
|
10
|
+
/** The two reasons an attachment is refused, in the chat's words. */
|
|
11
|
+
export interface AttachmentRefusedWords {
|
|
12
|
+
type: string;
|
|
13
|
+
size: string;
|
|
14
|
+
}
|
|
9
15
|
/** Every word the chat shows, which each app passes in. */
|
|
10
16
|
export interface ChatWords {
|
|
11
17
|
composerPlaceholder: string;
|
|
@@ -23,10 +29,7 @@ export interface ChatWords {
|
|
|
23
29
|
feedDown: string;
|
|
24
30
|
loading: string;
|
|
25
31
|
loadFailed: string;
|
|
26
|
-
attachmentRefused:
|
|
27
|
-
type: string;
|
|
28
|
-
size: string;
|
|
29
|
-
};
|
|
32
|
+
attachmentRefused: AttachmentRefusedWords;
|
|
30
33
|
fileKind: (name: string) => string;
|
|
31
34
|
fileSize: (bytes: number) => string;
|
|
32
35
|
messageTime: (ms: number) => string;
|
package/chat/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Conversation, type ConversationProps, type LoadState } from './components/Conversation.tsx';
|
|
2
|
+
export { Bubble, type BubbleProps } from './components/Bubble.tsx';
|
|
2
3
|
export type { ChatIcon, ChatIconName, ChatWords } from './components/parts.ts';
|
|
3
4
|
export { ATTACHMENT_TYPES, MAX_ATTACHMENT_BYTES, attachmentProblem, attachmentType } from './attachments.ts';
|
|
4
5
|
export { isSkip, type ChatAttachment, type ChatAuthor, type ChatMessage, type ChoiceItem, type MessageChoice, type PickedFile, } from './message.ts';
|
package/chat/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Conversation } from "./components/Conversation.js";
|
|
2
|
+
export { Bubble } from "./components/Bubble.js";
|
|
2
3
|
export { ATTACHMENT_TYPES, MAX_ATTACHMENT_BYTES, attachmentProblem, attachmentType } from "./attachments.js";
|
|
3
4
|
export { isSkip, } from "./message.js";
|
|
4
5
|
export { CLUSTER_GAP_MS, EMPTY_THREAD, groupThread, pendingId, withFailed, withoutMessage, withPending, withReceived, withReceivedAll, withSent, } from "./thread.js";
|
package/chat/thread.d.ts
CHANGED
|
@@ -25,16 +25,19 @@ export declare function withReceivedAll(thread: Thread, messages: ChatMessage[])
|
|
|
25
25
|
export declare const CLUSTER_GAP_MS = 120000;
|
|
26
26
|
/** Who a run is from: the member, Skip, or a person on the care team. */
|
|
27
27
|
export type RunSender = 'mine' | 'skip' | ChatAuthor;
|
|
28
|
-
export
|
|
28
|
+
export interface NoticeItem {
|
|
29
29
|
kind: 'notice';
|
|
30
30
|
message: ThreadMessage;
|
|
31
|
-
}
|
|
31
|
+
}
|
|
32
|
+
export interface UnsentItem {
|
|
32
33
|
kind: 'unsent';
|
|
33
34
|
message: ThreadMessage;
|
|
34
|
-
}
|
|
35
|
+
}
|
|
36
|
+
export interface RunItem {
|
|
35
37
|
kind: 'run';
|
|
36
38
|
sender: RunSender;
|
|
37
39
|
messages: ThreadMessage[];
|
|
38
|
-
}
|
|
40
|
+
}
|
|
41
|
+
export type ThreadItem = NoticeItem | UnsentItem | RunItem;
|
|
39
42
|
/** Notices and the member's unsent lines on their own, every other line in runs by sender, in order. */
|
|
40
43
|
export declare function groupThread(messages: ThreadMessage[], skipUserId: number | null): ThreadItem[];
|
package/chat/typing.js
CHANGED
|
@@ -15,8 +15,10 @@ export function createTypingSignal(send) {
|
|
|
15
15
|
};
|
|
16
16
|
return {
|
|
17
17
|
typed(text) {
|
|
18
|
-
if (!text.trim())
|
|
19
|
-
|
|
18
|
+
if (!text.trim()) {
|
|
19
|
+
stop();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
20
22
|
if (!typing)
|
|
21
23
|
send((typing = true));
|
|
22
24
|
if (idle)
|
|
@@ -29,7 +31,9 @@ export function createTypingSignal(send) {
|
|
|
29
31
|
/** Who on the care team is typing, each cleared after TYPING_CLEAR_MS without news. */
|
|
30
32
|
export function createTypingWatch(onChange) {
|
|
31
33
|
const timers = new Map();
|
|
32
|
-
const publish = () =>
|
|
34
|
+
const publish = () => {
|
|
35
|
+
onChange([...timers.keys()]);
|
|
36
|
+
};
|
|
33
37
|
const clear = (name) => {
|
|
34
38
|
const timer = timers.get(name);
|
|
35
39
|
if (!timer)
|
|
@@ -40,12 +44,16 @@ export function createTypingWatch(onChange) {
|
|
|
40
44
|
};
|
|
41
45
|
return {
|
|
42
46
|
update(name, typing) {
|
|
43
|
-
if (!typing)
|
|
44
|
-
|
|
47
|
+
if (!typing) {
|
|
48
|
+
clear(name);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
45
51
|
const timer = timers.get(name);
|
|
46
52
|
if (timer)
|
|
47
53
|
clearTimeout(timer);
|
|
48
|
-
timers.set(name, setTimeout(() =>
|
|
54
|
+
timers.set(name, setTimeout(() => {
|
|
55
|
+
clear(name);
|
|
56
|
+
}, TYPING_CLEAR_MS));
|
|
49
57
|
if (!timer)
|
|
50
58
|
publish();
|
|
51
59
|
},
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joinskips/brand",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0-develop.132",
|
|
4
4
|
"description": "Skips design tokens, Onest, logos, favicons, interface icons, member pictures and app icons.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://brand.joinskips.com",
|