diff --git a/.nvmrc b/.nvmrc index 0828ab7947..85aee5a534 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18 \ No newline at end of file +v20 \ No newline at end of file diff --git a/docusaurus/docs/reactnative/assets/guides/live-location-sharing/chat-screen.png b/docusaurus/docs/reactnative/assets/guides/live-location-sharing/chat-screen.png new file mode 100644 index 0000000000..7448b91fde Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/live-location-sharing/chat-screen.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/live-location-sharing/detail-screen.png b/docusaurus/docs/reactnative/assets/guides/live-location-sharing/detail-screen.png new file mode 100644 index 0000000000..b4fe186670 Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/live-location-sharing/detail-screen.png differ diff --git a/docusaurus/docs/reactnative/guides/live-location-sharing.mdx b/docusaurus/docs/reactnative/guides/live-location-sharing.mdx new file mode 100644 index 0000000000..1ea00626b0 --- /dev/null +++ b/docusaurus/docs/reactnative/guides/live-location-sharing.mdx @@ -0,0 +1,644 @@ +--- +id: live-location-sharing +title: Live location Sharing Message +--- + +In this cookbook, we will build a simple live location sharing message feature using [custom attachments](./custom-attachment.mdx). + +The goal is to create two screens: + +- Chat screen with the live location shown on map in a message +- A detailed map screen that is shown when the live location is tapped + +## Setup + +### Install Dependencies + +Run the following commands in your terminal of choice: + +```bash +yarn add @react-native-community/geolocation +yarn add react-native-maps +``` + +- `@react-native-community/geolocation` library is used to watch the current location of the user and then send it in the message +- `react-native-maps` is used to display the location of the user using the native maps present on iOS and Android + +### Configure location permissions + +**iOS** + +You need to include `NSLocationWhenInUseUsageDescription` and `NSLocationAlwaysAndWhenInUseUsageDescription` in `Info.plist` to enable geolocation when using the app. + +**Android** + +To request access to location, you need to add the following line to your app's `AndroidManifest.xml`: + +```xml + + +``` + +### Android: Add API Key + +On Android, one has to use [Google Maps](https://developers.google.com/maps/documentation/), which in turn requires you to obtain an [API key for the Android SDK](https://developers.google.com/maps/documentation/android-sdk/signup). On iOS, the native Apple Maps implementation is used and API keys are not necessary. + +Add your API key to your manifest file (`android/app/src/main/AndroidManifest.xml`): + +```xml + + + + +``` + +## Implementation outline + +We will use a custom attachment with a `type` set to `location`. This custom attachment will be part of the message the app sends to the channel. This attachment will carry the latest longitude, latitude, and timestamp when the location sharing has been stopped. + +On the sharer side, the application itself will poll for location updates and whenever a new location is emitted, it will update the appropriate message/attachment through our `channel.updateMessage()` APIs. + +On the recipient side, the application will listen for `message.updated` events and will update the interactive maps accordingly. + +## The attachment object + +We need to know the latitude and longitude data in a message to show the location. Additionally, we need the time when the live location sharing was stopped. To have this lets have our message to have the following structure: + +```js +const messageWithLocation = { + attachments: [ + { + type: 'location', + latitude: 50.212312, + longitude: -71.212659, + ended_at: '2012-07-14T01:00:00+01:00', + }, + ], +}; +``` + +In Typescript, we can define this in type using generics, + +```ts +import { DefaultStreamChatGenerics } from 'stream-chat-react-native'; +import { StreamChat } from 'stream-chat'; + +type LocalAttachmentType = DefaultStreamChatGenerics['attachmentType'] & { + latitude?: number; + longitude?: number; + ended_at?: string; +}; + +export type StreamChatGenerics = DefaultStreamChatGenerics & { + attachmentType: LocalAttachmentType; +}; + +// and use the generics when creating the client +const client = StreamChat.getInstance(''); +``` + +## Step 1: Create Live Location Sharing Context + +In order for the location to be started or stopped in different screens. Lets add the start or stop methods in a React Context. + +- The `Geolocation.watchPosition` method is used to watch the location of the user. +- The `client.updateMessage` method is used to update an existing message with the new location. + +Below is an implementation of the context: + +```ts title="InputButtons.tsx" +import React, {createContext, useContext} from 'react'; +import {useChatContext} from 'stream-chat-react-native'; +import Geolocation, { + GeolocationResponse, +} from '@react-native-community/geolocation'; + +Geolocation.setRNConfiguration({ + skipPermissionRequests: false, + authorizationLevel: 'always', + enableBackgroundLocationUpdates: true, +}); + +interface LiveLocationContextValue { + startLiveLocation: (messageId: string) => void; + stopLiveLocation: (messageId: string) => void; + isWatching: (messageId: string) => boolean; +} + +const LiveLocationContext = createContext({ + startLiveLocation: () => {}, + stopLiveLocation: () => {}, + isWatching: () => false, +}); + +export const useLiveLocationContext = () => { + return useContext(LiveLocationContext); +}; + +// a map of message IDs to live location watch IDs +const messageIdToLiveWatchMap = new Map(); + +const isWatching = (id: string) => { + return messageIdToLiveWatchMap.has(id); +}; + +export const LiveLocationContextProvider = ( + props: React.PropsWithChildren<{}>, +) => { + const {client} = useChatContext(); + + const lastLocationRef = React.useRef(); + + // watch live location and update message + const startLiveLocation = React.useCallback( + (id: string) => { + const watchId = Geolocation.watchPosition( + position => { + client.updateMessage({ + id, + attachments: [ + { + type: 'location', + latitude: position.coords.latitude, + longitude: position.coords.longitude, + }, + ], + }); + lastLocationRef.current = position; + }, + error => { + console.error('watchPosition', error); + }, + { + enableHighAccuracy: true, + timeout: 20000, + maximumAge: 1000, + interval: 5000, // android only + }, + ); + messageIdToLiveWatchMap.set(id, watchId); + }, + [client], + ); + + // stop watching live location and send message with ended time + const stopLiveLocation = React.useCallback( + (id: string) => { + const watchId = messageIdToLiveWatchMap.get(id); + if (watchId !== undefined) { + messageIdToLiveWatchMap.delete(id); + Geolocation.clearWatch(watchId); + if (lastLocationRef.current) { + client.updateMessage({ + id, + attachments: [ + { + type: 'location', + latitude: lastLocationRef.current.coords.latitude, + longitude: lastLocationRef.current.coords.longitude, + ended_at: new Date().toISOString(), + }, + ], + }); + } + } + }, + [client], + ); + + const contextValue: LiveLocationContextValue = { + startLiveLocation, + stopLiveLocation, + isWatching, + }; + + return ( + + {props.children} + + ); +}; + +``` + +Since the context needs the instance of the Stream Chat Client. Lets make sure that this component is added below the [`Chat` context component](../../core-components/chat). + +```ts + + + // ...add your screens here + + +``` + +## Step 2: Add Live Location Sharing Button + +Lets add a "Share Live Location" button next to input box. Channel component accepts a prop `InputButtons`, to add some custom buttons next to input box. When user presses this button, it should fetch the current location coordinates of user, and send a message on channel and then starting watching for live location. + +Below is an implementation of this button: + +```ts title="InputButtons.tsx" +import React from 'react'; +import {Pressable, StyleSheet} from 'react-native'; +import { + Channel, + useChannelContext, + InputButtons as DefaultInputButtons, + useTheme, +} from 'stream-chat-react-native'; +import Svg, {Path} from 'react-native-svg'; +import Geolocation from '@react-native-community/geolocation'; +import {useLiveLocationContext} from './LiveLocationContext'; + +// Icon for "Share Location" button, next to input box. +const ShareLocationIcon = () => { + const { + theme: { + colors: {grey}, + }, + } = useTheme(); + return ( + + + + + ); +}; + +const InputButtons: NonNullable< + React.ComponentProps['InputButtons'] +> = props => { + const {channel: currentChannel} = useChannelContext(); + const {startLiveLocation} = useLiveLocationContext(); + + const sendLiveLocation = async () => { + Geolocation.getCurrentPosition( + async position => { + // create message with initial location + const response = await currentChannel.sendMessage({ + attachments: [ + { + type: 'location', + latitude: position.coords.latitude, + longitude: position.coords.longitude, + }, + ], + }); + // then start watching for live location + startLiveLocation(response.message.id); + }, + error => { + console.error('getCurrentPosition', error); + }, + { + enableHighAccuracy: true, + timeout: 20000, + maximumAge: 1000, + }, + ); + }; + + return ( + <> + + + + + + ); +}; + +const styles = StyleSheet.create({ + liveLocationButton: { + paddingLeft: 5, + }, +}); + +export default InputButtons; +``` + +## Step 3: Create Message Card with map showing location + +Lets add a component to show the location in a map on a message with a button to stop sharing the location. Channel component accepts a prop called `Card` to render any type of custom attachment. + +Below is an implementation of this card: + +```ts title="Card.tsx" +import React, {useMemo} from 'react'; +import {Button, StyleSheet, useWindowDimensions} from 'react-native'; +import MapView, {Marker} from 'react-native-maps'; +import { + Channel, + Card as DefaultCard, + useMessageContext, + useMessageOverlayContext, + useOverlayContext, +} from 'stream-chat-react-native'; +import {useLiveLocationContext} from './LiveLocationContext'; +import {StreamChatGenerics} from './types'; + +const MapCard = ({ + latitude, + longitude, + ended_at, +}: { + latitude: number; + longitude: number; + ended_at?: string; +}) => { + const {width, height} = useWindowDimensions(); + const aspect_ratio = width / height; + const {stopLiveLocation} = useLiveLocationContext(); + + const {isMyMessage, message} = useMessageContext(); + const {data} = useMessageOverlayContext(); + const {overlay} = useOverlayContext(); + const overlayId = data?.message?.id; + // is this message shown on overlay? If yes, then don't show the button + const isOverlayOpen = overlay === 'message' && overlayId === message.id; + const showStopSharingButton = !ended_at && isMyMessage && !isOverlayOpen; + + // Convert ISO date string to Date object + const endedAtDate = ended_at ? new Date(ended_at) : null; + + // Format the date to a readable string + const formattedEndedAt = endedAtDate ? endedAtDate.toLocaleString() : ''; + + // this is to compute the zoom level and centre for the map view + const region = useMemo(() => { + const latitudeDelta = 0.02; + const longitudeDelta = latitudeDelta * aspect_ratio; + + // For reference, check - + // https://github.com/react-native-maps/react-native-maps/blob/master/example/src/examples/DisplayLatLng.tsx + return { + latitude, + longitude, + latitudeDelta, + longitudeDelta, + }; + }, [aspect_ratio, latitude, longitude]); + + return ( + <> + + + + {showStopSharingButton && ( +