@appsupportkit/support-sdk 0.3.0 → 0.4.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/lib/commonjs/api.js +49 -15
- package/lib/commonjs/api.js.map +1 -1
- package/lib/commonjs/components/BottomSheet.js +1 -1
- package/lib/commonjs/components/BottomSheet.js.map +1 -1
- package/lib/commonjs/components/LauncherButton.js +4 -4
- package/lib/commonjs/components/LauncherButton.js.map +1 -1
- package/lib/commonjs/components/SupportButton.js +123 -97
- package/lib/commonjs/components/SupportButton.js.map +1 -1
- package/lib/commonjs/components/icons.js +81 -110
- package/lib/commonjs/components/icons.js.map +1 -1
- package/lib/commonjs/components/primitives.js +0 -153
- package/lib/commonjs/components/primitives.js.map +1 -1
- package/lib/commonjs/components/screens/ChatScreen.js +487 -0
- package/lib/commonjs/components/screens/ChatScreen.js.map +1 -0
- package/lib/commonjs/i18n/strings/de.js +3 -0
- package/lib/commonjs/i18n/strings/de.js.map +1 -1
- package/lib/commonjs/i18n/strings/en.js +3 -0
- package/lib/commonjs/i18n/strings/en.js.map +1 -1
- package/lib/commonjs/i18n/strings/es.js +3 -0
- package/lib/commonjs/i18n/strings/es.js.map +1 -1
- package/lib/commonjs/i18n/strings/fr.js +3 -0
- package/lib/commonjs/i18n/strings/fr.js.map +1 -1
- package/lib/commonjs/i18n/strings/hi.js +3 -0
- package/lib/commonjs/i18n/strings/hi.js.map +1 -1
- package/lib/commonjs/i18n/strings/it.js +3 -0
- package/lib/commonjs/i18n/strings/it.js.map +1 -1
- package/lib/commonjs/i18n/strings/ja.js +3 -0
- package/lib/commonjs/i18n/strings/ja.js.map +1 -1
- package/lib/commonjs/i18n/strings/ko.js +3 -0
- package/lib/commonjs/i18n/strings/ko.js.map +1 -1
- package/lib/commonjs/i18n/strings/nl.js +3 -0
- package/lib/commonjs/i18n/strings/nl.js.map +1 -1
- package/lib/commonjs/i18n/strings/pl.js +3 -0
- package/lib/commonjs/i18n/strings/pl.js.map +1 -1
- package/lib/commonjs/i18n/strings/pt.js +3 -0
- package/lib/commonjs/i18n/strings/pt.js.map +1 -1
- package/lib/commonjs/i18n/strings/ru.js +3 -0
- package/lib/commonjs/i18n/strings/ru.js.map +1 -1
- package/lib/commonjs/i18n/strings/tr.js +3 -0
- package/lib/commonjs/i18n/strings/tr.js.map +1 -1
- package/lib/commonjs/i18n/strings/zh.js +3 -0
- package/lib/commonjs/i18n/strings/zh.js.map +1 -1
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/push.js +78 -3
- package/lib/commonjs/push.js.map +1 -1
- package/lib/commonjs/review.js +54 -0
- package/lib/commonjs/review.js.map +1 -0
- package/lib/commonjs/theme.js +142 -23
- package/lib/commonjs/theme.js.map +1 -1
- package/lib/commonjs/types.js +21 -3
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/useSupportThreads.js +22 -15
- package/lib/commonjs/useSupportThreads.js.map +1 -1
- package/lib/commonjs/utils.js +13 -33
- package/lib/commonjs/utils.js.map +1 -1
- package/lib/commonjs/version.js +13 -0
- package/lib/commonjs/version.js.map +1 -0
- package/lib/module/api.js +46 -13
- package/lib/module/api.js.map +1 -1
- package/lib/module/components/BottomSheet.js +1 -1
- package/lib/module/components/BottomSheet.js.map +1 -1
- package/lib/module/components/LauncherButton.js +5 -5
- package/lib/module/components/LauncherButton.js.map +1 -1
- package/lib/module/components/SupportButton.js +128 -102
- package/lib/module/components/SupportButton.js.map +1 -1
- package/lib/module/components/icons.js +80 -107
- package/lib/module/components/icons.js.map +1 -1
- package/lib/module/components/primitives.js +2 -146
- package/lib/module/components/primitives.js.map +1 -1
- package/lib/module/components/screens/ChatScreen.js +485 -0
- package/lib/module/components/screens/ChatScreen.js.map +1 -0
- package/lib/module/i18n/strings/de.js +3 -0
- package/lib/module/i18n/strings/de.js.map +1 -1
- package/lib/module/i18n/strings/en.js +3 -0
- package/lib/module/i18n/strings/en.js.map +1 -1
- package/lib/module/i18n/strings/es.js +3 -0
- package/lib/module/i18n/strings/es.js.map +1 -1
- package/lib/module/i18n/strings/fr.js +3 -0
- package/lib/module/i18n/strings/fr.js.map +1 -1
- package/lib/module/i18n/strings/hi.js +3 -0
- package/lib/module/i18n/strings/hi.js.map +1 -1
- package/lib/module/i18n/strings/it.js +3 -0
- package/lib/module/i18n/strings/it.js.map +1 -1
- package/lib/module/i18n/strings/ja.js +3 -0
- package/lib/module/i18n/strings/ja.js.map +1 -1
- package/lib/module/i18n/strings/ko.js +3 -0
- package/lib/module/i18n/strings/ko.js.map +1 -1
- package/lib/module/i18n/strings/nl.js +3 -0
- package/lib/module/i18n/strings/nl.js.map +1 -1
- package/lib/module/i18n/strings/pl.js +3 -0
- package/lib/module/i18n/strings/pl.js.map +1 -1
- package/lib/module/i18n/strings/pt.js +3 -0
- package/lib/module/i18n/strings/pt.js.map +1 -1
- package/lib/module/i18n/strings/ru.js +3 -0
- package/lib/module/i18n/strings/ru.js.map +1 -1
- package/lib/module/i18n/strings/tr.js +3 -0
- package/lib/module/i18n/strings/tr.js.map +1 -1
- package/lib/module/i18n/strings/zh.js +3 -0
- package/lib/module/i18n/strings/zh.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/push.js +77 -3
- package/lib/module/push.js.map +1 -1
- package/lib/module/review.js +48 -0
- package/lib/module/review.js.map +1 -0
- package/lib/module/theme.js +141 -23
- package/lib/module/theme.js.map +1 -1
- package/lib/module/types.js +21 -3
- package/lib/module/types.js.map +1 -1
- package/lib/module/useSupportThreads.js +22 -15
- package/lib/module/useSupportThreads.js.map +1 -1
- package/lib/module/utils.js +12 -32
- package/lib/module/utils.js.map +1 -1
- package/lib/module/version.js +9 -0
- package/lib/module/version.js.map +1 -0
- package/lib/typescript/api.d.ts +33 -12
- package/lib/typescript/api.d.ts.map +1 -1
- package/lib/typescript/components/SupportButton.d.ts +2 -2
- package/lib/typescript/components/SupportButton.d.ts.map +1 -1
- package/lib/typescript/components/icons.d.ts +5 -14
- package/lib/typescript/components/icons.d.ts.map +1 -1
- package/lib/typescript/components/primitives.d.ts +0 -31
- package/lib/typescript/components/primitives.d.ts.map +1 -1
- package/lib/typescript/components/screens/ChatScreen.d.ts +21 -0
- package/lib/typescript/components/screens/ChatScreen.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/de.d.ts.map +1 -1
- package/lib/typescript/i18n/strings/en.d.ts +3 -0
- package/lib/typescript/i18n/strings/en.d.ts.map +1 -1
- package/lib/typescript/i18n/strings/es.d.ts.map +1 -1
- package/lib/typescript/i18n/strings/fr.d.ts.map +1 -1
- package/lib/typescript/i18n/strings/hi.d.ts.map +1 -1
- package/lib/typescript/i18n/strings/it.d.ts.map +1 -1
- package/lib/typescript/i18n/strings/ja.d.ts.map +1 -1
- package/lib/typescript/i18n/strings/ko.d.ts.map +1 -1
- package/lib/typescript/i18n/strings/nl.d.ts.map +1 -1
- package/lib/typescript/i18n/strings/pl.d.ts.map +1 -1
- package/lib/typescript/i18n/strings/pt.d.ts.map +1 -1
- package/lib/typescript/i18n/strings/ru.d.ts.map +1 -1
- package/lib/typescript/i18n/strings/tr.d.ts.map +1 -1
- package/lib/typescript/i18n/strings/zh.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +3 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/push.d.ts +14 -2
- package/lib/typescript/push.d.ts.map +1 -1
- package/lib/typescript/review.d.ts +15 -0
- package/lib/typescript/review.d.ts.map +1 -0
- package/lib/typescript/theme.d.ts +23 -5
- package/lib/typescript/theme.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +54 -11
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/useSupportThreads.d.ts +9 -3
- package/lib/typescript/useSupportThreads.d.ts.map +1 -1
- package/lib/typescript/utils.d.ts +3 -2
- package/lib/typescript/utils.d.ts.map +1 -1
- package/lib/typescript/version.d.ts +7 -0
- package/lib/typescript/version.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/api.ts +59 -30
- package/src/components/BottomSheet.tsx +1 -1
- package/src/components/LauncherButton.tsx +4 -4
- package/src/components/SupportButton.tsx +140 -111
- package/src/components/icons.tsx +60 -101
- package/src/components/primitives.tsx +1 -163
- package/src/components/screens/ChatScreen.tsx +541 -0
- package/src/i18n/strings/de.ts +3 -0
- package/src/i18n/strings/en.ts +3 -0
- package/src/i18n/strings/es.ts +3 -0
- package/src/i18n/strings/fr.ts +3 -0
- package/src/i18n/strings/hi.ts +3 -0
- package/src/i18n/strings/it.ts +3 -0
- package/src/i18n/strings/ja.ts +3 -0
- package/src/i18n/strings/ko.ts +3 -0
- package/src/i18n/strings/nl.ts +3 -0
- package/src/i18n/strings/pl.ts +3 -0
- package/src/i18n/strings/pt.ts +3 -0
- package/src/i18n/strings/ru.ts +3 -0
- package/src/i18n/strings/tr.ts +3 -0
- package/src/i18n/strings/zh.ts +3 -0
- package/src/index.tsx +5 -0
- package/src/push.ts +100 -2
- package/src/review.ts +54 -0
- package/src/theme.ts +182 -28
- package/src/types.ts +53 -11
- package/src/useSupportThreads.ts +31 -18
- package/src/utils.ts +14 -21
- package/src/version.ts +6 -0
- package/lib/commonjs/components/screens/ComposeScreen.js +0 -219
- package/lib/commonjs/components/screens/ComposeScreen.js.map +0 -1
- package/lib/commonjs/components/screens/HomeScreen.js +0 -334
- package/lib/commonjs/components/screens/HomeScreen.js.map +0 -1
- package/lib/commonjs/components/screens/ThreadScreen.js +0 -339
- package/lib/commonjs/components/screens/ThreadScreen.js.map +0 -1
- package/lib/module/components/screens/ComposeScreen.js +0 -217
- package/lib/module/components/screens/ComposeScreen.js.map +0 -1
- package/lib/module/components/screens/HomeScreen.js +0 -330
- package/lib/module/components/screens/HomeScreen.js.map +0 -1
- package/lib/module/components/screens/ThreadScreen.js +0 -337
- package/lib/module/components/screens/ThreadScreen.js.map +0 -1
- package/lib/typescript/components/screens/ComposeScreen.d.ts +0 -12
- package/lib/typescript/components/screens/ComposeScreen.d.ts.map +0 -1
- package/lib/typescript/components/screens/HomeScreen.d.ts +0 -19
- package/lib/typescript/components/screens/HomeScreen.d.ts.map +0 -1
- package/lib/typescript/components/screens/ThreadScreen.d.ts +0 -12
- package/lib/typescript/components/screens/ThreadScreen.d.ts.map +0 -1
- package/src/components/screens/ComposeScreen.tsx +0 -229
- package/src/components/screens/HomeScreen.tsx +0 -346
- package/src/components/screens/ThreadScreen.tsx +0 -424
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
KeyboardAvoidingView,
|
|
4
|
+
Platform,
|
|
5
|
+
Pressable,
|
|
6
|
+
ScrollView,
|
|
7
|
+
Text,
|
|
8
|
+
TextInput,
|
|
9
|
+
View,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
import type { Translator } from '../../i18n';
|
|
12
|
+
import { LANGUAGE_LABELS, type Language } from '../../i18n/languages';
|
|
13
|
+
import type { SupportTheme } from '../../theme';
|
|
14
|
+
import type { Thread, ThreadMessage } from '../../types';
|
|
15
|
+
import { USER_MESSAGE_MAX, USER_MESSAGE_MIN } from '../../types';
|
|
16
|
+
import { dayLabel, formatDate, formatTime, isSameDay } from '../../utils';
|
|
17
|
+
import { ArrowUpIcon, CloseIcon, GlobeIcon } from '../icons';
|
|
18
|
+
import {
|
|
19
|
+
EmptyState,
|
|
20
|
+
ErrorState,
|
|
21
|
+
IconButton,
|
|
22
|
+
SkeletonBlock,
|
|
23
|
+
textStyles,
|
|
24
|
+
} from '../primitives';
|
|
25
|
+
|
|
26
|
+
/** Consecutive messages from one sender inside this window form a group. */
|
|
27
|
+
const GROUP_WINDOW_MS = 5 * 60 * 1000;
|
|
28
|
+
/** The counter only appears once the limit is actually in play. */
|
|
29
|
+
const COUNTER_VISIBLE_FROM = USER_MESSAGE_MAX - 100;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The chat is every conversation this user has had, in time order. The
|
|
33
|
+
* dashboard still works in conversations it can resolve, so a resolve shows
|
|
34
|
+
* here as a divider, and the next message simply starts the next one.
|
|
35
|
+
*/
|
|
36
|
+
type Item =
|
|
37
|
+
| { kind: 'message'; key: string; at: number; message: ThreadMessage }
|
|
38
|
+
| { kind: 'resolved'; key: string; at: number; resolvedAt: string };
|
|
39
|
+
|
|
40
|
+
interface Row {
|
|
41
|
+
item: Item;
|
|
42
|
+
firstInGroup: boolean;
|
|
43
|
+
lastInGroup: boolean;
|
|
44
|
+
/** Date separator to draw above this row, if any. */
|
|
45
|
+
day: string | null;
|
|
46
|
+
/** "New" separator above the first message the user hasn't seen. */
|
|
47
|
+
startsUnread: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildItems(threads: Thread[]): Item[] {
|
|
51
|
+
const items: Item[] = [];
|
|
52
|
+
for (const thread of threads) {
|
|
53
|
+
for (const message of thread.messages) {
|
|
54
|
+
items.push({
|
|
55
|
+
kind: 'message',
|
|
56
|
+
key: message.id,
|
|
57
|
+
at: Date.parse(message.created_at),
|
|
58
|
+
message,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (thread.status === 'resolved' && thread.resolved_at !== null) {
|
|
62
|
+
items.push({
|
|
63
|
+
kind: 'resolved',
|
|
64
|
+
key: `resolved-${thread.id}`,
|
|
65
|
+
at: Date.parse(thread.resolved_at),
|
|
66
|
+
resolvedAt: thread.resolved_at,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return items.sort((a, b) => a.at - b.at);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildRows(items: Item[], unreadCount: number, strings: Translator): Row[] {
|
|
74
|
+
// Unread messages are the newest support replies, so the divider goes
|
|
75
|
+
// above the Nth-from-last of them.
|
|
76
|
+
let remaining = unreadCount;
|
|
77
|
+
let firstUnreadKey: string | null = null;
|
|
78
|
+
for (let i = items.length - 1; i >= 0 && remaining > 0; i -= 1) {
|
|
79
|
+
const candidate = items[i];
|
|
80
|
+
if (candidate?.kind !== 'message' || candidate.message.sender !== 'admin') {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
remaining -= 1;
|
|
84
|
+
firstUnreadKey = candidate.key;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Grouped by sender alone: two replies from two different admins are two
|
|
88
|
+
// messages from "Support" on this side. A divider always breaks a group.
|
|
89
|
+
const joins = (a: Item | undefined, b: Item) =>
|
|
90
|
+
a !== undefined &&
|
|
91
|
+
a.kind === 'message' &&
|
|
92
|
+
b.kind === 'message' &&
|
|
93
|
+
a.message.sender === b.message.sender &&
|
|
94
|
+
Math.abs(b.at - a.at) < GROUP_WINDOW_MS &&
|
|
95
|
+
isSameDay(a.message.created_at, b.message.created_at) &&
|
|
96
|
+
b.key !== firstUnreadKey;
|
|
97
|
+
|
|
98
|
+
let lastDayAt: number | null = null;
|
|
99
|
+
return items.map((item, i) => {
|
|
100
|
+
const previous = items[i - 1];
|
|
101
|
+
const next = items[i + 1];
|
|
102
|
+
let day: string | null = null;
|
|
103
|
+
if (item.kind === 'message') {
|
|
104
|
+
const iso = item.message.created_at;
|
|
105
|
+
if (lastDayAt === null || !isSameDay(new Date(lastDayAt).toISOString(), iso)) {
|
|
106
|
+
day = dayLabel(iso, strings);
|
|
107
|
+
}
|
|
108
|
+
lastDayAt = item.at;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
item,
|
|
112
|
+
firstInGroup: day !== null || !joins(previous, item),
|
|
113
|
+
lastInGroup: next === undefined || !joins(item, next),
|
|
114
|
+
day,
|
|
115
|
+
startsUnread: item.key === firstUnreadKey,
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function Separator({
|
|
121
|
+
label,
|
|
122
|
+
theme,
|
|
123
|
+
tone = 'muted',
|
|
124
|
+
}: {
|
|
125
|
+
label: string;
|
|
126
|
+
theme: SupportTheme;
|
|
127
|
+
tone?: 'muted' | 'accent';
|
|
128
|
+
}) {
|
|
129
|
+
const color =
|
|
130
|
+
tone === 'accent' ? theme.colors.accentText : theme.colors.textTertiary;
|
|
131
|
+
const line = tone === 'accent' ? theme.colors.accentMuted : theme.colors.border;
|
|
132
|
+
return (
|
|
133
|
+
<View
|
|
134
|
+
style={{
|
|
135
|
+
flexDirection: 'row',
|
|
136
|
+
alignItems: 'center',
|
|
137
|
+
gap: 10,
|
|
138
|
+
paddingVertical: 6,
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
<View style={{ flex: 1, height: 1, backgroundColor: line }} />
|
|
142
|
+
<Text style={{ fontSize: 11, fontWeight: '600', letterSpacing: 0.3, color }}>
|
|
143
|
+
{label}
|
|
144
|
+
</Text>
|
|
145
|
+
<View style={{ flex: 1, height: 1, backgroundColor: line }} />
|
|
146
|
+
</View>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function MessageBubble({
|
|
151
|
+
message,
|
|
152
|
+
firstInGroup,
|
|
153
|
+
lastInGroup,
|
|
154
|
+
theme,
|
|
155
|
+
strings,
|
|
156
|
+
reviewLabel,
|
|
157
|
+
onOpenReview,
|
|
158
|
+
}: {
|
|
159
|
+
message: ThreadMessage;
|
|
160
|
+
firstInGroup: boolean;
|
|
161
|
+
lastInGroup: boolean;
|
|
162
|
+
theme: SupportTheme;
|
|
163
|
+
strings: Translator;
|
|
164
|
+
reviewLabel: string | null;
|
|
165
|
+
onOpenReview: (messageId: string) => void;
|
|
166
|
+
}) {
|
|
167
|
+
// A review request whose store isn't set up for this platform still
|
|
168
|
+
// reads as the reply it is; it just has no button.
|
|
169
|
+
const review = message.kind === 'review_request' && reviewLabel !== null;
|
|
170
|
+
const own = message.sender === 'user';
|
|
171
|
+
const round = theme.radii.bubble;
|
|
172
|
+
const tight = theme.radii.bubbleTail;
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<View
|
|
176
|
+
style={{
|
|
177
|
+
maxWidth: '86%',
|
|
178
|
+
alignSelf: own ? 'flex-end' : 'flex-start',
|
|
179
|
+
alignItems: own ? 'flex-end' : 'flex-start',
|
|
180
|
+
marginTop: firstInGroup ? 10 : 2,
|
|
181
|
+
gap: 4,
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
{!own && firstInGroup && (
|
|
185
|
+
<Text style={[textStyles.meta(theme), { paddingHorizontal: 4 }]}>
|
|
186
|
+
{strings.t('thread.support')}
|
|
187
|
+
</Text>
|
|
188
|
+
)}
|
|
189
|
+
<View
|
|
190
|
+
style={{
|
|
191
|
+
paddingHorizontal: 14,
|
|
192
|
+
paddingVertical: 10,
|
|
193
|
+
backgroundColor: own ? theme.colors.accent : theme.colors.surface,
|
|
194
|
+
borderWidth: own ? 0 : 1,
|
|
195
|
+
borderColor: theme.colors.border,
|
|
196
|
+
borderTopLeftRadius: own || firstInGroup ? round : tight,
|
|
197
|
+
borderBottomLeftRadius: own || lastInGroup ? round : tight,
|
|
198
|
+
borderTopRightRadius: !own || firstInGroup ? round : tight,
|
|
199
|
+
borderBottomRightRadius: !own || lastInGroup ? round : tight,
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
<Text
|
|
203
|
+
style={[
|
|
204
|
+
textStyles.body(theme),
|
|
205
|
+
own && { color: theme.colors.accentForeground },
|
|
206
|
+
]}
|
|
207
|
+
>
|
|
208
|
+
{message.body}
|
|
209
|
+
</Text>
|
|
210
|
+
{review && (
|
|
211
|
+
<Pressable
|
|
212
|
+
onPress={() => onOpenReview(message.id)}
|
|
213
|
+
accessibilityRole="link"
|
|
214
|
+
accessibilityLabel={reviewLabel}
|
|
215
|
+
style={({ pressed }) => ({
|
|
216
|
+
marginTop: 10,
|
|
217
|
+
height: 40,
|
|
218
|
+
paddingHorizontal: 14,
|
|
219
|
+
flexDirection: 'row',
|
|
220
|
+
alignItems: 'center',
|
|
221
|
+
justifyContent: 'center',
|
|
222
|
+
gap: 6,
|
|
223
|
+
borderRadius: theme.radii.control,
|
|
224
|
+
backgroundColor: pressed
|
|
225
|
+
? theme.colors.accentHover
|
|
226
|
+
: theme.colors.accent,
|
|
227
|
+
})}
|
|
228
|
+
>
|
|
229
|
+
<Text style={{ fontSize: 14, color: theme.colors.accentForeground }}>
|
|
230
|
+
★
|
|
231
|
+
</Text>
|
|
232
|
+
<Text
|
|
233
|
+
style={{
|
|
234
|
+
fontSize: 14,
|
|
235
|
+
fontWeight: '600',
|
|
236
|
+
color: theme.colors.accentForeground,
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
{reviewLabel}
|
|
240
|
+
</Text>
|
|
241
|
+
</Pressable>
|
|
242
|
+
)}
|
|
243
|
+
</View>
|
|
244
|
+
{lastInGroup && (
|
|
245
|
+
<Text style={[textStyles.meta(theme), { paddingHorizontal: 4 }]}>
|
|
246
|
+
{/* A reply whose translation is still in flight says so, rather
|
|
247
|
+
than showing English that changes under the reader. */}
|
|
248
|
+
{message.translation_pending
|
|
249
|
+
? strings.t('thread.translating')
|
|
250
|
+
: formatTime(message.created_at, strings)}
|
|
251
|
+
</Text>
|
|
252
|
+
)}
|
|
253
|
+
</View>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function ChatScreen({
|
|
258
|
+
theme,
|
|
259
|
+
strings,
|
|
260
|
+
language,
|
|
261
|
+
threads,
|
|
262
|
+
loading,
|
|
263
|
+
error,
|
|
264
|
+
onRetry,
|
|
265
|
+
onSend,
|
|
266
|
+
onChangeLanguage,
|
|
267
|
+
onClose,
|
|
268
|
+
reviewLabel,
|
|
269
|
+
onOpenReview,
|
|
270
|
+
}: {
|
|
271
|
+
theme: SupportTheme;
|
|
272
|
+
strings: Translator;
|
|
273
|
+
language: Language;
|
|
274
|
+
threads: Thread[] | null;
|
|
275
|
+
loading: boolean;
|
|
276
|
+
error: string | null;
|
|
277
|
+
onRetry: () => void;
|
|
278
|
+
/** Resolves null on success, or a user-facing error message. */
|
|
279
|
+
onSend: (message: string) => Promise<string | null>;
|
|
280
|
+
onChangeLanguage: () => void;
|
|
281
|
+
onClose: () => void;
|
|
282
|
+
/** The review button's label, or null when this platform's store isn't set up. */
|
|
283
|
+
reviewLabel: string | null;
|
|
284
|
+
onOpenReview: (messageId: string) => void;
|
|
285
|
+
}) {
|
|
286
|
+
const [draft, setDraft] = useState('');
|
|
287
|
+
const [sending, setSending] = useState(false);
|
|
288
|
+
const [failedMessage, setFailedMessage] = useState<string | null>(null);
|
|
289
|
+
const scrollRef = useRef<ScrollView>(null);
|
|
290
|
+
|
|
291
|
+
// Captured once threads first arrive: opening the chat marks everything
|
|
292
|
+
// read within a tick, but the "New" divider should stay put while the
|
|
293
|
+
// user reads.
|
|
294
|
+
const [unreadOnOpen, setUnreadOnOpen] = useState<number | null>(null);
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
if (unreadOnOpen === null && threads !== null) {
|
|
297
|
+
setUnreadOnOpen(threads.reduce((sum, t) => sum + t.unread_count, 0));
|
|
298
|
+
}
|
|
299
|
+
}, [threads, unreadOnOpen]);
|
|
300
|
+
|
|
301
|
+
const items = useMemo(() => buildItems(threads ?? []), [threads]);
|
|
302
|
+
const rows = useMemo(
|
|
303
|
+
() => buildRows(items, unreadOnOpen ?? 0, strings),
|
|
304
|
+
[items, unreadOnOpen, strings],
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const trimmed = draft.trim();
|
|
308
|
+
const valid =
|
|
309
|
+
trimmed.length >= USER_MESSAGE_MIN && trimmed.length <= USER_MESSAGE_MAX;
|
|
310
|
+
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
scrollRef.current?.scrollToEnd({ animated: false });
|
|
313
|
+
}, [items.length]);
|
|
314
|
+
|
|
315
|
+
async function handleSend() {
|
|
316
|
+
if (!valid || sending) return;
|
|
317
|
+
setSending(true);
|
|
318
|
+
setFailedMessage(null);
|
|
319
|
+
const errorMessage = await onSend(trimmed);
|
|
320
|
+
setSending(false);
|
|
321
|
+
if (errorMessage === null) setDraft('');
|
|
322
|
+
else setFailedMessage(errorMessage);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<KeyboardAvoidingView
|
|
327
|
+
style={{ flex: 1 }}
|
|
328
|
+
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
329
|
+
>
|
|
330
|
+
{/* Header */}
|
|
331
|
+
<View
|
|
332
|
+
style={{
|
|
333
|
+
flexDirection: 'row',
|
|
334
|
+
alignItems: 'center',
|
|
335
|
+
gap: 8,
|
|
336
|
+
paddingHorizontal: 16,
|
|
337
|
+
paddingTop: 4,
|
|
338
|
+
paddingBottom: 12,
|
|
339
|
+
borderBottomWidth: 1,
|
|
340
|
+
borderBottomColor: theme.colors.border,
|
|
341
|
+
}}
|
|
342
|
+
>
|
|
343
|
+
<Text style={[textStyles.title(theme), { flex: 1 }]}>
|
|
344
|
+
{strings.t('home.title')}
|
|
345
|
+
</Text>
|
|
346
|
+
{/* Language is the sheet's only preference, so it lives here rather
|
|
347
|
+
than behind a settings screen. */}
|
|
348
|
+
<Pressable
|
|
349
|
+
onPress={onChangeLanguage}
|
|
350
|
+
accessibilityRole="button"
|
|
351
|
+
accessibilityLabel={strings.t('lang.change')}
|
|
352
|
+
hitSlop={6}
|
|
353
|
+
style={({ pressed }) => ({
|
|
354
|
+
flexDirection: 'row',
|
|
355
|
+
alignItems: 'center',
|
|
356
|
+
gap: 6,
|
|
357
|
+
height: 32,
|
|
358
|
+
paddingHorizontal: 10,
|
|
359
|
+
borderRadius: theme.radii.pill,
|
|
360
|
+
borderWidth: 1,
|
|
361
|
+
borderColor: theme.colors.border,
|
|
362
|
+
backgroundColor: pressed
|
|
363
|
+
? theme.colors.surfaceActive
|
|
364
|
+
: theme.colors.surface,
|
|
365
|
+
})}
|
|
366
|
+
>
|
|
367
|
+
<GlobeIcon color={theme.colors.textSecondary} size={14} />
|
|
368
|
+
<Text style={[textStyles.meta(theme), { color: theme.colors.textSecondary }]}>
|
|
369
|
+
{LANGUAGE_LABELS[language]}
|
|
370
|
+
</Text>
|
|
371
|
+
</Pressable>
|
|
372
|
+
<IconButton
|
|
373
|
+
onPress={onClose}
|
|
374
|
+
theme={theme}
|
|
375
|
+
accessibilityLabel={strings.t('home.close')}
|
|
376
|
+
size={32}
|
|
377
|
+
>
|
|
378
|
+
<CloseIcon color={theme.colors.textSecondary} size={12} />
|
|
379
|
+
</IconButton>
|
|
380
|
+
</View>
|
|
381
|
+
|
|
382
|
+
{/* Messages */}
|
|
383
|
+
{error !== null && threads === null ? (
|
|
384
|
+
// Only take over the screen when there's nothing cached to show —
|
|
385
|
+
// a failed background poll shouldn't hide an already-loaded chat.
|
|
386
|
+
<View style={{ flex: 1 }}>
|
|
387
|
+
<ErrorState
|
|
388
|
+
message={error}
|
|
389
|
+
onRetry={onRetry}
|
|
390
|
+
theme={theme}
|
|
391
|
+
strings={strings}
|
|
392
|
+
/>
|
|
393
|
+
</View>
|
|
394
|
+
) : loading && threads === null ? (
|
|
395
|
+
<View style={{ flex: 1, paddingHorizontal: 16, paddingTop: 20, gap: 10 }}>
|
|
396
|
+
<SkeletonBlock width="62%" height={44} theme={theme} radius={theme.radii.bubble} />
|
|
397
|
+
<View style={{ alignItems: 'flex-end' }}>
|
|
398
|
+
<SkeletonBlock width="48%" height={44} theme={theme} radius={theme.radii.bubble} />
|
|
399
|
+
</View>
|
|
400
|
+
<SkeletonBlock width="70%" height={60} theme={theme} radius={theme.radii.bubble} />
|
|
401
|
+
</View>
|
|
402
|
+
) : items.length === 0 ? (
|
|
403
|
+
<View style={{ flex: 1, justifyContent: 'center' }}>
|
|
404
|
+
<EmptyState
|
|
405
|
+
message={strings.t('home.subtitle')}
|
|
406
|
+
hint={strings.t('home.emptyHint')}
|
|
407
|
+
theme={theme}
|
|
408
|
+
/>
|
|
409
|
+
</View>
|
|
410
|
+
) : (
|
|
411
|
+
<ScrollView
|
|
412
|
+
ref={scrollRef}
|
|
413
|
+
style={{ flex: 1 }}
|
|
414
|
+
contentContainerStyle={{ paddingHorizontal: 16, paddingVertical: 12 }}
|
|
415
|
+
keyboardShouldPersistTaps="handled"
|
|
416
|
+
onContentSizeChange={() =>
|
|
417
|
+
scrollRef.current?.scrollToEnd({ animated: false })
|
|
418
|
+
}
|
|
419
|
+
>
|
|
420
|
+
{rows.map((row) => (
|
|
421
|
+
<View key={row.item.key}>
|
|
422
|
+
{row.day !== null && <Separator label={row.day} theme={theme} />}
|
|
423
|
+
{row.startsUnread && (
|
|
424
|
+
<Separator label={strings.t('thread.new')} theme={theme} tone="accent" />
|
|
425
|
+
)}
|
|
426
|
+
{row.item.kind === 'message' ? (
|
|
427
|
+
<MessageBubble
|
|
428
|
+
message={row.item.message}
|
|
429
|
+
firstInGroup={row.firstInGroup}
|
|
430
|
+
lastInGroup={row.lastInGroup}
|
|
431
|
+
theme={theme}
|
|
432
|
+
strings={strings}
|
|
433
|
+
reviewLabel={reviewLabel}
|
|
434
|
+
onOpenReview={onOpenReview}
|
|
435
|
+
/>
|
|
436
|
+
) : (
|
|
437
|
+
<View style={{ marginTop: 14, marginBottom: 4 }}>
|
|
438
|
+
<Separator
|
|
439
|
+
label={strings.t('thread.resolvedAt', {
|
|
440
|
+
date: formatDate(row.item.resolvedAt, strings),
|
|
441
|
+
})}
|
|
442
|
+
theme={theme}
|
|
443
|
+
/>
|
|
444
|
+
</View>
|
|
445
|
+
)}
|
|
446
|
+
</View>
|
|
447
|
+
))}
|
|
448
|
+
</ScrollView>
|
|
449
|
+
)}
|
|
450
|
+
|
|
451
|
+
{/* Composer — always open: after a resolve, the next message starts
|
|
452
|
+
the next conversation. */}
|
|
453
|
+
<View
|
|
454
|
+
style={{
|
|
455
|
+
borderTopWidth: 1,
|
|
456
|
+
borderTopColor: theme.colors.border,
|
|
457
|
+
paddingHorizontal: 16,
|
|
458
|
+
paddingTop: 10,
|
|
459
|
+
paddingBottom: 4,
|
|
460
|
+
}}
|
|
461
|
+
>
|
|
462
|
+
<View style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 10 }}>
|
|
463
|
+
<TextInput
|
|
464
|
+
value={draft}
|
|
465
|
+
onChangeText={(v) => {
|
|
466
|
+
setDraft(v);
|
|
467
|
+
setFailedMessage(null);
|
|
468
|
+
}}
|
|
469
|
+
multiline
|
|
470
|
+
editable={!sending}
|
|
471
|
+
placeholder={strings.t('chat.placeholder')}
|
|
472
|
+
placeholderTextColor={theme.colors.textTertiary}
|
|
473
|
+
// Matches the sheet even when the theme is pinned against the
|
|
474
|
+
// device's own setting.
|
|
475
|
+
keyboardAppearance={theme.name}
|
|
476
|
+
selectionColor={theme.colors.accent}
|
|
477
|
+
style={{
|
|
478
|
+
flex: 1,
|
|
479
|
+
minHeight: 44,
|
|
480
|
+
maxHeight: 120,
|
|
481
|
+
borderWidth: 1,
|
|
482
|
+
borderColor: theme.colors.border,
|
|
483
|
+
borderRadius: theme.radii.cardLg,
|
|
484
|
+
backgroundColor: theme.colors.surface,
|
|
485
|
+
color: theme.colors.textPrimary,
|
|
486
|
+
fontSize: 15,
|
|
487
|
+
lineHeight: 20,
|
|
488
|
+
paddingHorizontal: 14,
|
|
489
|
+
paddingTop: 12,
|
|
490
|
+
paddingBottom: 12,
|
|
491
|
+
textAlignVertical: 'top',
|
|
492
|
+
}}
|
|
493
|
+
/>
|
|
494
|
+
<IconButton
|
|
495
|
+
onPress={handleSend}
|
|
496
|
+
theme={theme}
|
|
497
|
+
tone="accent"
|
|
498
|
+
size={44}
|
|
499
|
+
disabled={!valid || sending}
|
|
500
|
+
accessibilityLabel={strings.t(
|
|
501
|
+
sending ? 'thread.sendingA11y' : 'thread.sendA11y',
|
|
502
|
+
)}
|
|
503
|
+
>
|
|
504
|
+
<ArrowUpIcon color={theme.colors.accentForeground} size={19} />
|
|
505
|
+
</IconButton>
|
|
506
|
+
</View>
|
|
507
|
+
|
|
508
|
+
{(draft.length >= COUNTER_VISIBLE_FROM || failedMessage !== null) && (
|
|
509
|
+
<View
|
|
510
|
+
style={{
|
|
511
|
+
flexDirection: 'row',
|
|
512
|
+
alignItems: 'center',
|
|
513
|
+
gap: 10,
|
|
514
|
+
paddingTop: 8,
|
|
515
|
+
paddingHorizontal: 4,
|
|
516
|
+
}}
|
|
517
|
+
>
|
|
518
|
+
{failedMessage !== null && (
|
|
519
|
+
<Text style={{ fontSize: 12, color: theme.colors.statusDanger }}>
|
|
520
|
+
{failedMessage}
|
|
521
|
+
</Text>
|
|
522
|
+
)}
|
|
523
|
+
{draft.length >= COUNTER_VISIBLE_FROM && (
|
|
524
|
+
<Text
|
|
525
|
+
style={[
|
|
526
|
+
textStyles.meta(theme),
|
|
527
|
+
{ marginLeft: 'auto' },
|
|
528
|
+
draft.length > USER_MESSAGE_MAX && {
|
|
529
|
+
color: theme.colors.statusDanger,
|
|
530
|
+
},
|
|
531
|
+
]}
|
|
532
|
+
>
|
|
533
|
+
{draft.length}/{USER_MESSAGE_MAX}
|
|
534
|
+
</Text>
|
|
535
|
+
)}
|
|
536
|
+
</View>
|
|
537
|
+
)}
|
|
538
|
+
</View>
|
|
539
|
+
</KeyboardAvoidingView>
|
|
540
|
+
);
|
|
541
|
+
}
|
package/src/i18n/strings/de.ts
CHANGED
|
@@ -44,6 +44,9 @@ export const de: StringTable = {
|
|
|
44
44
|
'thread.resolvedAt': 'Erledigt · {date}',
|
|
45
45
|
'thread.readOnly': 'Dieses Gespräch ist erledigt und schreibgeschützt',
|
|
46
46
|
'thread.placeholder': 'Antwort schreiben…',
|
|
47
|
+
'chat.placeholder': 'Nachricht schreiben…',
|
|
48
|
+
'review.appStore': 'Im App Store bewerten',
|
|
49
|
+
'review.playStore': 'Bei Google Play bewerten',
|
|
47
50
|
'thread.sendA11y': 'Antwort senden',
|
|
48
51
|
'thread.sendingA11y': 'Wird gesendet',
|
|
49
52
|
'thread.translating': 'Wird übersetzt…',
|
package/src/i18n/strings/en.ts
CHANGED
|
@@ -55,6 +55,9 @@ export const en = {
|
|
|
55
55
|
'thread.resolvedAt': 'Resolved · {date}',
|
|
56
56
|
'thread.readOnly': 'This conversation is resolved and read-only',
|
|
57
57
|
'thread.placeholder': 'Write a reply…',
|
|
58
|
+
'chat.placeholder': 'Write a message…',
|
|
59
|
+
'review.appStore': 'Rate on the App Store',
|
|
60
|
+
'review.playStore': 'Rate on Google Play',
|
|
58
61
|
'thread.sendA11y': 'Send reply',
|
|
59
62
|
'thread.sendingA11y': 'Sending',
|
|
60
63
|
'thread.translating': 'Translating…',
|
package/src/i18n/strings/es.ts
CHANGED
|
@@ -43,6 +43,9 @@ export const es: StringTable = {
|
|
|
43
43
|
'thread.resolvedAt': 'Resuelta · {date}',
|
|
44
44
|
'thread.readOnly': 'Esta conversación está resuelta y es de solo lectura',
|
|
45
45
|
'thread.placeholder': 'Escribe una respuesta…',
|
|
46
|
+
'chat.placeholder': 'Escribe un mensaje…',
|
|
47
|
+
'review.appStore': 'Valorar en la App Store',
|
|
48
|
+
'review.playStore': 'Valorar en Google Play',
|
|
46
49
|
'thread.sendA11y': 'Enviar respuesta',
|
|
47
50
|
'thread.sendingA11y': 'Enviando',
|
|
48
51
|
'thread.translating': 'Traduciendo…',
|
package/src/i18n/strings/fr.ts
CHANGED
|
@@ -44,6 +44,9 @@ export const fr: StringTable = {
|
|
|
44
44
|
'thread.resolvedAt': 'Résolue · {date}',
|
|
45
45
|
'thread.readOnly': 'Cette conversation est résolue et en lecture seule',
|
|
46
46
|
'thread.placeholder': 'Écrivez une réponse…',
|
|
47
|
+
'chat.placeholder': 'Écrivez un message…',
|
|
48
|
+
'review.appStore': "Noter sur l'App Store",
|
|
49
|
+
'review.playStore': 'Noter sur Google Play',
|
|
47
50
|
'thread.sendA11y': 'Envoyer la réponse',
|
|
48
51
|
'thread.sendingA11y': 'Envoi en cours',
|
|
49
52
|
'thread.translating': 'Traduction…',
|
package/src/i18n/strings/hi.ts
CHANGED
|
@@ -43,6 +43,9 @@ export const hi: StringTable = {
|
|
|
43
43
|
'thread.resolvedAt': 'हल हो चुकी · {date}',
|
|
44
44
|
'thread.readOnly': 'यह बातचीत हल हो चुकी है और केवल पढ़ने के लिए है',
|
|
45
45
|
'thread.placeholder': 'जवाब लिखें…',
|
|
46
|
+
'chat.placeholder': 'संदेश लिखें…',
|
|
47
|
+
'review.appStore': 'App Store पर रेटिंग दें',
|
|
48
|
+
'review.playStore': 'Google Play पर रेटिंग दें',
|
|
46
49
|
'thread.sendA11y': 'जवाब भेजें',
|
|
47
50
|
'thread.sendingA11y': 'भेजा जा रहा है',
|
|
48
51
|
'thread.translating': 'अनुवाद हो रहा है…',
|
package/src/i18n/strings/it.ts
CHANGED
|
@@ -43,6 +43,9 @@ export const it: StringTable = {
|
|
|
43
43
|
'thread.resolvedAt': 'Risolta · {date}',
|
|
44
44
|
'thread.readOnly': 'Questa conversazione è risolta e in sola lettura',
|
|
45
45
|
'thread.placeholder': 'Scrivi una risposta…',
|
|
46
|
+
'chat.placeholder': 'Scrivi un messaggio…',
|
|
47
|
+
'review.appStore': "Valuta sull'App Store",
|
|
48
|
+
'review.playStore': 'Valuta su Google Play',
|
|
46
49
|
'thread.sendA11y': 'Invia risposta',
|
|
47
50
|
'thread.sendingA11y': 'Invio in corso',
|
|
48
51
|
'thread.translating': 'Traduzione…',
|
package/src/i18n/strings/ja.ts
CHANGED
|
@@ -43,6 +43,9 @@ export const ja: StringTable = {
|
|
|
43
43
|
'thread.resolvedAt': '解決済み · {date}',
|
|
44
44
|
'thread.readOnly': 'この会話は解決済みで、読み取り専用です',
|
|
45
45
|
'thread.placeholder': '返信を入力…',
|
|
46
|
+
'chat.placeholder': 'メッセージを入力…',
|
|
47
|
+
'review.appStore': 'App Storeで評価する',
|
|
48
|
+
'review.playStore': 'Google Playで評価する',
|
|
46
49
|
'thread.sendA11y': '返信を送信',
|
|
47
50
|
'thread.sendingA11y': '送信中',
|
|
48
51
|
'thread.translating': '翻訳中…',
|
package/src/i18n/strings/ko.ts
CHANGED
|
@@ -42,6 +42,9 @@ export const ko: StringTable = {
|
|
|
42
42
|
'thread.resolvedAt': '해결됨 · {date}',
|
|
43
43
|
'thread.readOnly': '이 대화는 해결되어 읽기 전용입니다',
|
|
44
44
|
'thread.placeholder': '답장을 입력하세요…',
|
|
45
|
+
'chat.placeholder': '메시지를 입력하세요…',
|
|
46
|
+
'review.appStore': 'App Store에서 평가하기',
|
|
47
|
+
'review.playStore': 'Google Play에서 평가하기',
|
|
45
48
|
'thread.sendA11y': '답장 보내기',
|
|
46
49
|
'thread.sendingA11y': '보내는 중',
|
|
47
50
|
'thread.translating': '번역 중…',
|
package/src/i18n/strings/nl.ts
CHANGED
|
@@ -43,6 +43,9 @@ export const nl: StringTable = {
|
|
|
43
43
|
'thread.resolvedAt': 'Afgehandeld · {date}',
|
|
44
44
|
'thread.readOnly': 'Dit gesprek is afgehandeld en alleen-lezen',
|
|
45
45
|
'thread.placeholder': 'Schrijf een antwoord…',
|
|
46
|
+
'chat.placeholder': 'Schrijf een bericht…',
|
|
47
|
+
'review.appStore': 'Beoordelen in de App Store',
|
|
48
|
+
'review.playStore': 'Beoordelen in Google Play',
|
|
46
49
|
'thread.sendA11y': 'Antwoord verzenden',
|
|
47
50
|
'thread.sendingA11y': 'Bezig met verzenden',
|
|
48
51
|
'thread.translating': 'Vertalen…',
|
package/src/i18n/strings/pl.ts
CHANGED
|
@@ -43,6 +43,9 @@ export const pl: StringTable = {
|
|
|
43
43
|
'thread.resolvedAt': 'Rozwiązane · {date}',
|
|
44
44
|
'thread.readOnly': 'Ta rozmowa jest rozwiązana i tylko do odczytu',
|
|
45
45
|
'thread.placeholder': 'Napisz odpowiedź…',
|
|
46
|
+
'chat.placeholder': 'Napisz wiadomość…',
|
|
47
|
+
'review.appStore': 'Oceń w App Store',
|
|
48
|
+
'review.playStore': 'Oceń w Google Play',
|
|
46
49
|
'thread.sendA11y': 'Wyślij odpowiedź',
|
|
47
50
|
'thread.sendingA11y': 'Wysyłanie',
|
|
48
51
|
'thread.translating': 'Tłumaczenie…',
|
package/src/i18n/strings/pt.ts
CHANGED
|
@@ -43,6 +43,9 @@ export const pt: StringTable = {
|
|
|
43
43
|
'thread.resolvedAt': 'Resolvida · {date}',
|
|
44
44
|
'thread.readOnly': 'Esta conversa está resolvida e é somente leitura',
|
|
45
45
|
'thread.placeholder': 'Escreva uma resposta…',
|
|
46
|
+
'chat.placeholder': 'Escreva uma mensagem…',
|
|
47
|
+
'review.appStore': 'Avaliar na App Store',
|
|
48
|
+
'review.playStore': 'Avaliar no Google Play',
|
|
46
49
|
'thread.sendA11y': 'Enviar resposta',
|
|
47
50
|
'thread.sendingA11y': 'Enviando',
|
|
48
51
|
'thread.translating': 'Traduzindo…',
|
package/src/i18n/strings/ru.ts
CHANGED
|
@@ -43,6 +43,9 @@ export const ru: StringTable = {
|
|
|
43
43
|
'thread.resolvedAt': 'Решено · {date}',
|
|
44
44
|
'thread.readOnly': 'Этот разговор решён и доступен только для чтения',
|
|
45
45
|
'thread.placeholder': 'Напишите ответ…',
|
|
46
|
+
'chat.placeholder': 'Напишите сообщение…',
|
|
47
|
+
'review.appStore': 'Оценить в App Store',
|
|
48
|
+
'review.playStore': 'Оценить в Google Play',
|
|
46
49
|
'thread.sendA11y': 'Отправить ответ',
|
|
47
50
|
'thread.sendingA11y': 'Отправка',
|
|
48
51
|
'thread.translating': 'Перевод…',
|
package/src/i18n/strings/tr.ts
CHANGED
|
@@ -43,6 +43,9 @@ export const tr: StringTable = {
|
|
|
43
43
|
'thread.resolvedAt': 'Çözüldü · {date}',
|
|
44
44
|
'thread.readOnly': 'Bu görüşme çözüldü ve salt okunur',
|
|
45
45
|
'thread.placeholder': 'Bir yanıt yazın…',
|
|
46
|
+
'chat.placeholder': 'Bir mesaj yazın…',
|
|
47
|
+
'review.appStore': "App Store'da değerlendir",
|
|
48
|
+
'review.playStore': "Google Play'de değerlendir",
|
|
46
49
|
'thread.sendA11y': 'Yanıtı gönder',
|
|
47
50
|
'thread.sendingA11y': 'Gönderiliyor',
|
|
48
51
|
'thread.translating': 'Çevriliyor…',
|