@upstep/react-native 0.6.6 → 0.6.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upstep/react-native",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "Upstep feedback SDK for React Native",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -6,6 +6,7 @@ import React, {
6
6
  useCallback,
7
7
  type ReactNode,
8
8
  } from "react";
9
+ import { LayoutAnimation, Platform, UIManager } from "react-native";
9
10
  import type {
10
11
  Feedback,
11
12
  FeedbackType,
@@ -15,6 +16,12 @@ import type {
15
16
  } from "@upstep/types";
16
17
  import { UpstepApiClient } from "./api";
17
18
 
19
+ // Enable LayoutAnimation on Android
20
+ if (Platform.OS === "android") {
21
+ const um = UIManager as unknown as { setLayoutAnimationEnabledExperimental?: (v: boolean) => void };
22
+ um.setLayoutAnimationEnabledExperimental?.(true);
23
+ }
24
+
18
25
  const DEFAULT_ACCENT = "#D97757";
19
26
 
20
27
  interface UpstepContextValue {
@@ -74,6 +81,22 @@ export function FeedbackProvider({
74
81
  );
75
82
 
76
83
  const vote = useCallback(async (feedbackId: string, value: VoteValue) => {
84
+ // Optimistic update: flip vote + re-sort with a smooth animation
85
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
86
+ setFeedItems((prev) => {
87
+ const updated = prev.map((f) => {
88
+ if (f.id !== feedbackId) return f;
89
+ const wasVoted = f.userVote === "UP";
90
+ return {
91
+ ...f,
92
+ upvotes: wasVoted ? f.upvotes - 1 : f.upvotes + 1,
93
+ userVote: wasVoted ? null : ("UP" as VoteValue),
94
+ };
95
+ });
96
+ return [...updated].sort((a, b) => b.upvotes - a.upvotes);
97
+ });
98
+
99
+ // Confirm with server, then silently sync
77
100
  await client.vote(feedbackId, value);
78
101
  await loadFeed();
79
102
  }, [client, loadFeed]);
@@ -66,6 +66,7 @@ export function FeedbackSheet() {
66
66
  const p = isDark ? DARK : LIGHT;
67
67
 
68
68
  const [nav, setNav] = useState<Nav>({ screen: "list" });
69
+ const [listTab, setListTab] = useState<"open" | "done">("open");
69
70
  const insets = useSafeAreaInsets();
70
71
 
71
72
  // Refresh list every time the modal opens
@@ -135,6 +136,30 @@ export function FeedbackSheet() {
135
136
  )}
136
137
  </View>
137
138
 
139
+ {/* Open / Completed tabs — only on list screen */}
140
+ {nav.screen === "list" && (
141
+ <View style={[styles.listTabBar, { borderBottomColor: p.border }]}>
142
+ <TouchableOpacity
143
+ onPress={() => setListTab("open")}
144
+ style={[styles.listTabBtn, listTab === "open" && { borderBottomColor: accentColor }]}
145
+ activeOpacity={0.7}
146
+ >
147
+ <Text style={[styles.listTabText, { color: listTab === "open" ? accentColor : p.textFaint }]}>
148
+ Open
149
+ </Text>
150
+ </TouchableOpacity>
151
+ <TouchableOpacity
152
+ onPress={() => setListTab("done")}
153
+ style={[styles.listTabBtn, listTab === "done" && { borderBottomColor: accentColor }]}
154
+ activeOpacity={0.7}
155
+ >
156
+ <Text style={[styles.listTabText, { color: listTab === "done" ? accentColor : p.textFaint }]}>
157
+ Completed
158
+ </Text>
159
+ </TouchableOpacity>
160
+ </View>
161
+ )}
162
+
138
163
  {/* Scrollable body */}
139
164
  <ScrollView
140
165
  style={styles.scroll}
@@ -146,6 +171,7 @@ export function FeedbackSheet() {
146
171
  <FeedListBody
147
172
  p={p}
148
173
  accent={accentColor}
174
+ tab={listTab}
149
175
  onSelect={(id) => setNav({ screen: "detail", feedbackId: id })}
150
176
  />
151
177
  )}
@@ -189,12 +215,16 @@ export function FeedbackSheet() {
189
215
  // ─── FeedListBody ─────────────────────────────────────────────────────────────
190
216
 
191
217
  function FeedListBody({
192
- p, accent, onSelect,
218
+ p, accent, tab, onSelect,
193
219
  }: {
194
- p: Palette; accent: string; onSelect: (id: string) => void;
220
+ p: Palette; accent: string; tab: "open" | "done"; onSelect: (id: string) => void;
195
221
  }) {
196
222
  const { feedItems, feedLoading, vote } = useUpstep();
197
223
 
224
+ const displayItems = tab === "done"
225
+ ? feedItems.filter((f) => f.status === "DONE")
226
+ : feedItems.filter((f) => f.status === "OPEN" || f.status === "IN_PROGRESS" || f.status === "PENDING");
227
+
198
228
  if (feedLoading) {
199
229
  return (
200
230
  <View style={styles.centered}>
@@ -203,11 +233,13 @@ function FeedListBody({
203
233
  );
204
234
  }
205
235
 
206
- if (feedItems.length === 0) {
236
+ if (displayItems.length === 0) {
207
237
  return (
208
238
  <View style={styles.centered}>
209
239
  <Text style={[styles.emptyText, { color: p.textFaint }]}>
210
- No feedback yet. Be the first to add one.
240
+ {tab === "done"
241
+ ? "No completed items yet."
242
+ : "No open feedback yet. Be the first to add one."}
211
243
  </Text>
212
244
  </View>
213
245
  );
@@ -215,7 +247,7 @@ function FeedListBody({
215
247
 
216
248
  return (
217
249
  <>
218
- {feedItems.map((f) => (
250
+ {displayItems.map((f) => (
219
251
  <FeedCard
220
252
  key={f.id}
221
253
  item={f}
@@ -606,6 +638,23 @@ const styles = StyleSheet.create({
606
638
  newBtn: { borderRadius: 20, paddingHorizontal: 14, paddingVertical: 7 },
607
639
  newBtnText: { color: "#fff", fontSize: 13, fontWeight: "700" },
608
640
 
641
+ // List tab bar
642
+ listTabBar: {
643
+ flexDirection: "row",
644
+ borderBottomWidth: StyleSheet.hairlineWidth,
645
+ },
646
+ listTabBtn: {
647
+ flex: 1,
648
+ alignItems: "center",
649
+ paddingVertical: 11,
650
+ borderBottomWidth: 2,
651
+ borderBottomColor: "transparent",
652
+ },
653
+ listTabText: {
654
+ fontSize: 13,
655
+ fontWeight: "600",
656
+ },
657
+
609
658
  // Scroll — flex: 1 fills all remaining space after the fixed header
610
659
  scroll: { flex: 1 },
611
660
  scrollContent: { flexGrow: 1, padding: 16, paddingBottom: 32 },