convex-feedback-ui 0.1.4 → 0.1.6
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/README.md +176 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/native/expo/ExpoFeedbackBody.d.ts +1 -1
- package/dist/native/expo/ExpoFeedbackBody.d.ts.map +1 -1
- package/dist/native/expo/ExpoFeedbackBody.js +2 -2
- package/dist/native/expo/ExpoFeedbackBody.js.map +1 -1
- package/dist/native/expo/FeedbackCreateStackLayout.d.ts +3 -0
- package/dist/native/expo/FeedbackCreateStackLayout.d.ts.map +1 -0
- package/dist/native/expo/FeedbackCreateStackLayout.js +35 -0
- package/dist/native/expo/FeedbackCreateStackLayout.js.map +1 -0
- package/dist/native/expo/FeedbackRoutes.d.ts +4 -0
- package/dist/native/expo/FeedbackRoutes.d.ts.map +1 -0
- package/dist/native/expo/FeedbackRoutes.js +76 -0
- package/dist/native/expo/FeedbackRoutes.js.map +1 -0
- package/dist/native/expo/FeedbackScreen.d.ts +1 -1
- package/dist/native/expo/FeedbackScreen.d.ts.map +1 -1
- package/dist/native/expo/FeedbackScreen.js +3 -2
- package/dist/native/expo/FeedbackScreen.js.map +1 -1
- package/dist/native/expo/FeedbackStack.d.ts +1 -1
- package/dist/native/expo/FeedbackStack.d.ts.map +1 -1
- package/dist/native/expo/FeedbackStack.js +4 -2
- package/dist/native/expo/FeedbackStack.js.map +1 -1
- package/dist/native/expo/FeedbackStackLayout.d.ts +3 -0
- package/dist/native/expo/FeedbackStackLayout.d.ts.map +1 -0
- package/dist/native/expo/FeedbackStackLayout.js +54 -0
- package/dist/native/expo/FeedbackStackLayout.js.map +1 -0
- package/dist/native/expo/RoutedFeedbackContext.d.ts +4 -0
- package/dist/native/expo/RoutedFeedbackContext.d.ts.map +1 -0
- package/dist/native/expo/RoutedFeedbackContext.js +11 -0
- package/dist/native/expo/RoutedFeedbackContext.js.map +1 -0
- package/dist/native/expo/RoutedFeedbackModalContext.d.ts +7 -0
- package/dist/native/expo/RoutedFeedbackModalContext.d.ts.map +1 -0
- package/dist/native/expo/RoutedFeedbackModalContext.js +7 -0
- package/dist/native/expo/RoutedFeedbackModalContext.js.map +1 -0
- package/dist/native/expo/index.d.ts +6 -0
- package/dist/native/expo/index.d.ts.map +1 -1
- package/dist/native/expo/index.js +6 -0
- package/dist/native/expo/index.js.map +1 -1
- package/dist/native/expo/metadata.d.ts +4 -0
- package/dist/native/expo/metadata.d.ts.map +1 -0
- package/dist/native/expo/metadata.js +44 -0
- package/dist/native/expo/metadata.js.map +1 -0
- package/dist/native/expo/routes.d.ts +16 -0
- package/dist/native/expo/routes.d.ts.map +1 -0
- package/dist/native/expo/routes.js +23 -0
- package/dist/native/expo/routes.js.map +1 -0
- package/dist/native/expo/types.d.ts +51 -1
- package/dist/native/expo/types.d.ts.map +1 -1
- package/dist/native/native/FeedbackScreen.d.ts +1 -1
- package/dist/native/native/FeedbackScreen.d.ts.map +1 -1
- package/dist/native/native/FeedbackScreen.js +3 -2
- package/dist/native/native/FeedbackScreen.js.map +1 -1
- package/dist/native/native/index.d.ts +2 -0
- package/dist/native/native/index.d.ts.map +1 -1
- package/dist/native/native/index.js +2 -0
- package/dist/native/native/index.js.map +1 -1
- package/dist/native/shared/metadata.d.ts +5 -0
- package/dist/native/shared/metadata.d.ts.map +1 -0
- package/dist/native/shared/metadata.js +31 -0
- package/dist/native/shared/metadata.js.map +1 -0
- package/dist/native/shared/ui/EntryDetail.d.ts.map +1 -1
- package/dist/native/shared/ui/EntryDetail.js +3 -1
- package/dist/native/shared/ui/EntryDetail.js.map +1 -1
- package/dist/native/shared/ui/FeedbackScreenList.d.ts +1 -1
- package/dist/native/shared/ui/FeedbackScreenList.d.ts.map +1 -1
- package/dist/native/shared/ui/FeedbackScreenList.js +2 -2
- package/dist/native/shared/ui/FeedbackScreenList.js.map +1 -1
- package/dist/native/shared/ui/MetadataModal.d.ts +6 -0
- package/dist/native/shared/ui/MetadataModal.d.ts.map +1 -0
- package/dist/native/shared/ui/MetadataModal.js +39 -0
- package/dist/native/shared/ui/MetadataModal.js.map +1 -0
- package/dist/native/shared/ui/NewEntry.d.ts +10 -0
- package/dist/native/shared/ui/NewEntry.d.ts.map +1 -1
- package/dist/native/shared/ui/NewEntry.js +11 -4
- package/dist/native/shared/ui/NewEntry.js.map +1 -1
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/metadata.d.ts +4 -0
- package/dist/react/metadata.d.ts.map +1 -0
- package/dist/react/metadata.js +40 -0
- package/dist/react/metadata.js.map +1 -0
- package/dist/react/screen.d.ts +1 -1
- package/dist/react/screen.d.ts.map +1 -1
- package/dist/react/screen.js +18 -4
- package/dist/react/screen.js.map +1 -1
- package/dist/react/styles.css +35 -0
- package/dist/shared/context/FeedbackBodyProvider.d.ts +1 -1
- package/dist/shared/context/FeedbackBodyProvider.d.ts.map +1 -1
- package/dist/shared/context/FeedbackBodyProvider.js +5 -1
- package/dist/shared/context/FeedbackBodyProvider.js.map +1 -1
- package/dist/shared/messages.d.ts.map +1 -1
- package/dist/shared/messages.js +9 -0
- package/dist/shared/messages.js.map +1 -1
- package/dist/shared/metadata.d.ts +6 -0
- package/dist/shared/metadata.d.ts.map +1 -0
- package/dist/shared/metadata.js +54 -0
- package/dist/shared/metadata.js.map +1 -0
- package/dist/shared/types/index.d.ts +1 -0
- package/dist/shared/types/index.d.ts.map +1 -1
- package/dist/shared/types/messages.d.ts +17 -0
- package/dist/shared/types/messages.d.ts.map +1 -1
- package/dist/shared/types/metadata.d.ts +22 -0
- package/dist/shared/types/metadata.d.ts.map +1 -0
- package/dist/shared/types/metadata.js +2 -0
- package/dist/shared/types/metadata.js.map +1 -0
- package/dist/shared/types/screen.d.ts +14 -1
- package/dist/shared/types/screen.d.ts.map +1 -1
- package/package.json +12 -8
- package/src/native/expo/ExpoFeedbackBody.tsx +6 -1
- package/src/native/expo/FeedbackCreateStackLayout.tsx +62 -0
- package/src/native/expo/FeedbackRoutes.tsx +204 -0
- package/src/native/expo/FeedbackScreen.tsx +4 -0
- package/src/native/expo/FeedbackStack.tsx +16 -4
- package/src/native/expo/FeedbackStackLayout.tsx +116 -0
- package/src/native/expo/README.md +24 -0
- package/src/native/expo/RoutedFeedbackContext.tsx +20 -0
- package/src/native/expo/RoutedFeedbackModalContext.tsx +14 -0
- package/src/native/expo/index.ts +6 -0
- package/src/native/expo/metadata.ts +62 -0
- package/src/native/expo/routes.ts +41 -0
- package/src/native/expo/types.ts +63 -1
- package/src/native/native/FeedbackScreen.tsx +4 -0
- package/src/native/native/index.ts +2 -0
- package/src/native/shared/metadata.ts +39 -0
- package/src/native/shared/ui/EntryDetail.tsx +14 -0
- package/src/native/shared/ui/FeedbackScreenList.tsx +2 -1
- package/src/native/shared/ui/MetadataModal.tsx +112 -0
- package/src/native/shared/ui/NewEntry.tsx +18 -4
- package/src/react/index.ts +2 -0
- package/src/react/metadata.ts +51 -0
- package/src/react/screen.tsx +89 -2
- package/src/react/styles.css +35 -0
- package/src/shared/context/FeedbackBodyProvider.tsx +8 -0
- package/src/shared/messages.ts +9 -0
- package/src/shared/metadata.ts +84 -0
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/messages.ts +24 -0
- package/src/shared/types/metadata.ts +55 -0
- package/src/shared/types/screen.ts +17 -2
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Stack,
|
|
3
|
+
useNavigation,
|
|
4
|
+
type NativeStackNavigationOptions,
|
|
5
|
+
} from "expo-router";
|
|
6
|
+
import { useCallback, useMemo } from "react";
|
|
7
|
+
|
|
8
|
+
import { useFeedbackBody } from "../../shared/context/FeedbackBodyProvider.js";
|
|
9
|
+
import { useFeedbackUi } from "../../shared/context/FeedbackProvider.js";
|
|
10
|
+
import { RoutedFeedbackModalProvider } from "./RoutedFeedbackModalContext.js";
|
|
11
|
+
import { useRoutedFeedback } from "./RoutedFeedbackContext.js";
|
|
12
|
+
import type { FeedbackCreateStackLayoutProps } from "./types.js";
|
|
13
|
+
|
|
14
|
+
export function FeedbackCreateStackLayout({
|
|
15
|
+
screenOptions,
|
|
16
|
+
createOptions,
|
|
17
|
+
entryOptions,
|
|
18
|
+
}: FeedbackCreateStackLayoutProps) {
|
|
19
|
+
const parentNavigation = useNavigation();
|
|
20
|
+
const { enabledKinds } = useFeedbackBody();
|
|
21
|
+
const { messages, theme } = useFeedbackUi();
|
|
22
|
+
const { routes } = useRoutedFeedback();
|
|
23
|
+
const dismiss = useCallback(
|
|
24
|
+
() => parentNavigation.goBack(),
|
|
25
|
+
[parentNavigation],
|
|
26
|
+
);
|
|
27
|
+
const modal = useMemo(() => ({ dismiss }), [dismiss]);
|
|
28
|
+
const createTitle =
|
|
29
|
+
enabledKinds.length === 1
|
|
30
|
+
? messages.newFeedback[enabledKinds.at(0)!]
|
|
31
|
+
: messages.board.createEntry;
|
|
32
|
+
const defaults: NativeStackNavigationOptions = {
|
|
33
|
+
headerShown: true,
|
|
34
|
+
headerBackButtonDisplayMode: "minimal",
|
|
35
|
+
contentStyle: { backgroundColor: theme.colors.background },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<RoutedFeedbackModalProvider value={modal}>
|
|
40
|
+
<Stack>
|
|
41
|
+
<Stack.Screen
|
|
42
|
+
name="index"
|
|
43
|
+
options={{
|
|
44
|
+
...defaults,
|
|
45
|
+
headerTitle: createTitle,
|
|
46
|
+
...screenOptions,
|
|
47
|
+
...createOptions,
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
<Stack.Screen
|
|
51
|
+
name={routes.entry}
|
|
52
|
+
options={{
|
|
53
|
+
...defaults,
|
|
54
|
+
headerTitle: messages.board.title,
|
|
55
|
+
...screenOptions,
|
|
56
|
+
...entryOptions,
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
</Stack>
|
|
60
|
+
</RoutedFeedbackModalProvider>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import type { SearchBarCommands } from "react-native-screens";
|
|
4
|
+
import type { EntryKind } from "convex-feedback";
|
|
5
|
+
|
|
6
|
+
import { useFeedbackBody } from "../../shared/context/FeedbackBodyProvider.js";
|
|
7
|
+
import { useFeedbackUi } from "../../shared/context/FeedbackProvider.js";
|
|
8
|
+
import { EntryDetail } from "../shared/ui/EntryDetail.js";
|
|
9
|
+
import { FeedbackScreenList } from "../shared/ui/FeedbackScreenList.js";
|
|
10
|
+
import { CreateEntryForm } from "../shared/ui/NewEntry.js";
|
|
11
|
+
import { FeedbackBoard } from "../shared/ui/primitives.js";
|
|
12
|
+
import { feedbackRouteHref } from "./routes.js";
|
|
13
|
+
import { useRoutedFeedbackModal } from "./RoutedFeedbackModalContext.js";
|
|
14
|
+
import { useRoutedFeedback } from "./RoutedFeedbackContext.js";
|
|
15
|
+
|
|
16
|
+
export function FeedbackBoardScreen() {
|
|
17
|
+
const { query, isSearching, setQuery, setIsSearching } = useFeedbackBody();
|
|
18
|
+
const { messages, theme } = useFeedbackUi();
|
|
19
|
+
const { routes, colors, androidToolbarIcons } = useRoutedFeedback();
|
|
20
|
+
const router = useRouter();
|
|
21
|
+
const searchRef = useRef<SearchBarCommands>(null);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const searching = query.trim().length > 0;
|
|
25
|
+
if (searching !== isSearching) setIsSearching(searching);
|
|
26
|
+
}, [query, isSearching, setIsSearching]);
|
|
27
|
+
|
|
28
|
+
const openEntry = (entryId: string) => {
|
|
29
|
+
if ((query || isSearching) && searchRef.current) {
|
|
30
|
+
searchRef.current.blur();
|
|
31
|
+
}
|
|
32
|
+
router.push(feedbackRouteHref(routes.entry, { entryId }), {
|
|
33
|
+
relativeToDirectory: true,
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<FeedbackBoard.Root {...colors}>
|
|
40
|
+
<FeedbackScreenList
|
|
41
|
+
showSelectedEntry={false}
|
|
42
|
+
hideBackButton
|
|
43
|
+
onEntryOpen={openEntry}
|
|
44
|
+
/>
|
|
45
|
+
</FeedbackBoard.Root>
|
|
46
|
+
|
|
47
|
+
<Stack.Toolbar placement="right">
|
|
48
|
+
<Stack.Toolbar.Button
|
|
49
|
+
icon={
|
|
50
|
+
process.env.EXPO_OS === "ios" ? "plus" : androidToolbarIcons.create
|
|
51
|
+
}
|
|
52
|
+
variant="prominent"
|
|
53
|
+
accessibilityLabel={messages.board.createEntry}
|
|
54
|
+
onPress={() =>
|
|
55
|
+
router.push(feedbackRouteHref(routes.create), {
|
|
56
|
+
relativeToDirectory: true,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
tintColor={theme.colors.primary}
|
|
60
|
+
>
|
|
61
|
+
{messages.board.createEntry}
|
|
62
|
+
</Stack.Toolbar.Button>
|
|
63
|
+
</Stack.Toolbar>
|
|
64
|
+
<Stack.Toolbar placement="left">
|
|
65
|
+
<Stack.Toolbar.Button
|
|
66
|
+
hidden={!router.canGoBack()}
|
|
67
|
+
icon={
|
|
68
|
+
process.env.EXPO_OS === "ios"
|
|
69
|
+
? "chevron.backward"
|
|
70
|
+
: androidToolbarIcons.back
|
|
71
|
+
}
|
|
72
|
+
accessibilityLabel={messages.entry.back}
|
|
73
|
+
onPress={() => router.back()}
|
|
74
|
+
>
|
|
75
|
+
{messages.entry.back}
|
|
76
|
+
</Stack.Toolbar.Button>
|
|
77
|
+
</Stack.Toolbar>
|
|
78
|
+
<Stack.SearchBar
|
|
79
|
+
ref={searchRef}
|
|
80
|
+
placeholder={messages.board.searchPlaceholder}
|
|
81
|
+
onChangeText={(event) => setQuery(event.nativeEvent.text)}
|
|
82
|
+
onFocus={() => setIsSearching(true)}
|
|
83
|
+
onBlur={() => setIsSearching(query.trim().length > 0)}
|
|
84
|
+
obscureBackground={false}
|
|
85
|
+
allowToolbarIntegration
|
|
86
|
+
hideNavigationBar={false}
|
|
87
|
+
textColor={theme.colors.text}
|
|
88
|
+
/>
|
|
89
|
+
<Stack.Toolbar placement="bottom">
|
|
90
|
+
<Stack.Toolbar.SearchBarSlot />
|
|
91
|
+
</Stack.Toolbar>
|
|
92
|
+
</>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function FeedbackEntryScreen() {
|
|
97
|
+
const params = useLocalSearchParams<{
|
|
98
|
+
entryId?: string | string[];
|
|
99
|
+
}>();
|
|
100
|
+
const entryId = Array.isArray(params.entryId)
|
|
101
|
+
? params.entryId[0]
|
|
102
|
+
: params.entryId;
|
|
103
|
+
|
|
104
|
+
if (!entryId) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
'FeedbackEntryScreen requires an "entryId" dynamic route parameter.',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return <FeedbackEntryRouteContent entryId={entryId} />;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function FeedbackEntryRouteContent({ entryId }: { entryId: string }) {
|
|
114
|
+
const { hooks } = useFeedbackBody();
|
|
115
|
+
const { messages, theme } = useFeedbackUi();
|
|
116
|
+
const { colors, androidToolbarIcons } = useRoutedFeedback();
|
|
117
|
+
const modal = useRoutedFeedbackModal();
|
|
118
|
+
const router = useRouter();
|
|
119
|
+
const entry = hooks.useEntry(entryId);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<>
|
|
123
|
+
{entry && <Stack.Screen options={{ headerTitle: entry.title }} />}
|
|
124
|
+
<FeedbackBoard.Root {...colors}>
|
|
125
|
+
<FeedbackBoard.List style={{ padding: theme.spacing }}>
|
|
126
|
+
<EntryDetail
|
|
127
|
+
entryId={entryId}
|
|
128
|
+
hideBackButton
|
|
129
|
+
onBack={() => router.back()}
|
|
130
|
+
/>
|
|
131
|
+
</FeedbackBoard.List>
|
|
132
|
+
</FeedbackBoard.Root>
|
|
133
|
+
{modal && (
|
|
134
|
+
<Stack.Toolbar placement="right">
|
|
135
|
+
<Stack.Toolbar.Button
|
|
136
|
+
icon={
|
|
137
|
+
process.env.EXPO_OS === "ios"
|
|
138
|
+
? "xmark"
|
|
139
|
+
: androidToolbarIcons.close
|
|
140
|
+
}
|
|
141
|
+
accessibilityLabel={messages.form.cancel}
|
|
142
|
+
onPress={modal.dismiss}
|
|
143
|
+
>
|
|
144
|
+
{messages.form.cancel}
|
|
145
|
+
</Stack.Toolbar.Button>
|
|
146
|
+
</Stack.Toolbar>
|
|
147
|
+
)}
|
|
148
|
+
</>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function CreateFeedbackScreen() {
|
|
153
|
+
const { enabledKinds } = useFeedbackBody();
|
|
154
|
+
const { messages, theme } = useFeedbackUi();
|
|
155
|
+
const { routes, colors, androidToolbarIcons } = useRoutedFeedback();
|
|
156
|
+
const modal = useRoutedFeedbackModal();
|
|
157
|
+
const router = useRouter();
|
|
158
|
+
const [kind, setKind] = useState<EntryKind>(enabledKinds[0] ?? "feedback");
|
|
159
|
+
const [title, setTitle] = useState("");
|
|
160
|
+
const [body, setBody] = useState("");
|
|
161
|
+
|
|
162
|
+
const entryHref = (entryId: string) =>
|
|
163
|
+
feedbackRouteHref(routes.entry, { entryId });
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<>
|
|
167
|
+
<FeedbackBoard.Root {...colors}>
|
|
168
|
+
<FeedbackBoard.List
|
|
169
|
+
style={{
|
|
170
|
+
padding: theme.spacing,
|
|
171
|
+
paddingBottom: theme.spacing * 2,
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
<CreateEntryForm
|
|
175
|
+
kind={kind}
|
|
176
|
+
onKindChange={setKind}
|
|
177
|
+
title={title}
|
|
178
|
+
onTitleChange={setTitle}
|
|
179
|
+
body={body}
|
|
180
|
+
onBodyChange={setBody}
|
|
181
|
+
onOpenSuggestion={(entryId) =>
|
|
182
|
+
router.push(entryHref(entryId), { relativeToDirectory: true })
|
|
183
|
+
}
|
|
184
|
+
onCreated={(entryId) => {
|
|
185
|
+
router.dismissTo(entryHref(entryId));
|
|
186
|
+
}}
|
|
187
|
+
/>
|
|
188
|
+
</FeedbackBoard.List>
|
|
189
|
+
</FeedbackBoard.Root>
|
|
190
|
+
|
|
191
|
+
<Stack.Toolbar placement="left">
|
|
192
|
+
<Stack.Toolbar.Button
|
|
193
|
+
icon={
|
|
194
|
+
process.env.EXPO_OS === "ios" ? "xmark" : androidToolbarIcons.close
|
|
195
|
+
}
|
|
196
|
+
accessibilityLabel={messages.form.cancel}
|
|
197
|
+
onPress={() => (modal ? modal.dismiss() : router.dismiss())}
|
|
198
|
+
>
|
|
199
|
+
{messages.form.cancel}
|
|
200
|
+
</Stack.Toolbar.Button>
|
|
201
|
+
</Stack.Toolbar>
|
|
202
|
+
</>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
@@ -3,6 +3,7 @@ import { FeedbackProvider } from "../../shared/context/FeedbackProvider";
|
|
|
3
3
|
import { kinds } from "../../shared/helpers";
|
|
4
4
|
import { ExpoFeedbackBody } from "./ExpoFeedbackBody";
|
|
5
5
|
import type { ExpoFeedbackScreenProps } from "./types";
|
|
6
|
+
import { collectExpoMetadata } from "./metadata";
|
|
6
7
|
|
|
7
8
|
export function FeedbackScreen({
|
|
8
9
|
hooks,
|
|
@@ -14,6 +15,7 @@ export function FeedbackScreen({
|
|
|
14
15
|
enabledKinds = kinds,
|
|
15
16
|
maxCommentDepth = 5,
|
|
16
17
|
debounceDuration = 300,
|
|
18
|
+
collectMetadata,
|
|
17
19
|
transformComments,
|
|
18
20
|
renderActor,
|
|
19
21
|
...props
|
|
@@ -27,6 +29,8 @@ export function FeedbackScreen({
|
|
|
27
29
|
enabledKinds={enabledKinds}
|
|
28
30
|
maxCommentDepth={maxCommentDepth}
|
|
29
31
|
debounceDuration={debounceDuration}
|
|
32
|
+
collectMetadata={collectMetadata}
|
|
33
|
+
collectStandardMetadata={collectExpoMetadata}
|
|
30
34
|
transformComments={transformComments}
|
|
31
35
|
renderActor={renderActor}
|
|
32
36
|
>
|
|
@@ -6,6 +6,7 @@ import type { FeedbackStackProps } from "./types";
|
|
|
6
6
|
export function FeedbackStack({
|
|
7
7
|
searchRef,
|
|
8
8
|
stackOptions,
|
|
9
|
+
androidToolbarIcons = {},
|
|
9
10
|
children,
|
|
10
11
|
}: FeedbackStackProps) {
|
|
11
12
|
const {
|
|
@@ -54,19 +55,30 @@ export function FeedbackStack({
|
|
|
54
55
|
/>
|
|
55
56
|
<Stack.Toolbar placement="right">
|
|
56
57
|
<Stack.Toolbar.Button
|
|
57
|
-
icon=
|
|
58
|
+
icon={
|
|
59
|
+
process.env.EXPO_OS === "ios" ? "plus" : androidToolbarIcons.create
|
|
60
|
+
}
|
|
58
61
|
variant="prominent"
|
|
59
62
|
accessibilityLabel={messages.board.createEntry}
|
|
60
63
|
onPress={() => setShowForm(true)}
|
|
61
64
|
tintColor={theme.colors.primary}
|
|
62
|
-
|
|
65
|
+
>
|
|
66
|
+
{messages.board.createEntry}
|
|
67
|
+
</Stack.Toolbar.Button>
|
|
63
68
|
</Stack.Toolbar>
|
|
64
69
|
<Stack.Toolbar placement="left">
|
|
65
70
|
<Stack.Toolbar.Button
|
|
66
71
|
hidden={!canGoBack}
|
|
67
|
-
icon=
|
|
72
|
+
icon={
|
|
73
|
+
process.env.EXPO_OS === "ios"
|
|
74
|
+
? "chevron.backward"
|
|
75
|
+
: androidToolbarIcons.back
|
|
76
|
+
}
|
|
77
|
+
accessibilityLabel={messages.entry.back}
|
|
68
78
|
onPress={handleBackPress}
|
|
69
|
-
|
|
79
|
+
>
|
|
80
|
+
{messages.entry.back}
|
|
81
|
+
</Stack.Toolbar.Button>
|
|
70
82
|
</Stack.Toolbar>
|
|
71
83
|
|
|
72
84
|
{children}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Stack, type NativeStackNavigationOptions } from "expo-router";
|
|
2
|
+
|
|
3
|
+
import { FeedbackBodyProvider } from "../../shared/context/FeedbackBodyProvider.js";
|
|
4
|
+
import { FeedbackProvider } from "../../shared/context/FeedbackProvider.js";
|
|
5
|
+
import { kinds } from "../../shared/helpers.js";
|
|
6
|
+
import { mergeFeedbackMessages } from "../../shared/messages.js";
|
|
7
|
+
import { mergeFeedbackTheme } from "../../shared/theme.js";
|
|
8
|
+
import { collectExpoMetadata } from "./metadata.js";
|
|
9
|
+
import { RoutedFeedbackProvider } from "./RoutedFeedbackContext.js";
|
|
10
|
+
import { resolveFeedbackRoutes } from "./routes.js";
|
|
11
|
+
import type { FeedbackStackLayoutProps } from "./types.js";
|
|
12
|
+
|
|
13
|
+
export function FeedbackStackLayout({
|
|
14
|
+
hooks,
|
|
15
|
+
messages,
|
|
16
|
+
theme,
|
|
17
|
+
unstyled,
|
|
18
|
+
entrySort = "top",
|
|
19
|
+
commentSort = "top",
|
|
20
|
+
enabledKinds = kinds,
|
|
21
|
+
maxCommentDepth = 5,
|
|
22
|
+
debounceDuration = 300,
|
|
23
|
+
collectMetadata,
|
|
24
|
+
transformComments,
|
|
25
|
+
renderActor,
|
|
26
|
+
routes: routeOverrides,
|
|
27
|
+
screenOptions,
|
|
28
|
+
boardOptions,
|
|
29
|
+
entryOptions,
|
|
30
|
+
createOptions,
|
|
31
|
+
androidToolbarIcons = {},
|
|
32
|
+
primaryColor,
|
|
33
|
+
primaryForeground,
|
|
34
|
+
backgroundColor,
|
|
35
|
+
surfaceColor,
|
|
36
|
+
textColor,
|
|
37
|
+
mutedColor,
|
|
38
|
+
borderColor,
|
|
39
|
+
dangerColor,
|
|
40
|
+
}: FeedbackStackLayoutProps) {
|
|
41
|
+
const routes = resolveFeedbackRoutes(routeOverrides);
|
|
42
|
+
const resolvedMessages = mergeFeedbackMessages(messages);
|
|
43
|
+
const resolvedTheme = mergeFeedbackTheme(theme);
|
|
44
|
+
const colors = {
|
|
45
|
+
primaryColor,
|
|
46
|
+
primaryForeground,
|
|
47
|
+
backgroundColor,
|
|
48
|
+
surfaceColor,
|
|
49
|
+
textColor,
|
|
50
|
+
mutedColor,
|
|
51
|
+
borderColor,
|
|
52
|
+
dangerColor,
|
|
53
|
+
};
|
|
54
|
+
const background = backgroundColor ?? resolvedTheme.colors.background;
|
|
55
|
+
const boardTitle =
|
|
56
|
+
enabledKinds.length === 1
|
|
57
|
+
? resolvedMessages.kindsPlural[enabledKinds.at(0)!]
|
|
58
|
+
: resolvedMessages.board.title;
|
|
59
|
+
const defaults: NativeStackNavigationOptions = {
|
|
60
|
+
headerShown: true,
|
|
61
|
+
headerBackButtonDisplayMode: "minimal",
|
|
62
|
+
contentStyle: { backgroundColor: background },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<FeedbackProvider messages={messages} theme={theme} unstyled={unstyled}>
|
|
67
|
+
<FeedbackBodyProvider
|
|
68
|
+
hooks={hooks}
|
|
69
|
+
entrySort={entrySort}
|
|
70
|
+
commentSort={commentSort}
|
|
71
|
+
enabledKinds={enabledKinds}
|
|
72
|
+
maxCommentDepth={maxCommentDepth}
|
|
73
|
+
debounceDuration={debounceDuration}
|
|
74
|
+
collectMetadata={collectMetadata}
|
|
75
|
+
collectStandardMetadata={collectExpoMetadata}
|
|
76
|
+
transformComments={transformComments}
|
|
77
|
+
renderActor={renderActor}
|
|
78
|
+
>
|
|
79
|
+
<RoutedFeedbackProvider value={{ routes, colors, androidToolbarIcons }}>
|
|
80
|
+
<Stack>
|
|
81
|
+
<Stack.Screen
|
|
82
|
+
name={routes.board}
|
|
83
|
+
options={{
|
|
84
|
+
...defaults,
|
|
85
|
+
headerTitle: boardTitle,
|
|
86
|
+
headerTransparent: true,
|
|
87
|
+
headerShadowVisible: true,
|
|
88
|
+
...screenOptions,
|
|
89
|
+
...boardOptions,
|
|
90
|
+
}}
|
|
91
|
+
/>
|
|
92
|
+
<Stack.Screen
|
|
93
|
+
name={routes.entry}
|
|
94
|
+
options={{
|
|
95
|
+
...defaults,
|
|
96
|
+
headerTitle: boardTitle,
|
|
97
|
+
...screenOptions,
|
|
98
|
+
...entryOptions,
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
<Stack.Screen
|
|
102
|
+
name={routes.create}
|
|
103
|
+
options={{
|
|
104
|
+
...defaults,
|
|
105
|
+
headerShown: false,
|
|
106
|
+
presentation: "modal",
|
|
107
|
+
...screenOptions,
|
|
108
|
+
...createOptions,
|
|
109
|
+
}}
|
|
110
|
+
/>
|
|
111
|
+
</Stack>
|
|
112
|
+
</RoutedFeedbackProvider>
|
|
113
|
+
</FeedbackBodyProvider>
|
|
114
|
+
</FeedbackProvider>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -15,3 +15,27 @@ To replace a control with Expo UI, use render-function children on an interactiv
|
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Containers such as `FeedbackEntry.Content`, `FeedbackForm.Root`, and `Comment.Children` accept arbitrary children, so custom Expo UI fields/buttons can also be placed directly inside the compound layout.
|
|
18
|
+
|
|
19
|
+
## Routed screens
|
|
20
|
+
|
|
21
|
+
Use `FeedbackStackLayout` with `FeedbackBoardScreen`,
|
|
22
|
+
`FeedbackCreateStackLayout`, `FeedbackEntryScreen`, and
|
|
23
|
+
`CreateFeedbackScreen` when board state should be shared across real Expo
|
|
24
|
+
Router routes. Consumer applications must create the corresponding board and
|
|
25
|
+
entry files plus a nested create directory containing `_layout.tsx`,
|
|
26
|
+
`index.tsx`, and `[entryId].tsx`, because Expo Router discovers pages from the
|
|
27
|
+
application's route directory.
|
|
28
|
+
|
|
29
|
+
Re-export `feedbackStackSettings` from the route layout as
|
|
30
|
+
`unstable_settings` so a deep link to the create modal anchors the board behind
|
|
31
|
+
it. Re-export `feedbackCreateStackSettings` from the nested create layout so
|
|
32
|
+
suggested entries retain the form behind them. Use
|
|
33
|
+
`createFeedbackStackSettings(routes)` when overriding the default route names.
|
|
34
|
+
|
|
35
|
+
Toolbar actions retain their package-provided SF Symbols on iOS. Pass
|
|
36
|
+
`androidToolbarIcons` to `FeedbackScreen` or `FeedbackStackLayout` with Android
|
|
37
|
+
`ImageSourcePropType` values for the create, back, and close actions. Missing
|
|
38
|
+
Android images fall back to text labels.
|
|
39
|
+
|
|
40
|
+
The existing `FeedbackScreen` remains the self-contained API for conditional
|
|
41
|
+
in-screen navigation.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { RoutedFeedbackContextValue } from "./types.js";
|
|
3
|
+
|
|
4
|
+
const RoutedFeedbackContext = createContext<RoutedFeedbackContextValue | null>(
|
|
5
|
+
null,
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
export const RoutedFeedbackProvider = RoutedFeedbackContext.Provider;
|
|
9
|
+
|
|
10
|
+
export function useRoutedFeedback(): RoutedFeedbackContextValue {
|
|
11
|
+
const value = useContext(RoutedFeedbackContext);
|
|
12
|
+
|
|
13
|
+
if (value === null) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"Routed feedback screens must be rendered inside FeedbackStackLayout.",
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
interface RoutedFeedbackModalContextValue {
|
|
4
|
+
dismiss: () => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const RoutedFeedbackModalContext =
|
|
8
|
+
createContext<RoutedFeedbackModalContextValue | null>(null);
|
|
9
|
+
|
|
10
|
+
export const RoutedFeedbackModalProvider = RoutedFeedbackModalContext.Provider;
|
|
11
|
+
|
|
12
|
+
export function useRoutedFeedbackModal() {
|
|
13
|
+
return useContext(RoutedFeedbackModalContext);
|
|
14
|
+
}
|
package/src/native/expo/index.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
export * from "../../shared/context/FeedbackBodyProvider.js";
|
|
2
2
|
export * from "../../shared/context/FeedbackProvider.js";
|
|
3
3
|
export * from "../../shared/messages.js";
|
|
4
|
+
export * from "../../shared/metadata.js";
|
|
4
5
|
export * from "../../shared/theme.js";
|
|
5
6
|
export type * from "../../shared/types/index.js";
|
|
6
7
|
export * from "../shared/ui/primitives.js";
|
|
8
|
+
export * from "./metadata.js";
|
|
7
9
|
export * from "./FeedbackScreen.js";
|
|
10
|
+
export * from "./FeedbackCreateStackLayout.js";
|
|
11
|
+
export * from "./FeedbackStackLayout.js";
|
|
12
|
+
export * from "./FeedbackRoutes.js";
|
|
13
|
+
export * from "./routes.js";
|
|
8
14
|
export type * from "./types.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { FeedbackMetadataValue } from "convex-feedback";
|
|
2
|
+
import Constants from "expo-constants";
|
|
3
|
+
import { Platform } from "react-native";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
StandardMetadataKey,
|
|
7
|
+
EnabledStandardMetadataSelection,
|
|
8
|
+
} from "../../shared/types/metadata.js";
|
|
9
|
+
import {
|
|
10
|
+
collectNativeMetadata,
|
|
11
|
+
defaultNativeMetadataKeys,
|
|
12
|
+
} from "../shared/metadata.js";
|
|
13
|
+
|
|
14
|
+
const defaultExpoMetadataKeys: readonly StandardMetadataKey[] = [
|
|
15
|
+
...defaultNativeMetadataKeys,
|
|
16
|
+
"appVersion",
|
|
17
|
+
"buildNumber",
|
|
18
|
+
"applicationId",
|
|
19
|
+
"expoRuntimeVersion",
|
|
20
|
+
"executionEnvironment",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function collectExpoMetadata(
|
|
24
|
+
selection: EnabledStandardMetadataSelection,
|
|
25
|
+
): Partial<Record<StandardMetadataKey, FeedbackMetadataValue>> {
|
|
26
|
+
const selected = selection === true ? defaultExpoMetadataKeys : selection;
|
|
27
|
+
const keys = new Set(selected);
|
|
28
|
+
const metadata = collectNativeMetadata(selected);
|
|
29
|
+
const config = Constants.expoConfig;
|
|
30
|
+
|
|
31
|
+
if (keys.has("appVersion") && config?.version)
|
|
32
|
+
metadata.appVersion = config.version;
|
|
33
|
+
|
|
34
|
+
if (keys.has("buildNumber")) {
|
|
35
|
+
const buildNumber =
|
|
36
|
+
Platform.OS === "ios"
|
|
37
|
+
? (Constants.platform?.ios?.buildNumber ?? config?.ios?.buildNumber)
|
|
38
|
+
: Platform.OS === "android"
|
|
39
|
+
? (Constants.platform?.android?.versionCode ??
|
|
40
|
+
config?.android?.versionCode)
|
|
41
|
+
: undefined;
|
|
42
|
+
if (buildNumber !== undefined && buildNumber !== null)
|
|
43
|
+
metadata.buildNumber = buildNumber;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (keys.has("applicationId")) {
|
|
47
|
+
const applicationId =
|
|
48
|
+
Platform.OS === "ios"
|
|
49
|
+
? config?.ios?.bundleIdentifier
|
|
50
|
+
: Platform.OS === "android"
|
|
51
|
+
? config?.android?.package
|
|
52
|
+
: undefined;
|
|
53
|
+
if (applicationId) metadata.applicationId = applicationId;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (keys.has("expoRuntimeVersion") && Constants.expoRuntimeVersion)
|
|
57
|
+
metadata.expoRuntimeVersion = Constants.expoRuntimeVersion;
|
|
58
|
+
if (keys.has("executionEnvironment"))
|
|
59
|
+
metadata.executionEnvironment = Constants.executionEnvironment;
|
|
60
|
+
|
|
61
|
+
return metadata;
|
|
62
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Href } from "expo-router";
|
|
2
|
+
import type { FeedbackRouteNames } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const defaultFeedbackRoutes: FeedbackRouteNames = {
|
|
5
|
+
board: "index",
|
|
6
|
+
entry: "[entryId]",
|
|
7
|
+
create: "new",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function resolveFeedbackRoutes(
|
|
11
|
+
routes: Partial<FeedbackRouteNames> = {},
|
|
12
|
+
): FeedbackRouteNames {
|
|
13
|
+
const resolved = { ...defaultFeedbackRoutes, ...routes };
|
|
14
|
+
|
|
15
|
+
if (!resolved.entry.includes("[entryId]")) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'The routed feedback entry route must contain the "[entryId]" dynamic segment.',
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return resolved;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createFeedbackStackSettings(
|
|
25
|
+
routes?: Partial<FeedbackRouteNames>,
|
|
26
|
+
) {
|
|
27
|
+
return { anchor: resolveFeedbackRoutes(routes).board };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const feedbackStackSettings = createFeedbackStackSettings();
|
|
31
|
+
|
|
32
|
+
/** Anchor the create form beneath suggested-entry detail screens. */
|
|
33
|
+
export const feedbackCreateStackSettings = { anchor: "index" };
|
|
34
|
+
|
|
35
|
+
export function feedbackRouteHref(
|
|
36
|
+
route: string,
|
|
37
|
+
params?: Record<string, string>,
|
|
38
|
+
): Href {
|
|
39
|
+
const pathname = route.startsWith(".") ? route : `./${route}`;
|
|
40
|
+
return params === undefined ? pathname : { pathname, params };
|
|
41
|
+
}
|