Skip to content

Commit dd94992

Browse files
authored
fix: addressing QA redesign remarks (#3515)
## 🎯 Goal This PR fixes a set of SDK-side UI and interaction bugs across attachments, reactions, failed-message actions, and voice messages. - fixed ephemeral `Giphy` action layout so narrow `Giphy`s no longer collapse the action buttons - fixed failed messages showing the full action list on long press - failed/error messages now only show the restricted actions immediately - fixed expanded reactions view so it opens unfiltered and shows all reactions by default - fixed reaction selector crash when selecting the last reaction tab - fixed live reaction updates so removing/updating reactions no longer duplicates entries in the list - show `You` for the current user in the reactions list instead of the user name - updated the SDK bottom sheet top corners to use `primitives.radius4xl` - removed leftover pressed opacity behavior from image/video attachments - removed the file/audio icon from reply previews for voice recordings - fixed voice message time labels to show remaining time instead of elapsed time ## 🛠 Implementation details <!-- Provide a description of the implementation --> ## 🎨 UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## 🧪 Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## ☑️ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android
1 parent 6c2e98e commit dd94992

File tree

31 files changed

+445
-120
lines changed

31 files changed

+445
-120
lines changed

examples/SampleApp/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { watchLocation } from './src/utils/watchLocation';
5555
Geolocation.setRNConfiguration({
5656
skipPermissionRequests: false,
5757
authorizationLevel: 'always',
58+
locationProvider: 'playServices',
5859
});
5960

6061
import type { LocalMessage, StreamChat, TextComposerMiddleware } from 'stream-chat';

examples/SampleApp/src/components/LocationSharing/CreateLocationModal.tsx

Lines changed: 204 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { useState, useEffect, useMemo, useRef } from 'react';
22
import {
3+
ActivityIndicator,
34
Alert,
45
AlertButton,
6+
Linking,
57
Modal,
8+
PermissionsAndroid,
69
Text,
710
View,
811
Pressable,
@@ -11,7 +14,10 @@ import {
1114
Image,
1215
Platform,
1316
} from 'react-native';
14-
import Geolocation, { GeolocationResponse } from '@react-native-community/geolocation';
17+
import Geolocation, {
18+
GeolocationError,
19+
GeolocationResponse,
20+
} from '@react-native-community/geolocation';
1521
import {
1622
useChatContext,
1723
useMessageComposer,
@@ -26,12 +32,18 @@ type LiveLocationCreateModalProps = {
2632
};
2733

2834
const endedAtDurations = [60000, 600000, 3600000]; // 1 min, 10 mins, 1 hour
35+
const LOCATION_PERMISSION_ERROR = 'Location permission is required.';
36+
const LOCATION_SETTINGS_ERROR = 'Location permission is blocked. Enable it in Settings.';
2937

3038
export const LiveLocationCreateModal = ({
3139
visible,
3240
onRequestClose,
3341
}: LiveLocationCreateModalProps) => {
3442
const [location, setLocation] = useState<GeolocationResponse>();
43+
const [locationError, setLocationError] = useState<string | null>(null);
44+
const [locationPermissionIssue, setLocationPermissionIssue] = useState(false);
45+
const [permissionBlocked, setPermissionBlocked] = useState(false);
46+
const [locationSetupAttempt, setLocationSetupAttempt] = useState(0);
3547
const messageComposer = useMessageComposer();
3648
const { width, height } = useWindowDimensions();
3749
const { client } = useChatContext();
@@ -43,6 +55,7 @@ export const LiveLocationCreateModal = ({
4355
const { t } = useTranslationContext();
4456
const mapRef = useRef<MapView | null>(null);
4557
const markerRef = useRef<MapMarker | null>(null);
58+
const watchIdRef = useRef<number | null>(null);
4659

4760
const aspect_ratio = width / height;
4861

@@ -59,10 +72,75 @@ export const LiveLocationCreateModal = ({
5972
}
6073
}, [aspect_ratio, location]);
6174

75+
const ensureAndroidLocationPermission = async (): Promise<{
76+
blocked: boolean;
77+
granted: boolean;
78+
}> => {
79+
if (Platform.OS !== 'android') {
80+
return { blocked: false, granted: true };
81+
}
82+
83+
const finePermission = PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION;
84+
const coarsePermission = PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION;
85+
const [hasFinePermission, hasCoarsePermission] = await Promise.all([
86+
PermissionsAndroid.check(finePermission),
87+
PermissionsAndroid.check(coarsePermission),
88+
]);
89+
90+
if (hasFinePermission || hasCoarsePermission) {
91+
setPermissionBlocked(false);
92+
return { blocked: false, granted: true };
93+
}
94+
95+
const result = await PermissionsAndroid.requestMultiple([finePermission, coarsePermission]);
96+
const fineResult = result[finePermission];
97+
const coarseResult = result[coarsePermission];
98+
const granted =
99+
fineResult === PermissionsAndroid.RESULTS.GRANTED ||
100+
coarseResult === PermissionsAndroid.RESULTS.GRANTED;
101+
102+
if (granted) {
103+
setPermissionBlocked(false);
104+
return { blocked: false, granted: true };
105+
}
106+
107+
const blocked =
108+
fineResult === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN ||
109+
coarseResult === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN;
110+
setPermissionBlocked(blocked);
111+
return { blocked, granted: false };
112+
};
113+
114+
const isPermissionError = (error: GeolocationError) =>
115+
error.code === error.PERMISSION_DENIED || error.code === 1;
116+
117+
const retryLocationSetup = () => {
118+
setLocation(undefined);
119+
setPermissionBlocked(false);
120+
setLocationPermissionIssue(false);
121+
setLocationError(null);
122+
setLocationSetupAttempt((current) => current + 1);
123+
};
124+
125+
const openSettings = async () => {
126+
try {
127+
await Linking.openSettings();
128+
} catch (error) {
129+
console.error('openSettings', error);
130+
}
131+
};
132+
62133
useEffect(() => {
63-
let watchId: number | null = null;
64-
const watchLocationHandler = async () => {
65-
watchId = await Geolocation.watchPosition(
134+
if (!visible) {
135+
return;
136+
}
137+
138+
let isMounted = true;
139+
const startWatchingLocation = () => {
140+
setLocationError(null);
141+
setLocationPermissionIssue(false);
142+
143+
watchIdRef.current = Geolocation.watchPosition(
66144
(position) => {
67145
setLocation(position);
68146
const newPosition = {
@@ -74,12 +152,16 @@ export const LiveLocationCreateModal = ({
74152
if (mapRef.current?.animateToRegion) {
75153
mapRef.current.animateToRegion(newPosition, 500);
76154
}
77-
// This is android only
78155
if (Platform.OS === 'android' && markerRef.current?.animateMarkerToCoordinate) {
79156
markerRef.current.animateMarkerToCoordinate(newPosition, 500);
80157
}
81158
},
82159
(error) => {
160+
if (!isMounted) {
161+
return;
162+
}
163+
setLocationPermissionIssue(isPermissionError(error));
164+
setLocationError(error.message || 'Unable to fetch your current location.');
83165
console.error('watchPosition', error);
84166
},
85167
{
@@ -90,13 +172,41 @@ export const LiveLocationCreateModal = ({
90172
},
91173
);
92174
};
175+
176+
const watchLocationHandler = async () => {
177+
const { blocked, granted } = await ensureAndroidLocationPermission();
178+
if (!granted) {
179+
if (isMounted) {
180+
setLocationPermissionIssue(true);
181+
setLocationError(blocked ? LOCATION_SETTINGS_ERROR : LOCATION_PERMISSION_ERROR);
182+
}
183+
return;
184+
}
185+
186+
if (Platform.OS === 'android') {
187+
startWatchingLocation();
188+
return;
189+
}
190+
191+
Geolocation.requestAuthorization(undefined, (error) => {
192+
if (!isMounted) {
193+
return;
194+
}
195+
setLocationPermissionIssue(isPermissionError(error));
196+
setLocationError(error.message || 'Location permission is required.');
197+
console.error('requestAuthorization', error);
198+
});
199+
startWatchingLocation();
200+
};
93201
watchLocationHandler();
94202
return () => {
95-
if (watchId) {
96-
Geolocation.clearWatch(watchId);
203+
isMounted = false;
204+
if (watchIdRef.current !== null) {
205+
Geolocation.clearWatch(watchIdRef.current);
206+
watchIdRef.current = null;
97207
}
98208
};
99-
}, [aspect_ratio]);
209+
}, [aspect_ratio, locationSetupAttempt, visible]);
100210

101211
const buttons = [
102212
{
@@ -132,24 +242,34 @@ export const LiveLocationCreateModal = ({
132242
text: 'Share Current Location',
133243
description: 'Share your current location once',
134244
onPress: async () => {
135-
Geolocation.getCurrentPosition(async (position) => {
136-
if (position.coords) {
137-
await messageComposer.locationComposer.setData({
138-
latitude: position.coords.latitude,
139-
longitude: position.coords.longitude,
140-
});
141-
await messageComposer.sendLocation();
142-
onRequestClose();
143-
}
144-
});
245+
const { blocked, granted } = await ensureAndroidLocationPermission();
246+
if (!granted) {
247+
setLocationPermissionIssue(true);
248+
setLocationError(blocked ? LOCATION_SETTINGS_ERROR : LOCATION_PERMISSION_ERROR);
249+
return;
250+
}
251+
252+
setLocationPermissionIssue(false);
253+
Geolocation.getCurrentPosition(
254+
async (position) => {
255+
if (position.coords) {
256+
await messageComposer.locationComposer.setData({
257+
latitude: position.coords.latitude,
258+
longitude: position.coords.longitude,
259+
});
260+
await messageComposer.sendLocation();
261+
onRequestClose();
262+
}
263+
},
264+
(error) => {
265+
setLocationPermissionIssue(isPermissionError(error));
266+
setLocationError(error.message || 'Unable to fetch your current location.');
267+
},
268+
);
145269
},
146270
},
147271
];
148272

149-
if (!location && client) {
150-
return null;
151-
}
152-
153273
return (
154274
<Modal
155275
animationType='slide'
@@ -165,13 +285,13 @@ export const LiveLocationCreateModal = ({
165285
<View style={styles.rightContent} />
166286
</View>
167287

168-
<MapView
169-
cameraZoomRange={{ maxCenterCoordinateDistance: 2000 }}
170-
initialRegion={region}
171-
ref={mapRef}
172-
style={styles.mapView}
173-
>
174-
{location && (
288+
{location && region ? (
289+
<MapView
290+
cameraZoomRange={{ maxCenterCoordinateDistance: 2000 }}
291+
initialRegion={region}
292+
ref={mapRef}
293+
style={styles.mapView}
294+
>
175295
<Marker
176296
coordinate={{
177297
latitude: location.coords.latitude,
@@ -183,13 +303,45 @@ export const LiveLocationCreateModal = ({
183303
>
184304
<View style={styles.markerWrapper}>
185305
<Image
186-
source={{ uri: client.user?.image || '' }}
306+
source={{ uri: client?.user?.image || '' }}
187307
style={[styles.markerImage, { borderColor: accent_blue }]}
188308
/>
189309
</View>
190310
</Marker>
191-
)}
192-
</MapView>
311+
</MapView>
312+
) : (
313+
<View style={styles.loadingContainer}>
314+
<ActivityIndicator color={accent_blue} />
315+
<Text style={[styles.loadingText, { color: grey }]}>
316+
{locationError || t('Fetching your current location...')}
317+
</Text>
318+
{permissionBlocked || (Platform.OS === 'ios' && locationPermissionIssue) ? (
319+
<Pressable
320+
onPress={openSettings}
321+
style={({ pressed }) => [
322+
styles.settingsButton,
323+
{ borderColor: pressed ? accent_blue : grey_whisper },
324+
]}
325+
>
326+
<Text style={[styles.settingsButtonText, { color: accent_blue }]}>
327+
{t('Open Settings')}
328+
</Text>
329+
</Pressable>
330+
) : locationPermissionIssue && Platform.OS === 'android' ? (
331+
<Pressable
332+
onPress={retryLocationSetup}
333+
style={({ pressed }) => [
334+
styles.settingsButton,
335+
{ borderColor: pressed ? accent_blue : grey_whisper },
336+
]}
337+
>
338+
<Text style={[styles.settingsButtonText, { color: accent_blue }]}>
339+
{t('Allow Location')}
340+
</Text>
341+
</Pressable>
342+
) : null}
343+
</View>
344+
)}
193345
<View style={styles.buttons}>
194346
{buttons.map((button, index) => (
195347
<Pressable
@@ -218,6 +370,26 @@ const styles = StyleSheet.create({
218370
width: 'auto',
219371
flex: 3,
220372
},
373+
loadingContainer: {
374+
flex: 3,
375+
alignItems: 'center',
376+
justifyContent: 'center',
377+
gap: 12,
378+
},
379+
loadingText: {
380+
fontSize: 14,
381+
},
382+
settingsButton: {
383+
borderWidth: 1,
384+
borderRadius: 8,
385+
marginTop: 8,
386+
paddingHorizontal: 12,
387+
paddingVertical: 10,
388+
},
389+
settingsButtonText: {
390+
fontSize: 14,
391+
fontWeight: '600',
392+
},
221393
textStyle: {
222394
fontSize: 12,
223395
color: 'gray',

package/src/components/Attachment/Audio/AudioAttachment.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,11 @@ export const AudioAttachment = (props: AudioAttachmentProps) => {
202202

203203
const maxDurationLabel = useMemo(() => getAudioDurationLabel(duration), [duration]);
204204

205+
const remainingDuration = useMemo(() => Math.max(duration - position, 0), [duration, position]);
206+
205207
const progressDuration = useMemo(
206-
() => getAudioDurationLabel(position || duration),
207-
[duration, position],
208+
() => getAudioDurationLabel(remainingDuration || duration),
209+
[duration, remainingDuration],
208210
);
209211

210212
return (

package/src/components/Attachment/Gallery.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -327,14 +327,7 @@ const GalleryThumbnail = ({
327327
});
328328
}
329329
}}
330-
style={({ pressed }) => [
331-
styles.imageContainer,
332-
{
333-
opacity: pressed ? 0.8 : 1,
334-
flex: thumbnail.flex,
335-
},
336-
imageContainer,
337-
]}
330+
style={[styles.imageContainer, { flex: thumbnail.flex }, imageContainer]}
338331
testID={`gallery-${invertedDirections ? 'row' : 'column'}-${colIndex}-item-${rowIndex}`}
339332
{...additionalPressableProps}
340333
>

0 commit comments

Comments
 (0)