Skip to content

feat: add static location and live location support #2587

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 24, 2025
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
65 changes: 64 additions & 1 deletion examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ChannelSort,
LocalMessage,
TextComposerMiddleware,
LiveLocationManagerConstructorParameters,
} from 'stream-chat';
import {
AIStateIndicator,
Expand All @@ -18,9 +19,10 @@ import {
Thread,
ThreadList,
useCreateChatClient,
useMessageComposer,
VirtualizedMessageList as MessageList,
Window,
useChatContext,
useLiveLocationSharingManager,
} from 'stream-chat-react';
import { createTextComposerEmojiMiddleware, EmojiPicker } from 'stream-chat-react/emojis';
import { init, SearchIndex } from 'emoji-mart';
Expand Down Expand Up @@ -55,13 +57,73 @@ const sort: ChannelSort = { pinned_at: 1, last_message_at: -1, updated_at: -1 };
// @ts-ignore
const isMessageAIGenerated = (message: LocalMessage) => !!message?.ai_generated;

const ShareLiveLocation = () => {
const { channel } = useChatContext();

return (
<button
onClick={() => {
console.log('trying to fetch location');
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
console.log('got location ', position);
channel?.startLiveLocationSharing({
latitude,
longitude,
end_time: new Date(Date.now() + 1 * 1000 * 3600 * 24).toISOString(),
});
},
console.warn,
{ timeout: 2000 },
);
}}
>
location
</button>
);
};

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,
tokenOrProvider: userToken,
userData: { id: userId },
});

useLiveLocationSharingManager({
client: chatClient,
watchLocation: watchLocationNormal,
});

useEffect(() => {
if (!chatClient) return;

Expand Down Expand Up @@ -97,6 +159,7 @@ const App = () => {
<MessageList returnAllReadData />
<AIStateIndicator />
<MessageInput focus audioRecordingEnabled />
<ShareLiveLocation />
</Window>
<Thread virtualized />
</Channel>
Expand Down
3 changes: 1 addition & 2 deletions i18next-parser.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ module.exports = {
namespaceSeparator: false,
output: 'src/i18n/$LOCALE.json',
sort(a, b) {
return a < b ? -1 : 1; // alfabetical order
return a < b ? -1 : 1; // alphabetical order
},
useKeysAsDefaultValue: true,
verbose: true,
};
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@
"emoji-mart": "^5.4.0",
"react": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.14.0",
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.14.0",
"stream-chat": "^9.10.1"
"stream-chat": "^9.12.0"
},
"peerDependenciesMeta": {
"@breezystack/lamejs": {
Expand Down Expand Up @@ -183,7 +183,7 @@
"@playwright/test": "^1.42.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@stream-io/stream-chat-css": "^5.11.1",
"@stream-io/stream-chat-css": "^5.11.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
Expand Down Expand Up @@ -236,7 +236,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"semantic-release": "^24.2.3",
"stream-chat": "^9.10.1",
"stream-chat": "^9.12.0",
"ts-jest": "^29.2.5",
"typescript": "^5.4.5",
"typescript-eslint": "^8.17.0"
Expand Down
40 changes: 28 additions & 12 deletions src/components/Attachment/Attachment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
isFileAttachment,
isImageAttachment,
isScrapedContent,
isSharedLocationResponse,
isVideoAttachment,
isVoiceRecordingAttachment,
} from 'stream-chat';
Expand All @@ -13,6 +14,7 @@ import {
CardContainer,
FileContainer,
GalleryContainer,
GeolocationContainer,
ImageContainer,
MediaContainer,
UnsupportedAttachmentContainer,
Expand All @@ -21,7 +23,7 @@ import {
import { SUPPORTED_VIDEO_FORMATS } from './utils';

import type { ReactPlayerProps } from 'react-player';
import type { Attachment as StreamAttachment } from 'stream-chat';
import type { SharedLocationResponse, Attachment as StreamAttachment } from 'stream-chat';
import type { AttachmentActionsProps } from './AttachmentActions';
import type { AudioProps } from './Audio';
import type { VoiceRecordingProps } from './VoiceRecording';
Expand All @@ -31,6 +33,7 @@ import type { GalleryProps, ImageProps } from '../Gallery';
import type { UnsupportedAttachmentProps } from './UnsupportedAttachment';
import type { ActionHandlerReturnType } from '../Message/hooks/useActionHandler';
import type { GroupedRenderedAttachment } from './utils';
import type { GeolocationProps } from './Geolocation';

const CONTAINER_MAP = {
audio: AudioContainer,
Expand All @@ -49,12 +52,13 @@ export const ATTACHMENT_GROUPS_ORDER = [
'audio',
'voiceRecording',
'file',
'geolocation',
'unsupported',
] as const;

export type AttachmentProps = {
/** The message attachments to render, see [attachment structure](https://getstream.io/chat/docs/javascript/message_format/?language=javascript) **/
attachments: StreamAttachment[];
attachments: (StreamAttachment | SharedLocationResponse)[];
/** The handler function to call when an action is performed on an attachment, examples include canceling a \/giphy command or shuffling the results. */
actionHandler?: ActionHandlerReturnType;
/** Custom UI component for displaying attachment actions, defaults to and accepts same props as: [AttachmentActions](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Attachment/AttachmentActions.tsx) */
Expand All @@ -67,6 +71,7 @@ export type AttachmentProps = {
File?: React.ComponentType<FileAttachmentProps>;
/** 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<GalleryProps>;
Geolocation?: React.ComponentType<GeolocationProps>;
/** 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<ImageProps>;
/** Optional flag to signal that an attachment is a displayed as a part of a quoted message */
Expand Down Expand Up @@ -113,16 +118,26 @@ const renderGroupedAttachments = ({
.filter((attachment) => !isImageAttachment(attachment))
.reduce<GroupedRenderedAttachment>(
(typeMap, attachment) => {
const attachmentType = getAttachmentType(attachment);

const Container = CONTAINER_MAP[attachmentType];
typeMap[attachmentType].push(
<Container
key={`${attachmentType}-${typeMap[attachmentType].length}`}
{...rest}
attachment={attachment}
/>,
);
if (isSharedLocationResponse(attachment)) {
typeMap.geolocation.push(
<GeolocationContainer
{...rest}
key='geolocation-container'
location={attachment}
/>,
);
} else {
const attachmentType = getAttachmentType(attachment);

const Container = CONTAINER_MAP[attachmentType];
typeMap[attachmentType].push(
<Container
key={`${attachmentType}-${typeMap[attachmentType].length}`}
{...rest}
attachment={attachment}
/>,
);
}

return typeMap;
},
Expand All @@ -137,6 +152,7 @@ const renderGroupedAttachments = ({
image: [],
// eslint-disable-next-line sort-keys
gallery: [],
geolocation: [],
voiceRecording: [],
},
);
Expand Down
22 changes: 18 additions & 4 deletions src/components/Attachment/AttachmentContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ import React, { useLayoutEffect, useRef, useState } from 'react';
import ReactPlayer from 'react-player';
import clsx from 'clsx';
import * as linkify from 'linkifyjs';
import type { Attachment, LocalAttachment } from 'stream-chat';
import type { Attachment, LocalAttachment, SharedLocationResponse } from 'stream-chat';
import { isSharedLocationResponse } from 'stream-chat';

import { AttachmentActions as DefaultAttachmentActions } from './AttachmentActions';
import { Audio as DefaultAudio } from './Audio';
import { VoiceRecording as DefaultVoiceRecording } from './VoiceRecording';
import { Gallery as DefaultGallery, ImageComponent as DefaultImage } from '../Gallery';
import { Card as DefaultCard } from './Card';
import { FileAttachment as DefaultFile } from './FileAttachment';
import { Geolocation as DefaultGeolocation } from './Geolocation';
import { UnsupportedAttachment as DefaultUnsupportedAttachment } from './UnsupportedAttachment';
import type {
AttachmentComponentType,
GalleryAttachment,
GeolocationContainerProps,
RenderAttachmentProps,
RenderGalleryProps,
} from './utils';
Expand All @@ -26,7 +29,7 @@ import type {
} from '../../types/types';

export type AttachmentContainerProps = {
attachment: Attachment | GalleryAttachment;
attachment: Attachment | GalleryAttachment | SharedLocationResponse;
componentType: AttachmentComponentType;
};
export const AttachmentWithinContainer = ({
Expand All @@ -37,7 +40,7 @@ export const AttachmentWithinContainer = ({
const isGAT = isGalleryAttachmentType(attachment);
let extra = '';

if (!isGAT) {
if (!isGAT && !isSharedLocationResponse(attachment)) {
extra =
componentType === 'card' && !attachment?.image_url && !attachment?.thumb_url
? 'no-image'
Expand All @@ -50,7 +53,9 @@ export const AttachmentWithinContainer = ({
'str-chat__message-attachment str-chat__message-attachment-dynamic-size',
{
[`str-chat__message-attachment--${componentType}`]: componentType,
[`str-chat__message-attachment--${attachment?.type}`]: attachment?.type,
[`str-chat__message-attachment--${(attachment as Attachment)?.type}`]: (
attachment as Attachment
)?.type,
[`str-chat__message-attachment--${componentType}--${extra}`]:
componentType && extra,
'str-chat__message-attachment--svg-image': isSvgAttachment(attachment),
Expand Down Expand Up @@ -288,6 +293,15 @@ export const MediaContainer = (props: RenderAttachmentProps) => {
);
};

export const GeolocationContainer = ({
Geolocation = DefaultGeolocation,
location,
}: GeolocationContainerProps) => (
<AttachmentWithinContainer attachment={location} componentType='geolocation'>
<Geolocation location={location} />
</AttachmentWithinContainer>
);

export const UnsupportedAttachmentContainer = ({
attachment,
UnsupportedAttachment = DefaultUnsupportedAttachment,
Expand Down
Loading