convex-feedback-ui 0.1.3 → 0.1.5

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 (104) hide show
  1. package/README.md +53 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/native/expo/FeedbackScreen.d.ts +1 -1
  4. package/dist/native/expo/FeedbackScreen.d.ts.map +1 -1
  5. package/dist/native/expo/FeedbackScreen.js +3 -2
  6. package/dist/native/expo/FeedbackScreen.js.map +1 -1
  7. package/dist/native/expo/index.d.ts +2 -0
  8. package/dist/native/expo/index.d.ts.map +1 -1
  9. package/dist/native/expo/index.js +2 -0
  10. package/dist/native/expo/index.js.map +1 -1
  11. package/dist/native/expo/metadata.d.ts +4 -0
  12. package/dist/native/expo/metadata.d.ts.map +1 -0
  13. package/dist/native/expo/metadata.js +44 -0
  14. package/dist/native/expo/metadata.js.map +1 -0
  15. package/dist/native/native/FeedbackScreen.d.ts +1 -1
  16. package/dist/native/native/FeedbackScreen.d.ts.map +1 -1
  17. package/dist/native/native/FeedbackScreen.js +3 -2
  18. package/dist/native/native/FeedbackScreen.js.map +1 -1
  19. package/dist/native/native/index.d.ts +2 -0
  20. package/dist/native/native/index.d.ts.map +1 -1
  21. package/dist/native/native/index.js +2 -0
  22. package/dist/native/native/index.js.map +1 -1
  23. package/dist/native/shared/metadata.d.ts +5 -0
  24. package/dist/native/shared/metadata.d.ts.map +1 -0
  25. package/dist/native/shared/metadata.js +31 -0
  26. package/dist/native/shared/metadata.js.map +1 -0
  27. package/dist/native/shared/ui/CommentBranch.d.ts +2 -0
  28. package/dist/native/shared/ui/CommentBranch.d.ts.map +1 -1
  29. package/dist/native/shared/ui/CommentBranch.js +12 -2
  30. package/dist/native/shared/ui/CommentBranch.js.map +1 -1
  31. package/dist/native/shared/ui/EntryDetail.d.ts.map +1 -1
  32. package/dist/native/shared/ui/EntryDetail.js +3 -1
  33. package/dist/native/shared/ui/EntryDetail.js.map +1 -1
  34. package/dist/native/shared/ui/FeedbackScreenHeader.d.ts.map +1 -1
  35. package/dist/native/shared/ui/FeedbackScreenHeader.js +10 -8
  36. package/dist/native/shared/ui/FeedbackScreenHeader.js.map +1 -1
  37. package/dist/native/shared/ui/FeedbackScreenList.js +1 -1
  38. package/dist/native/shared/ui/FeedbackScreenList.js.map +1 -1
  39. package/dist/native/shared/ui/MetadataModal.d.ts +6 -0
  40. package/dist/native/shared/ui/MetadataModal.d.ts.map +1 -0
  41. package/dist/native/shared/ui/MetadataModal.js +39 -0
  42. package/dist/native/shared/ui/MetadataModal.js.map +1 -0
  43. package/dist/native/shared/ui/NewEntry.d.ts.map +1 -1
  44. package/dist/native/shared/ui/NewEntry.js +10 -3
  45. package/dist/native/shared/ui/NewEntry.js.map +1 -1
  46. package/dist/react/index.d.ts +2 -0
  47. package/dist/react/index.d.ts.map +1 -1
  48. package/dist/react/index.js +2 -0
  49. package/dist/react/index.js.map +1 -1
  50. package/dist/react/metadata.d.ts +4 -0
  51. package/dist/react/metadata.d.ts.map +1 -0
  52. package/dist/react/metadata.js +40 -0
  53. package/dist/react/metadata.js.map +1 -0
  54. package/dist/react/screen.d.ts +1 -1
  55. package/dist/react/screen.d.ts.map +1 -1
  56. package/dist/react/screen.js +18 -4
  57. package/dist/react/screen.js.map +1 -1
  58. package/dist/react/styles.css +35 -0
  59. package/dist/shared/context/FeedbackBodyProvider.d.ts +1 -1
  60. package/dist/shared/context/FeedbackBodyProvider.d.ts.map +1 -1
  61. package/dist/shared/context/FeedbackBodyProvider.js +5 -1
  62. package/dist/shared/context/FeedbackBodyProvider.js.map +1 -1
  63. package/dist/shared/messages.d.ts.map +1 -1
  64. package/dist/shared/messages.js +9 -0
  65. package/dist/shared/messages.js.map +1 -1
  66. package/dist/shared/metadata.d.ts +6 -0
  67. package/dist/shared/metadata.d.ts.map +1 -0
  68. package/dist/shared/metadata.js +54 -0
  69. package/dist/shared/metadata.js.map +1 -0
  70. package/dist/shared/types/index.d.ts +1 -0
  71. package/dist/shared/types/index.d.ts.map +1 -1
  72. package/dist/shared/types/messages.d.ts +17 -0
  73. package/dist/shared/types/messages.d.ts.map +1 -1
  74. package/dist/shared/types/metadata.d.ts +22 -0
  75. package/dist/shared/types/metadata.d.ts.map +1 -0
  76. package/dist/shared/types/metadata.js +2 -0
  77. package/dist/shared/types/metadata.js.map +1 -0
  78. package/dist/shared/types/screen.d.ts +10 -1
  79. package/dist/shared/types/screen.d.ts.map +1 -1
  80. package/package.json +6 -2
  81. package/src/native/expo/FeedbackScreen.tsx +4 -0
  82. package/src/native/expo/index.ts +2 -0
  83. package/src/native/expo/metadata.ts +62 -0
  84. package/src/native/native/FeedbackScreen.tsx +4 -0
  85. package/src/native/native/index.ts +2 -0
  86. package/src/native/shared/metadata.ts +39 -0
  87. package/src/native/shared/ui/CommentBranch.tsx +36 -2
  88. package/src/native/shared/ui/EntryDetail.tsx +14 -0
  89. package/src/native/shared/ui/FeedbackScreenHeader.tsx +23 -21
  90. package/src/native/shared/ui/FeedbackScreenList.tsx +1 -1
  91. package/src/native/shared/ui/MetadataModal.tsx +112 -0
  92. package/src/native/shared/ui/NewEntry.tsx +17 -3
  93. package/src/react/index.ts +2 -0
  94. package/src/react/metadata.ts +51 -0
  95. package/src/react/screen.tsx +89 -2
  96. package/src/react/styles.css +35 -0
  97. package/src/shared/context/FeedbackBodyProvider.tsx +8 -0
  98. package/src/shared/messages.ts +9 -0
  99. package/src/shared/metadata.ts +84 -0
  100. package/src/shared/types/index.ts +1 -0
  101. package/src/shared/types/messages.ts +24 -0
  102. package/src/shared/types/metadata.ts +55 -0
  103. package/src/shared/types/screen.ts +13 -2
  104. package/src/native/shared/ui/ReplyList.tsx +0 -41
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "convex-feedback-ui",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Customizable React and React Native UI primitives for convex-feedback.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -36,12 +36,13 @@
36
36
  "prepack": "npm run build"
37
37
  },
38
38
  "peerDependencies": {
39
- "convex-feedback": "^0.1.0",
39
+ "convex-feedback": "^0.1.4",
40
40
  "react": ">=18.2.0",
41
41
  "react-native": ">=0.83.0",
42
42
  "react-native-safe-area-context": ">=5.4.0",
43
43
  "react-native-screens": ">=4.23.0",
44
44
  "expo": ">=55.0.0",
45
+ "expo-constants": ">=55.0.0",
45
46
  "expo-router": ">=55.0.0"
46
47
  },
47
48
  "peerDependenciesMeta": {
@@ -57,6 +58,9 @@
57
58
  "expo": {
58
59
  "optional": true
59
60
  },
61
+ "expo-constants": {
62
+ "optional": true
63
+ },
60
64
  "expo-router": {
61
65
  "optional": true
62
66
  }
@@ -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
  >
@@ -1,8 +1,10 @@
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";
8
10
  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
+ }
@@ -3,6 +3,7 @@ import { FeedbackProvider } from "../../shared/context/FeedbackProvider";
3
3
  import { kinds } from "../../shared/helpers";
4
4
  import type { FeedbackScreenRootProps } from "../../shared/types";
5
5
  import { FeedbackScreenContent } from "../shared/ui/BodyContent";
6
+ import { collectNativeMetadata } from "../shared/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={collectNativeMetadata}
30
34
  transformComments={transformComments}
31
35
  renderActor={renderActor}
32
36
  >
@@ -1,7 +1,9 @@
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 "../shared/metadata.js";
7
9
  export * from "./FeedbackScreen.js";
@@ -0,0 +1,39 @@
1
+ import type { FeedbackMetadataValue } from "convex-feedback";
2
+ import { Dimensions, PixelRatio, Platform } from "react-native";
3
+
4
+ import type {
5
+ StandardMetadataKey,
6
+ EnabledStandardMetadataSelection,
7
+ } from "../../shared/types/metadata.js";
8
+
9
+ export const defaultNativeMetadataKeys: readonly StandardMetadataKey[] = [
10
+ "platform",
11
+ "osVersion",
12
+ "deviceModel",
13
+ "screenWidth",
14
+ "screenHeight",
15
+ "pixelRatio",
16
+ "fontScale",
17
+ ];
18
+
19
+ export function collectNativeMetadata(
20
+ selection: EnabledStandardMetadataSelection,
21
+ ): Partial<Record<StandardMetadataKey, FeedbackMetadataValue>> {
22
+ const keys = new Set(
23
+ selection === true ? defaultNativeMetadataKeys : selection,
24
+ );
25
+ const metadata: Partial<Record<StandardMetadataKey, FeedbackMetadataValue>> =
26
+ {};
27
+ const screen = Dimensions.get("screen");
28
+
29
+ if (keys.has("platform")) metadata.platform = Platform.OS;
30
+ if (keys.has("osVersion")) metadata.osVersion = Platform.Version;
31
+ if (keys.has("deviceModel") && Platform.OS === "android")
32
+ metadata.deviceModel = Platform.constants.Model;
33
+ if (keys.has("screenWidth")) metadata.screenWidth = screen.width;
34
+ if (keys.has("screenHeight")) metadata.screenHeight = screen.height;
35
+ if (keys.has("pixelRatio")) metadata.pixelRatio = PixelRatio.get();
36
+ if (keys.has("fontScale")) metadata.fontScale = PixelRatio.getFontScale();
37
+
38
+ return metadata;
39
+ }
@@ -1,13 +1,47 @@
1
- import { useState } from "react";
1
+ import { useMemo, useState } from "react";
2
2
  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
6
  import type { FeedbackScreenCommentBranchProps } from "../../../shared/types";
7
7
  import { Button } from "./Button";
8
- import { ReplyList } from "./ReplyList";
9
8
  import { Comment, FeedbackForm } from "./primitives.js";
10
9
 
10
+ import type { FeedbackScreenReplyListProps } from "../../../shared/types";
11
+
12
+ export function ReplyList({
13
+ entryId,
14
+ parentCommentId,
15
+ }: FeedbackScreenReplyListProps) {
16
+ const { hooks, commentSort, transformComments } = useFeedbackBody();
17
+ const { messages } = useFeedbackUi();
18
+
19
+ const replies = hooks.useComments({
20
+ entryId,
21
+ parentCommentId,
22
+ sort: commentSort,
23
+ });
24
+
25
+ const visible = useMemo(
26
+ () => transformComments?.(replies.results) ?? replies.results,
27
+ [replies.results, transformComments],
28
+ );
29
+
30
+ return (
31
+ <Comment.Children>
32
+ {visible.map((reply) => (
33
+ <CommentBranch key={reply.id} comment={reply} entryId={entryId} />
34
+ ))}
35
+ {replies.status === "CanLoadMore" && (
36
+ <Button
37
+ label={messages.comments.loadMore}
38
+ onPress={() => replies.loadMore(hooks.pageSizes.replies)}
39
+ />
40
+ )}
41
+ </Comment.Children>
42
+ );
43
+ }
44
+
11
45
  export function CommentBranch({
12
46
  comment,
13
47
  entryId,
@@ -7,6 +7,7 @@ import type { FeedbackScreenEntryDetailProps } from "../../../shared/types";
7
7
  import { Button } from "./Button";
8
8
  import { CommentBranch } from "./CommentBranch";
9
9
  import { FeedbackEntry, FeedbackForm } from "./primitives";
10
+ import { MetadataModal } from "./MetadataModal";
10
11
 
11
12
  export function EntryDetail({
12
13
  entryId,
@@ -21,6 +22,7 @@ export function EntryDetail({
21
22
  const setUpvote = hooks.useSetEntryUpvote();
22
23
  const createComment = hooks.useCreateComment();
23
24
  const [body, setBody] = useState("");
25
+ const [showMetadata, setShowMetadata] = useState(false);
24
26
  const visible = useMemo(
25
27
  () => transformComments?.(comments.results) ?? comments.results,
26
28
  [comments.results, transformComments],
@@ -66,6 +68,18 @@ export function EntryDetail({
66
68
  <FeedbackEntry.CommentCount />
67
69
  </FeedbackEntry.Content>
68
70
  </FeedbackEntry.Root>
71
+ {entry.metadata !== undefined && (
72
+ <Button
73
+ label={messages.metadata.view}
74
+ onPress={() => setShowMetadata(true)}
75
+ />
76
+ )}
77
+ {showMetadata && entry.metadata !== undefined && (
78
+ <MetadataModal
79
+ metadata={entry.metadata}
80
+ onRequestClose={() => setShowMetadata(false)}
81
+ />
82
+ )}
69
83
  <Text
70
84
  style={{ color: theme.colors.text, fontSize: 18, fontWeight: "700" }}
71
85
  >
@@ -1,4 +1,5 @@
1
1
  import { Pressable, Text } from "react-native";
2
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
2
3
  import { useFeedbackBody } from "../../../shared/context/FeedbackBodyProvider";
3
4
  import { useFeedbackUi } from "../../../shared/context/FeedbackProvider";
4
5
  import { FeedbackBoard } from "./primitives";
@@ -6,6 +7,7 @@ import { FeedbackBoard } from "./primitives";
6
7
  export function FeedbackScreenHeader() {
7
8
  const { query, setShowForm, setQuery, enabledKinds } = useFeedbackBody();
8
9
  const { messages, theme } = useFeedbackUi();
10
+ const { top } = useSafeAreaInsets();
9
11
 
10
12
  const pageTitle =
11
13
  enabledKinds.length === 1
@@ -13,28 +15,28 @@ export function FeedbackScreenHeader() {
13
15
  : messages.board.title;
14
16
 
15
17
  return (
16
- <>
17
- <FeedbackBoard.Header>
18
- <FeedbackBoard.Title>{pageTitle}</FeedbackBoard.Title>
19
- <Text style={{ color: theme.colors.mutedText }}>
20
- {messages.board.subtitle}
21
- </Text>
22
- <Pressable
23
- onPress={() => setShowForm(true)}
24
- style={{
25
- alignSelf: "flex-start",
26
- paddingVertical: 9,
27
- paddingHorizontal: 12,
28
- borderRadius: 9,
29
- backgroundColor: theme.colors.primary,
30
- }}
18
+ <FeedbackBoard.Header style={{ padding: theme.spacing, paddingTop: top }}>
19
+ <FeedbackBoard.Title>{pageTitle}</FeedbackBoard.Title>
20
+ <Text style={{ color: theme.colors.mutedText }}>
21
+ {messages.board.subtitle}
22
+ </Text>
23
+ <Pressable
24
+ onPress={() => setShowForm(true)}
25
+ style={{
26
+ alignSelf: "flex-start",
27
+ paddingVertical: 9,
28
+ paddingHorizontal: 12,
29
+ borderRadius: 9,
30
+ backgroundColor: theme.colors.primary,
31
+ }}
32
+ >
33
+ <Text
34
+ style={{ color: theme.colors.primaryForeground, fontWeight: "700" }}
31
35
  >
32
- <Text style={{ color: theme.colors.text, fontWeight: "700" }}>
33
- {messages.board.createEntry}
34
- </Text>
35
- </Pressable>
36
- </FeedbackBoard.Header>
36
+ {messages.board.createEntry}
37
+ </Text>
38
+ </Pressable>
37
39
  <FeedbackBoard.Search value={query} onValueChange={(q) => setQuery(q)} />
38
- </>
40
+ </FeedbackBoard.Header>
39
41
  );
40
42
  }
@@ -31,7 +31,7 @@ export function FeedbackScreenList({
31
31
 
32
32
  return (
33
33
  <>
34
- <FeedbackBoard.List style={{ paddingHorizontal: theme.spacing }}>
34
+ <FeedbackBoard.List style={{ padding: theme.spacing }}>
35
35
  {selectedEntryId ? (
36
36
  <EntryDetail
37
37
  entryId={selectedEntryId}
@@ -0,0 +1,112 @@
1
+ import type { FeedbackMetadata } from "convex-feedback";
2
+ import { Modal, Pressable, ScrollView, Text, View } from "react-native";
3
+
4
+ import { useFeedbackUi } from "../../../shared/context/FeedbackProvider";
5
+ import { formatMetadataKey } from "../../../shared/metadata";
6
+ import { Button } from "./Button";
7
+
8
+ export function MetadataModal({
9
+ metadata,
10
+ onRequestClose,
11
+ }: {
12
+ metadata: FeedbackMetadata;
13
+ onRequestClose: () => void;
14
+ }) {
15
+ const { messages, theme } = useFeedbackUi();
16
+ const sections = [
17
+ [messages.metadata.standard, metadata.standard],
18
+ [messages.metadata.additional, metadata.additional],
19
+ ] as const;
20
+ const hasValues = sections.some(
21
+ ([, values]) => values !== undefined && Object.keys(values).length > 0,
22
+ );
23
+
24
+ return (
25
+ <Modal
26
+ visible
27
+ transparent
28
+ animationType="fade"
29
+ onRequestClose={onRequestClose}
30
+ role="dialog"
31
+ >
32
+ <Pressable
33
+ accessible={false}
34
+ focusable={false}
35
+ onPress={onRequestClose}
36
+ style={{
37
+ flex: 1,
38
+ justifyContent: "center",
39
+ padding: 20,
40
+ backgroundColor: "rgba(0, 0, 0, 0.4)",
41
+ }}
42
+ >
43
+ <Pressable
44
+ accessible={false}
45
+ focusable={false}
46
+ onPress={(event) => event.stopPropagation()}
47
+ style={{
48
+ maxHeight: "80%",
49
+ gap: 14,
50
+ padding: 18,
51
+ borderRadius: theme.radius,
52
+ backgroundColor: theme.colors.surface,
53
+ }}
54
+ >
55
+ <Text
56
+ style={{
57
+ color: theme.colors.text,
58
+ fontSize: 18,
59
+ fontWeight: "700",
60
+ }}
61
+ >
62
+ {messages.metadata.title}
63
+ </Text>
64
+ <ScrollView contentContainerStyle={{ gap: 16 }}>
65
+ {hasValues ? (
66
+ sections.map(([label, values]) =>
67
+ values === undefined ||
68
+ Object.keys(values).length === 0 ? null : (
69
+ <View key={label} style={{ gap: 8 }}>
70
+ <Text
71
+ style={{ color: theme.colors.text, fontWeight: "700" }}
72
+ >
73
+ {label}
74
+ </Text>
75
+ {Object.entries(values).map(([key, value]) => (
76
+ <View
77
+ key={key}
78
+ style={{
79
+ gap: 3,
80
+ paddingBottom: 8,
81
+ borderBottomWidth: 1,
82
+ borderBottomColor: theme.colors.border,
83
+ }}
84
+ >
85
+ <Text
86
+ style={{
87
+ color: theme.colors.mutedText,
88
+ fontWeight: "600",
89
+ }}
90
+ >
91
+ {formatMetadataKey(key)}
92
+ </Text>
93
+ <Text selectable style={{ color: theme.colors.text }}>
94
+ {String(value)}
95
+ </Text>
96
+ </View>
97
+ ))}
98
+ </View>
99
+ ),
100
+ )
101
+ ) : (
102
+ <Text style={{ color: theme.colors.mutedText }}>
103
+ {messages.metadata.empty}
104
+ </Text>
105
+ )}
106
+ </ScrollView>
107
+ <Button label={messages.metadata.close} onPress={onRequestClose} />
108
+ </Pressable>
109
+ </Pressable>
110
+ </Modal>
111
+ );
112
+ }
@@ -10,9 +10,10 @@ import {
10
10
  View,
11
11
  } from "react-native";
12
12
 
13
+ import { useFeedbackBody } from "../../../shared/context/FeedbackBodyProvider";
13
14
  import { useFeedbackUi } from "../../../shared/context/FeedbackProvider";
14
15
  import type { FeedbackScreenEntryModalProps } from "../../../shared/types";
15
- import { useFeedbackBody } from "../../native";
16
+ import { collectEntryMetadata } from "../../../shared/metadata";
16
17
  import { EntryDetail } from "./EntryDetail";
17
18
  import { FeedbackForm } from "./primitives";
18
19
 
@@ -166,7 +167,8 @@ function CreateForm({
166
167
  onBodyChange: (body: string) => void;
167
168
  onOpenSuggestion: (id: string) => void;
168
169
  }) {
169
- const { hooks, enabledKinds } = useFeedbackBody();
170
+ const { hooks, enabledKinds, collectMetadata, collectStandardMetadata } =
171
+ useFeedbackBody();
170
172
  const { messages, theme } = useFeedbackUi();
171
173
  const [submitting, setSubmitting] = useState(false);
172
174
  const create = hooks.useCreateEntry();
@@ -185,7 +187,19 @@ function CreateForm({
185
187
  setSubmitting(true);
186
188
 
187
189
  try {
188
- onCreated(await create({ kind, title, body }));
190
+ const metadata = await collectEntryMetadata(
191
+ collectMetadata,
192
+ kind,
193
+ collectStandardMetadata,
194
+ );
195
+ onCreated(
196
+ await create({
197
+ kind,
198
+ title,
199
+ body,
200
+ ...(metadata === undefined ? {} : { metadata }),
201
+ }),
202
+ );
189
203
  } finally {
190
204
  setSubmitting(false);
191
205
  }
@@ -1,6 +1,8 @@
1
1
  export * from "../shared/messages.js";
2
+ export * from "../shared/metadata.js";
2
3
  export * from "../shared/theme.js";
3
4
  export type * from "../shared/types";
4
5
  export * from "./css.js";
6
+ export * from "./metadata.js";
5
7
  export * from "./primitives.js";
6
8
  export * from "./screen.js";
@@ -0,0 +1,51 @@
1
+ import type { FeedbackMetadataValue } from "convex-feedback";
2
+
3
+ import type {
4
+ StandardMetadataKey,
5
+ EnabledStandardMetadataSelection,
6
+ } from "../shared/types/metadata.js";
7
+
8
+ const defaultWebMetadataKeys: readonly StandardMetadataKey[] = [
9
+ "platform",
10
+ "userAgent",
11
+ "language",
12
+ "timezone",
13
+ "screenWidth",
14
+ "screenHeight",
15
+ "viewportWidth",
16
+ "viewportHeight",
17
+ "devicePixelRatio",
18
+ ];
19
+
20
+ export function collectWebMetadata(
21
+ selection: EnabledStandardMetadataSelection,
22
+ ): Partial<Record<StandardMetadataKey, FeedbackMetadataValue>> {
23
+ const keys = new Set(selection === true ? defaultWebMetadataKeys : selection);
24
+ const metadata: Partial<Record<StandardMetadataKey, FeedbackMetadataValue>> =
25
+ {};
26
+ const browserNavigator =
27
+ typeof navigator === "undefined" ? undefined : navigator;
28
+ const browserWindow = typeof window === "undefined" ? undefined : window;
29
+
30
+ if (keys.has("platform")) metadata.platform = "web";
31
+ if (keys.has("userAgent") && browserNavigator?.userAgent)
32
+ metadata.userAgent = browserNavigator.userAgent;
33
+ if (keys.has("language") && browserNavigator?.language)
34
+ metadata.language = browserNavigator.language;
35
+ if (keys.has("timezone")) {
36
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
37
+ if (timezone) metadata.timezone = timezone;
38
+ }
39
+ if (keys.has("screenWidth") && browserWindow?.screen)
40
+ metadata.screenWidth = browserWindow.screen.width;
41
+ if (keys.has("screenHeight") && browserWindow?.screen)
42
+ metadata.screenHeight = browserWindow.screen.height;
43
+ if (keys.has("viewportWidth") && browserWindow)
44
+ metadata.viewportWidth = browserWindow.innerWidth;
45
+ if (keys.has("viewportHeight") && browserWindow)
46
+ metadata.viewportHeight = browserWindow.innerHeight;
47
+ if (keys.has("devicePixelRatio") && browserWindow)
48
+ metadata.devicePixelRatio = browserWindow.devicePixelRatio;
49
+
50
+ return metadata;
51
+ }