Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/check-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
uses: ./.github/actions/install-and-build-sdk
- name: Lint
run: yarn lint
- name: Typecheck tests
run: yarn workspace stream-chat-react-native-core test:typecheck
- name: Typecheck packages and example apps
run: yarn typecheck
- name: Test
run: yarn test:coverage
13 changes: 13 additions & 0 deletions configs/typescript-config/base.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Stream Base",
"compilerOptions": {
"target": "ES2022",
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true
}
}
24 changes: 24 additions & 0 deletions configs/typescript-config/library.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Stream Library (React Native)",
"extends": "./base.json",
"compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "esnext",
"moduleResolution": "bundler",
"jsx": "react-native",
"allowJs": true,
"checkJs": false,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noImplicitUseStrict": false,
"noStrictGenericChecks": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noEmitOnError": false
}
}
16 changes: 16 additions & 0 deletions configs/typescript-config/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@stream-io/typescript-config",
"version": "0.0.0",
"description": "Shared TypeScript config presets for the Stream Chat React Native SDK monorepo.",
"license": "SEE LICENSE IN LICENSE",
"private": true,
"files": [
"base.json",
"library.json"
],
"exports": {
"./base.json": "./base.json",
"./library.json": "./library.json",
"./package.json": "./package.json"
}
}
4 changes: 2 additions & 2 deletions examples/ExpoMessaging/app/channel/[cid]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ export default function ChannelScreen() {
payload,
) => {
const { message, defaultHandler, emitter } = payload;
const { shared_location } = message ?? {};
if (emitter === 'messageContent' && shared_location) {
const shared_location = message?.shared_location;
if (emitter === 'messageContent' && message && shared_location) {
// Create url params from shared_location
const params = Object.entries(shared_location)
.map(([key, value]) => `${key}=${value}`)
Expand Down
36 changes: 25 additions & 11 deletions examples/ExpoMessaging/app/map/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import MapView, { MapMarker, Marker } from 'react-native-maps';
import { SafeAreaView } from 'react-native-safe-area-context';

import { Stack, useLocalSearchParams } from 'expo-router';
import { SharedLocationResponse, StreamChat } from 'stream-chat';
import { Channel, SharedLocationResponse, StreamChat } from 'stream-chat';
import { useChatContext, useHandleLiveLocationEvents, useTheme } from 'stream-chat-expo';

import { AppContext } from '../../context/AppContext';

import type { AppTheme } from '@/types/theme';

export type SharedLiveLocationParamsStringType = SharedLocationResponse & {
latitude: string;
longitude: string;
Expand All @@ -40,20 +42,27 @@ const MapScreenFooter = ({
theme: {
colors: { accent_blue, accent_red, grey },
},
} = useTheme();
const liveLocationActive = isLiveLocationStopped ? false : new Date(end_at) > new Date();
} = useTheme() as unknown as { theme: AppTheme };
const endedAtDate = end_at ? new Date(end_at) : null;
const liveLocationActive = isLiveLocationStopped
? false
: endedAtDate
? endedAtDate > new Date()
: false;
const formattedEndedAt = endedAtDate ? endedAtDate.toLocaleString() : '';

const stopSharingLiveLocation = useCallback(async () => {
if (!channel || !locationResponse) {
return;
}
await channel.stopLiveLocationSharing(locationResponse);
}, [channel, locationResponse]);

if (!end_at) {
return null;
}

const isCurrentUser = user_id === client.user.id;
const isCurrentUser = user_id === client.user?.id;
if (!isCurrentUser) {
return (
<View style={styles.footer}>
Expand Down Expand Up @@ -107,7 +116,7 @@ export default function MapScreen() {
theme: {
colors: { accent_blue },
},
} = useTheme();
} = useTheme() as unknown as { theme: AppTheme };

const { width, height } = useWindowDimensions();
const aspect_ratio = width / height;
Expand All @@ -131,7 +140,10 @@ export default function MapScreen() {
}, []);

const { isLiveLocationStopped, locationResponse } = useHandleLiveLocationEvents({
channel,
// This screen is only reached by navigating from an open channel (which sets `channel`
// in AppContext), so a channel is always present here even though the context type
// allows `undefined`.
channel: channel as Channel,
messageId: shared_location.message_id,
onLocationUpdate,
});
Expand All @@ -152,13 +164,15 @@ export default function MapScreen() {
const region = useMemo(() => {
const latitudeDelta = 0.1;
const longitudeDelta = latitudeDelta * aspect_ratio;
// Fall back to the initial coordinates parsed from the route params when no live
// location update has arrived yet, so the values are always concrete numbers.
return {
latitude: locationResponse?.latitude,
longitude: locationResponse?.longitude,
latitude: locationResponse?.latitude ?? parseFloat(shared_location.latitude),
longitude: locationResponse?.longitude ?? parseFloat(shared_location.longitude),
latitudeDelta,
longitudeDelta,
};
}, [aspect_ratio, locationResponse]);
}, [aspect_ratio, locationResponse, shared_location.latitude, shared_location.longitude]);

return (
<SafeAreaView style={styles.container} edges={['bottom']}>
Expand All @@ -181,7 +195,7 @@ export default function MapScreen() {
<View style={styles.markerWrapper}>
<Image
style={[styles.markerImage, { borderColor: accent_blue }]}
source={{ uri: client.user.image }}
source={{ uri: client.user?.image }}
/>
</View>
</Marker>
Expand All @@ -193,7 +207,7 @@ export default function MapScreen() {
client={client}
shared_location={shared_location}
locationResponse={locationResponse}
isLiveLocationStopped={isLiveLocationStopped}
isLiveLocationStopped={isLiveLocationStopped ?? undefined}
/>
</SafeAreaView>
);
Expand Down
5 changes: 4 additions & 1 deletion examples/ExpoMessaging/components/ChatWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ export const ChatWrapper = ({ children }: PropsWithChildren) => {

streami18n.registerTranslation('en', {
...enTranslations,
// Custom translation key used by the live-location feature. It is not part of the
// SDK's `enTranslations` (a closed key set), but i18next resolves arbitrary keys at
// runtime, so we cast to the expected parameter type to register it.
'timestamp/Location end at': '{{ milliseconds | durationFormatter(withSuffix: false) }}',
});
} as typeof enTranslations);

const theme = useStreamChatTheme();
const componentOverrides = useExpoMessagingComponentOverrides();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
useTranslationContext,
} from 'stream-chat-expo';

import type { AppTheme } from '@/types/theme';

type LiveLocationCreateModalProps = {
visible: boolean;
onRequestClose: () => void;
Expand All @@ -41,7 +43,7 @@ export const LiveLocationCreateModal = ({
theme: {
colors: { accent_blue, grey, grey_whisper },
},
} = useTheme();
} = useTheme() as unknown as { theme: AppTheme };
const { t } = useTranslationContext();
const mapRef = useRef<MapView | null>(null);
const markerRef = useRef<MapMarker | null>(null);
Expand Down Expand Up @@ -112,10 +114,13 @@ export const LiveLocationCreateModal = ({
const options: AlertButton[] = endedAtDurations.map((offsetMs) => ({
text: t('timestamp/Location end at', { milliseconds: offsetMs }),
onPress: async () => {
if (!location) {
return;
}
await messageComposer.locationComposer.setData({
durationMs: offsetMs,
latitude: location?.latitude,
longitude: location?.longitude,
latitude: location.latitude,
longitude: location.longitude,
});
await messageComposer.sendLocation();
onRequestClose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
useTheme,
} from 'stream-chat-expo';

import type { AppTheme } from '@/types/theme';

const MessageLocationFooter = ({
client,
shared_location,
Expand All @@ -32,9 +34,9 @@ const MessageLocationFooter = ({
theme: {
colors: { grey },
},
} = useTheme();
const liveLocationActive = new Date(end_at) > new Date();
} = useTheme() as unknown as { theme: AppTheme };
const endedAtDate = end_at ? new Date(end_at) : null;
const liveLocationActive = endedAtDate ? endedAtDate > new Date() : false;
const formattedEndedAt = endedAtDate ? endedAtDate.toLocaleString() : '';

const stopSharingLiveLocation = useCallback(async () => {
Expand All @@ -44,7 +46,7 @@ const MessageLocationFooter = ({
if (!end_at) {
return null;
}
const isCurrentUser = user_id === client.user.id;
const isCurrentUser = user_id === client.user?.id;
if (!isCurrentUser) {
return (
<View style={styles.footer}>
Expand Down Expand Up @@ -72,7 +74,10 @@ const MessageLocationFooter = ({
export const MessageLocation = ({ message }: MessageLocationProps) => {
const { client } = useChatContext();
const { shared_location } = message;
const { latitude, longitude } = shared_location || {};
// Coordinates are only rendered when `shared_location` exists (the component returns
// `null` below otherwise). Default to 0 so the values are concrete numbers for the
// `Region`/`LatLng` shapes the hooks below build.
const { latitude = 0, longitude = 0 } = shared_location || {};
const mapRef = useRef<MapView | null>(null);
const markerRef = useRef<MapMarker | null>(null);

Expand All @@ -83,7 +88,7 @@ export const MessageLocation = ({ message }: MessageLocationProps) => {
theme: {
colors: { accent_blue },
},
} = useTheme();
} = useTheme() as unknown as { theme: AppTheme };

const region = useMemo(() => {
const latitudeDelta = 0.1;
Expand Down Expand Up @@ -132,7 +137,7 @@ export const MessageLocation = ({ message }: MessageLocationProps) => {
<View style={styles.markerWrapper}>
<Image
style={[styles.markerImage, { borderColor: accent_blue }]}
source={{ uri: client.user.image }}
source={{ uri: client.user?.image }}
/>
</View>
</Marker>
Expand Down
4 changes: 2 additions & 2 deletions examples/ExpoMessaging/context/AppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export type AppContextType = {

export const AppContext = createContext<AppContextType>({
channel: undefined,
setChannel: undefined,
setThread: undefined,
setChannel: () => {},
setThread: () => {},
thread: undefined,
});

Expand Down
2 changes: 1 addition & 1 deletion examples/ExpoMessaging/hooks/useStreamChatTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { DeepPartial, Theme } from 'stream-chat-expo';

export const useStreamChatTheme = () => {
const colorScheme = useColorScheme();
const getChatStyle = (): DeepPartial<Theme> => ({
const getChatStyle = (): DeepPartial<Theme> & { colors: Record<string, string> } => ({
avatar: {
image: {
height: 32,
Expand Down
4 changes: 3 additions & 1 deletion examples/ExpoMessaging/icons/ShareLocationIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import Svg, { Path } from 'react-native-svg';

import { useTheme } from 'stream-chat-expo';

import type { AppTheme } from '@/types/theme';

// Icon for "Share Location" button, next to input box.
export const ShareLocationIcon = () => {
const {
theme: {
colors: { grey },
},
} = useTheme();
} = useTheme() as unknown as { theme: AppTheme };
return (
<Svg width={28} height={28} viewBox='0 0 24 24' fill='none'>
<Path
Expand Down
3 changes: 2 additions & 1 deletion examples/ExpoMessaging/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@
"devDependencies": {
"@rnx-kit/metro-config": "^2.1.2",
"@rnx-kit/metro-resolver-symlinks": "^0.2.6",
"@stream-io/typescript-config": "workspace:^",
"@types/react": "~19.2.10",
"typescript": "~5.9.2"
"typescript": "6.0.3"
},
"expo": {
"install": {
Expand Down
3 changes: 1 addition & 2 deletions examples/ExpoMessaging/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"extends": "expo/tsconfig.base",
"extends": ["expo/tsconfig.base", "@stream-io/typescript-config/base.json"],
"compilerOptions": {
"strict": true,
"paths": {
"@/*": ["./*"]
}
Expand Down
10 changes: 10 additions & 0 deletions examples/ExpoMessaging/types/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Theme } from 'stream-chat-expo';

/**
* The SDK's `Theme` type no longer has a top-level `colors` palette (it moved to a
* semantics/primitives token model). This app, however, injects a custom `colors`
* palette at runtime via `<OverlayProvider value={{ style }}>` (see `useStreamChatTheme`),
* so `theme.colors` does exist at runtime. `AppTheme` augments the SDK type to reflect
* that runtime shape so components can read `theme.colors.X` without migrating the colors.
*/
export type AppTheme = Theme & { colors: Record<string, string> };
3 changes: 2 additions & 1 deletion examples/SampleApp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,11 @@
"@react-native/metro-config": "0.85.3",
"@react-native/typescript-config": "0.85.3",
"@rnx-kit/metro-config": "^2.1.0",
"@stream-io/typescript-config": "workspace:^",
"@types/lodash.mergewith": "^4.6.9",
"@types/react": "^19.2.0",
"babel-plugin-react-compiler": "^1.0.0",
"typescript": "5.9.3"
"typescript": "6.0.3"
},
"engines": {
"node": ">=20.19.4"
Expand Down
Loading
Loading