Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions examples/ExpoMessaging/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,38 @@
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true,
"usesIcloudStorage": true,
"bundleIdentifier": "io.stream.expomessagingapp",
"appleTeamId": "EHV7XZLAHA"
},
"android": {
"config": {
"googleMaps": {
"apiKey": "AIzaSyDVh35biMyXbOjt74CQyO1dlqSMlrdHOOA"
}
},
"package": "io.stream.expomessagingapp",
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"permissions": [
"android.permission.RECORD_AUDIO",
"android.permission.MODIFY_AUDIO_SETTINGS"
]
"permissions": ["android.permission.RECORD_AUDIO", "android.permission.MODIFY_AUDIO_SETTINGS"]
},
"web": {
"favicon": "./assets/favicon.png",
"bundler": "metro"
},
"scheme": "ExpoMessaging",
"plugins": [
[
"expo-location",
{
"locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location."
}
],
"expo-router",
[
"expo-image-picker",
Expand All @@ -63,6 +69,7 @@
"microphonePermission": "$(PRODUCT_NAME) would like to use your microphone for voice recording."
}
],

"./plugins/keyboardInsetMainActivityListener.js"
]
}
Expand Down
11 changes: 8 additions & 3 deletions examples/ExpoMessaging/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import React from 'react';
import { Stack } from 'expo-router';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { ChatWrapper } from '../components/ChatWrapper';
import { AppProvider } from '../context/AppContext';
import { StyleSheet } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { LiveLocationManagerProvider } from 'stream-chat-expo';
import { watchLocation } from '../utils/watchLocation';

export default function Layout() {
return (
<SafeAreaProvider>
<GestureHandlerRootView style={styles.container}>
<ChatWrapper>
<AppProvider>
<Stack />
</AppProvider>
<LiveLocationManagerProvider watchLocation={watchLocation}>
<AppProvider>
<Stack />
</AppProvider>
</LiveLocationManagerProvider>
</ChatWrapper>
</GestureHandlerRootView>
</SafeAreaProvider>
Expand Down
21 changes: 20 additions & 1 deletion examples/ExpoMessaging/app/channel/[cid]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Stack, useRouter } from 'expo-router';
import { AuthProgressLoader } from '../../../components/AuthProgressLoader';
import { AppContext } from '../../../context/AppContext';
import { useHeaderHeight } from '@react-navigation/elements';
import InputButtons from '../../../components/InputButtons';
import { MessageLocation } from '../../../components/LocationSharing/MessageLocation';

export default function ChannelScreen() {
const router = useRouter();
Expand All @@ -15,14 +17,31 @@ export default function ChannelScreen() {
return <AuthProgressLoader />;
}

const onPressMessage: NonNullable<React.ComponentProps<typeof Channel>['onPressMessage']> = (
payload,
) => {
const { message, defaultHandler, emitter } = payload;
const { shared_location } = message;
if (emitter === 'messageContent' && shared_location) {
// Create url params from shared_location
const params = Object.entries(shared_location)
.map(([key, value]) => `${key}=${value}`)
.join('&');
router.push(`/map/${message.id}?${params}`);
}
defaultHandler?.();
};

return (
<SafeAreaView>
<Stack.Screen options={{ title: 'Channel Screen' }} />
{channel && (
<Channel
audioRecordingEnabled={true}
channel={channel}
onPressMessage={onPressMessage}
keyboardVerticalOffset={headerHeight}
MessageLocation={MessageLocation}
thread={thread}
>
<View style={{ flex: 1 }}>
Expand All @@ -32,7 +51,7 @@ export default function ChannelScreen() {
router.push(`/channel/${channel.cid}/thread/${thread.cid}`);
}}
/>
<MessageInput />
<MessageInput InputButtons={InputButtons} />
</View>
</Channel>
)}
Expand Down
232 changes: 232 additions & 0 deletions examples/ExpoMessaging/app/map/[id].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { Stack, useLocalSearchParams } from 'expo-router';
import {
Platform,
Pressable,
useWindowDimensions,
StyleSheet,
View,
Image,
Text,
} from 'react-native';
import { useContext, useMemo, useCallback, useRef } from 'react';
import { AppContext } from '../../context/AppContext';
import { useChatContext, useHandleLiveLocationEvents, useTheme } from 'stream-chat-expo';
import { SafeAreaView } from 'react-native-safe-area-context';
import MapView, { MapMarker, Marker } from 'react-native-maps';
import { SharedLocationResponse, StreamChat } from 'stream-chat';

export type SharedLiveLocationParamsStringType = SharedLocationResponse & {
latitude: string;
longitude: string;
};

const MapScreenFooter = ({
client,
shared_location,
locationResponse,
isLiveLocationStopped,
}: {
client: StreamChat;
shared_location: SharedLocationResponse;
locationResponse?: SharedLocationResponse;
isLiveLocationStopped?: boolean;
}) => {
const { channel } = useContext(AppContext);
const { end_at, user_id } = shared_location;
const {
theme: {
colors: { accent_blue, accent_red, grey },
},
} = useTheme();
const liveLocationActive = isLiveLocationStopped ? false : new Date(end_at) > new Date();
const endedAtDate = end_at ? new Date(end_at) : null;
const formattedEndedAt = endedAtDate ? endedAtDate.toLocaleString() : '';

const stopSharingLiveLocation = useCallback(async () => {
await channel.stopLiveLocationSharing(locationResponse);
}, [channel, locationResponse]);

if (!end_at) {
return null;
}

const isCurrentUser = user_id === client.user.id;
if (!isCurrentUser) {
return (
<View style={styles.footer}>
<Text style={[styles.footerText, { color: liveLocationActive ? accent_blue : accent_red }]}>
{liveLocationActive ? 'Live Location' : 'Live Location ended'}
</Text>
<Text style={[styles.footerDescription, { color: grey }]}>
{liveLocationActive
? `Live until: ${formattedEndedAt}`
: `Location last updated at: ${formattedEndedAt}`}
</Text>
</View>
);
}

if (liveLocationActive) {
return (
<View style={styles.footer}>
<Pressable
style={({ pressed }) => [styles.footerButton, { opacity: pressed ? 0.5 : 1 }]}
onPress={stopSharingLiveLocation}
hitSlop={10}
>
<Text style={[styles.footerText, { color: accent_red }]}>Stop Sharing</Text>
</Pressable>

<Text style={[styles.footerDescription, { color: grey }]}>
Live until: {formattedEndedAt}
</Text>
</View>
);
}

return (
<View style={styles.footer}>
<Text style={[styles.footerText, { color: accent_red }]}>Live Location ended</Text>
<Text style={[styles.footerDescription, { color: grey }]}>
Location last updated at: {formattedEndedAt}
</Text>
</View>
);
};

export default function MapScreen() {
const { client } = useChatContext();
const shared_location = useLocalSearchParams<SharedLiveLocationParamsStringType>();
const { channel } = useContext(AppContext);
const mapRef = useRef<MapView | null>(null);
const markerRef = useRef<MapMarker | null>(null);
const {
theme: {
colors: { accent_blue },
},
} = useTheme();

const { width, height } = useWindowDimensions();
const aspect_ratio = width / height;

const onLocationUpdate = useCallback((location: SharedLocationResponse) => {
const newPosition = {
latitude: location.latitude,
longitude: location.longitude,
latitudeDelta: 0.1,
longitudeDelta: 0.1 * aspect_ratio,
};
// Animate the map to the new position
if (mapRef.current?.animateToRegion) {
mapRef.current.animateToRegion(newPosition, 500);
}
// This is android only
if (Platform.OS === 'android' && markerRef.current?.animateMarkerToCoordinate) {
markerRef.current.animateMarkerToCoordinate(newPosition, 500);
}
}, []);

const { isLiveLocationStopped, locationResponse } = useHandleLiveLocationEvents({
channel,
messageId: shared_location.message_id,
onLocationUpdate,
});

const initialRegion = useMemo(() => {
const latitudeDelta = 0.1;
const longitudeDelta = latitudeDelta * aspect_ratio;

return {
latitude: parseFloat(shared_location.latitude),
longitude: parseFloat(shared_location.longitude),
latitudeDelta,
longitudeDelta,
};
}, [aspect_ratio]);

const region = useMemo(() => {
const latitudeDelta = 0.1;
const longitudeDelta = latitudeDelta * aspect_ratio;
return {
latitude: locationResponse?.latitude,
longitude: locationResponse?.longitude,
latitudeDelta,
longitudeDelta,
};
}, [aspect_ratio, locationResponse]);

return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<Stack.Screen options={{ title: 'Map Screen' }} />
<MapView
cameraZoomRange={{ maxCenterCoordinateDistance: 3000 }}
initialRegion={initialRegion}
ref={mapRef}
style={styles.mapView}
>
{shared_location.end_at ? (
<Marker
coordinate={
!locationResponse
? { latitude: initialRegion.latitude, longitude: initialRegion.longitude }
: { latitude: region.latitude, longitude: region.longitude }
}
ref={markerRef}
>
<View style={styles.markerWrapper}>
<Image
style={[styles.markerImage, { borderColor: accent_blue }]}
source={{ uri: client.user.image }}
/>
</View>
</Marker>
) : (
<Marker coordinate={initialRegion} ref={markerRef} pinColor={accent_blue} />
)}
</MapView>
<MapScreenFooter
client={client}
shared_location={shared_location}
locationResponse={locationResponse}
isLiveLocationStopped={isLiveLocationStopped}
/>
</SafeAreaView>
);
}

const IMAGE_SIZE = 35;

const styles = StyleSheet.create({
container: {
flex: 1,
},
mapView: {
width: 'auto',
flex: 3,
},
markerWrapper: {
overflow: 'hidden', // REQUIRED for rounded corners to show on Android
},
markerImage: {
width: IMAGE_SIZE,
height: IMAGE_SIZE,
borderRadius: IMAGE_SIZE / 2,
resizeMode: 'cover', // or 'contain' if image is cropped
borderWidth: 2,
},
footer: {
marginVertical: 8,
},
footerButton: {
padding: 4,
},
footerText: {
textAlign: 'center',
fontSize: 14,
},
footerDescription: {
textAlign: 'center',
fontSize: 12,
marginTop: 4,
},
});
7 changes: 7 additions & 0 deletions examples/ExpoMessaging/components/ChatWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { PropsWithChildren } from 'react';
import {
Chat,
enTranslations,
OverlayProvider,
SqliteClient,
Streami18n,
Expand All @@ -24,6 +25,12 @@ export const ChatWrapper = ({ children }: PropsWithChildren<{}>) => {
userData: user,
tokenOrProvider: userToken,
});

streami18n.registerTranslation('en', {
...enTranslations,
'timestamp/Location end at': '{{ milliseconds | durationFormatter(withSuffix: false) }}',
});

const theme = useStreamChatTheme();

if (!chatClient) {
Expand Down
Loading
Loading