Skip to content

Commit 3529618

Browse files
authored
Merge pull request #3181 from GetStream/develop
Next Release
2 parents 118117b + 208b981 commit 3529618

File tree

73 files changed

+2450
-107
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+2450
-107
lines changed

examples/ExpoMessaging/app.json

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,38 @@
1515
"updates": {
1616
"fallbackToCacheTimeout": 0
1717
},
18-
"assetBundlePatterns": [
19-
"**/*"
20-
],
18+
"assetBundlePatterns": ["**/*"],
2119
"ios": {
2220
"supportsTablet": true,
2321
"usesIcloudStorage": true,
2422
"bundleIdentifier": "io.stream.expomessagingapp",
2523
"appleTeamId": "EHV7XZLAHA"
2624
},
2725
"android": {
26+
"config": {
27+
"googleMaps": {
28+
"apiKey": "AIzaSyDVh35biMyXbOjt74CQyO1dlqSMlrdHOOA"
29+
}
30+
},
2831
"package": "io.stream.expomessagingapp",
2932
"adaptiveIcon": {
3033
"foregroundImage": "./assets/adaptive-icon.png",
3134
"backgroundColor": "#ffffff"
3235
},
33-
"permissions": [
34-
"android.permission.RECORD_AUDIO",
35-
"android.permission.MODIFY_AUDIO_SETTINGS"
36-
]
36+
"permissions": ["android.permission.RECORD_AUDIO", "android.permission.MODIFY_AUDIO_SETTINGS"]
3737
},
3838
"web": {
3939
"favicon": "./assets/favicon.png",
4040
"bundler": "metro"
4141
},
4242
"scheme": "ExpoMessaging",
4343
"plugins": [
44+
[
45+
"expo-location",
46+
{
47+
"locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location."
48+
}
49+
],
4450
"expo-router",
4551
[
4652
"expo-image-picker",
@@ -63,6 +69,7 @@
6369
"microphonePermission": "$(PRODUCT_NAME) would like to use your microphone for voice recording."
6470
}
6571
],
72+
6673
"./plugins/keyboardInsetMainActivityListener.js"
6774
]
6875
}

examples/ExpoMessaging/app/_layout.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1+
import React from 'react';
12
import { Stack } from 'expo-router';
23
import { GestureHandlerRootView } from 'react-native-gesture-handler';
34
import { ChatWrapper } from '../components/ChatWrapper';
45
import { AppProvider } from '../context/AppContext';
56
import { StyleSheet } from 'react-native';
67
import { SafeAreaProvider } from 'react-native-safe-area-context';
8+
import { LiveLocationManagerProvider } from 'stream-chat-expo';
9+
import { watchLocation } from '../utils/watchLocation';
710

811
export default function Layout() {
912
return (
1013
<SafeAreaProvider>
1114
<GestureHandlerRootView style={styles.container}>
1215
<ChatWrapper>
13-
<AppProvider>
14-
<Stack />
15-
</AppProvider>
16+
<LiveLocationManagerProvider watchLocation={watchLocation}>
17+
<AppProvider>
18+
<Stack />
19+
</AppProvider>
20+
</LiveLocationManagerProvider>
1621
</ChatWrapper>
1722
</GestureHandlerRootView>
1823
</SafeAreaProvider>

examples/ExpoMessaging/app/channel/[cid]/index.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { Stack, useRouter } from 'expo-router';
55
import { AuthProgressLoader } from '../../../components/AuthProgressLoader';
66
import { AppContext } from '../../../context/AppContext';
77
import { useHeaderHeight } from '@react-navigation/elements';
8+
import InputButtons from '../../../components/InputButtons';
9+
import { MessageLocation } from '../../../components/LocationSharing/MessageLocation';
810

911
export default function ChannelScreen() {
1012
const router = useRouter();
@@ -15,14 +17,31 @@ export default function ChannelScreen() {
1517
return <AuthProgressLoader />;
1618
}
1719

20+
const onPressMessage: NonNullable<React.ComponentProps<typeof Channel>['onPressMessage']> = (
21+
payload,
22+
) => {
23+
const { message, defaultHandler, emitter } = payload;
24+
const { shared_location } = message;
25+
if (emitter === 'messageContent' && shared_location) {
26+
// Create url params from shared_location
27+
const params = Object.entries(shared_location)
28+
.map(([key, value]) => `${key}=${value}`)
29+
.join('&');
30+
router.push(`/map/${message.id}?${params}`);
31+
}
32+
defaultHandler?.();
33+
};
34+
1835
return (
1936
<SafeAreaView>
2037
<Stack.Screen options={{ title: 'Channel Screen' }} />
2138
{channel && (
2239
<Channel
2340
audioRecordingEnabled={true}
2441
channel={channel}
42+
onPressMessage={onPressMessage}
2543
keyboardVerticalOffset={headerHeight}
44+
MessageLocation={MessageLocation}
2645
thread={thread}
2746
>
2847
<View style={{ flex: 1 }}>
@@ -32,7 +51,7 @@ export default function ChannelScreen() {
3251
router.push(`/channel/${channel.cid}/thread/${thread.cid}`);
3352
}}
3453
/>
35-
<MessageInput />
54+
<MessageInput InputButtons={InputButtons} />
3655
</View>
3756
</Channel>
3857
)}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { Stack, useLocalSearchParams } from 'expo-router';
2+
import {
3+
Platform,
4+
Pressable,
5+
useWindowDimensions,
6+
StyleSheet,
7+
View,
8+
Image,
9+
Text,
10+
} from 'react-native';
11+
import { useContext, useMemo, useCallback, useRef } from 'react';
12+
import { AppContext } from '../../context/AppContext';
13+
import { useChatContext, useHandleLiveLocationEvents, useTheme } from 'stream-chat-expo';
14+
import { SafeAreaView } from 'react-native-safe-area-context';
15+
import MapView, { MapMarker, Marker } from 'react-native-maps';
16+
import { SharedLocationResponse, StreamChat } from 'stream-chat';
17+
18+
export type SharedLiveLocationParamsStringType = SharedLocationResponse & {
19+
latitude: string;
20+
longitude: string;
21+
};
22+
23+
const MapScreenFooter = ({
24+
client,
25+
shared_location,
26+
locationResponse,
27+
isLiveLocationStopped,
28+
}: {
29+
client: StreamChat;
30+
shared_location: SharedLocationResponse;
31+
locationResponse?: SharedLocationResponse;
32+
isLiveLocationStopped?: boolean;
33+
}) => {
34+
const { channel } = useContext(AppContext);
35+
const { end_at, user_id } = shared_location;
36+
const {
37+
theme: {
38+
colors: { accent_blue, accent_red, grey },
39+
},
40+
} = useTheme();
41+
const liveLocationActive = isLiveLocationStopped ? false : new Date(end_at) > new Date();
42+
const endedAtDate = end_at ? new Date(end_at) : null;
43+
const formattedEndedAt = endedAtDate ? endedAtDate.toLocaleString() : '';
44+
45+
const stopSharingLiveLocation = useCallback(async () => {
46+
await channel.stopLiveLocationSharing(locationResponse);
47+
}, [channel, locationResponse]);
48+
49+
if (!end_at) {
50+
return null;
51+
}
52+
53+
const isCurrentUser = user_id === client.user.id;
54+
if (!isCurrentUser) {
55+
return (
56+
<View style={styles.footer}>
57+
<Text style={[styles.footerText, { color: liveLocationActive ? accent_blue : accent_red }]}>
58+
{liveLocationActive ? 'Live Location' : 'Live Location ended'}
59+
</Text>
60+
<Text style={[styles.footerDescription, { color: grey }]}>
61+
{liveLocationActive
62+
? `Live until: ${formattedEndedAt}`
63+
: `Location last updated at: ${formattedEndedAt}`}
64+
</Text>
65+
</View>
66+
);
67+
}
68+
69+
if (liveLocationActive) {
70+
return (
71+
<View style={styles.footer}>
72+
<Pressable
73+
style={({ pressed }) => [styles.footerButton, { opacity: pressed ? 0.5 : 1 }]}
74+
onPress={stopSharingLiveLocation}
75+
hitSlop={10}
76+
>
77+
<Text style={[styles.footerText, { color: accent_red }]}>Stop Sharing</Text>
78+
</Pressable>
79+
80+
<Text style={[styles.footerDescription, { color: grey }]}>
81+
Live until: {formattedEndedAt}
82+
</Text>
83+
</View>
84+
);
85+
}
86+
87+
return (
88+
<View style={styles.footer}>
89+
<Text style={[styles.footerText, { color: accent_red }]}>Live Location ended</Text>
90+
<Text style={[styles.footerDescription, { color: grey }]}>
91+
Location last updated at: {formattedEndedAt}
92+
</Text>
93+
</View>
94+
);
95+
};
96+
97+
export default function MapScreen() {
98+
const { client } = useChatContext();
99+
const shared_location = useLocalSearchParams<SharedLiveLocationParamsStringType>();
100+
const { channel } = useContext(AppContext);
101+
const mapRef = useRef<MapView | null>(null);
102+
const markerRef = useRef<MapMarker | null>(null);
103+
const {
104+
theme: {
105+
colors: { accent_blue },
106+
},
107+
} = useTheme();
108+
109+
const { width, height } = useWindowDimensions();
110+
const aspect_ratio = width / height;
111+
112+
const onLocationUpdate = useCallback((location: SharedLocationResponse) => {
113+
const newPosition = {
114+
latitude: location.latitude,
115+
longitude: location.longitude,
116+
latitudeDelta: 0.1,
117+
longitudeDelta: 0.1 * aspect_ratio,
118+
};
119+
// Animate the map to the new position
120+
if (mapRef.current?.animateToRegion) {
121+
mapRef.current.animateToRegion(newPosition, 500);
122+
}
123+
// This is android only
124+
if (Platform.OS === 'android' && markerRef.current?.animateMarkerToCoordinate) {
125+
markerRef.current.animateMarkerToCoordinate(newPosition, 500);
126+
}
127+
}, []);
128+
129+
const { isLiveLocationStopped, locationResponse } = useHandleLiveLocationEvents({
130+
channel,
131+
messageId: shared_location.message_id,
132+
onLocationUpdate,
133+
});
134+
135+
const initialRegion = useMemo(() => {
136+
const latitudeDelta = 0.1;
137+
const longitudeDelta = latitudeDelta * aspect_ratio;
138+
139+
return {
140+
latitude: parseFloat(shared_location.latitude),
141+
longitude: parseFloat(shared_location.longitude),
142+
latitudeDelta,
143+
longitudeDelta,
144+
};
145+
}, [aspect_ratio]);
146+
147+
const region = useMemo(() => {
148+
const latitudeDelta = 0.1;
149+
const longitudeDelta = latitudeDelta * aspect_ratio;
150+
return {
151+
latitude: locationResponse?.latitude,
152+
longitude: locationResponse?.longitude,
153+
latitudeDelta,
154+
longitudeDelta,
155+
};
156+
}, [aspect_ratio, locationResponse]);
157+
158+
return (
159+
<SafeAreaView style={styles.container} edges={['bottom']}>
160+
<Stack.Screen options={{ title: 'Map Screen' }} />
161+
<MapView
162+
cameraZoomRange={{ maxCenterCoordinateDistance: 3000 }}
163+
initialRegion={initialRegion}
164+
ref={mapRef}
165+
style={styles.mapView}
166+
>
167+
{shared_location.end_at ? (
168+
<Marker
169+
coordinate={
170+
!locationResponse
171+
? { latitude: initialRegion.latitude, longitude: initialRegion.longitude }
172+
: { latitude: region.latitude, longitude: region.longitude }
173+
}
174+
ref={markerRef}
175+
>
176+
<View style={styles.markerWrapper}>
177+
<Image
178+
style={[styles.markerImage, { borderColor: accent_blue }]}
179+
source={{ uri: client.user.image }}
180+
/>
181+
</View>
182+
</Marker>
183+
) : (
184+
<Marker coordinate={initialRegion} ref={markerRef} pinColor={accent_blue} />
185+
)}
186+
</MapView>
187+
<MapScreenFooter
188+
client={client}
189+
shared_location={shared_location}
190+
locationResponse={locationResponse}
191+
isLiveLocationStopped={isLiveLocationStopped}
192+
/>
193+
</SafeAreaView>
194+
);
195+
}
196+
197+
const IMAGE_SIZE = 35;
198+
199+
const styles = StyleSheet.create({
200+
container: {
201+
flex: 1,
202+
},
203+
mapView: {
204+
width: 'auto',
205+
flex: 3,
206+
},
207+
markerWrapper: {
208+
overflow: 'hidden', // REQUIRED for rounded corners to show on Android
209+
},
210+
markerImage: {
211+
width: IMAGE_SIZE,
212+
height: IMAGE_SIZE,
213+
borderRadius: IMAGE_SIZE / 2,
214+
resizeMode: 'cover', // or 'contain' if image is cropped
215+
borderWidth: 2,
216+
},
217+
footer: {
218+
marginVertical: 8,
219+
},
220+
footerButton: {
221+
padding: 4,
222+
},
223+
footerText: {
224+
textAlign: 'center',
225+
fontSize: 14,
226+
},
227+
footerDescription: {
228+
textAlign: 'center',
229+
fontSize: 12,
230+
marginTop: 4,
231+
},
232+
});

examples/ExpoMessaging/components/ChatWrapper.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { PropsWithChildren } from 'react';
22
import {
33
Chat,
4+
enTranslations,
45
OverlayProvider,
56
SqliteClient,
67
Streami18n,
@@ -24,6 +25,12 @@ export const ChatWrapper = ({ children }: PropsWithChildren<{}>) => {
2425
userData: user,
2526
tokenOrProvider: userToken,
2627
});
28+
29+
streami18n.registerTranslation('en', {
30+
...enTranslations,
31+
'timestamp/Location end at': '{{ milliseconds | durationFormatter(withSuffix: false) }}',
32+
});
33+
2734
const theme = useStreamChatTheme();
2835

2936
if (!chatClient) {

0 commit comments

Comments
 (0)