@squad-sports/react-native 1.3.1
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/LICENSE +21 -0
- package/README.md +97 -0
- package/package.json +46 -0
- package/src/SquadExperience.tsx +200 -0
- package/src/SquadProvider.tsx +232 -0
- package/src/SquadSportsSDK.ts +286 -0
- package/src/__tests__/DeepLinkHandler.test.ts +101 -0
- package/src/__tests__/ErrorBoundary.test.tsx +161 -0
- package/src/__tests__/EventProcessor.test.ts +241 -0
- package/src/__tests__/PushNotificationHandler.test.ts +91 -0
- package/src/__tests__/SecureStorage.test.ts +62 -0
- package/src/__tests__/SquadSportsSDK.test.ts +278 -0
- package/src/__tests__/VerificationCooldown.test.ts +153 -0
- package/src/components/ErrorBoundary.tsx +129 -0
- package/src/components/SOTD/SOTDComponents.tsx +101 -0
- package/src/components/audio/AudioPlayerRow.tsx +189 -0
- package/src/components/audio/recording/AudioRecording.tsx +232 -0
- package/src/components/communities/CommunityComponents.tsx +78 -0
- package/src/components/dialogs/AllDialogs.tsx +123 -0
- package/src/components/dialogs/ConfirmDialog.tsx +77 -0
- package/src/components/dialogs/PermissionDialog.tsx +132 -0
- package/src/components/dragAndDrop/DragAndDrop.tsx +56 -0
- package/src/components/events/EventComponents.tsx +93 -0
- package/src/components/feed/ChatBannerCard.tsx +94 -0
- package/src/components/feed/FeedLoadingSkeleton.tsx +43 -0
- package/src/components/feed/FreestyleCard.tsx +119 -0
- package/src/components/feed/InterstitialOverlay.tsx +190 -0
- package/src/components/feed/PollCard.tsx +158 -0
- package/src/components/feed/SponsoredContentCard.tsx +118 -0
- package/src/components/freestyle/FreestyleComponents.tsx +148 -0
- package/src/components/index.ts +42 -0
- package/src/components/message/MessageCard.tsx +166 -0
- package/src/components/message/MessageComponents.tsx +143 -0
- package/src/components/poll/PollComponents.tsx +226 -0
- package/src/components/sentinels/Sentinels.tsx +175 -0
- package/src/components/squad/FeedSquad.tsx +54 -0
- package/src/components/toasts/Toasts.tsx +88 -0
- package/src/components/ux/RootUXComponents.tsx +157 -0
- package/src/components/ux/action-sheet/ActionSheet.tsx +67 -0
- package/src/components/ux/bottom-sheet/BottomSheetComponents.tsx +93 -0
- package/src/components/ux/bottom-sheet/SquadBottomSheet.tsx +119 -0
- package/src/components/ux/buttons/Button.tsx +37 -0
- package/src/components/ux/buttons/InfoButton.tsx +24 -0
- package/src/components/ux/buttons/XButton.tsx +27 -0
- package/src/components/ux/carousel/Carousel.tsx +134 -0
- package/src/components/ux/emoji-picker/EmojiReactionPicker.tsx +139 -0
- package/src/components/ux/errors/ErrorHint.tsx +46 -0
- package/src/components/ux/inputs/CodeInput.tsx +121 -0
- package/src/components/ux/inputs/DatePicker.tsx +76 -0
- package/src/components/ux/inputs/MaskedTextInput.tsx +95 -0
- package/src/components/ux/inputs/PhoneNumberInput.tsx +90 -0
- package/src/components/ux/inputs/SearchTextInput.tsx +70 -0
- package/src/components/ux/inputs/TextInput.tsx +58 -0
- package/src/components/ux/layout/AvoidKeyboardScreen.tsx +58 -0
- package/src/components/ux/layout/BlurOverlay.tsx +26 -0
- package/src/components/ux/layout/CrossFade.tsx +30 -0
- package/src/components/ux/layout/DismissKeyboardOnBlur.tsx +14 -0
- package/src/components/ux/layout/LoadingOverlay.tsx +60 -0
- package/src/components/ux/layout/NetworkBanner.tsx +64 -0
- package/src/components/ux/layout/PermissionsCTAContent.tsx +54 -0
- package/src/components/ux/layout/RefreshControl.tsx +7 -0
- package/src/components/ux/layout/Screen.tsx +31 -0
- package/src/components/ux/layout/ScreenHeader.tsx +89 -0
- package/src/components/ux/layout/TabBar.tsx +39 -0
- package/src/components/ux/layout/Toast.tsx +116 -0
- package/src/components/ux/layout/TransparentOverlay.tsx +21 -0
- package/src/components/ux/navigation/BackButton.tsx +29 -0
- package/src/components/ux/navigation/LinkButton.tsx +21 -0
- package/src/components/ux/navigation/UrlButton.tsx +25 -0
- package/src/components/ux/scroll/RefreshableFlatList.tsx +35 -0
- package/src/components/ux/shapes/Shapes.tsx +23 -0
- package/src/components/ux/text/Typography.tsx +28 -0
- package/src/components/ux/user-image/UserImage.tsx +75 -0
- package/src/components/ux/user-image/UserImageVariants.tsx +104 -0
- package/src/components/wallet/WalletComponents.tsx +116 -0
- package/src/contexts/AuthContext.tsx +45 -0
- package/src/contexts/EventProcessorContext.tsx +41 -0
- package/src/contexts/PlayerQueueContext.tsx +95 -0
- package/src/hooks/useAuth.ts +23 -0
- package/src/hooks/useDataRefresh.ts +30 -0
- package/src/hooks/useEventProcessor.ts +6 -0
- package/src/hooks/useImageOptimization.ts +59 -0
- package/src/hooks/useOnboardingStepGuard.ts +36 -0
- package/src/hooks/usePendingNavigation.ts +26 -0
- package/src/hooks/useSquadData.ts +84 -0
- package/src/hooks/useUserCreated.ts +25 -0
- package/src/hooks/useUserUpdate.ts +25 -0
- package/src/hooks/useViewabilityTracker.ts +40 -0
- package/src/index.ts +109 -0
- package/src/navigation/SquadNavigator.tsx +262 -0
- package/src/realtime/DeepLinkHandler.ts +113 -0
- package/src/realtime/EventProcessor.ts +313 -0
- package/src/realtime/NetworkMonitor.ts +84 -0
- package/src/realtime/OfflineQueue.ts +133 -0
- package/src/realtime/PushNotificationHandler.ts +125 -0
- package/src/realtime/useRealtimeSync.ts +84 -0
- package/src/screens/auth/EmailVerificationScreen.tsx +201 -0
- package/src/screens/auth/EnterCodeScreen.tsx +253 -0
- package/src/screens/auth/EnterEmailScreen.tsx +234 -0
- package/src/screens/auth/LandingScreen.tsx +90 -0
- package/src/screens/auth/LoginScreen.tsx +126 -0
- package/src/screens/events/EventScreen.tsx +163 -0
- package/src/screens/freestyle/CommunityFreestyleScreen.tsx +82 -0
- package/src/screens/freestyle/FreestyleCreationScreen.tsx +255 -0
- package/src/screens/freestyle/FreestyleListensScreen.tsx +44 -0
- package/src/screens/freestyle/FreestyleReactionsScreen.tsx +44 -0
- package/src/screens/freestyle/FreestyleScreen.tsx +64 -0
- package/src/screens/home/HomeScreen.tsx +365 -0
- package/src/screens/home/slivers/SquadCircle.tsx +77 -0
- package/src/screens/invite/InvitationQrCodeScreen.tsx +114 -0
- package/src/screens/invite/InviteScreen.tsx +175 -0
- package/src/screens/messaging/MessagingScreen.tsx +360 -0
- package/src/screens/onboarding/OnboardingAccountSetupScreen.tsx +221 -0
- package/src/screens/onboarding/OnboardingTeamSelectScreen.tsx +215 -0
- package/src/screens/onboarding/OnboardingWelcomeScreen.tsx +93 -0
- package/src/screens/polls/PollResponseScreen.tsx +229 -0
- package/src/screens/polls/PollSummationScreen.tsx +78 -0
- package/src/screens/profile/ProfileScreen.tsx +234 -0
- package/src/screens/settings/BlockedUsersScreen.tsx +139 -0
- package/src/screens/settings/DeleteAccountScreen.tsx +182 -0
- package/src/screens/settings/EditProfileScreen.tsx +154 -0
- package/src/screens/settings/NetworkStatusScreen.tsx +59 -0
- package/src/screens/settings/SettingsScreen.tsx +194 -0
- package/src/screens/squad-line/AddCallTitleScreen.tsx +293 -0
- package/src/screens/wallet/WalletScreen.tsx +174 -0
- package/src/services/AuthStateManager.ts +93 -0
- package/src/services/NavigationService.ts +40 -0
- package/src/services/UserDataManager.ts +59 -0
- package/src/services/UserUpdateService.ts +31 -0
- package/src/services/VerificationStateManager.ts +41 -0
- package/src/squad-line/CallScreen.tsx +158 -0
- package/src/squad-line/IncomingCallOverlay.tsx +113 -0
- package/src/squad-line/SquadLineClient.ts +327 -0
- package/src/squad-line/useSquadLine.ts +80 -0
- package/src/state/audio.ts +38 -0
- package/src/state/client.ts +26 -0
- package/src/state/communities.ts +45 -0
- package/src/state/contacts.ts +28 -0
- package/src/state/device-info.ts +22 -0
- package/src/state/events.ts +16 -0
- package/src/state/features.ts +57 -0
- package/src/state/index.ts +121 -0
- package/src/state/invitations.ts +16 -0
- package/src/state/modal-keys.ts +63 -0
- package/src/state/modal-queue.ts +104 -0
- package/src/state/navigation.ts +34 -0
- package/src/state/permissions.ts +43 -0
- package/src/state/session.ts +223 -0
- package/src/state/squaddie-of-the-day.ts +21 -0
- package/src/state/sync/crdt.ts +70 -0
- package/src/state/sync/dependable.ts +213 -0
- package/src/state/sync/feed-v2.ts +42 -0
- package/src/state/sync/messages.ts +44 -0
- package/src/state/sync/offline-support.ts +42 -0
- package/src/state/sync/polls.ts +37 -0
- package/src/state/sync/refresh.ts +24 -0
- package/src/state/sync/squad-v2.ts +25 -0
- package/src/state/ui.ts +36 -0
- package/src/state/user.ts +46 -0
- package/src/state/wallet.ts +26 -0
- package/src/storage/SecureStorage.ts +77 -0
- package/src/theme/ThemeContext.tsx +159 -0
- package/src/types/modules.d.ts +165 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import React, { useCallback, useState, useRef, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Pressable,
|
|
7
|
+
Animated,
|
|
8
|
+
Alert,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import { useNavigation } from '@react-navigation/native';
|
|
11
|
+
import { Audio } from 'expo-av';
|
|
12
|
+
|
|
13
|
+
import { useApiClient } from '../../SquadProvider';
|
|
14
|
+
import { useTheme, Colors } from '../../theme/ThemeContext';
|
|
15
|
+
import ScreenHeader from '../../components/ux/layout/ScreenHeader';
|
|
16
|
+
import Button from '../../components/ux/buttons/Button';
|
|
17
|
+
import { TitleMedium, BodyRegular, BodySmall } from '../../components/ux/text/Typography';
|
|
18
|
+
|
|
19
|
+
type RecordingState = 'idle' | 'recording' | 'recorded' | 'uploading';
|
|
20
|
+
|
|
21
|
+
export function FreestyleCreationScreen() {
|
|
22
|
+
const navigation = useNavigation();
|
|
23
|
+
const apiClient = useApiClient();
|
|
24
|
+
const { theme } = useTheme();
|
|
25
|
+
|
|
26
|
+
const [state, setState] = useState<RecordingState>('idle');
|
|
27
|
+
const [duration, setDuration] = useState(0);
|
|
28
|
+
const [recordingUri, setRecordingUri] = useState<string | null>(null);
|
|
29
|
+
const pulseAnim = useRef(new Animated.Value(1)).current;
|
|
30
|
+
const timerRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
|
|
31
|
+
const recordingRef = useRef<Audio.Recording | null>(null);
|
|
32
|
+
|
|
33
|
+
// Request mic permissions on mount
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const requestPermissions = async () => {
|
|
36
|
+
const { status } = await Audio.requestPermissionsAsync();
|
|
37
|
+
if (status !== 'granted') {
|
|
38
|
+
Alert.alert(
|
|
39
|
+
'Microphone Access',
|
|
40
|
+
'Squad needs microphone access to record freestyles. Please enable it in Settings.',
|
|
41
|
+
[{ text: 'OK', onPress: () => navigation.goBack() }],
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
await Audio.setAudioModeAsync({
|
|
45
|
+
allowsRecordingIOS: true,
|
|
46
|
+
playsInSilentModeIOS: true,
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
requestPermissions();
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
// Cleanup recording on unmount
|
|
53
|
+
if (recordingRef.current) {
|
|
54
|
+
recordingRef.current.stopAndUnloadAsync().catch(() => {});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}, [navigation]);
|
|
58
|
+
|
|
59
|
+
const startRecording = useCallback(async () => {
|
|
60
|
+
try {
|
|
61
|
+
const recording = new Audio.Recording();
|
|
62
|
+
await recording.prepareToRecordAsync(Audio.RecordingOptionsPresets.HIGH_QUALITY);
|
|
63
|
+
await recording.startAsync();
|
|
64
|
+
recordingRef.current = recording;
|
|
65
|
+
|
|
66
|
+
setState('recording');
|
|
67
|
+
setDuration(0);
|
|
68
|
+
|
|
69
|
+
// Start timer
|
|
70
|
+
timerRef.current = setInterval(() => {
|
|
71
|
+
setDuration(prev => prev + 1);
|
|
72
|
+
}, 1000);
|
|
73
|
+
|
|
74
|
+
// Start pulse animation
|
|
75
|
+
Animated.loop(
|
|
76
|
+
Animated.sequence([
|
|
77
|
+
Animated.timing(pulseAnim, { toValue: 1.15, duration: 800, useNativeDriver: true }),
|
|
78
|
+
Animated.timing(pulseAnim, { toValue: 1, duration: 800, useNativeDriver: true }),
|
|
79
|
+
]),
|
|
80
|
+
).start();
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error('[FreestyleCreation] Failed to start recording:', err);
|
|
83
|
+
Alert.alert('Recording Error', 'Failed to start recording. Please check microphone permissions.');
|
|
84
|
+
}
|
|
85
|
+
}, [pulseAnim]);
|
|
86
|
+
|
|
87
|
+
const stopRecording = useCallback(async () => {
|
|
88
|
+
if (timerRef.current) clearInterval(timerRef.current);
|
|
89
|
+
pulseAnim.stopAnimation();
|
|
90
|
+
pulseAnim.setValue(1);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
if (recordingRef.current) {
|
|
94
|
+
await recordingRef.current.stopAndUnloadAsync();
|
|
95
|
+
const uri = recordingRef.current.getURI();
|
|
96
|
+
setRecordingUri(uri);
|
|
97
|
+
recordingRef.current = null;
|
|
98
|
+
}
|
|
99
|
+
setState('recorded');
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error('[FreestyleCreation] Failed to stop recording:', err);
|
|
102
|
+
setState('idle');
|
|
103
|
+
}
|
|
104
|
+
}, [pulseAnim]);
|
|
105
|
+
|
|
106
|
+
const submitFreestyle = useCallback(async () => {
|
|
107
|
+
if (!recordingUri) return;
|
|
108
|
+
|
|
109
|
+
setState('uploading');
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
// Read the audio file as bytes
|
|
113
|
+
const response = await fetch(recordingUri);
|
|
114
|
+
const blob = await response.blob();
|
|
115
|
+
const buffer = new Uint8Array(await (blob as any).arrayBuffer());
|
|
116
|
+
|
|
117
|
+
// Create freestyle via API (protobuf)
|
|
118
|
+
const { Freestyle } = await import('@squad-sports/core');
|
|
119
|
+
const freestyle = new Freestyle({});
|
|
120
|
+
const created = await apiClient.createFreestyle(freestyle);
|
|
121
|
+
|
|
122
|
+
if (created?.id) {
|
|
123
|
+
// Upload audio to the created freestyle
|
|
124
|
+
await apiClient.uploadFreestyleAudio(created.id, buffer);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
navigation.goBack();
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error('[FreestyleCreation] Error submitting:', err);
|
|
130
|
+
Alert.alert('Upload Failed', 'Failed to post your freestyle. Please try again.');
|
|
131
|
+
setState('recorded');
|
|
132
|
+
}
|
|
133
|
+
}, [recordingUri, apiClient, navigation]);
|
|
134
|
+
|
|
135
|
+
const discard = useCallback(() => {
|
|
136
|
+
if (timerRef.current) clearInterval(timerRef.current);
|
|
137
|
+
setRecordingUri(null);
|
|
138
|
+
setState('idle');
|
|
139
|
+
setDuration(0);
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
// Playback for review
|
|
143
|
+
const playback = useCallback(async () => {
|
|
144
|
+
if (!recordingUri) return;
|
|
145
|
+
try {
|
|
146
|
+
const { sound } = await Audio.Sound.createAsync({ uri: recordingUri });
|
|
147
|
+
await sound.playAsync();
|
|
148
|
+
sound.setOnPlaybackStatusUpdate((status) => {
|
|
149
|
+
if (status.isLoaded && status.didJustFinish) {
|
|
150
|
+
sound.unloadAsync();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
} catch {}
|
|
154
|
+
}, [recordingUri]);
|
|
155
|
+
|
|
156
|
+
const formatTime = (seconds: number) => {
|
|
157
|
+
const m = Math.floor(seconds / 60);
|
|
158
|
+
const s = seconds % 60;
|
|
159
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<View style={[styles.container, { backgroundColor: theme.screenBackground }]}>
|
|
164
|
+
<ScreenHeader title="New Freestyle" />
|
|
165
|
+
|
|
166
|
+
<View style={styles.content}>
|
|
167
|
+
<TitleMedium style={styles.prompt}>What's on your mind?</TitleMedium>
|
|
168
|
+
<BodyRegular style={styles.hint}>
|
|
169
|
+
Record an audio freestyle to share with your squad
|
|
170
|
+
</BodyRegular>
|
|
171
|
+
|
|
172
|
+
<View style={styles.recordingArea}>
|
|
173
|
+
{(state === 'recording' || state === 'recorded') && (
|
|
174
|
+
<Text style={styles.timer}>{formatTime(duration)}</Text>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
<Animated.View style={{ transform: [{ scale: state === 'recording' ? pulseAnim : 1 }] }}>
|
|
178
|
+
<Pressable
|
|
179
|
+
style={[
|
|
180
|
+
styles.recordButton,
|
|
181
|
+
state === 'recording' && styles.recordButtonActive,
|
|
182
|
+
{ borderColor: state === 'recording' ? Colors.red : theme.buttonColor },
|
|
183
|
+
]}
|
|
184
|
+
onPress={
|
|
185
|
+
state === 'idle' ? startRecording
|
|
186
|
+
: state === 'recording' ? stopRecording
|
|
187
|
+
: state === 'recorded' ? playback
|
|
188
|
+
: undefined
|
|
189
|
+
}
|
|
190
|
+
>
|
|
191
|
+
{state === 'recording' ? (
|
|
192
|
+
<View style={styles.stopIcon} />
|
|
193
|
+
) : state === 'recorded' ? (
|
|
194
|
+
<Text style={[styles.playIcon, { color: theme.buttonColor }]}>{'>'}</Text>
|
|
195
|
+
) : (
|
|
196
|
+
<View style={[styles.micIcon, { backgroundColor: theme.buttonColor }]} />
|
|
197
|
+
)}
|
|
198
|
+
</Pressable>
|
|
199
|
+
</Animated.View>
|
|
200
|
+
|
|
201
|
+
<BodySmall style={styles.recordHint}>
|
|
202
|
+
{state === 'idle' && 'Tap to record'}
|
|
203
|
+
{state === 'recording' && 'Tap to stop'}
|
|
204
|
+
{state === 'recorded' && 'Tap to preview'}
|
|
205
|
+
{state === 'uploading' && 'Uploading...'}
|
|
206
|
+
</BodySmall>
|
|
207
|
+
</View>
|
|
208
|
+
</View>
|
|
209
|
+
|
|
210
|
+
{state === 'recorded' && (
|
|
211
|
+
<View style={styles.footer}>
|
|
212
|
+
<Button style={styles.discardButton} onPress={discard}>
|
|
213
|
+
<Text style={styles.discardText}>Discard</Text>
|
|
214
|
+
</Button>
|
|
215
|
+
<Button
|
|
216
|
+
style={[styles.submitButton, { backgroundColor: theme.buttonColor }]}
|
|
217
|
+
onPress={submitFreestyle}
|
|
218
|
+
>
|
|
219
|
+
<Text style={[styles.submitText, { color: theme.buttonText }]}>
|
|
220
|
+
Share Freestyle
|
|
221
|
+
</Text>
|
|
222
|
+
</Button>
|
|
223
|
+
</View>
|
|
224
|
+
)}
|
|
225
|
+
</View>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const styles = StyleSheet.create({
|
|
230
|
+
container: { flex: 1 },
|
|
231
|
+
content: { flex: 1, alignItems: 'center', paddingHorizontal: 24, paddingTop: 32 },
|
|
232
|
+
prompt: { color: Colors.white, textAlign: 'center', marginBottom: 8 },
|
|
233
|
+
hint: { color: Colors.gray6, textAlign: 'center', marginBottom: 48 },
|
|
234
|
+
recordingArea: { alignItems: 'center', gap: 24 },
|
|
235
|
+
timer: { color: Colors.white, fontSize: 48, fontWeight: '300', fontVariant: ['tabular-nums'] },
|
|
236
|
+
recordButton: {
|
|
237
|
+
width: 88, height: 88, borderRadius: 44, borderWidth: 4,
|
|
238
|
+
justifyContent: 'center', alignItems: 'center',
|
|
239
|
+
backgroundColor: 'rgba(110, 130, 231, 0.1)',
|
|
240
|
+
},
|
|
241
|
+
recordButtonActive: { backgroundColor: 'rgba(255, 68, 120, 0.1)' },
|
|
242
|
+
micIcon: { width: 24, height: 36, borderRadius: 12 },
|
|
243
|
+
stopIcon: { width: 24, height: 24, borderRadius: 4, backgroundColor: Colors.red },
|
|
244
|
+
playIcon: { fontSize: 28, fontWeight: '700' },
|
|
245
|
+
recordHint: { color: Colors.gray6 },
|
|
246
|
+
footer: { flexDirection: 'row', paddingHorizontal: 24, paddingBottom: 32, gap: 12 },
|
|
247
|
+
discardButton: {
|
|
248
|
+
flex: 1, height: 52, borderRadius: 26,
|
|
249
|
+
justifyContent: 'center', alignItems: 'center',
|
|
250
|
+
borderWidth: 1, borderColor: Colors.gray5,
|
|
251
|
+
},
|
|
252
|
+
discardText: { color: Colors.white, fontSize: 15, fontWeight: '600' },
|
|
253
|
+
submitButton: { flex: 2, height: 52, borderRadius: 26, justifyContent: 'center', alignItems: 'center' },
|
|
254
|
+
submitText: { fontSize: 15, fontWeight: '600' },
|
|
255
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freestyle listens — who listened to a freestyle.
|
|
3
|
+
* Ported from squad-demo/src/screens/FreestyleListens.tsx.
|
|
4
|
+
*/
|
|
5
|
+
import React, { useEffect, useState } from 'react';
|
|
6
|
+
import { View, StyleSheet, FlatList } from 'react-native';
|
|
7
|
+
import { useRoute } from '@react-navigation/native';
|
|
8
|
+
import { useTheme, Colors } from '../../theme/ThemeContext';
|
|
9
|
+
import ScreenHeader from '../../components/ux/layout/ScreenHeader';
|
|
10
|
+
import { FreestyleUserListen } from '../../components/freestyle/FreestyleComponents';
|
|
11
|
+
import { BodyRegular } from '../../components/ux/text/Typography';
|
|
12
|
+
|
|
13
|
+
export function FreestyleListensScreen() {
|
|
14
|
+
const route = useRoute<any>();
|
|
15
|
+
const { theme } = useTheme();
|
|
16
|
+
const freestyleId = route.params?.freestyleId;
|
|
17
|
+
const [listens, setListens] = useState<Array<{ userName: string; userImageUrl?: string; listenedAt?: string }>>([]);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<View style={[styles.container, { backgroundColor: theme.screenBackground }]}>
|
|
21
|
+
<ScreenHeader title="Listens" />
|
|
22
|
+
<FlatList
|
|
23
|
+
data={listens}
|
|
24
|
+
keyExtractor={(_, i) => String(i)}
|
|
25
|
+
renderItem={({ item }) => (
|
|
26
|
+
<FreestyleUserListen userName={item.userName} userImageUrl={item.userImageUrl} listenedAt={item.listenedAt} />
|
|
27
|
+
)}
|
|
28
|
+
contentContainerStyle={styles.list}
|
|
29
|
+
ListEmptyComponent={
|
|
30
|
+
<View style={styles.empty}>
|
|
31
|
+
<BodyRegular style={styles.emptyText}>No listens yet</BodyRegular>
|
|
32
|
+
</View>
|
|
33
|
+
}
|
|
34
|
+
/>
|
|
35
|
+
</View>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const styles = StyleSheet.create({
|
|
40
|
+
container: { flex: 1 },
|
|
41
|
+
list: { paddingBottom: 48 },
|
|
42
|
+
empty: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingTop: 48 },
|
|
43
|
+
emptyText: { color: Colors.gray6 },
|
|
44
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freestyle reactions — who reacted to a freestyle.
|
|
3
|
+
* Ported from squad-demo/src/screens/FreestyleReactions.tsx.
|
|
4
|
+
*/
|
|
5
|
+
import React, { useEffect, useState } from 'react';
|
|
6
|
+
import { View, StyleSheet, FlatList } from 'react-native';
|
|
7
|
+
import { useRoute } from '@react-navigation/native';
|
|
8
|
+
import { useTheme, Colors } from '../../theme/ThemeContext';
|
|
9
|
+
import ScreenHeader from '../../components/ux/layout/ScreenHeader';
|
|
10
|
+
import { FreestyleUserReaction } from '../../components/freestyle/FreestyleComponents';
|
|
11
|
+
import { BodyRegular } from '../../components/ux/text/Typography';
|
|
12
|
+
|
|
13
|
+
export function FreestyleReactionsScreen() {
|
|
14
|
+
const route = useRoute<any>();
|
|
15
|
+
const { theme } = useTheme();
|
|
16
|
+
const freestyleId = route.params?.freestyleId;
|
|
17
|
+
const [reactions, setReactions] = useState<Array<{ userName: string; userImageUrl?: string; emoji: string }>>([]);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<View style={[styles.container, { backgroundColor: theme.screenBackground }]}>
|
|
21
|
+
<ScreenHeader title="Reactions" />
|
|
22
|
+
<FlatList
|
|
23
|
+
data={reactions}
|
|
24
|
+
keyExtractor={(_, i) => String(i)}
|
|
25
|
+
renderItem={({ item }) => (
|
|
26
|
+
<FreestyleUserReaction userName={item.userName} userImageUrl={item.userImageUrl} emoji={item.emoji} />
|
|
27
|
+
)}
|
|
28
|
+
contentContainerStyle={styles.list}
|
|
29
|
+
ListEmptyComponent={
|
|
30
|
+
<View style={styles.empty}>
|
|
31
|
+
<BodyRegular style={styles.emptyText}>No reactions yet</BodyRegular>
|
|
32
|
+
</View>
|
|
33
|
+
}
|
|
34
|
+
/>
|
|
35
|
+
</View>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const styles = StyleSheet.create({
|
|
40
|
+
container: { flex: 1 },
|
|
41
|
+
list: { paddingBottom: 48 },
|
|
42
|
+
empty: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingTop: 48 },
|
|
43
|
+
emptyText: { color: Colors.gray6 },
|
|
44
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Individual freestyle view screen.
|
|
3
|
+
* Ported from squad-demo/src/screens/Freestyle.tsx.
|
|
4
|
+
*/
|
|
5
|
+
import React, { useEffect, useState } from 'react';
|
|
6
|
+
import { View, StyleSheet, FlatList } from 'react-native';
|
|
7
|
+
import { useRoute } from '@react-navigation/native';
|
|
8
|
+
import { useApiClient } from '../../SquadProvider';
|
|
9
|
+
import { useTheme, Colors } from '../../theme/ThemeContext';
|
|
10
|
+
import ScreenHeader from '../../components/ux/layout/ScreenHeader';
|
|
11
|
+
import { FreestyleFeedItem, FreestyleUserReaction } from '../../components/freestyle/FreestyleComponents';
|
|
12
|
+
import { AudioPlayerRow } from '../../components/audio/AudioPlayerRow';
|
|
13
|
+
|
|
14
|
+
export function FreestyleScreen() {
|
|
15
|
+
const route = useRoute<any>();
|
|
16
|
+
const apiClient = useApiClient();
|
|
17
|
+
const { theme } = useTheme();
|
|
18
|
+
|
|
19
|
+
const freestyleId = route.params?.freestyleId;
|
|
20
|
+
const [freestyle, setFreestyle] = useState<any>(null);
|
|
21
|
+
const [reactions, setReactions] = useState<any[]>([]);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
// Load freestyle and reactions from feed cache or API
|
|
25
|
+
}, [freestyleId]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<View style={[styles.container, { backgroundColor: theme.screenBackground }]}>
|
|
29
|
+
<ScreenHeader title="Freestyle" />
|
|
30
|
+
{freestyle && (
|
|
31
|
+
<FlatList
|
|
32
|
+
ListHeaderComponent={
|
|
33
|
+
<FreestyleFeedItem
|
|
34
|
+
id={freestyle.id}
|
|
35
|
+
audioUrl={freestyle.audioUrl}
|
|
36
|
+
duration={freestyle.duration}
|
|
37
|
+
creatorName={freestyle.creator?.displayName}
|
|
38
|
+
creatorImageUrl={freestyle.creator?.imageUrl}
|
|
39
|
+
createdAt={freestyle.createdAt}
|
|
40
|
+
listenCount={freestyle.listenCount}
|
|
41
|
+
reactionCount={freestyle.reactionCount}
|
|
42
|
+
promptText={freestyle.prompt?.text}
|
|
43
|
+
/>
|
|
44
|
+
}
|
|
45
|
+
data={reactions}
|
|
46
|
+
keyExtractor={(_, i) => String(i)}
|
|
47
|
+
renderItem={({ item }) => (
|
|
48
|
+
<FreestyleUserReaction
|
|
49
|
+
userName={item.creator?.displayName ?? ''}
|
|
50
|
+
userImageUrl={item.creator?.imageUrl}
|
|
51
|
+
emoji={item.emojiLabel ?? ''}
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
54
|
+
contentContainerStyle={styles.list}
|
|
55
|
+
/>
|
|
56
|
+
)}
|
|
57
|
+
</View>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const styles = StyleSheet.create({
|
|
62
|
+
container: { flex: 1 },
|
|
63
|
+
list: { padding: 24 },
|
|
64
|
+
});
|