convex-feedback-ui 0.1.2 → 0.1.4

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.
Files changed (165) hide show
  1. package/README.md +119 -18
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/native/expo/ExpoFeedbackBody.d.ts +4 -0
  4. package/dist/native/expo/ExpoFeedbackBody.d.ts.map +1 -0
  5. package/dist/native/expo/ExpoFeedbackBody.js +26 -0
  6. package/dist/native/expo/ExpoFeedbackBody.js.map +1 -0
  7. package/dist/native/expo/FeedbackScreen.d.ts +3 -0
  8. package/dist/native/expo/FeedbackScreen.d.ts.map +1 -0
  9. package/dist/native/expo/FeedbackScreen.js +9 -0
  10. package/dist/native/expo/FeedbackScreen.js.map +1 -0
  11. package/dist/native/expo/FeedbackStack.d.ts +3 -0
  12. package/dist/native/expo/FeedbackStack.d.ts.map +1 -0
  13. package/dist/native/expo/FeedbackStack.js +41 -0
  14. package/dist/native/expo/FeedbackStack.js.map +1 -0
  15. package/dist/native/expo/index.d.ts +9 -0
  16. package/dist/native/expo/index.d.ts.map +1 -0
  17. package/dist/native/expo/index.js +7 -0
  18. package/dist/native/expo/index.js.map +1 -0
  19. package/dist/native/expo/types.d.ts +20 -0
  20. package/dist/native/expo/types.d.ts.map +1 -0
  21. package/dist/native/expo/types.js +2 -0
  22. package/dist/native/expo/types.js.map +1 -0
  23. package/dist/native/native/FeedbackScreen.d.ts +3 -0
  24. package/dist/native/native/FeedbackScreen.d.ts.map +1 -0
  25. package/dist/native/native/FeedbackScreen.js +9 -0
  26. package/dist/native/native/FeedbackScreen.js.map +1 -0
  27. package/dist/native/native/index.d.ts +8 -0
  28. package/dist/native/native/index.d.ts.map +1 -0
  29. package/dist/native/native/index.js +7 -0
  30. package/dist/native/native/index.js.map +1 -0
  31. package/dist/native/shared/helpers.d.ts +3 -0
  32. package/dist/native/shared/helpers.d.ts.map +1 -0
  33. package/dist/native/shared/helpers.js +4 -0
  34. package/dist/native/shared/helpers.js.map +1 -0
  35. package/dist/native/shared/ui/BodyContent.d.ts +3 -0
  36. package/dist/native/shared/ui/BodyContent.d.ts.map +1 -0
  37. package/dist/native/shared/ui/BodyContent.js +18 -0
  38. package/dist/native/shared/ui/BodyContent.js.map +1 -0
  39. package/dist/native/shared/ui/Button.d.ts +5 -0
  40. package/dist/native/shared/ui/Button.d.ts.map +1 -0
  41. package/dist/native/shared/ui/Button.js +15 -0
  42. package/dist/native/shared/ui/Button.js.map +1 -0
  43. package/dist/native/shared/ui/CommentBranch.d.ts +5 -0
  44. package/dist/native/shared/ui/CommentBranch.d.ts.map +1 -0
  45. package/dist/native/shared/ui/CommentBranch.js +45 -0
  46. package/dist/native/shared/ui/CommentBranch.js.map +1 -0
  47. package/dist/native/shared/ui/EntryCard.d.ts +3 -0
  48. package/dist/native/shared/ui/EntryCard.d.ts.map +1 -0
  49. package/dist/native/shared/ui/EntryCard.js +13 -0
  50. package/dist/native/shared/ui/EntryCard.js.map +1 -0
  51. package/dist/native/shared/ui/EntryDetail.d.ts +3 -0
  52. package/dist/native/shared/ui/EntryDetail.d.ts.map +1 -0
  53. package/dist/native/shared/ui/EntryDetail.js +37 -0
  54. package/dist/native/shared/ui/EntryDetail.js.map +1 -0
  55. package/dist/native/shared/ui/FeedbackScreenHeader.d.ts +2 -0
  56. package/dist/native/shared/ui/FeedbackScreenHeader.d.ts.map +1 -0
  57. package/dist/native/shared/ui/FeedbackScreenHeader.js +22 -0
  58. package/dist/native/shared/ui/FeedbackScreenHeader.js.map +1 -0
  59. package/dist/native/shared/ui/FeedbackScreenList.d.ts +3 -0
  60. package/dist/native/shared/ui/FeedbackScreenList.d.ts.map +1 -0
  61. package/dist/native/shared/ui/FeedbackScreenList.js +19 -0
  62. package/dist/native/shared/ui/FeedbackScreenList.js.map +1 -0
  63. package/dist/native/shared/ui/NewEntry.d.ts +5 -0
  64. package/dist/native/shared/ui/NewEntry.d.ts.map +1 -0
  65. package/dist/native/shared/ui/NewEntry.js +158 -0
  66. package/dist/native/shared/ui/NewEntry.js.map +1 -0
  67. package/dist/native/shared/ui/ReplyList.d.ts +3 -0
  68. package/dist/native/shared/ui/ReplyList.d.ts.map +1 -0
  69. package/dist/native/shared/ui/ReplyList.js +19 -0
  70. package/dist/native/shared/ui/ReplyList.js.map +1 -0
  71. package/dist/native/{primitives.d.ts → shared/ui/primitives.d.ts} +10 -12
  72. package/dist/native/shared/ui/primitives.d.ts.map +1 -0
  73. package/dist/native/{primitives.js → shared/ui/primitives.js} +19 -25
  74. package/dist/native/shared/ui/primitives.js.map +1 -0
  75. package/dist/react/{context.d.ts → css.d.ts} +2 -4
  76. package/dist/react/css.d.ts.map +1 -0
  77. package/dist/react/css.js +18 -0
  78. package/dist/react/css.js.map +1 -0
  79. package/dist/react/index.d.ts +1 -1
  80. package/dist/react/index.d.ts.map +1 -1
  81. package/dist/react/index.js +1 -1
  82. package/dist/react/index.js.map +1 -1
  83. package/dist/react/primitives.d.ts.map +1 -1
  84. package/dist/react/primitives.js +2 -1
  85. package/dist/react/primitives.js.map +1 -1
  86. package/dist/react/screen.d.ts +3 -3
  87. package/dist/react/screen.d.ts.map +1 -1
  88. package/dist/react/screen.js +25 -22
  89. package/dist/react/screen.js.map +1 -1
  90. package/dist/shared/context/FeedbackBodyProvider.d.ts +5 -0
  91. package/dist/shared/context/FeedbackBodyProvider.d.ts.map +1 -0
  92. package/dist/shared/context/FeedbackBodyProvider.js +43 -0
  93. package/dist/shared/context/FeedbackBodyProvider.js.map +1 -0
  94. package/dist/{native/context.d.ts → shared/context/FeedbackProvider.d.ts} +2 -2
  95. package/dist/shared/context/FeedbackProvider.d.ts.map +1 -0
  96. package/dist/{native/context.js → shared/context/FeedbackProvider.js} +3 -3
  97. package/dist/shared/context/FeedbackProvider.js.map +1 -0
  98. package/dist/shared/helpers.d.ts +7 -0
  99. package/dist/shared/helpers.d.ts.map +1 -0
  100. package/dist/shared/helpers.js +24 -0
  101. package/dist/shared/helpers.js.map +1 -0
  102. package/dist/shared/messages.d.ts.map +1 -1
  103. package/dist/shared/messages.js +23 -14
  104. package/dist/shared/messages.js.map +1 -1
  105. package/dist/shared/theme.d.ts.map +1 -1
  106. package/dist/shared/theme.js +1 -0
  107. package/dist/shared/theme.js.map +1 -1
  108. package/dist/shared/types/context.d.ts +4 -0
  109. package/dist/shared/types/context.d.ts.map +1 -1
  110. package/dist/shared/types/messages.d.ts +12 -1
  111. package/dist/shared/types/messages.d.ts.map +1 -1
  112. package/dist/shared/types/screen.d.ts +55 -18
  113. package/dist/shared/types/screen.d.ts.map +1 -1
  114. package/dist/shared/types/theme.d.ts +4 -0
  115. package/dist/shared/types/theme.d.ts.map +1 -1
  116. package/package.json +27 -12
  117. package/src/native/expo/ExpoFeedbackBody.tsx +48 -0
  118. package/src/native/expo/FeedbackScreen.tsx +37 -0
  119. package/src/native/expo/FeedbackStack.tsx +99 -0
  120. package/src/native/expo/README.md +17 -0
  121. package/src/native/expo/index.ts +8 -0
  122. package/src/native/expo/types.ts +23 -0
  123. package/src/native/native/FeedbackScreen.tsx +37 -0
  124. package/src/native/native/index.ts +7 -0
  125. package/src/native/shared/helpers.ts +9 -0
  126. package/src/native/shared/ui/BodyContent.tsx +43 -0
  127. package/src/native/shared/ui/Button.tsx +29 -0
  128. package/src/native/shared/ui/CommentBranch.tsx +122 -0
  129. package/src/native/shared/ui/EntryCard.tsx +37 -0
  130. package/src/native/shared/ui/EntryDetail.tsx +103 -0
  131. package/src/native/shared/ui/FeedbackScreenHeader.tsx +42 -0
  132. package/src/native/shared/ui/FeedbackScreenList.tsx +61 -0
  133. package/src/native/shared/ui/NewEntry.tsx +375 -0
  134. package/src/native/{primitives.tsx → shared/ui/primitives.tsx} +84 -92
  135. package/src/react/{context.tsx → css.tsx} +2 -44
  136. package/src/react/index.ts +1 -1
  137. package/src/react/primitives.tsx +2 -1
  138. package/src/react/screen.tsx +67 -123
  139. package/src/shared/context/FeedbackBodyProvider.tsx +82 -0
  140. package/src/{native/context.tsx → shared/context/FeedbackProvider.tsx} +3 -3
  141. package/src/shared/helpers.ts +33 -0
  142. package/src/shared/messages.ts +23 -22
  143. package/src/shared/theme.ts +1 -0
  144. package/src/shared/types/context.ts +5 -0
  145. package/src/shared/types/messages.ts +16 -1
  146. package/src/shared/types/screen.ts +69 -23
  147. package/src/shared/types/theme.ts +5 -0
  148. package/dist/native/context.d.ts.map +0 -1
  149. package/dist/native/context.js.map +0 -1
  150. package/dist/native/index.d.ts +0 -7
  151. package/dist/native/index.d.ts.map +0 -1
  152. package/dist/native/index.js +0 -6
  153. package/dist/native/index.js.map +0 -1
  154. package/dist/native/primitives.d.ts.map +0 -1
  155. package/dist/native/primitives.js.map +0 -1
  156. package/dist/native/screen.d.ts +0 -3
  157. package/dist/native/screen.d.ts.map +0 -1
  158. package/dist/native/screen.js +0 -284
  159. package/dist/native/screen.js.map +0 -1
  160. package/dist/react/context.d.ts.map +0 -1
  161. package/dist/react/context.js +0 -41
  162. package/dist/react/context.js.map +0 -1
  163. package/src/native/index.ts +0 -6
  164. package/src/native/screen.tsx +0 -831
  165. /package/src/native/{README.md → native/README.md} +0 -0
@@ -1,831 +0,0 @@
1
- import type { EntryKind } from "convex-feedback";
2
- import { useMemo, useState } from "react";
3
- import {
4
- ActivityIndicator,
5
- Alert,
6
- Modal,
7
- Platform,
8
- Pressable,
9
- ScrollView,
10
- Text,
11
- View,
12
- } from "react-native";
13
-
14
- import type {
15
- FeedbackScreenBodyProps,
16
- FeedbackScreenCommentBranchProps,
17
- FeedbackScreenEntryCardProps,
18
- FeedbackScreenEntryDetailProps,
19
- FeedbackScreenEntryModalProps,
20
- FeedbackScreenReplyListProps,
21
- FeedbackScreenRootProps,
22
- } from "../shared/types";
23
- import { FeedbackProvider, useFeedbackUi } from "./context.js";
24
- import {
25
- Comment,
26
- FeedbackBoard,
27
- FeedbackEntry,
28
- FeedbackForm,
29
- } from "./primitives.js";
30
-
31
- const kinds: readonly EntryKind[] = [
32
- "feedback",
33
- "feature_request",
34
- "bug_report",
35
- ];
36
-
37
- const isIos26OrNewer =
38
- Platform.OS === "ios" &&
39
- Number.parseInt(String(Platform.Version).split(".")[0] ?? "0", 10) >= 26;
40
-
41
- export function FeedbackScreen({
42
- hooks,
43
- messages,
44
- theme,
45
- unstyled,
46
- entrySort = "top",
47
- commentSort = "top",
48
- enabledKinds = kinds,
49
- maxCommentDepth = 5,
50
- transformComments,
51
- renderActor,
52
- ...colors
53
- }: FeedbackScreenRootProps) {
54
- return (
55
- <FeedbackProvider messages={messages} theme={theme} unstyled={unstyled}>
56
- <ScreenBody
57
- hooks={hooks}
58
- entrySort={entrySort}
59
- commentSort={commentSort}
60
- enabledKinds={enabledKinds}
61
- maxCommentDepth={maxCommentDepth}
62
- transformComments={transformComments}
63
- renderActor={renderActor}
64
- {...colors}
65
- />
66
- </FeedbackProvider>
67
- );
68
- }
69
-
70
- function ScreenBody({
71
- hooks,
72
- entrySort,
73
- commentSort,
74
- enabledKinds,
75
- maxCommentDepth,
76
- transformComments,
77
- renderActor,
78
- ...colors
79
- }: FeedbackScreenBodyProps) {
80
- const { messages, theme } = useFeedbackUi();
81
- const [query, setQuery] = useState("");
82
- const [selectedEntryId, setSelectedEntryId] = useState<string | null>(null);
83
- const [showForm, setShowForm] = useState(false);
84
- const list = hooks.useEntries({ sort: entrySort, kinds: enabledKinds });
85
- const search = hooks.useSearchEntries({
86
- searchQuery: query,
87
- kinds: enabledKinds,
88
- });
89
-
90
- if (selectedEntryId !== null) {
91
- return (
92
- <FeedbackBoard.Root {...colors}>
93
- <EntryDetail
94
- hooks={hooks}
95
- entryId={selectedEntryId}
96
- onBack={() => setSelectedEntryId(null)}
97
- commentSort={commentSort}
98
- maxCommentDepth={maxCommentDepth}
99
- transformComments={transformComments}
100
- renderActor={renderActor}
101
- />
102
- </FeedbackBoard.Root>
103
- );
104
- }
105
-
106
- const searching = query.trim().length > 0;
107
- const entries = searching ? search : list.results;
108
-
109
- return (
110
- <>
111
- <FeedbackBoard.Root {...colors}>
112
- <FeedbackBoard.Header>
113
- <FeedbackBoard.Title />
114
- <Text style={{ color: theme.colors.mutedText }}>
115
- {messages.board.subtitle}
116
- </Text>
117
- <Pressable
118
- onPress={() => setShowForm(true)}
119
- style={{
120
- alignSelf: "flex-start",
121
- paddingVertical: 9,
122
- paddingHorizontal: 12,
123
- borderRadius: 9,
124
- backgroundColor: theme.colors.primary,
125
- }}
126
- >
127
- <Text style={{ color: "#ffffff", fontWeight: "700" }}>
128
- {messages.board.createEntry}
129
- </Text>
130
- </Pressable>
131
- </FeedbackBoard.Header>
132
- <FeedbackBoard.Search value={query} onValueChange={setQuery} />
133
- <FeedbackBoard.List>
134
- {(entries ?? []).map((entry) => (
135
- <EntryCard
136
- key={entry.id}
137
- entry={entry}
138
- hooks={hooks}
139
- onOpen={() => setSelectedEntryId(entry.id)}
140
- />
141
- ))}
142
- </FeedbackBoard.List>
143
- {!searching && list.status === "CanLoadMore" ? (
144
- <NativeButton
145
- label={messages.board.loadMore}
146
- onPress={() => list.loadMore(hooks.pageSizes.entries)}
147
- />
148
- ) : null}
149
- </FeedbackBoard.Root>
150
- {showForm ? (
151
- <CreateEntryModal
152
- hooks={hooks}
153
- enabledKinds={enabledKinds}
154
- onRequestClose={() => setShowForm(false)}
155
- onCreated={(id) => {
156
- setShowForm(false);
157
- setSelectedEntryId(id);
158
- }}
159
- />
160
- ) : null}
161
- </>
162
- );
163
- }
164
-
165
- function NativeButton({
166
- label,
167
- onPress,
168
- }: {
169
- label: string;
170
- onPress: () => void;
171
- }) {
172
- const { theme } = useFeedbackUi();
173
- return (
174
- <Pressable
175
- onPress={onPress}
176
- style={{
177
- alignSelf: "flex-start",
178
- paddingVertical: 8,
179
- paddingHorizontal: 11,
180
- borderRadius: 8,
181
- borderWidth: 1,
182
- borderColor: theme.colors.border,
183
- }}
184
- >
185
- <Text style={{ color: theme.colors.text, fontWeight: "600" }}>
186
- {label}
187
- </Text>
188
- </Pressable>
189
- );
190
- }
191
-
192
- function EntryCard({ entry, hooks, onOpen }: FeedbackScreenEntryCardProps) {
193
- const setUpvote = hooks.useSetEntryUpvote();
194
- return (
195
- <FeedbackEntry.Root entry={entry}>
196
- <FeedbackEntry.Upvote
197
- onToggle={(active) =>
198
- void setUpvote({ entryId: entry.id, desiredState: active })
199
- }
200
- />
201
- <Pressable onPress={onOpen} style={{ flex: 1, gap: 4 }}>
202
- <View
203
- style={{
204
- flexDirection: "row",
205
- alignItems: "center",
206
- justifyContent: "space-between",
207
- gap: 8,
208
- }}
209
- >
210
- <FeedbackEntry.Kind />
211
- <FeedbackEntry.Status />
212
- </View>
213
- <FeedbackEntry.Title />
214
- <FeedbackEntry.Body numberOfLines={3} />
215
- <FeedbackEntry.CommentCount />
216
- </Pressable>
217
- </FeedbackEntry.Root>
218
- );
219
- }
220
-
221
- function CreateEntryModal({
222
- hooks,
223
- enabledKinds,
224
- onRequestClose,
225
- onCreated,
226
- }: FeedbackScreenEntryModalProps & {
227
- onRequestClose: () => void;
228
- }) {
229
- const { messages, theme } = useFeedbackUi();
230
- const isIos = Platform.OS === "ios";
231
-
232
- const [previewEntryId, setPreviewEntryId] = useState<string | null>(null);
233
-
234
- const [kind, setKind] = useState<EntryKind>(enabledKinds[0] ?? "feedback");
235
- const [title, setTitle] = useState("");
236
- const [body, setBody] = useState("");
237
-
238
- return (
239
- <Modal
240
- visible
241
- animationType="slide"
242
- presentationStyle={isIos ? "formSheet" : "overFullScreen"}
243
- transparent={!isIos}
244
- allowSwipeDismissal={isIos}
245
- onRequestClose={onRequestClose}
246
- {...(isIos
247
- ? {
248
- backdropColor: isIos26OrNewer
249
- ? "transparent"
250
- : theme.colors.background,
251
- }
252
- : {})}
253
- >
254
- <View
255
- style={
256
- isIos
257
- ? {
258
- flex: 1,
259
- backgroundColor: isIos26OrNewer
260
- ? "transparent"
261
- : theme.colors.background,
262
- }
263
- : {
264
- flex: 1,
265
- justifyContent: "flex-end",
266
- backgroundColor: "rgba(0, 0, 0, 0.35)",
267
- }
268
- }
269
- >
270
- {!isIos ? (
271
- <Pressable
272
- accessibilityRole="button"
273
- accessibilityLabel={messages.form.cancel}
274
- onPress={onRequestClose}
275
- style={{
276
- position: "absolute",
277
- top: 0,
278
- right: 0,
279
- bottom: 0,
280
- left: 0,
281
- }}
282
- />
283
- ) : null}
284
-
285
- <View
286
- style={
287
- isIos
288
- ? {
289
- flex: 1,
290
- backgroundColor: "transparent",
291
- }
292
- : {
293
- maxHeight: "92%",
294
- minHeight: 360,
295
- overflow: "hidden",
296
- borderTopLeftRadius: theme.radius * 2,
297
- borderTopRightRadius: theme.radius * 2,
298
- backgroundColor: theme.colors.background,
299
- }
300
- }
301
- >
302
- <View
303
- style={{
304
- flexDirection: "row",
305
- alignItems: "center",
306
- justifyContent: "space-between",
307
- paddingHorizontal: 16,
308
- paddingVertical: 14,
309
- borderBottomWidth: 1,
310
- borderBottomColor: theme.colors.border,
311
- }}
312
- >
313
- <Text
314
- style={{
315
- color: theme.colors.text,
316
- fontSize: 18,
317
- fontWeight: "700",
318
- }}
319
- >
320
- {messages.board.createEntry}
321
- </Text>
322
-
323
- <Pressable
324
- accessibilityRole="button"
325
- onPress={onRequestClose}
326
- hitSlop={8}
327
- >
328
- <Text
329
- style={{
330
- color: theme.colors.primary,
331
- fontWeight: "600",
332
- }}
333
- >
334
- {messages.form.cancel}
335
- </Text>
336
- </Pressable>
337
- </View>
338
-
339
- <ScrollView
340
- keyboardShouldPersistTaps="handled"
341
- keyboardDismissMode={isIos ? "interactive" : "on-drag"}
342
- contentInsetAdjustmentBehavior="automatic"
343
- contentContainerStyle={{
344
- padding: 16,
345
- paddingBottom: 32,
346
- }}
347
- >
348
- {previewEntryId !== null ? (
349
- <EntryDetail
350
- hooks={hooks}
351
- entryId={previewEntryId}
352
- onBack={() => setPreviewEntryId(null)}
353
- commentSort="top"
354
- maxCommentDepth={5}
355
- />
356
- ) : (
357
- <CreateForm
358
- hooks={hooks}
359
- enabledKinds={enabledKinds}
360
- kind={kind}
361
- onKindChange={setKind}
362
- title={title}
363
- onTitleChange={setTitle}
364
- body={body}
365
- onBodyChange={setBody}
366
- onCreated={onCreated}
367
- onOpenSuggestion={setPreviewEntryId}
368
- />
369
- )}
370
- </ScrollView>
371
- </View>
372
- </View>
373
- </Modal>
374
- );
375
- }
376
-
377
- function CreateForm({
378
- hooks,
379
- enabledKinds,
380
- kind,
381
- onKindChange,
382
- title,
383
- onTitleChange,
384
- body,
385
- onBodyChange,
386
- onCreated,
387
- onOpenSuggestion,
388
- }: FeedbackScreenEntryModalProps & {
389
- kind: EntryKind;
390
- onKindChange: (kind: EntryKind) => void;
391
- title: string;
392
- onTitleChange: (title: string) => void;
393
- body: string;
394
- onBodyChange: (body: string) => void;
395
- onOpenSuggestion: (id: string) => void;
396
- }) {
397
- const { messages, theme } = useFeedbackUi();
398
- const [submitting, setSubmitting] = useState(false);
399
- const create = hooks.useCreateEntry();
400
- const similar = hooks.useSimilarEntries({ title, body, kind, limit: 3 });
401
-
402
- const suggestions = useMemo(() => {
403
- if (similar === undefined) return [];
404
- return [...similar.exact, ...similar.similar];
405
- }, [similar]);
406
-
407
- const submit = async () => {
408
- if (submitting || title.trim().length === 0 || body.trim().length === 0) {
409
- return;
410
- }
411
-
412
- setSubmitting(true);
413
-
414
- try {
415
- onCreated(await create({ kind, title, body }));
416
- } finally {
417
- setSubmitting(false);
418
- }
419
- };
420
-
421
- const requestSubmit = () => {
422
- if (
423
- submitting ||
424
- similar === undefined ||
425
- title.trim().length === 0 ||
426
- body.trim().length === 0
427
- ) {
428
- return;
429
- }
430
-
431
- if (suggestions.length === 0) {
432
- void submit();
433
- return;
434
- }
435
-
436
- Alert.alert(
437
- similar.exact.length > 0
438
- ? messages.form.exactDuplicate
439
- : messages.form.possibleDuplicates,
440
- messages.form.duplicateWarning,
441
- [
442
- {
443
- text: messages.form.cancel,
444
- style: "cancel",
445
- },
446
- {
447
- text: messages.form.submitAnyway,
448
- onPress: () => {
449
- void submit();
450
- },
451
- },
452
- ],
453
- );
454
- };
455
-
456
- return (
457
- <FeedbackForm.Root>
458
- <Text style={{ color: theme.colors.text, fontWeight: "600" }}>
459
- {messages.form.kind}
460
- </Text>
461
- <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 7 }}>
462
- {enabledKinds.map((value) => (
463
- <Pressable
464
- key={value}
465
- onPress={() => onKindChange(value)}
466
- style={{
467
- paddingVertical: 6,
468
- paddingHorizontal: 9,
469
- borderRadius: 999,
470
- borderWidth: 1,
471
- borderColor:
472
- value === kind ? theme.colors.primary : theme.colors.border,
473
- }}
474
- >
475
- <Text
476
- style={{
477
- color:
478
- value === kind
479
- ? theme.colors.primary
480
- : theme.colors.mutedText,
481
- }}
482
- >
483
- {messages.kinds[value]}
484
- </Text>
485
- </Pressable>
486
- ))}
487
- </View>
488
- <FeedbackForm.Input
489
- value={title}
490
- onChangeText={onTitleChange}
491
- placeholder={messages.form.titlePlaceholder}
492
- />
493
- <FeedbackForm.Textarea
494
- value={body}
495
- onChangeText={onBodyChange}
496
- placeholder={messages.form.bodyPlaceholder}
497
- />
498
- {suggestions.length > 0 ? (
499
- <View
500
- style={{
501
- gap: 8,
502
- padding: 10,
503
- borderWidth: 1,
504
- borderColor: theme.colors.border,
505
- borderRadius: theme.radius,
506
- backgroundColor: theme.colors.surfaceMuted,
507
- }}
508
- >
509
- <Text
510
- style={{
511
- color: theme.colors.text,
512
- fontWeight: "600",
513
- }}
514
- >
515
- {similar?.exact.length
516
- ? messages.form.exactDuplicate
517
- : messages.form.possibleDuplicates}
518
- </Text>
519
-
520
- {suggestions.map((entry) => (
521
- <Pressable
522
- key={entry.id}
523
- onPress={() => onOpenSuggestion(entry.id)}
524
- style={{
525
- gap: 4,
526
- padding: 10,
527
- borderWidth: 1,
528
- borderColor: theme.colors.border,
529
- borderRadius: Math.max(8, theme.radius - 2),
530
- backgroundColor: theme.colors.surface,
531
- }}
532
- >
533
- <View
534
- style={{
535
- flexDirection: "row",
536
- alignItems: "center",
537
- justifyContent: "space-between",
538
- gap: 8,
539
- }}
540
- >
541
- <Text
542
- style={{
543
- flexShrink: 1,
544
- color: theme.colors.mutedText,
545
- fontSize: 12,
546
- fontWeight: "600",
547
- }}
548
- >
549
- {messages.kinds[entry.kind]}
550
- </Text>
551
-
552
- <Text
553
- style={{
554
- color: theme.colors.mutedText,
555
- fontSize: 12,
556
- }}
557
- >
558
- {messages.statuses[entry.status]}
559
- </Text>
560
- </View>
561
-
562
- <Text
563
- numberOfLines={2}
564
- style={{
565
- color: theme.colors.text,
566
- fontWeight: "700",
567
- }}
568
- >
569
- {entry.title}
570
- </Text>
571
-
572
- <Text
573
- numberOfLines={2}
574
- style={{
575
- color: theme.colors.mutedText,
576
- lineHeight: 18,
577
- }}
578
- >
579
- {entry.body}
580
- </Text>
581
-
582
- <Text
583
- style={{
584
- color: theme.colors.mutedText,
585
- fontSize: 12,
586
- }}
587
- >
588
- ▲ {entry.upvoteCount} ·{" "}
589
- {messages.entry.comments(entry.commentCount)}
590
- </Text>
591
- </Pressable>
592
- ))}
593
- </View>
594
- ) : null}
595
- <FeedbackForm.Submit submitting={submitting} onPress={requestSubmit} />
596
- </FeedbackForm.Root>
597
- );
598
- }
599
-
600
- function EntryDetail({
601
- hooks,
602
- entryId,
603
- onBack,
604
- commentSort,
605
- maxCommentDepth,
606
- transformComments,
607
- renderActor,
608
- }: FeedbackScreenEntryDetailProps) {
609
- const { messages, theme } = useFeedbackUi();
610
- const entry = hooks.useEntry(entryId);
611
- const comments = hooks.useComments({ entryId, sort: commentSort });
612
- const setUpvote = hooks.useSetEntryUpvote();
613
- const createComment = hooks.useCreateComment();
614
- const [body, setBody] = useState("");
615
- const visible = useMemo(
616
- () => transformComments?.(comments.results) ?? comments.results,
617
- [comments.results, transformComments],
618
- );
619
-
620
- if (entry === undefined)
621
- return (
622
- <View
623
- style={{
624
- flex: 1,
625
- gap: 8,
626
- alignItems: "center",
627
- justifyContent: "center",
628
- }}
629
- >
630
- <ActivityIndicator size="large" />
631
- <Text style={{ color: theme.colors.mutedText }}>
632
- {messages.board.loading}
633
- </Text>
634
- </View>
635
- );
636
- if (entry === null)
637
- return <NativeButton label={messages.entry.back} onPress={onBack} />;
638
-
639
- return (
640
- <View style={{ gap: 12 }}>
641
- <NativeButton label={messages.entry.back} onPress={onBack} />
642
- <FeedbackEntry.Root entry={entry}>
643
- <FeedbackEntry.Upvote
644
- onToggle={(active) =>
645
- void setUpvote({ entryId, desiredState: active })
646
- }
647
- />
648
- <FeedbackEntry.Content style={{ gap: 5 }}>
649
- <FeedbackEntry.Status />
650
- <FeedbackEntry.Title />
651
- <FeedbackEntry.Body />
652
- <FeedbackEntry.CommentCount />
653
- </FeedbackEntry.Content>
654
- </FeedbackEntry.Root>
655
- <Text
656
- style={{ color: theme.colors.text, fontSize: 18, fontWeight: "700" }}
657
- >
658
- {messages.comments.title}
659
- </Text>
660
- <FeedbackForm.Root>
661
- <FeedbackForm.Textarea
662
- value={body}
663
- onChangeText={setBody}
664
- placeholder={messages.comments.placeholder}
665
- />
666
- <FeedbackForm.Submit
667
- onPress={async () => {
668
- if (body.trim().length === 0) return;
669
- await createComment({ entryId, body });
670
- setBody("");
671
- }}
672
- >
673
- {messages.comments.submit}
674
- </FeedbackForm.Submit>
675
- </FeedbackForm.Root>
676
- <View style={{ gap: 8 }}>
677
- {visible.map((comment) => (
678
- <CommentBranch
679
- key={comment.id}
680
- comment={comment}
681
- entryId={entryId}
682
- hooks={hooks}
683
- commentSort={commentSort}
684
- maxCommentDepth={maxCommentDepth}
685
- transformComments={transformComments}
686
- renderActor={renderActor}
687
- />
688
- ))}
689
- </View>
690
- {comments.status === "CanLoadMore" ? (
691
- <NativeButton
692
- label={messages.comments.loadMore}
693
- onPress={() => comments.loadMore(hooks.pageSizes.comments)}
694
- />
695
- ) : null}
696
- </View>
697
- );
698
- }
699
-
700
- function CommentBranch({
701
- comment,
702
- entryId,
703
- hooks,
704
- commentSort,
705
- maxCommentDepth,
706
- transformComments,
707
- renderActor,
708
- }: FeedbackScreenCommentBranchProps) {
709
- const { messages, theme } = useFeedbackUi();
710
- const [expanded, setExpanded] = useState(false);
711
- const [replying, setReplying] = useState(false);
712
- const [replyBody, setReplyBody] = useState("");
713
- const setLike = hooks.useSetCommentLike();
714
- const createComment = hooks.useCreateComment();
715
- return (
716
- <Comment.Root comment={comment}>
717
- {renderActor === undefined ? null : (
718
- <View>{renderActor(comment.actorId)}</View>
719
- )}
720
- <Comment.Body />
721
- <View
722
- style={{
723
- flexDirection: "row",
724
- flexWrap: "wrap",
725
- alignItems: "center",
726
- gap: 4,
727
- }}
728
- >
729
- <Comment.Like
730
- onToggle={(active) =>
731
- void setLike({ commentId: comment.id, desiredState: active })
732
- }
733
- />
734
- {comment.body !== null && comment.depth < maxCommentDepth ? (
735
- <Comment.Reply onActivate={() => setReplying((value) => !value)} />
736
- ) : null}
737
- <Comment.RepliesButton
738
- expanded={expanded}
739
- onExpandedChange={setExpanded}
740
- />
741
- </View>
742
- {replying ? (
743
- <FeedbackForm.Root>
744
- <FeedbackForm.Textarea
745
- value={replyBody}
746
- onChangeText={setReplyBody}
747
- placeholder={messages.comments.placeholder}
748
- />
749
- <FeedbackForm.Submit
750
- onPress={async () => {
751
- if (replyBody.trim().length === 0) return;
752
- await createComment({
753
- entryId,
754
- parentCommentId: comment.id,
755
- body: replyBody,
756
- });
757
- setReplyBody("");
758
- setReplying(false);
759
- setExpanded(true);
760
- }}
761
- >
762
- {messages.comments.reply}
763
- </FeedbackForm.Submit>
764
- <NativeButton
765
- label={messages.comments.cancelReply}
766
- onPress={() => setReplying(false)}
767
- />
768
- </FeedbackForm.Root>
769
- ) : null}
770
- {expanded ? (
771
- <ReplyList
772
- hooks={hooks}
773
- entryId={entryId}
774
- parentCommentId={comment.id}
775
- commentSort={commentSort}
776
- maxCommentDepth={maxCommentDepth}
777
- transformComments={transformComments}
778
- renderActor={renderActor}
779
- />
780
- ) : null}
781
- {comment.deletedAt !== undefined ? (
782
- <Text style={{ color: theme.colors.mutedText, fontSize: 11 }}>
783
- {messages.comments.deleted}
784
- </Text>
785
- ) : null}
786
- </Comment.Root>
787
- );
788
- }
789
-
790
- function ReplyList({
791
- hooks,
792
- entryId,
793
- parentCommentId,
794
- commentSort,
795
- maxCommentDepth,
796
- transformComments,
797
- renderActor,
798
- }: FeedbackScreenReplyListProps) {
799
- const { messages } = useFeedbackUi();
800
- const replies = hooks.useComments({
801
- entryId,
802
- parentCommentId,
803
- sort: commentSort,
804
- });
805
- const visible = useMemo(
806
- () => transformComments?.(replies.results) ?? replies.results,
807
- [replies.results, transformComments],
808
- );
809
- return (
810
- <Comment.Children>
811
- {visible.map((reply) => (
812
- <CommentBranch
813
- key={reply.id}
814
- comment={reply}
815
- entryId={entryId}
816
- hooks={hooks}
817
- commentSort={commentSort}
818
- maxCommentDepth={maxCommentDepth}
819
- transformComments={transformComments}
820
- renderActor={renderActor}
821
- />
822
- ))}
823
- {replies.status === "CanLoadMore" ? (
824
- <NativeButton
825
- label={messages.comments.loadMore}
826
- onPress={() => replies.loadMore(hooks.pageSizes.replies)}
827
- />
828
- ) : null}
829
- </Comment.Children>
830
- );
831
- }