From 06b3bd32a913da58c4a97a0cb570d4cf0692ceee Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Fri, 20 Dec 2024 00:03:31 +0100 Subject: [PATCH 1/2] Initial commit --- examples/vite/src/App.tsx | 81 ++++++++++++++++++- src/components/Attachment/Attachment.tsx | 1 + .../hooks/useLiveLocationSharingManager.ts | 35 ++++++++ src/components/Attachment/index.ts | 1 + 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/components/Attachment/hooks/useLiveLocationSharingManager.ts diff --git a/examples/vite/src/App.tsx b/examples/vite/src/App.tsx index f8df1ed806..deed3e23e8 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,9 @@ import { useCreateChatClient, ThreadList, ChatView, + useChatContext, + useLiveLocationSharingManager, + Attachment, } from 'stream-chat-react'; import 'stream-chat-react/css/v2/index.css'; @@ -64,6 +72,53 @@ 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); +}; + const App = () => { const chatClient = useCreateChatClient({ apiKey, @@ -71,6 +126,13 @@ const App = () => { userData: { id: userId }, }); + const manager = useLiveLocationSharingManager({ + client: chatClient ?? undefined, + watchLocation: watchLocationTimed, + }); + + // const s = useStateStore(manager?.state) + if (!chatClient) return <>Loading...; return ( @@ -86,12 +148,27 @@ const App = () => { showChannelSearch additionalChannelSearchProps={{ searchForChannels: true }} /> - + { + const [attachment] = props.attachments ?? []; + + if (attachment?.type === 'live_location') { + return ( +
+ lat: {attachment.latitude}, lng: {attachment.longitude} +
+ ); + } + + return ; + }} + > +
diff --git a/src/components/Attachment/Attachment.tsx b/src/components/Attachment/Attachment.tsx index f1c32c43a8..a29c4d4d42 100644 --- a/src/components/Attachment/Attachment.tsx +++ b/src/components/Attachment/Attachment.tsx @@ -41,6 +41,7 @@ const CONTAINER_MAP = { media: MediaContainer, unsupported: UnsupportedAttachmentContainer, voiceRecording: VoiceRecordingContainer, + // geolocation: () =>
, } as const; export const ATTACHMENT_GROUPS_ORDER = [ diff --git a/src/components/Attachment/hooks/useLiveLocationSharingManager.ts b/src/components/Attachment/hooks/useLiveLocationSharingManager.ts new file mode 100644 index 0000000000..4b9c0b94e4 --- /dev/null +++ b/src/components/Attachment/hooks/useLiveLocationSharingManager.ts @@ -0,0 +1,35 @@ +import { LiveLocationManager } from 'stream-chat'; +import type { LiveLocationManagerConstructorParameters } from 'stream-chat'; +import { useEffect, useMemo } from 'react'; + +type PartialKeys = { + [L in keyof T]: L extends K ? T[L] | undefined : T[L]; +}; + +export const useLiveLocationSharingManager = ({ + client, + retrieveAndDeserialize, + serializeAndStore, + watchLocation, +}: PartialKeys) => { + 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'; From 3b8f2e16ad08f0a8036c1b2cc86b23ce6df9e24a Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Tue, 7 Jan 2025 20:04:30 +0100 Subject: [PATCH 2/2] Geolocation attachment --- examples/vite/src/App.tsx | 32 ++++--------- src/components/Attachment/Attachment.tsx | 10 +++- .../Attachment/AttachmentContainer.tsx | 12 +++++ src/components/Attachment/Geolocation.tsx | 48 +++++++++++++++++++ .../hooks/useLiveLocationSharingManager.ts | 18 +++---- 5 files changed, 88 insertions(+), 32 deletions(-) create mode 100644 src/components/Attachment/Geolocation.tsx diff --git a/examples/vite/src/App.tsx b/examples/vite/src/App.tsx index deed3e23e8..3d5a1dffbb 100644 --- a/examples/vite/src/App.tsx +++ b/examples/vite/src/App.tsx @@ -20,7 +20,6 @@ import { ChatView, useChatContext, useLiveLocationSharingManager, - Attachment, } from 'stream-chat-react'; import 'stream-chat-react/css/v2/index.css'; @@ -90,7 +89,7 @@ const ShareLiveLocation = () => { }); }, console.warn, - { timeout: 200 }, + { timeout: 2000 }, ); }} > @@ -116,7 +115,10 @@ const watchLocationTimed: LiveLocationManagerConstructorParameters['watchLocatio }); }, 5000); - return () => clearInterval(timer); + return () => { + clearInterval(timer); + console.log('cleanup'); + }; }; const App = () => { @@ -126,13 +128,11 @@ const App = () => { userData: { id: userId }, }); - const manager = useLiveLocationSharingManager({ - client: chatClient ?? undefined, - watchLocation: watchLocationTimed, + useLiveLocationSharingManager({ + client: chatClient, + watchLocation: watchLocationNormal, }); - // const s = useStateStore(manager?.state) - if (!chatClient) return <>Loading...; return ( @@ -148,21 +148,7 @@ const App = () => { showChannelSearch additionalChannelSearchProps={{ searchForChannels: true }} /> - { - const [attachment] = props.attachments ?? []; - - if (attachment?.type === 'live_location') { - return ( -
- lat: {attachment.latitude}, lng: {attachment.longitude} -
- ); - } - - return ; - }} - > + diff --git a/src/components/Attachment/Attachment.tsx b/src/components/Attachment/Attachment.tsx index a29c4d4d42..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,10 +39,10 @@ const CONTAINER_MAP = { audio: AudioContainer, card: CardContainer, file: FileContainer, + geolocation: GeolocationContainer, media: MediaContainer, unsupported: UnsupportedAttachmentContainer, voiceRecording: VoiceRecordingContainer, - // geolocation: () =>
, } as const; export const ATTACHMENT_GROUPS_ORDER = [ @@ -52,6 +53,7 @@ export const ATTACHMENT_GROUPS_ORDER = [ 'audio', 'voiceRecording', 'file', + 'geolocation', 'unsupported', ] as const; @@ -72,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 */ @@ -145,6 +150,7 @@ const renderGroupedAttachments = < image: [], // eslint-disable-next-line sort-keys gallery: [], + geolocation: [], voiceRecording: [], }, ); @@ -184,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 index 4b9c0b94e4..22270f2d6c 100644 --- a/src/components/Attachment/hooks/useLiveLocationSharingManager.ts +++ b/src/components/Attachment/hooks/useLiveLocationSharingManager.ts @@ -1,21 +1,23 @@ import { LiveLocationManager } from 'stream-chat'; -import type { LiveLocationManagerConstructorParameters } from 'stream-chat'; import { useEffect, useMemo } from 'react'; +import type { + ExtendableGenerics, + LiveLocationManagerConstructorParameters, + StreamChat, +} from 'stream-chat'; -type PartialKeys = { - [L in keyof T]: L extends K ? T[L] | undefined : T[L]; -}; - -export const useLiveLocationSharingManager = ({ +export const useLiveLocationSharingManager = ({ client, retrieveAndDeserialize, serializeAndStore, watchLocation, -}: PartialKeys) => { +}: Omit, 'client'> & { + client?: StreamChat | null; +}) => { const manager = useMemo(() => { if (!client) return null; - return new LiveLocationManager({ + return new LiveLocationManager({ client, retrieveAndDeserialize, serializeAndStore,