diff --git a/examples/vite/src/App.tsx b/examples/vite/src/App.tsx index f8df1ed806..3d5a1dffbb 100644 --- a/examples/vite/src/App.tsx +++ b/examples/vite/src/App.tsx @@ -1,4 +1,9 @@ -import { ChannelFilters, ChannelOptions, ChannelSort } from 'stream-chat'; +import { + ChannelFilters, + ChannelOptions, + ChannelSort, + LiveLocationManagerConstructorParameters, +} from 'stream-chat'; import { AIStateIndicator, Channel, @@ -13,6 +18,8 @@ import { useCreateChatClient, ThreadList, ChatView, + useChatContext, + useLiveLocationSharingManager, } from 'stream-chat-react'; import 'stream-chat-react/css/v2/index.css'; @@ -64,6 +71,56 @@ type StreamChatGenerics = { userType: LocalUserType; }; +const ShareLiveLocation = () => { + const { channel } = useChatContext(); + + return ( + + ); +}; + +const watchLocationNormal: LiveLocationManagerConstructorParameters['watchLocation'] = ( + watcher, +) => { + const watch = navigator.geolocation.watchPosition((position) => { + watcher({ latitude: position.coords.latitude, longitude: position.coords.longitude }); + }); + + return () => navigator.geolocation.clearWatch(watch); +}; + +const watchLocationTimed: LiveLocationManagerConstructorParameters['watchLocation'] = (watcher) => { + const timer = setInterval(() => { + navigator.geolocation.getCurrentPosition((position) => { + watcher({ latitude: position.coords.latitude, longitude: position.coords.longitude }); + }); + }, 5000); + + return () => { + clearInterval(timer); + console.log('cleanup'); + }; +}; + const App = () => { const chatClient = useCreateChatClient({ apiKey, @@ -71,6 +128,11 @@ const App = () => { userData: { id: userId }, }); + useLiveLocationSharingManager({ + client: chatClient, + watchLocation: watchLocationNormal, + }); + if (!chatClient) return <>Loading...; return ( @@ -92,6 +154,7 @@ const App = () => { + diff --git a/src/components/Attachment/Attachment.tsx b/src/components/Attachment/Attachment.tsx index f1c32c43a8..47356aba9f 100644 --- a/src/components/Attachment/Attachment.tsx +++ b/src/components/Attachment/Attachment.tsx @@ -17,6 +17,7 @@ import { CardContainer, FileContainer, GalleryContainer, + GeolocationContainer, ImageContainer, MediaContainer, UnsupportedAttachmentContainer, @@ -38,6 +39,7 @@ const CONTAINER_MAP = { audio: AudioContainer, card: CardContainer, file: FileContainer, + geolocation: GeolocationContainer, media: MediaContainer, unsupported: UnsupportedAttachmentContainer, voiceRecording: VoiceRecordingContainer, @@ -51,6 +53,7 @@ export const ATTACHMENT_GROUPS_ORDER = [ 'audio', 'voiceRecording', 'file', + 'geolocation', 'unsupported', ] as const; @@ -71,6 +74,9 @@ export type AttachmentProps< File?: React.ComponentType>; /** Custom UI component for displaying a gallery of image type attachments, defaults to and accepts same props as: [Gallery](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Gallery/Gallery.tsx) */ Gallery?: React.ComponentType>; + Geolocation?: React.ComponentType<{ + attachment: StreamAttachment; + }>; /** Custom UI component for displaying an image type attachment, defaults to and accepts same props as: [Image](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Gallery/Image.tsx) */ Image?: React.ComponentType; /** Optional flag to signal that an attachment is a displayed as a part of a quoted message */ @@ -144,6 +150,7 @@ const renderGroupedAttachments = < image: [], // eslint-disable-next-line sort-keys gallery: [], + geolocation: [], voiceRecording: [], }, ); @@ -183,6 +190,8 @@ const getAttachmentType = < return 'voiceRecording'; } else if (isFileAttachment(attachment)) { return 'file'; + } else if (attachment.type === 'live_location' || attachment.type === 'static_location') { + return 'geolocation'; } return 'unsupported'; diff --git a/src/components/Attachment/AttachmentContainer.tsx b/src/components/Attachment/AttachmentContainer.tsx index 94e73d179a..7a721bdaec 100644 --- a/src/components/Attachment/AttachmentContainer.tsx +++ b/src/components/Attachment/AttachmentContainer.tsx @@ -11,6 +11,7 @@ import { Gallery as DefaultGallery, ImageComponent as DefaultImage } from '../Ga import { Card as DefaultCard } from './Card'; import { FileAttachment as DefaultFile } from './FileAttachment'; import { UnsupportedAttachment as DefaultUnsupportedAttachment } from './UnsupportedAttachment'; +import { Geolocation as DefaultGeolocation } from './Geolocation'; import { AttachmentComponentType, GalleryAttachment, @@ -318,6 +319,17 @@ export const MediaContainer = < ); }; +export const GeolocationContainer = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics +>({ + attachment, + Geolocation = DefaultGeolocation, +}: RenderAttachmentProps) => ( + <> + + +); + export const UnsupportedAttachmentContainer = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics >({ diff --git a/src/components/Attachment/Geolocation.tsx b/src/components/Attachment/Geolocation.tsx new file mode 100644 index 0000000000..366a8ed1bd --- /dev/null +++ b/src/components/Attachment/Geolocation.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import type { Attachment, DefaultGenerics, ExtendableGenerics } from 'stream-chat'; +import { useChatContext, useMessageContext } from '../../context'; + +export const Geolocation = ({ + attachment, +}: { + attachment: Attachment; +}) => { + const { channel } = useChatContext(); + const { isMyMessage, message } = useMessageContext(); + + const stoppedSharing = !!attachment.stopped_sharing; + const expired: boolean = + typeof attachment.end_time === 'string' && Date.now() > new Date(attachment.end_time).getTime(); + + return ( +
+ {attachment.type === 'live_location' && !stoppedSharing && !expired && isMyMessage() && ( + + )} + {/* TODO: {MAP} */} + + lat: {attachment.latitude}, lng: {attachment.longitude} + + {(stoppedSharing || expired) && Location sharing ended} +
+ ); +}; diff --git a/src/components/Attachment/hooks/useLiveLocationSharingManager.ts b/src/components/Attachment/hooks/useLiveLocationSharingManager.ts new file mode 100644 index 0000000000..22270f2d6c --- /dev/null +++ b/src/components/Attachment/hooks/useLiveLocationSharingManager.ts @@ -0,0 +1,37 @@ +import { LiveLocationManager } from 'stream-chat'; +import { useEffect, useMemo } from 'react'; +import type { + ExtendableGenerics, + LiveLocationManagerConstructorParameters, + StreamChat, +} from 'stream-chat'; + +export const useLiveLocationSharingManager = ({ + client, + retrieveAndDeserialize, + serializeAndStore, + watchLocation, +}: Omit, 'client'> & { + client?: StreamChat | null; +}) => { + const manager = useMemo(() => { + if (!client) return null; + + return new LiveLocationManager({ + client, + retrieveAndDeserialize, + serializeAndStore, + watchLocation, + }); + }, [client, retrieveAndDeserialize, serializeAndStore, watchLocation]); + + useEffect(() => { + if (!manager) return; + + manager.registerSubscriptions(); + + return () => manager.unregisterSubscriptions(); + }, [manager]); + + return manager; +}; diff --git a/src/components/Attachment/index.ts b/src/components/Attachment/index.ts index f1385fe7d5..31fce5f014 100644 --- a/src/components/Attachment/index.ts +++ b/src/components/Attachment/index.ts @@ -8,3 +8,4 @@ export * from './components'; export * from './UnsupportedAttachment'; export * from './FileAttachment'; export * from './utils'; +export * from './hooks/useLiveLocationSharingManager';