---
id: geolocation_attachment
title: Geolocation Attachment & Live Location Sharing
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

In this comprehensive example, we demonstrate how to build a live location sharing feature. Chat users will have the ability to send their location to a channel and display it through a custom `Attachment` component that displays coordinates using the [Google Maps API](https://developers.google.com/maps/documentation/javascript/overview).

The feature flow has three distinct steps:

- Click `Share Location` button with confirmation dialog
- Confirm, send and register a message for live location updates
- Render coordinates on a Google Maps overlay sent as a message attachment

## Prerequisities

We've prepared [an example geolocation context/controller](#geoloccontext) that we'll be referencing throughout the guide. This controller takes care of registering and updating messages with the current location. Note that this is an _example_ controller that should be expanded on based on your needs and requirements. This controller is missing logic such as end-of-life of the messages with live location updates, manually stopping location updates, limiting the amount of messages with live location updates or proper error handling. Treat it as a baseline to get you started.

:::note
If you wish to use only one-time location sharing functionality, you don't need [`GeoLocContext`](#geoloccontext) controller/context and can safely omit it.
:::

## Custom Message Input With Location Sharing Button

The first step in our location sharing flow is to add a custom button beside message input that on click allows a chat user to begin the process of sending their coordinates to the channel.

In this example, our custom handler function will utilise `window.confirm` dialog to get the user confirmation before requesting location data.

:::note
We'll be using `registerMessageIds` function from our pre-defined [`GeoLocContext`](#geoloccontext) to save messages to a `localStorage` to be later loaded in case of a page reload so that we can keep sharing our location.
:::

<Tabs groupId='example'>
<TabItem value='tsx' label='React'>

```tsx title="custom-components.tsx"
import { MessageInput, useChannelStateContext } from 'stream-chat-react';

import { useGeoLocContext } from './geo-loc-context';

import './message-input-with-location-button.css';

export const MessageInputWithLocationButton = () => {
  const { channel } = useChannelStateContext();
  const { registerMessageIds } = useGeoLocContext();

  return (
    <div className='message-input-container'>
      <MessageInput focus />
      <button
        onClick={() => {
          const shouldShareLocation = window.confirm('Would you like to share your location?');

          if (!shouldShareLocation) return;

          navigator.geolocation.getCurrentPosition(
            ({ coords: { latitude, longitude } }) => {
              channel
                .sendMessage({
                  attachments: [
                    {
                      type: 'location',
                      latitude,
                      longitude,
                    },
                  ],
                })
                .then((response) => {
                  registerMessageIds([response.message.id]);
                });
            },
            console.log,
            { maximumAge: 0, timeout: 500 },
          );
        }}
        className='location-share-button'
      >
        🗺️
      </button>
    </div>
  );
};
```

</TabItem>
<TabItem value='css' label='CSS'>

```css title="message-input-with-location-button.css"
.message-input-container {
  display: flex;
}

.location-share-button {
  border-radius: 9999px;
  margin: 5px 5px 5px 0px;
  line-height: 1;
  padding: 5px 10px;
}
```

</TabItem>
</Tabs>

![](../../../assets/geolocation-attachment-0.png)
![](../../../assets/geolocation-attachment-1.png)

## Custom Map Attachment

Now that we're able to send a message with an attachment of type `location`, we need to build a custom `Attachment` component that can this new type. If a message attachment is not of type `location`, meaning it's a standard library type, we return the default `Attachment` component.

When our custom component receives an attachment of type `location`, we pass the geolocation coordinates to the `Map` and `Marker` components from [`@vis.gl/react-google-maps`](https://www.npmjs.com/package/@vis.gl/react-google-maps). This library is a React-based wrapper around the Google Maps API and displays a map and geolocation as a React component. See the [`@vis.gl/react-google-maps`](https://www.npmjs.com/package/@vis.gl/react-google-maps) documentation for more information.

:::important
In order to interact with the Google Maps API, you must [set up an account](https://developers.google.com/maps/documentation/javascript/cloud-setup) and generate an API key.
:::

:::note
Since we've added new fields to our `location` type message attachment, we must extend the default attachment type to support `latitude` and `longitude` through `StreamChatGenerics` which we can then pass to `AttachmentProps` type.
:::

```tsx title="custom-components.tsx"
import { Attachment } from 'stream-chat-react';
import { APIProvider, Map, Marker } from '@vis.gl/react-google-maps';

import type { AttachmentProps } from 'stream-chat-react';

export type StreamChatGenerics = {
  attachmentType: { latitude: number; longitude: number };
  // ▽ defaults
  commandType: string;
} & Record<
  | 'channelType'
  | 'eventType'
  | 'messageType'
  | 'pollOptionType'
  | 'pollType'
  | 'reactionType'
  | 'userType',
  Record<string, unknown>
>;

const GOOGLE_MAPS_API_KEY = 'key';

export const AttachmentWithMap = (props: AttachmentProps<StreamChatGenerics>) => {
  const [locationAttachment] = props.attachments;

  if (locationAttachment.type === 'location') {
    const { latitude, longitude } = locationAttachment;

    return (
      <APIProvider apiKey={GOOGLE_MAPS_API_KEY}>
        <Map
          style={{ width: '300px', height: '300px' }}
          defaultCenter={{ lat: latitude, lng: longitude }}
          defaultZoom={16}
          disableDefaultUI={true}
          gestureHandling='greedy'
        >
          <Marker position={{ lat: latitude, lng: longitude }} />
        </Map>
      </APIProvider>
    );
  }

  return <Attachment {...props} />;
};
```

## Implementation

Now that each individual piece has been constructed, we can assemble all of the snippets into the final code example.

```tsx title="app.tsx"
import { Chat, ChannelList, Channel, Window, Thread } from 'stream-chat-react';

import { AttachmentWithMap, MessageInputWithLocationButton } from './custom-components';
import { GeoLocContextProvider } from './geo-loc-context';

import type { StreamChatGenerics } from './custom-components';

const App = () => {
  const chatClient = useCreateChatClient<StreamChatGenerics>(/*...*/);

  if (!chatClient) return <div>Loading...</div>;

  return (
    <Chat client={chatClient}>
      <ChannelList />
      <Channel Attachment={AttachmentWithMap}>
        <Window>
          <ChannelHeader />
          <MessageList />
          <MessageInputWithLocationButton />
        </Window>
        <Thread />
      </Channel>
    </Chat>
  );
};
```

![](../../../assets/geolocation-attachment-2.png)

## GeoLocContext

```tsx title="geo-loc-context.tsx"
import React, { useState, useEffect, useCallback, useRef, useContext } from 'react';
import { useChatContext } from 'stream-chat-react';

import type { PropsWithChildren } from 'react';

type GeoLocContextValue = {
  registeredMessageIds: string[];
  registerMessageIds: (messageIds: string[]) => void;
  unregisterMessageIds: (messageIds: string[]) => void;
};

const GeoLocContext = React.createContext<GeoLocContextValue>({
  registeredMessageIds: [],
  registerMessageIds: () => {},
  unregisterMessageIds: () => {},
});

export const useGeoLocContext = () => useContext(GeoLocContext);

const constructStorageKey = (userId: string) => {
  return `$geoloc.registeredMessageIds-${userId}`;
};

export const GeoLocContextProvider = ({ children }: PropsWithChildren) => {
  const { client } = useChatContext();

  const [registeredMessageIds, setRegisteredMessageIds] = useState(() => {
    const registeredMessageIdsString = window.localStorage.getItem(
      constructStorageKey(client.userID!),
    );

    if (!registeredMessageIdsString) return [];

    try {
      const messageIds = JSON.parse(atob(registeredMessageIdsString));

      return (messageIds as unknown) as string[];
    } catch (error) {
      console.log(error);
      return [];
    }
  });

  const registerMessageIds = useCallback((messageIds: string[]) => {
    setRegisteredMessageIds((currentMessageIds) => currentMessageIds.concat(messageIds));
  }, []);

  const unregisterMessageIds = useCallback((messageIds: string[]) => {
    setRegisteredMessageIds((currentMessageIds) =>
      currentMessageIds.filter((messageId) => !messageIds.includes(messageId)),
    );
  }, []);

  const handlePositionChangeRef = useRef<(position: GeolocationPosition) => Promise<void>>();

  handlePositionChangeRef.current = async (position) => {
    const settled = await Promise.allSettled(
      registeredMessageIds.map((messageId) =>
        client.partialUpdateMessage(messageId, {
          set: {
            attachments: [
              {
                type: 'location',
                latitude: position.coords.latitude,
                longitude: position.coords.longitude,
              },
            ],
          },
        }),
      ),
    );

    const idsToDelete = [];
    for (let index = 0; index < settled.length; index++) {
      if (settled[index].status !== 'rejected') continue;
      idsToDelete.push(registeredMessageIds[index]);
    }

    unregisterMessageIds(idsToDelete);
  };

  useEffect(() => {
    // sync storage
    const oldValue = window.localStorage.getItem(constructStorageKey(client.userID!));
    const newValue = btoa(JSON.stringify(registeredMessageIds));

    if (newValue === oldValue) return;

    window.localStorage.setItem(constructStorageKey(client.userID!), newValue);
  }, [client, registeredMessageIds]);

  const shouldWatch = registeredMessageIds.length !== 0;

  useEffect(() => {
    let promise: Promise<void> | null = null;

    if (!shouldWatch) return;

    const position = navigator.geolocation.watchPosition(
      (newPosition) => {
        if (promise) return;

        promise = handlePositionChangeRef.current!(newPosition).finally(() => {
          promise = null;
        });
      },
      console.log,
      { enableHighAccuracy: true },
    );

    return () => {
      navigator.geolocation.clearWatch(position);
    };
  }, [shouldWatch]);

  return (
    <GeoLocContext.Provider
      value={{ registeredMessageIds, registerMessageIds, unregisterMessageIds }}
    >
      {children}
    </GeoLocContext.Provider>
  );
};
```
