convex-feedback-ui 0.1.8 → 0.2.0-beta.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/README.md +30 -1
- 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/FeedbackRoutes.d.ts.map +1 -1
- package/dist/native/expo/FeedbackRoutes.js +17 -9
- package/dist/native/expo/FeedbackRoutes.js.map +1 -1
- 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 +2 -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 +15 -8
- package/dist/native/expo/FeedbackStack.js.map +1 -1
- package/dist/native/expo/FeedbackStackLayout.d.ts +1 -1
- package/dist/native/expo/FeedbackStackLayout.d.ts.map +1 -1
- package/dist/native/expo/FeedbackStackLayout.js +7 -2
- package/dist/native/expo/FeedbackStackLayout.js.map +1 -1
- package/dist/native/expo/index.d.ts +1 -0
- package/dist/native/expo/index.d.ts.map +1 -1
- package/dist/native/expo/index.js +1 -0
- package/dist/native/expo/index.js.map +1 -1
- package/dist/native/expo/types.d.ts +13 -0
- 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 +2 -2
- package/dist/native/native/FeedbackScreen.js.map +1 -1
- package/dist/native/native/index.d.ts +1 -0
- package/dist/native/native/index.d.ts.map +1 -1
- package/dist/native/native/index.js +1 -0
- package/dist/native/native/index.js.map +1 -1
- package/dist/native/shared/helpers.d.ts +6 -1
- package/dist/native/shared/helpers.d.ts.map +1 -1
- package/dist/native/shared/helpers.js +33 -0
- package/dist/native/shared/helpers.js.map +1 -1
- package/dist/native/shared/ui/BodyContent.d.ts.map +1 -1
- package/dist/native/shared/ui/BodyContent.js +8 -2
- package/dist/native/shared/ui/BodyContent.js.map +1 -1
- package/dist/native/shared/ui/Button.d.ts +2 -1
- package/dist/native/shared/ui/Button.d.ts.map +1 -1
- package/dist/native/shared/ui/Button.js +3 -2
- package/dist/native/shared/ui/Button.js.map +1 -1
- package/dist/native/shared/ui/CommentBranch.d.ts.map +1 -1
- package/dist/native/shared/ui/CommentBranch.js +33 -11
- package/dist/native/shared/ui/CommentBranch.js.map +1 -1
- package/dist/native/shared/ui/EntryCard.d.ts.map +1 -1
- package/dist/native/shared/ui/EntryCard.js +13 -1
- package/dist/native/shared/ui/EntryCard.js.map +1 -1
- package/dist/native/shared/ui/EntryDetail.d.ts.map +1 -1
- package/dist/native/shared/ui/EntryDetail.js +23 -5
- package/dist/native/shared/ui/EntryDetail.js.map +1 -1
- package/dist/native/shared/ui/FeedbackScreenHeader.d.ts.map +1 -1
- package/dist/native/shared/ui/FeedbackScreenHeader.js +7 -2
- package/dist/native/shared/ui/FeedbackScreenHeader.js.map +1 -1
- package/dist/native/shared/ui/FeedbackScreenList.d.ts.map +1 -1
- package/dist/native/shared/ui/FeedbackScreenList.js +16 -8
- package/dist/native/shared/ui/FeedbackScreenList.js.map +1 -1
- package/dist/native/shared/ui/NewEntry.d.ts.map +1 -1
- package/dist/native/shared/ui/NewEntry.js +13 -11
- package/dist/native/shared/ui/NewEntry.js.map +1 -1
- package/dist/native/shared/ui/RoadmapScreen.d.ts +3 -0
- package/dist/native/shared/ui/RoadmapScreen.d.ts.map +1 -0
- package/dist/native/shared/ui/RoadmapScreen.js +73 -0
- package/dist/native/shared/ui/RoadmapScreen.js.map +1 -0
- package/dist/react/action.d.ts +14 -0
- package/dist/react/action.d.ts.map +1 -0
- package/dist/react/action.js +38 -0
- package/dist/react/action.js.map +1 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/roadmap.d.ts +3 -0
- package/dist/react/roadmap.d.ts.map +1 -0
- package/dist/react/roadmap.js +59 -0
- package/dist/react/roadmap.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 +99 -42
- package/dist/react/screen.js.map +1 -1
- package/dist/react/styles.css +42 -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 +8 -2
- package/dist/shared/context/FeedbackBodyProvider.js.map +1 -1
- package/dist/shared/helpers.d.ts +6 -0
- package/dist/shared/helpers.d.ts.map +1 -1
- package/dist/shared/helpers.js +12 -0
- package/dist/shared/helpers.js.map +1 -1
- 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 +1 -1
- package/dist/shared/types/messages.d.ts.map +1 -1
- package/dist/shared/types/roadmap.d.ts +17 -0
- package/dist/shared/types/roadmap.d.ts.map +1 -0
- package/dist/shared/types/roadmap.js +2 -0
- package/dist/shared/types/roadmap.js.map +1 -0
- package/dist/shared/types/screen.d.ts +10 -1
- package/dist/shared/types/screen.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/native/expo/ExpoFeedbackBody.tsx +2 -0
- package/src/native/expo/FeedbackRoutes.tsx +54 -30
- package/src/native/expo/FeedbackScreen.tsx +2 -0
- package/src/native/expo/FeedbackStack.tsx +46 -27
- package/src/native/expo/FeedbackStackLayout.tsx +11 -1
- package/src/native/expo/README.md +15 -0
- package/src/native/expo/index.ts +1 -0
- package/src/native/expo/types.ts +12 -0
- package/src/native/native/FeedbackScreen.tsx +2 -0
- package/src/native/native/README.md +4 -0
- package/src/native/native/index.ts +1 -0
- package/src/native/shared/helpers.ts +38 -1
- package/src/native/shared/ui/BodyContent.tsx +15 -2
- package/src/native/shared/ui/Button.tsx +4 -0
- package/src/native/shared/ui/CommentBranch.tsx +58 -15
- package/src/native/shared/ui/EntryCard.tsx +21 -3
- package/src/native/shared/ui/EntryDetail.tsx +42 -8
- package/src/native/shared/ui/FeedbackScreenHeader.tsx +9 -1
- package/src/native/shared/ui/FeedbackScreenList.tsx +28 -13
- package/src/native/shared/ui/NewEntry.tsx +25 -12
- package/src/native/shared/ui/RoadmapScreen.tsx +345 -0
- package/src/react/README.md +1 -1
- package/src/react/action.tsx +56 -0
- package/src/react/index.ts +1 -0
- package/src/react/roadmap.tsx +303 -0
- package/src/react/screen.tsx +199 -75
- package/src/react/styles.css +42 -0
- package/src/shared/context/FeedbackBodyProvider.tsx +9 -1
- package/src/shared/helpers.ts +16 -0
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/messages.ts +1 -1
- package/src/shared/types/roadmap.ts +25 -0
- package/src/shared/types/screen.ts +12 -1
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { Stack, useRouter } from "expo-router";
|
|
2
|
+
import { Fragment } from "react";
|
|
2
3
|
import { useFeedbackBody } from "../../shared/context/FeedbackBodyProvider";
|
|
3
4
|
import { useFeedbackUi } from "../../shared/context/FeedbackProvider";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
allowAuthenticatedAction,
|
|
7
|
+
createEntryLabel,
|
|
8
|
+
entryStatusChoices,
|
|
9
|
+
} from "../../shared/helpers";
|
|
5
10
|
import type { FeedbackStackProps } from "./types";
|
|
6
11
|
|
|
7
12
|
export function FeedbackStack({
|
|
8
13
|
searchRef,
|
|
9
14
|
stackOptions,
|
|
10
15
|
androidToolbarIcons = {},
|
|
16
|
+
BottomToolbarWrapper,
|
|
11
17
|
children,
|
|
12
18
|
}: FeedbackStackProps) {
|
|
13
19
|
const {
|
|
@@ -20,9 +26,12 @@ export function FeedbackStack({
|
|
|
20
26
|
setSelectedEntryId,
|
|
21
27
|
statusFilter,
|
|
22
28
|
setStatusFilter,
|
|
29
|
+
isAuthenticated,
|
|
30
|
+
onUnauthenticated,
|
|
23
31
|
} = useFeedbackBody();
|
|
24
32
|
const { messages, theme } = useFeedbackUi();
|
|
25
33
|
const router = useRouter();
|
|
34
|
+
const ToolbarWrapper = BottomToolbarWrapper ?? Fragment;
|
|
26
35
|
|
|
27
36
|
const canGoBack = !!selectedEntryId || router.canGoBack();
|
|
28
37
|
|
|
@@ -64,7 +73,12 @@ export function FeedbackStack({
|
|
|
64
73
|
}
|
|
65
74
|
variant="prominent"
|
|
66
75
|
accessibilityLabel={createEntryLabel(enabledKinds, messages)}
|
|
67
|
-
|
|
76
|
+
disabled={isAuthenticated === undefined}
|
|
77
|
+
onPress={() => {
|
|
78
|
+
if (allowAuthenticatedAction(isAuthenticated, onUnauthenticated)) {
|
|
79
|
+
setShowForm(true);
|
|
80
|
+
}
|
|
81
|
+
}}
|
|
68
82
|
tintColor={theme.colors.primary}
|
|
69
83
|
>
|
|
70
84
|
{createEntryLabel(enabledKinds, messages)}
|
|
@@ -91,7 +105,10 @@ export function FeedbackStack({
|
|
|
91
105
|
<Stack.SearchBar
|
|
92
106
|
onChangeText={(q) => {
|
|
93
107
|
if (selectedEntryId) setSelectedEntryId(null);
|
|
94
|
-
setQuery(
|
|
108
|
+
setQuery(
|
|
109
|
+
(q as unknown as { nativeEvent: { text: string } }).nativeEvent
|
|
110
|
+
.text,
|
|
111
|
+
);
|
|
95
112
|
}}
|
|
96
113
|
onFocus={() => {
|
|
97
114
|
if (selectedEntryId) setSelectedEntryId(null);
|
|
@@ -104,30 +121,32 @@ export function FeedbackStack({
|
|
|
104
121
|
ref={searchRef}
|
|
105
122
|
textColor={theme.colors.text}
|
|
106
123
|
/>
|
|
107
|
-
<
|
|
108
|
-
<Stack.Toolbar
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
124
|
+
<ToolbarWrapper>
|
|
125
|
+
<Stack.Toolbar placement="bottom">
|
|
126
|
+
<Stack.Toolbar.SearchBarSlot />
|
|
127
|
+
<Stack.Toolbar.Spacer />
|
|
128
|
+
<Stack.Toolbar.Menu
|
|
129
|
+
icon={
|
|
130
|
+
process.env.EXPO_OS === "ios"
|
|
131
|
+
? "line.3.horizontal.decrease"
|
|
132
|
+
: androidToolbarIcons.filter
|
|
133
|
+
}
|
|
134
|
+
title={messages.board.statusFilter}
|
|
135
|
+
accessibilityLabel={messages.board.statusFilter}
|
|
136
|
+
tintColor={theme.colors.text}
|
|
137
|
+
>
|
|
138
|
+
{entryStatusChoices(messages).map(({ value, label }) => (
|
|
139
|
+
<Stack.Toolbar.MenuAction
|
|
140
|
+
key={value}
|
|
141
|
+
isOn={statusFilter === value}
|
|
142
|
+
onPress={() => setStatusFilter(value)}
|
|
143
|
+
>
|
|
144
|
+
{label}
|
|
145
|
+
</Stack.Toolbar.MenuAction>
|
|
146
|
+
))}
|
|
147
|
+
</Stack.Toolbar.Menu>
|
|
148
|
+
</Stack.Toolbar>
|
|
149
|
+
</ToolbarWrapper>
|
|
131
150
|
</>
|
|
132
151
|
);
|
|
133
152
|
}
|
|
@@ -25,12 +25,14 @@ export function FeedbackStackLayout({
|
|
|
25
25
|
loading,
|
|
26
26
|
transformComments,
|
|
27
27
|
renderActor,
|
|
28
|
+
onUnauthenticated,
|
|
28
29
|
routes: routeOverrides,
|
|
29
30
|
screenOptions,
|
|
30
31
|
boardOptions,
|
|
31
32
|
entryOptions,
|
|
32
33
|
createOptions,
|
|
33
34
|
androidToolbarIcons = {},
|
|
35
|
+
BottomToolbarWrapper,
|
|
34
36
|
primaryColor,
|
|
35
37
|
primaryForeground,
|
|
36
38
|
backgroundColor,
|
|
@@ -80,8 +82,16 @@ export function FeedbackStackLayout({
|
|
|
80
82
|
collectStandardMetadata={collectExpoMetadata}
|
|
81
83
|
transformComments={transformComments}
|
|
82
84
|
renderActor={renderActor}
|
|
85
|
+
onUnauthenticated={onUnauthenticated}
|
|
83
86
|
>
|
|
84
|
-
<RoutedFeedbackProvider
|
|
87
|
+
<RoutedFeedbackProvider
|
|
88
|
+
value={{
|
|
89
|
+
routes,
|
|
90
|
+
colors,
|
|
91
|
+
androidToolbarIcons,
|
|
92
|
+
bottomToolbarWrapper: BottomToolbarWrapper,
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
85
95
|
<Stack>
|
|
86
96
|
<Stack.Screen
|
|
87
97
|
name={routes.board}
|
|
@@ -37,5 +37,20 @@ Toolbar actions retain their package-provided SF Symbols on iOS. Pass
|
|
|
37
37
|
`ImageSourcePropType` values for the create, back, close, and filter actions.
|
|
38
38
|
Missing Android images fall back to text labels.
|
|
39
39
|
|
|
40
|
+
> [!WARNING]
|
|
41
|
+
> If the filter toolbar button is intermittently broken, especially on iOS 26+, pass `Host` from `@expo/ui/swift-ui` to `BottomToolbarWrapper`. This wraps every bottom toolbar:
|
|
42
|
+
>
|
|
43
|
+
> ```tsx
|
|
44
|
+
> import { Host } from "@expo/ui/swift-ui";
|
|
45
|
+
>
|
|
46
|
+
> <FeedbackScreen hooks={feedbackHooks} BottomToolbarWrapper={Host} />;
|
|
47
|
+
> ```
|
|
48
|
+
|
|
49
|
+
Use the same prop on `FeedbackStackLayout` for routed navigation.
|
|
50
|
+
|
|
40
51
|
The existing `FeedbackScreen` remains the self-contained API for conditional
|
|
41
52
|
in-screen navigation.
|
|
53
|
+
|
|
54
|
+
`RoadmapScreen` is also exported from this entry point for public roadmap
|
|
55
|
+
browsing. Pass `onEntryOpen` for host navigation and `onUnauthenticated` for
|
|
56
|
+
the host's sign-in UI.
|
package/src/native/expo/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from "../../shared/metadata.js";
|
|
|
5
5
|
export * from "../../shared/theme.js";
|
|
6
6
|
export type * from "../../shared/types/index.js";
|
|
7
7
|
export * from "../shared/ui/primitives.js";
|
|
8
|
+
export * from "../shared/ui/RoadmapScreen.js";
|
|
8
9
|
export * from "./metadata.js";
|
|
9
10
|
export * from "./FeedbackScreen.js";
|
|
10
11
|
export * from "./FeedbackCreateStackLayout.js";
|
package/src/native/expo/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { StackScreenProps } from "expo-router";
|
|
2
|
+
import type { ComponentType, ReactNode } from "react";
|
|
2
3
|
import type { ImageSourcePropType } from "react-native";
|
|
3
4
|
import type { SearchBarCommands } from "react-native-screens";
|
|
4
5
|
import type {
|
|
@@ -6,16 +7,22 @@ import type {
|
|
|
6
7
|
FeedbackScreenRootProps,
|
|
7
8
|
} from "../../shared/types";
|
|
8
9
|
|
|
10
|
+
/** Optional wrapper used around Expo Router bottom toolbars. */
|
|
11
|
+
export type BottomToolbarWrapper = ComponentType<{ children: ReactNode }>;
|
|
12
|
+
|
|
9
13
|
interface FeedbackScreenWithStack {
|
|
10
14
|
useStack?: true;
|
|
11
15
|
StackOptions?: StackScreenProps["options"];
|
|
12
16
|
/** Android image sources for toolbar actions. iOS continues to use SF Symbols. */
|
|
13
17
|
androidToolbarIcons?: FeedbackAndroidToolbarIcons;
|
|
18
|
+
/** Optional component wrapping the bottom search/filter toolbar. */
|
|
19
|
+
BottomToolbarWrapper?: BottomToolbarWrapper;
|
|
14
20
|
}
|
|
15
21
|
interface FeedbackScreenWithoutStack {
|
|
16
22
|
useStack?: false;
|
|
17
23
|
StackOptions?: never;
|
|
18
24
|
androidToolbarIcons?: never;
|
|
25
|
+
BottomToolbarWrapper?: never;
|
|
19
26
|
}
|
|
20
27
|
export type FeedbackScreenStackProps =
|
|
21
28
|
FeedbackScreenWithStack | FeedbackScreenWithoutStack;
|
|
@@ -27,6 +34,8 @@ export interface FeedbackStackProps {
|
|
|
27
34
|
searchRef: React.RefObject<SearchBarCommands | null>;
|
|
28
35
|
stackOptions?: StackScreenProps["options"];
|
|
29
36
|
androidToolbarIcons?: FeedbackAndroidToolbarIcons;
|
|
37
|
+
/** Optional component wrapping the bottom search/filter toolbar. */
|
|
38
|
+
BottomToolbarWrapper?: BottomToolbarWrapper;
|
|
30
39
|
children: React.ReactNode;
|
|
31
40
|
}
|
|
32
41
|
|
|
@@ -69,6 +78,8 @@ export interface FeedbackStackLayoutProps extends FeedbackScreenRootProps {
|
|
|
69
78
|
createOptions?: FeedbackStackScreenOptions;
|
|
70
79
|
/** Android image sources for toolbar actions. iOS continues to use SF Symbols. */
|
|
71
80
|
androidToolbarIcons?: FeedbackAndroidToolbarIcons;
|
|
81
|
+
/** Optional component wrapping the bottom search/filter toolbar. */
|
|
82
|
+
BottomToolbarWrapper?: BottomToolbarWrapper;
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
export interface FeedbackCreateStackLayoutProps {
|
|
@@ -84,4 +95,5 @@ export interface RoutedFeedbackContextValue {
|
|
|
84
95
|
routes: FeedbackRouteNames;
|
|
85
96
|
colors: FeedbackColorProps;
|
|
86
97
|
androidToolbarIcons: FeedbackAndroidToolbarIcons;
|
|
98
|
+
bottomToolbarWrapper?: BottomToolbarWrapper;
|
|
87
99
|
}
|
|
@@ -20,6 +20,7 @@ export function FeedbackScreen({
|
|
|
20
20
|
loading,
|
|
21
21
|
transformComments,
|
|
22
22
|
renderActor,
|
|
23
|
+
onUnauthenticated,
|
|
23
24
|
...props
|
|
24
25
|
}: FeedbackScreenRootProps) {
|
|
25
26
|
return (
|
|
@@ -37,6 +38,7 @@ export function FeedbackScreen({
|
|
|
37
38
|
collectStandardMetadata={collectNativeMetadata}
|
|
38
39
|
transformComments={transformComments}
|
|
39
40
|
renderActor={renderActor}
|
|
41
|
+
onUnauthenticated={onUnauthenticated}
|
|
40
42
|
>
|
|
41
43
|
<FeedbackScreenContent {...props} />
|
|
42
44
|
</FeedbackBodyProvider>
|
|
@@ -15,3 +15,7 @@ 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
|
+
`RoadmapScreen` provides the public roadmap and renders attached entries with
|
|
20
|
+
the same `FeedbackEntry` primitives. Pass `onUnauthenticated` to handle auth
|
|
21
|
+
when an anonymous visitor votes.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Alert, type StyleProp } from "react-native";
|
|
2
3
|
|
|
3
4
|
export function combineStyle<T>(
|
|
4
5
|
enabled: boolean,
|
|
@@ -7,3 +8,39 @@ export function combineStyle<T>(
|
|
|
7
8
|
): StyleProp<T> {
|
|
8
9
|
return enabled ? [fallback, style] : style;
|
|
9
10
|
}
|
|
11
|
+
|
|
12
|
+
function errorMessage(error: unknown): string {
|
|
13
|
+
return error instanceof Error ? error.message : "Something went wrong.";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Runs a native UI mutation and offers a single retry action on failure. */
|
|
17
|
+
export function useNativeAction() {
|
|
18
|
+
const [pending, setPending] = useState(false);
|
|
19
|
+
|
|
20
|
+
const run = async <T>(
|
|
21
|
+
action: () => Promise<T>,
|
|
22
|
+
title: string,
|
|
23
|
+
retryAction: () => Promise<unknown> = action,
|
|
24
|
+
): Promise<T | undefined> => {
|
|
25
|
+
if (pending) return undefined;
|
|
26
|
+
setPending(true);
|
|
27
|
+
try {
|
|
28
|
+
return await action();
|
|
29
|
+
} catch (error) {
|
|
30
|
+
Alert.alert(title, errorMessage(error), [
|
|
31
|
+
{ text: "Cancel", style: "cancel" },
|
|
32
|
+
{
|
|
33
|
+
text: "Retry",
|
|
34
|
+
onPress: () => {
|
|
35
|
+
void run(retryAction, title, retryAction);
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
return undefined;
|
|
40
|
+
} finally {
|
|
41
|
+
setPending(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return { pending, run };
|
|
46
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useFeedbackBody } from "../../../shared/context/FeedbackBodyProvider.js";
|
|
2
2
|
import type { FeedbackScreenContentProps } from "../../../shared/types";
|
|
3
|
+
import { allowAuthenticatedAction } from "../../../shared/helpers.js";
|
|
3
4
|
import { FeedbackScreenHeader } from "./FeedbackScreenHeader";
|
|
4
5
|
import { FeedbackScreenList } from "./FeedbackScreenList";
|
|
5
6
|
import { CreateEntryModal } from "./NewEntry";
|
|
@@ -12,13 +13,25 @@ export function FeedbackScreenContent({
|
|
|
12
13
|
onCreateEntry,
|
|
13
14
|
...colors
|
|
14
15
|
}: FeedbackScreenContentProps) {
|
|
15
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
showForm,
|
|
18
|
+
setShowForm,
|
|
19
|
+
setSelectedEntryId,
|
|
20
|
+
isAuthenticated,
|
|
21
|
+
onUnauthenticated,
|
|
22
|
+
} = useFeedbackBody();
|
|
16
23
|
|
|
17
24
|
const handleEntryOpen = (entryId: string) => {
|
|
18
25
|
setSelectedEntryId(entryId);
|
|
19
26
|
onEntryOpen?.(entryId);
|
|
20
27
|
};
|
|
21
28
|
|
|
29
|
+
const handleCreateEntry = () => {
|
|
30
|
+
if (allowAuthenticatedAction(isAuthenticated, onUnauthenticated)) {
|
|
31
|
+
(onCreateEntry ?? (() => setShowForm(true)))();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
22
35
|
return (
|
|
23
36
|
<>
|
|
24
37
|
<FeedbackBoard.Root {...colors}>
|
|
@@ -27,7 +40,7 @@ export function FeedbackScreenContent({
|
|
|
27
40
|
<FeedbackScreenList
|
|
28
41
|
hideBackButton={hideBackButton}
|
|
29
42
|
onEntryOpen={handleEntryOpen}
|
|
30
|
-
onCreateEntry={
|
|
43
|
+
onCreateEntry={handleCreateEntry}
|
|
31
44
|
/>
|
|
32
45
|
</FeedbackBoard.Root>
|
|
33
46
|
|
|
@@ -5,16 +5,19 @@ export function Button({
|
|
|
5
5
|
label,
|
|
6
6
|
onPress,
|
|
7
7
|
variant = "default",
|
|
8
|
+
disabled = false,
|
|
8
9
|
}: {
|
|
9
10
|
label: string;
|
|
10
11
|
onPress: () => void;
|
|
11
12
|
variant?: "default" | "primary";
|
|
13
|
+
disabled?: boolean;
|
|
12
14
|
}) {
|
|
13
15
|
const { theme } = useFeedbackUi();
|
|
14
16
|
const primary = variant === "primary";
|
|
15
17
|
return (
|
|
16
18
|
<Pressable
|
|
17
19
|
onPress={onPress}
|
|
20
|
+
disabled={disabled}
|
|
18
21
|
style={{
|
|
19
22
|
alignSelf: primary ? "center" : "flex-start",
|
|
20
23
|
paddingVertical: 8,
|
|
@@ -23,6 +26,7 @@ export function Button({
|
|
|
23
26
|
borderWidth: 1,
|
|
24
27
|
borderColor: primary ? theme.colors.primary : theme.colors.border,
|
|
25
28
|
backgroundColor: primary ? theme.colors.primary : undefined,
|
|
29
|
+
opacity: disabled ? 0.55 : 1,
|
|
26
30
|
}}
|
|
27
31
|
>
|
|
28
32
|
<Text
|
|
@@ -3,9 +3,11 @@ import { Text, View } from "react-native";
|
|
|
3
3
|
|
|
4
4
|
import { useFeedbackBody } from "../../../shared/context/FeedbackBodyProvider.js";
|
|
5
5
|
import { useFeedbackUi } from "../../../shared/context/FeedbackProvider.js";
|
|
6
|
+
import { allowAuthenticatedAction } from "../../../shared/helpers.js";
|
|
6
7
|
import type { FeedbackScreenCommentBranchProps } from "../../../shared/types";
|
|
7
8
|
import { Button } from "./Button";
|
|
8
9
|
import { Comment, FeedbackForm } from "./primitives.js";
|
|
10
|
+
import { useNativeAction } from "../helpers.js";
|
|
9
11
|
|
|
10
12
|
import type { FeedbackScreenReplyListProps } from "../../../shared/types";
|
|
11
13
|
|
|
@@ -32,9 +34,11 @@ export function ReplyList({
|
|
|
32
34
|
{visible.map((reply) => (
|
|
33
35
|
<CommentBranch key={reply.id} comment={reply} entryId={entryId} />
|
|
34
36
|
))}
|
|
35
|
-
{replies.status === "CanLoadMore"
|
|
37
|
+
{(replies.status === "CanLoadMore" ||
|
|
38
|
+
replies.status === "LoadingMore") && (
|
|
36
39
|
<Button
|
|
37
40
|
label={messages.comments.loadMore}
|
|
41
|
+
disabled={replies.status === "LoadingMore"}
|
|
38
42
|
onPress={() => replies.loadMore(hooks.pageSizes.replies)}
|
|
39
43
|
/>
|
|
40
44
|
)}
|
|
@@ -46,13 +50,34 @@ export function CommentBranch({
|
|
|
46
50
|
comment,
|
|
47
51
|
entryId,
|
|
48
52
|
}: FeedbackScreenCommentBranchProps) {
|
|
49
|
-
const {
|
|
53
|
+
const {
|
|
54
|
+
hooks,
|
|
55
|
+
maxCommentDepth,
|
|
56
|
+
renderActor,
|
|
57
|
+
isAuthenticated,
|
|
58
|
+
onUnauthenticated,
|
|
59
|
+
} = useFeedbackBody();
|
|
50
60
|
const { messages, theme } = useFeedbackUi();
|
|
51
61
|
const [expanded, setExpanded] = useState(false);
|
|
52
62
|
const [replying, setReplying] = useState(false);
|
|
53
63
|
const [replyBody, setReplyBody] = useState("");
|
|
54
64
|
const setLike = hooks.useSetCommentLike();
|
|
55
65
|
const createComment = hooks.useCreateComment();
|
|
66
|
+
const likeAction = useNativeAction();
|
|
67
|
+
const replyAction = useNativeAction();
|
|
68
|
+
|
|
69
|
+
const toggleLike = (desiredState: boolean) => {
|
|
70
|
+
if (
|
|
71
|
+
!allowAuthenticatedAction(isAuthenticated, onUnauthenticated) ||
|
|
72
|
+
likeAction.pending
|
|
73
|
+
) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
void likeAction.run(
|
|
77
|
+
() => setLike({ commentId: comment.id, desiredState }),
|
|
78
|
+
"Could not update like",
|
|
79
|
+
);
|
|
80
|
+
};
|
|
56
81
|
|
|
57
82
|
return (
|
|
58
83
|
<Comment.Root comment={comment}>
|
|
@@ -69,12 +94,20 @@ export function CommentBranch({
|
|
|
69
94
|
}}
|
|
70
95
|
>
|
|
71
96
|
<Comment.Like
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
97
|
+
disabled={isAuthenticated === undefined || likeAction.pending}
|
|
98
|
+
onToggle={toggleLike}
|
|
75
99
|
/>
|
|
76
100
|
{comment.body !== null && comment.depth < maxCommentDepth ? (
|
|
77
|
-
<Comment.Reply
|
|
101
|
+
<Comment.Reply
|
|
102
|
+
disabled={isAuthenticated === undefined || replyAction.pending}
|
|
103
|
+
onActivate={() => {
|
|
104
|
+
if (
|
|
105
|
+
allowAuthenticatedAction(isAuthenticated, onUnauthenticated)
|
|
106
|
+
) {
|
|
107
|
+
setReplying((value) => !value);
|
|
108
|
+
}
|
|
109
|
+
}}
|
|
110
|
+
/>
|
|
78
111
|
) : null}
|
|
79
112
|
<Comment.RepliesButton
|
|
80
113
|
expanded={expanded}
|
|
@@ -89,16 +122,26 @@ export function CommentBranch({
|
|
|
89
122
|
placeholder={messages.comments.placeholder}
|
|
90
123
|
/>
|
|
91
124
|
<FeedbackForm.Submit
|
|
92
|
-
|
|
125
|
+
disabled={isAuthenticated === undefined || replyAction.pending}
|
|
126
|
+
submitting={replyAction.pending}
|
|
127
|
+
onPress={() => {
|
|
93
128
|
if (replyBody.trim().length === 0) return;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
129
|
+
if (
|
|
130
|
+
!allowAuthenticatedAction(isAuthenticated, onUnauthenticated) ||
|
|
131
|
+
replyAction.pending
|
|
132
|
+
) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
void replyAction.run(async () => {
|
|
136
|
+
await createComment({
|
|
137
|
+
entryId,
|
|
138
|
+
parentCommentId: comment.id,
|
|
139
|
+
body: replyBody,
|
|
140
|
+
});
|
|
141
|
+
setReplyBody("");
|
|
142
|
+
setReplying(false);
|
|
143
|
+
setExpanded(true);
|
|
144
|
+
}, "Could not add reply");
|
|
102
145
|
}}
|
|
103
146
|
>
|
|
104
147
|
{messages.comments.reply}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Pressable, View } from "react-native";
|
|
2
2
|
|
|
3
3
|
import type { FeedbackScreenEntryCardProps } from "../../../shared/types";
|
|
4
|
+
import { allowAuthenticatedAction } from "../../../shared/helpers.js";
|
|
5
|
+
import { useFeedbackBody } from "../../../shared/context/FeedbackBodyProvider.js";
|
|
6
|
+
import { useNativeAction } from "../helpers.js";
|
|
4
7
|
import { FeedbackEntry } from "./primitives";
|
|
5
8
|
|
|
6
9
|
export function EntryCard({
|
|
@@ -8,13 +11,28 @@ export function EntryCard({
|
|
|
8
11
|
hooks,
|
|
9
12
|
onOpen,
|
|
10
13
|
}: FeedbackScreenEntryCardProps) {
|
|
14
|
+
const { isAuthenticated, onUnauthenticated } = useFeedbackBody();
|
|
11
15
|
const setUpvote = hooks.useSetEntryUpvote();
|
|
16
|
+
const action = useNativeAction();
|
|
17
|
+
|
|
18
|
+
const toggleUpvote = (desiredState: boolean) => {
|
|
19
|
+
if (
|
|
20
|
+
!allowAuthenticatedAction(isAuthenticated, onUnauthenticated) ||
|
|
21
|
+
action.pending
|
|
22
|
+
) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
void action.run(
|
|
26
|
+
() => setUpvote({ entryId: entry.id, desiredState }),
|
|
27
|
+
"Could not update vote",
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
12
31
|
return (
|
|
13
32
|
<FeedbackEntry.Root entry={entry}>
|
|
14
33
|
<FeedbackEntry.Upvote
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
34
|
+
disabled={isAuthenticated === undefined || action.pending}
|
|
35
|
+
onToggle={toggleUpvote}
|
|
18
36
|
/>
|
|
19
37
|
<Pressable onPress={onOpen} style={{ flex: 1, gap: 4 }}>
|
|
20
38
|
<View
|
|
@@ -3,18 +3,26 @@ import { ActivityIndicator, Text, View } from "react-native";
|
|
|
3
3
|
|
|
4
4
|
import { useFeedbackBody } from "../../../shared/context/FeedbackBodyProvider";
|
|
5
5
|
import { useFeedbackUi } from "../../../shared/context/FeedbackProvider";
|
|
6
|
+
import { allowAuthenticatedAction } from "../../../shared/helpers.js";
|
|
6
7
|
import type { FeedbackScreenEntryDetailProps } from "../../../shared/types";
|
|
7
8
|
import { Button } from "./Button";
|
|
8
9
|
import { CommentBranch } from "./CommentBranch";
|
|
9
10
|
import { FeedbackEntry, FeedbackForm } from "./primitives";
|
|
10
11
|
import { MetadataModal } from "./MetadataModal";
|
|
12
|
+
import { useNativeAction } from "../helpers.js";
|
|
11
13
|
|
|
12
14
|
export function EntryDetail({
|
|
13
15
|
entryId,
|
|
14
16
|
onBack,
|
|
15
17
|
hideBackButton,
|
|
16
18
|
}: FeedbackScreenEntryDetailProps) {
|
|
17
|
-
const {
|
|
19
|
+
const {
|
|
20
|
+
hooks,
|
|
21
|
+
commentSort,
|
|
22
|
+
transformComments,
|
|
23
|
+
isAuthenticated,
|
|
24
|
+
onUnauthenticated,
|
|
25
|
+
} = useFeedbackBody();
|
|
18
26
|
|
|
19
27
|
const { messages, theme } = useFeedbackUi();
|
|
20
28
|
const entry = hooks.useEntry(entryId);
|
|
@@ -23,6 +31,21 @@ export function EntryDetail({
|
|
|
23
31
|
const createComment = hooks.useCreateComment();
|
|
24
32
|
const [body, setBody] = useState("");
|
|
25
33
|
const [showMetadata, setShowMetadata] = useState(false);
|
|
34
|
+
const upvoteAction = useNativeAction();
|
|
35
|
+
const commentAction = useNativeAction();
|
|
36
|
+
|
|
37
|
+
const toggleUpvote = (desiredState: boolean) => {
|
|
38
|
+
if (
|
|
39
|
+
!allowAuthenticatedAction(isAuthenticated, onUnauthenticated) ||
|
|
40
|
+
upvoteAction.pending
|
|
41
|
+
) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
void upvoteAction.run(
|
|
45
|
+
() => setUpvote({ entryId, desiredState }),
|
|
46
|
+
"Could not update vote",
|
|
47
|
+
);
|
|
48
|
+
};
|
|
26
49
|
const visible = useMemo(
|
|
27
50
|
() => transformComments?.(comments.results) ?? comments.results,
|
|
28
51
|
[comments.results, transformComments],
|
|
@@ -57,9 +80,8 @@ export function EntryDetail({
|
|
|
57
80
|
)}
|
|
58
81
|
<FeedbackEntry.Root entry={entry}>
|
|
59
82
|
<FeedbackEntry.Upvote
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
83
|
+
disabled={isAuthenticated === undefined || upvoteAction.pending}
|
|
84
|
+
onToggle={toggleUpvote}
|
|
63
85
|
/>
|
|
64
86
|
<FeedbackEntry.Content style={{ gap: 5 }}>
|
|
65
87
|
<FeedbackEntry.Status />
|
|
@@ -92,10 +114,20 @@ export function EntryDetail({
|
|
|
92
114
|
placeholder={messages.comments.placeholder}
|
|
93
115
|
/>
|
|
94
116
|
<FeedbackForm.Submit
|
|
95
|
-
|
|
117
|
+
disabled={isAuthenticated === undefined || commentAction.pending}
|
|
118
|
+
submitting={commentAction.pending}
|
|
119
|
+
onPress={() => {
|
|
96
120
|
if (body.trim().length === 0) return;
|
|
97
|
-
|
|
98
|
-
|
|
121
|
+
if (
|
|
122
|
+
!allowAuthenticatedAction(isAuthenticated, onUnauthenticated) ||
|
|
123
|
+
commentAction.pending
|
|
124
|
+
) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
void commentAction.run(async () => {
|
|
128
|
+
await createComment({ entryId, body });
|
|
129
|
+
setBody("");
|
|
130
|
+
}, "Could not add comment");
|
|
99
131
|
}}
|
|
100
132
|
>
|
|
101
133
|
{messages.comments.submit}
|
|
@@ -106,9 +138,11 @@ export function EntryDetail({
|
|
|
106
138
|
<CommentBranch key={comment.id} comment={comment} entryId={entryId} />
|
|
107
139
|
))}
|
|
108
140
|
</View>
|
|
109
|
-
{comments.status === "CanLoadMore"
|
|
141
|
+
{(comments.status === "CanLoadMore" ||
|
|
142
|
+
comments.status === "LoadingMore") && (
|
|
110
143
|
<Button
|
|
111
144
|
label={messages.comments.loadMore}
|
|
145
|
+
disabled={comments.status === "LoadingMore"}
|
|
112
146
|
onPress={() => comments.loadMore(hooks.pageSizes.comments)}
|
|
113
147
|
/>
|
|
114
148
|
)}
|
|
@@ -3,6 +3,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
|
3
3
|
import { useFeedbackBody } from "../../../shared/context/FeedbackBodyProvider";
|
|
4
4
|
import { useFeedbackUi } from "../../../shared/context/FeedbackProvider";
|
|
5
5
|
import { createEntryLabel, entryStatusChoices } from "../../../shared/helpers";
|
|
6
|
+
import { allowAuthenticatedAction } from "../../../shared/helpers";
|
|
6
7
|
import { ChoiceChips, FeedbackBoard } from "./primitives";
|
|
7
8
|
|
|
8
9
|
export function FeedbackScreenHeader() {
|
|
@@ -14,6 +15,8 @@ export function FeedbackScreenHeader() {
|
|
|
14
15
|
enabledKinds,
|
|
15
16
|
statusFilter,
|
|
16
17
|
setStatusFilter,
|
|
18
|
+
isAuthenticated,
|
|
19
|
+
onUnauthenticated,
|
|
17
20
|
} = useFeedbackBody();
|
|
18
21
|
const { messages, theme } = useFeedbackUi();
|
|
19
22
|
const { top } = useSafeAreaInsets();
|
|
@@ -36,7 +39,12 @@ export function FeedbackScreenHeader() {
|
|
|
36
39
|
onValueChange={setStatusFilter}
|
|
37
40
|
/>
|
|
38
41
|
<Pressable
|
|
39
|
-
|
|
42
|
+
disabled={isAuthenticated === undefined}
|
|
43
|
+
onPress={() => {
|
|
44
|
+
if (allowAuthenticatedAction(isAuthenticated, onUnauthenticated)) {
|
|
45
|
+
setShowForm(true);
|
|
46
|
+
}
|
|
47
|
+
}}
|
|
40
48
|
style={{
|
|
41
49
|
alignSelf: "flex-start",
|
|
42
50
|
paddingVertical: 9,
|