Info:
This is release candidate documentation for Stream Chat React Native SDK v10. For the latest stable version, see the latest version (v9).

Location sharing

Location sharing sends where a user is, either once or live for a while. This page covers what the SDK provides and what your app adds: the share UI, the map in the message, and the device location that keeps a live share current.

What users get

The SDK has no location UI of its own. It doesn't read the device's location, and it ships no map. With the pieces this page describes, an app gives users this:

The user taps a location button in the attachment picker. Their app finds their position and asks whether to share it once or live, for 15 minutes or an hour. Sharing sends the location as a message of its own, which shows as a map with a pin. A live location reads "Live until" and the end time, and the user's own has a Stop sharing button. When the time is up or they stop, it reads that sharing has ended. While their live share runs, their app keeps sending their position, so the pin moves for everyone in the channel.

The SDK renders one part by itself, the channel list's preview. A channel whose latest message is a location shows "Location", or "Live Location" while the share runs.

How it works across the layers

  • State. The composer's locationComposer holds the location being shared. A shared location is stored with its message as message.shared_location, not in message.attachments. A LiveLocationManager keeps the user's live shares updated.
  • UI. LiveLocationManagerProvider creates the manager. The MessageLocation override key renders a location in a message, and has no default. useHandleLiveLocationEvents follows one shared location. The share UI is your app's.
  • Layout. Not involved. A full-screen map is a screen of your own navigation.

Sharing a location

  1. Your UI reads the device's position, for example with @react-native-community/geolocation or expo-location.
  2. It calls LocationComposer.setData({ latitude, longitude, durationMs }). durationMs is set for a live location only.
  3. It calls MessageComposer.sendLocation(), which sends a message carrying only the location, through Channel.sendSharedLocation(). In a thread's composer, it does nothing.
  4. When the share fails, MessageComposer.sendLocation() publishes an api:location:create:failed notification, "Failed to share location", and throws.

A live location is valid only for at least location.minShareDurationMs, one minute by default. MessageComposer.send() also puts a location that LocationComposer.setData() set on the message being written, but the SDK's composer shows no preview of it. Message composer explains locationComposer.

Keeping a live location current

LiveLocationManager sends the device's new positions for every live location the user is sharing, including shares started on another of the user's devices. LiveLocationManagerProvider creates one:

  1. On mount, the manager loads the user's active live locations with client.getUserLiveLocations().
  2. When the user starts a new live share, the live_location_sharing.started event adds it. Stopping it or deleting its message removes it.
  3. While at least one share is active, the manager calls your watchLocation function. Each position it reports is sent to every active share with client.updateLiveLocation(), at most once every liveLocationManager.minUpdateThrottleMs (3 seconds by default).
  4. Each share is dropped when its end time passes. Channel.stopLiveLocationSharing({ message_id }) ends one early.

The other clients receive the new position in the message's message.updated event. The message list re-renders the message when its shared_location coordinates or end time change.

Turning it on or off

Location sharing is on by default. Two sides decide:

  • The client: messageComposer.location.enabled, true by default.
  • The server: the channel type's shared_locations setting.

To turn it off in every composer:

client.config.set({
  messageComposer: { location: { enabled: false } },
});

With it off, LocationComposer.setData() does nothing, so nothing is sent. Locations work in channels only, not in threads. Configuration lists every setting both sides decide.

Reading the device's location needs a location package and its permissions, which aren't part of the SDK's installation. The Location sharing UI cookbook sets them up, with Google Maps for Android.

Live locations need the manager

Without a LiveLocationManager, live shares are created, but their position never changes. Render LiveLocationManagerProvider once, inside Chat and after the user is connected, and pass it how to watch the position:

LiveLocation.tsx
import Geolocation from "@react-native-community/geolocation";
import type { PropsWithChildren } from "react";
import type { WatchLocation } from "stream-chat";
import { LiveLocationManagerProvider } from "stream-chat-react-native";

// Declared once: a new function on every render would create a new manager.
const watchLocation: WatchLocation = (handler) => {
  const watchId = Geolocation.watchPosition(
    ({ coords }) =>
      handler({ latitude: coords.latitude, longitude: coords.longitude }),
    (error) => console.warn("Watching the location failed", error),
    { enableHighAccuracy: true },
  );

  return () => Geolocation.clearWatch(watchId);
};

export const LiveLocation = ({ children }: PropsWithChildren) => (
  <LiveLocationManagerProvider watchLocation={watchLocation}>
    {children}
  </LiveLocationManagerProvider>
);

watchLocation receives a handler to call with each position, and returns a function that stops watching. To report less often, report on a timer instead. The manager is created with the client's connected user, and throws when the client has no user yet, so render the provider only once client.connectUser() has finished, as the client from useCreateChatClient does. Keep it mounted while the user is signed in: it stops sending positions when it unmounts. Watching the location while the app is in the background needs the location package's background mode.

The provider also takes getDeviceId, which sets the manager's deviceId. By default it's react-native- with the platform and the user id. The device id stored on a new share comes from messageComposer.location.getDeviceId instead, a random id by default.

Customizing

Show a location in a message

MessageLocation receives the message. This one shows the position on a map with react-native-maps, and under it the state of a live location, with a Stop sharing button on the user's own:

import { Pressable, StyleSheet, Text, View } from "react-native";
import MapView, { Marker } from "react-native-maps";
import { convertTimestampToDate, nowNs } from "stream-chat";
import { useChannelContext, useChatContext } from "stream-chat-react-native";
import type {
  ComponentOverrides,
  MessageLocationProps,
} from "stream-chat-react-native";

const MessageMap = ({ message }: MessageLocationProps) => {
  const { client } = useChatContext();
  const { channel } = useChannelContext();
  const location = message.shared_location;
  if (!location) return null;

  const { end_at, latitude, longitude, message_id, user_id } = location;
  const isLive = end_at != null && end_at > nowNs();
  const isOwn = user_id === client.userId;

  return (
    <View>
      <MapView
        region={{
          latitude,
          latitudeDelta: 0.02,
          longitude,
          longitudeDelta: 0.02,
        }}
        scrollEnabled={false}
        style={styles.map}
        zoomEnabled={false}
      >
        <Marker coordinate={{ latitude, longitude }} />
      </MapView>
      {end_at == null ? null : isLive && isOwn ? (
        <Pressable
          onPress={() => channel.stopLiveLocationSharing({ message_id })}
        >
          <Text style={styles.footer}>Stop sharing</Text>
        </Pressable>
      ) : (
        <Text style={styles.footer}>
          {isLive
            ? `Live until ${convertTimestampToDate(end_at)?.toLocaleTimeString()}`
            : "Location sharing ended"}
        </Text>
      )}
    </View>
  );
};

// Pass to `WithComponents`, declared once at module scope.
export const overrides: ComponentOverrides = { MessageLocation: MessageMap };

const styles = StyleSheet.create({
  footer: { padding: 8 },
  map: { height: 160, width: 240 },
});

end_at is a timestamp in nanoseconds, so compare it with nowNs(), and turn it into a Date with convertTimestampToDate(). A live location's card re-renders with each new position, so the pin follows. messageContentOrder on Channel places the location among the message's parts, after the attachments and before the text by default.

Share from the attachment picker

A button of your own in the attachment picker's selection bar can share the position. This one shares it once, with @react-native-community/geolocation, and is left out in threads:

import Geolocation from "@react-native-community/geolocation";
import { StyleSheet, View } from "react-native";
import type { MessageComposer } from "stream-chat";
import {
  AttachmentTypePickerButton,
  CameraPickerButton,
  FilePickerButton,
  MediaPickerButton,
  PollPickerButton,
  useComponentsContext,
  useMessageComposer,
  useThreadContext,
} from "stream-chat-react-native";
import type { ComponentOverrides } from "stream-chat-react-native";

const shareCurrentLocation = (composer: MessageComposer) =>
  Geolocation.getCurrentPosition(async ({ coords }) => {
    composer.locationComposer.setData({
      latitude: coords.latitude,
      longitude: coords.longitude,
    });
    try {
      await composer.sendLocation();
    } catch {
      // The composer has published `api:location:create:failed`.
    }
  });

const LocationPickerButton = () => {
  const { icons } = useComponentsContext();
  const messageComposer = useMessageComposer();
  const { thread } = useThreadContext();
  if (thread) return null;

  return (
    <AttachmentTypePickerButton
      Icon={icons.MapPin}
      onPress={() => shareCurrentLocation(messageComposer)}
    />
  );
};

const SelectionBarWithLocation = () => (
  <View style={styles.bar}>
    <MediaPickerButton />
    <CameraPickerButton />
    <FilePickerButton />
    <PollPickerButton />
    <LocationPickerButton />
  </View>
);

// Pass to `WithComponents`, declared once at module scope.
export const overrides: ComponentOverrides = {
  AttachmentPickerSelectionBar: SelectionBarWithLocation,
};

const styles = StyleSheet.create({
  bar: { alignItems: "center", flexDirection: "row", paddingHorizontal: 12 },
});

For a live location, pass durationMs to LocationComposer.setData(), for example from a dialog that offers the durations. The Location sharing UI cookbook builds that dialog, as a content of the attachment picker.

Follow a live location on a map screen

useHandleLiveLocationEvents({ channel, messageId, onLocationUpdate }) follows one message's location for a screen of its own, outside the message list. It returns the latest locationResponse, and isLiveLocationStopped, which turns true when an update arrives whose end time has passed. Until then it's null, so check end_at yourself for a share that ended before the screen opened. onLocationUpdate, called with each new location, can move the map.

To open that screen when the user taps a location, use onPressMessage on Channel: a tap on the message's content has the emitter messageContent, and the payload carries the message with its shared_location.

Reference