diff --git a/example/README.md b/example/README.md
index b246a7b..81709fa 100644
--- a/example/README.md
+++ b/example/README.md
@@ -6,6 +6,8 @@ This is a new [**React Native**] project, bootstrapped using [`@react-native-com
> **Note**: Make sure you have completed the [React Native - Environment Setup] instructions up to
> the "Creating a new application" step, before proceeding.
+- install rust e.g. `brew install rust`
+- prepare module via `npm run prepare` or `yarn prepare`
## Step 1: Start the Metro Server
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index f0abb01..5407eaa 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -128,5 +128,4 @@ if (isNewArchitectureEnabled()) {
workingDir "$rootDir/../../"
commandLine "npx", "bob", "build", "--target", "codegen"
}
- preBuild.dependsOn invokeLibraryCodegen
-}
\ No newline at end of file
+}
diff --git a/example/android/build.gradle b/example/android/build.gradle
index 7a01913..8cfd749 100644
--- a/example/android/build.gradle
+++ b/example/android/build.gradle
@@ -1,10 +1,10 @@
buildscript {
ext {
- buildToolsVersion = "35.0.0"
+ buildToolsVersion = "36.0.0"
minSdkVersion = 24
- compileSdkVersion = 35
+ compileSdkVersion = 36
targetSdkVersion = 34
- ndkVersion = "26.1.10909125"
+ ndkVersion = "27.1.12297006"
kotlinVersion = "1.9.25"
}
repositories {
diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties
index 79eb9d0..ed4c299 100644
--- a/example/android/gradle/wrapper/gradle-wrapper.properties
+++ b/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/example/package.json b/example/package.json
index 37b2bb6..877f416 100644
--- a/example/package.json
+++ b/example/package.json
@@ -10,20 +10,23 @@
"build:ios": "react-native build-ios --scheme ReactNativeMatrixSdkExample --mode Debug --extra-params \"-sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO\""
},
"dependencies": {
- "react": "18.3.1",
- "react-native": "0.76.9"
+ "@react-navigation/native": "^7.3.18",
+ "@react-navigation/native-stack": "^7.18.10",
+ "react": "19.2.7",
+ "react-native": "^0.86.0",
+ "react-native-safe-area-context": "^5.9.1",
+ "react-native-screens": "^4.27.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
- "@react-native-community/cli": "15.0.1",
- "@react-native-community/cli-platform-android": "15.0.1",
- "@react-native-community/cli-platform-ios": "15.0.1",
- "@react-native/babel-preset": "0.76.9",
- "@react-native/metro-config": "0.76.9",
- "@react-native/typescript-config": "0.76.9",
- "react-native-builder-bob": "^0.31.0"
+ "@react-native-community/cli": "^20.2.0",
+ "@react-native-community/cli-platform-android": "^20.2.0",
+ "@react-native-community/cli-platform-ios": "^20.2.0",
+ "@react-native/babel-preset": "^0.86.0",
+ "@react-native/metro-config": "^0.86.0",
+ "@react-native/typescript-config": "0.86.0"
},
"engines": {
"node": ">=18"
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 39adef7..4c3c926 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -1,43 +1,38 @@
-import * as React from 'react';
+import { NavigationContainer } from '@react-navigation/native';
-import { StyleSheet, View, Text, TextInput, Button } from 'react-native';
-import { ClientBuilder } from '@unomed/react-native-matrix-sdk';
+import { AppNavigationStack, CHAT, CHAT_LIST } from './AppNavigator';
+import { ChatListScreen } from './ChatListScreen';
+import { ChatScreen } from './ChatScreen';
+import { LoginScreen } from './LoginScreen';
-export default function App() {
- const [homeserver, setHomeserver] = React.useState("https://matrix.org");
- const [status, setStatus] = React.useState("");
-
- const updateHomeserverLoginDetails = React.useCallback(async () => {
- if (!homeserver.length) {
- setStatus("");
- return;
- }
+import { MatrixProvider, useMatrix } from './use-matrix';
- try {
- const client = await (new ClientBuilder()).homeserverUrl(homeserver).build();
- const loginDetails = await client.homeserverLoginDetails();
+function ExampleApp() {
+ const { session } = useMatrix();
- setStatus(`url: ${loginDetails.url()}\n`
- + `supportsOidcLogin: ${loginDetails.supportsOauthLogin()}\n`
- + `supportsPasswordLogin: ${loginDetails.supportsPasswordLogin()}`);
- } catch (error) {
- setStatus(`${error}`);
- }
- }, [homeserver]);
+ if (!session) {
+ return
+ }
return (
-
-
-
- {status}
-
+
+
+
+
+
+
);
}
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- alignItems: 'center',
- justifyContent: 'center',
- }
-});
+
+export default function App() {
+ return (
+
+
+
+ );
+}
diff --git a/example/src/AppNavigator.tsx b/example/src/AppNavigator.tsx
new file mode 100644
index 0000000..f252530
--- /dev/null
+++ b/example/src/AppNavigator.tsx
@@ -0,0 +1,27 @@
+import { createNativeStackNavigator, type NativeStackScreenProps } from '@react-navigation/native-stack';
+
+export const LOGIN = 'login';
+export const CHAT_LIST = 'chat-list';
+export const CHAT = 'chat';
+
+export type AppNavigationType = {
+ [LOGIN]: undefined;
+ [CHAT_LIST]: undefined;
+ [CHAT]: { id: string };
+};
+
+export type LoginScreenProps = NativeStackScreenProps<
+ AppNavigationType,
+ typeof LOGIN
+>;
+export type ChatListScreenProps = NativeStackScreenProps<
+ AppNavigationType,
+ typeof CHAT_LIST
+>;
+export type ChatScreenProps = NativeStackScreenProps<
+ AppNavigationType,
+ typeof CHAT
+>;
+
+export const AppNavigationStack =
+ createNativeStackNavigator();
diff --git a/example/src/ChatListScreen.tsx b/example/src/ChatListScreen.tsx
new file mode 100644
index 0000000..4474a79
--- /dev/null
+++ b/example/src/ChatListScreen.tsx
@@ -0,0 +1,68 @@
+import { type FunctionComponent } from 'react';
+import { Pressable, ScrollView, Text, View } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+
+import { CHAT, type ChatListScreenProps } from './AppNavigator';
+import { styles } from './styles';
+import { useMatrix, type MatrixAPI } from './use-matrix';
+import { Picture } from './Picture';
+import { Footer } from './Footer';
+import { roomWithNewestMessageFirst } from './matrix-lib/sort';
+import { MessagePreview } from './ChatScreen';
+
+export const ChatListScreen: FunctionComponent = ({ navigation }) => {
+ const { roomMap, messageMap, spaceMap, session, userMap } = useMatrix();
+ const ownUserId = session?.userId;
+ const ownUser = ownUserId ? userMap[ownUserId] : undefined;
+
+ return (
+
+
+
+ {ownUser?.displayName}
+
+
+ {
+ Object.entries(roomMap)
+ .sort(([_idA,roomA],[_idB, roomB]) => roomWithNewestMessageFirst(roomA, roomB))
+ .map(([id, chat], index) => navigation.navigate(CHAT, { id })} style={[
+ { paddingVertical: 5},
+ index === 0 ? { borderTopWidth: 1, borderTopColor: 'black'} : {},
+ { borderBottomWidth: 1, borderBottomColor: 'black' }
+ ]}>
+
+ )
+ }
+
+
+
+ );
+}
+
+type Chat = MatrixAPI['roomMap'][keyof MatrixAPI['roomMap']];
+type MessageList = MatrixAPI['messageMap'][keyof MatrixAPI['messageMap']]
+const ChatRow: FunctionComponent<{ chat: Chat, messageList: MessageList, space: string | undefined }> = ({ chat, messageList, space }) => {
+ const lastMessage = messageList[messageList.length - 1];
+ const unreadMessages = messageList.filter(({ isRead }) => !isRead);
+ return
+
+
+
+ {chat.displayName()} {space ? `(${space.split(' ')[0]})` : ''}
+ {lastMessage?.timestamp.toLocaleString()}
+
+
+ {
+ lastMessage
+ ?
+ : no message
+ }
+ {
+ unreadMessages.length > 0
+ ? {unreadMessages.length}
+ : null
+ }
+
+
+
+}
diff --git a/example/src/ChatScreen.tsx b/example/src/ChatScreen.tsx
new file mode 100644
index 0000000..b405649
--- /dev/null
+++ b/example/src/ChatScreen.tsx
@@ -0,0 +1,157 @@
+import { useRef, type FunctionComponent } from 'react';
+import { Button, Image, Pressable, ScrollView, Text, TextInput, View, type DimensionValue, type StyleProp, type ViewStyle } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+
+import { type ChatScreenProps } from './AppNavigator';
+import { styles } from './styles';
+import { useMatrix, type Message } from './use-matrix';
+import { Picture } from './Picture';
+import { Footer } from './Footer';
+import { newestMessageLast } from './matrix-lib/sort';
+import { MessageType_Tags } from '@unomed/react-native-matrix-sdk';
+
+export const ChatScreen: FunctionComponent = ({ navigation, route }) => {
+ const chatId = route.params.id;
+ const { roomMap, messageMap } = useMatrix();
+ const room = roomMap[chatId]!; // if you hit this route the room will exists
+ const messageList = messageMap[chatId] ?? [];
+ const scrollViewRef = useRef(null);
+
+ return (
+
+
+ navigation.goBack()}>
+ <
+
+
+
+ {room.displayName()}
+
+
+
+ scrollViewRef.current?.scrollToEnd({ animated: true })}
+ >
+ {
+ messageList.length === 0
+ ?
+ no messages available
+
+ : messageList
+ .sort(newestMessageLast)
+ .map((message) => )
+ }
+
+
+
+
+ );
+}
+
+export const MessageRow: FunctionComponent<{ message: Message }> = ({ message }) => {
+ const { session, userMap } = useMatrix();
+ const isOwnMessage = message.userId === session?.userId;
+ const user = userMap[message.userId];
+
+ if (isOwnMessage) {
+ return
+
+
+
+
+ }
+
+ return
+
+
+ {user?.displayName}
+
+
+
+}
+
+export const MessageContent: FunctionComponent<{ message: Message }> = ({ message }) => {
+ switch (message.tag) {
+ case MessageType_Tags.Emote:
+ return {message.inner.content.body};
+
+ case MessageType_Tags.Audio: {
+ const { filename } = message.inner.content;
+ return ♬ Audio Message: {filename};
+ }
+
+ case MessageType_Tags.File: {
+ const { filename } = message.inner.content;
+ return 💾 File:{filename};
+ }
+
+ case MessageType_Tags.Gallery: {
+ const { body } = message.inner.content;
+ return 🖼️ Gallery:{body};
+ }
+
+ case MessageType_Tags.Image: {
+ const { source, filename, caption, info } = message.inner.content;
+ const aspectRatio = (info?.width && info.height) ? Number(info.width / info.height) : 1
+ return
+
+ {caption ?? filename}
+ ;
+ }
+
+ case MessageType_Tags.Location: {
+ const { description, body } = message.inner.content;
+ return 📍 Location:{description ?? body};
+ }
+
+ case MessageType_Tags.Notice:
+ return 📝 Notice:{message.inner.content.body};
+
+ case MessageType_Tags.Video: {
+ const { filename, caption } = message.inner.content;
+ return 🎥 Video:{caption ?? filename};
+ }
+
+ case MessageType_Tags.Other:
+ case MessageType_Tags.Text:
+ default:
+ return {message.body};
+ }
+}
+
+export const MessagePreview: FunctionComponent<{ message: Message }> = ({ message }) => {
+ switch (message.tag) {
+ case MessageType_Tags.Emote:
+ return {message.inner.content.body};
+
+ case MessageType_Tags.Audio:
+ return ♬ {message.inner.content.filename};
+
+ case MessageType_Tags.File:
+ return 💾 {message.inner.content.filename};
+
+ case MessageType_Tags.Gallery:
+ return 🖼️ {message.inner.content.body};
+
+ case MessageType_Tags.Image:
+ return 🖼️ {message.inner.content.caption};
+
+ case MessageType_Tags.Location: {
+ const { description, body } = message.inner.content;
+ return 📍 {description ?? body};
+ }
+
+ case MessageType_Tags.Notice:
+ return 📝 {message.inner.content.body};
+
+ case MessageType_Tags.Video:
+ return 🎬 {message.inner.content.filename};
+
+ case MessageType_Tags.Other:
+ case MessageType_Tags.Text:
+ default:
+ return {message.body};
+ }
+}
\ No newline at end of file
diff --git a/example/src/Footer.tsx b/example/src/Footer.tsx
new file mode 100644
index 0000000..98d4f0e
--- /dev/null
+++ b/example/src/Footer.tsx
@@ -0,0 +1,33 @@
+import type { FunctionComponent } from "react";
+import { Button, Text, View } from "react-native";
+
+import { SyncServiceState } from "@unomed/react-native-matrix-sdk";
+import { useMatrix } from "./use-matrix";
+
+export const Footer: FunctionComponent = () => {
+ const { synchronizeStatus, synchronize } = useMatrix();
+ return
+
+
+}
+
+export const SyncStatus = ({ synchronizeStatus }: { synchronizeStatus: SyncServiceState | undefined }) => {
+ if (!synchronizeStatus || synchronizeStatus === undefined) {
+ return No sync status;
+ }
+ switch (synchronizeStatus) {
+ // case SyncServiceState.Idle:
+ // return Sync is idle;
+ case SyncServiceState.Offline:
+ return Offline;
+ case SyncServiceState.Error:
+ return Sync error;
+ case SyncServiceState.Running:
+ return Syncing...;
+ case SyncServiceState.Terminated:
+ return Sync terminated;
+ default:
+ return Unknown sync status;
+ }
+}
\ No newline at end of file
diff --git a/example/src/LoginScreen.tsx b/example/src/LoginScreen.tsx
new file mode 100644
index 0000000..0451b11
--- /dev/null
+++ b/example/src/LoginScreen.tsx
@@ -0,0 +1,62 @@
+import { useState, type FunctionComponent } from 'react';
+import { Button, ScrollView, Text, TextInput, View } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+
+import { styles } from './styles';
+import { useMatrix, type Credentials } from './use-matrix';
+
+export const LoginScreen: FunctionComponent<{ afterLogin?: () => void }> = ({ afterLogin }) => {
+ const [loggingIn, setLoggingIn] = useState(false);
+ const [credentials, setCredentials] = useState>({
+ url: 'https://matrix.org',
+ });
+ const { login } = useMatrix();
+
+ return (
+
+
+
+ Login
+ setCredentials((old) => ({ ...old, url }))}
+ autoCapitalize="none"
+ autoCorrect={false}
+ />
+ setCredentials((old) => ({ ...old, username }))}
+ autoCapitalize="none"
+ autoCorrect={false}
+ />
+ setCredentials((old) => ({ ...old, password }))}
+ secureTextEntry
+ autoCapitalize="none"
+ />
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/example/src/Picture.tsx b/example/src/Picture.tsx
new file mode 100644
index 0000000..71423fc
--- /dev/null
+++ b/example/src/Picture.tsx
@@ -0,0 +1,23 @@
+import { useEffect, useState, type FunctionComponent } from "react";
+import { Image, View, type ViewProps } from "react-native";
+
+import { useMatrix } from "./use-matrix";
+
+export const Picture: FunctionComponent<{ url: string | undefined } & ViewProps> = ({ style, url }) => {
+ const [loadedImageData, setLoadedImageData] = useState();
+ const { loadMedia } = useMatrix();
+
+ useEffect(() => {
+ (async () => {
+ const media = await loadMedia(url);
+ if (media?.url) {
+ setLoadedImageData(media.url);
+ }
+ })()
+ }, []);
+
+ if (loadedImageData) {
+ return ;
+ }
+ return
+}
\ No newline at end of file
diff --git a/example/src/matrix-lib/load-media.ts b/example/src/matrix-lib/load-media.ts
new file mode 100644
index 0000000..f33ec80
--- /dev/null
+++ b/example/src/matrix-lib/load-media.ts
@@ -0,0 +1,26 @@
+import { MediaSource, type ClientLike } from "@unomed/react-native-matrix-sdk";
+
+export interface Media { url: string };
+
+const mediaCache = new Map>();
+
+export async function loadMedia(client: ClientLike, url: string): Promise {
+ const cached = mediaCache.get(url);
+ if (cached) {
+ return cached;
+ }
+
+ const loadPromise = loadMediaUncached(client, url).catch((error) => {
+ mediaCache.delete(url);
+ throw error;
+ });
+
+ mediaCache.set(url, loadPromise);
+ return loadPromise;
+}
+
+async function loadMediaUncached(client: ClientLike, url: string): Promise {
+ const mediaSource = MediaSource.fromUrl(url);
+ const handle = await client.getMediaFile(mediaSource, undefined, 'image/jpeg', true, undefined);
+ return { url: `file://${handle.path()}` };
+}
diff --git a/example/src/matrix-lib/login.ts b/example/src/matrix-lib/login.ts
new file mode 100644
index 0000000..be0e342
--- /dev/null
+++ b/example/src/matrix-lib/login.ts
@@ -0,0 +1,34 @@
+import { ClientBuilder, Session, SlidingSyncVersionBuilder, type ClientLike } from "@unomed/react-native-matrix-sdk";
+
+export interface Credentials {
+ url: string;
+ username: string;
+ password: string;
+};
+export interface Device {
+ id: string;
+ name: string;
+}
+
+export async function login(credentials: Credentials, device: Device): Promise<{
+ client: ClientLike;
+ session: Session;
+}> {
+ const { url, username, password } = credentials;
+
+ const client = await new ClientBuilder()
+ .homeserverUrl(url)
+ .slidingSyncVersionBuilder(SlidingSyncVersionBuilder.DiscoverNative)
+ .build();
+ await client.login(
+ username,
+ password,
+ device.name,
+ device.id,
+ );
+ const session = await client.session();
+ return {
+ client,
+ session,
+ }
+}
\ No newline at end of file
diff --git a/example/src/matrix-lib/sort.ts b/example/src/matrix-lib/sort.ts
new file mode 100644
index 0000000..698c469
--- /dev/null
+++ b/example/src/matrix-lib/sort.ts
@@ -0,0 +1,10 @@
+import type { MessageContent, Room } from "@unomed/react-native-matrix-sdk";
+
+export function roomWithNewestMessageFirst(a: Room, b: Room): number {
+ // FIXME
+ return 0;
+}
+
+export function newestMessageLast(a: { timestamp: Date }, b: { timestamp: Date }): number {
+ return a.timestamp.getTime() - b.timestamp.getTime();
+}
\ No newline at end of file
diff --git a/example/src/matrix-lib/synchronize.ts b/example/src/matrix-lib/synchronize.ts
new file mode 100644
index 0000000..9b5bfe6
--- /dev/null
+++ b/example/src/matrix-lib/synchronize.ts
@@ -0,0 +1,203 @@
+import type { ClientLike, Mentions, MessageType, RoomLike, RoomMember, SpaceRoom, SyncServiceState, TimelineDiff, TimelineItemLike, UserProfile } from "@unomed/react-native-matrix-sdk";
+
+export type Message = {
+ id: string;
+ timestamp: Date;
+ isRead: boolean;
+ userId: string;
+ body: string;
+ isEdited: boolean;
+ mentions?: Mentions;
+} & MessageType;
+
+export interface SynchronizeListener {
+ onChange: (data: Partial<{
+ synchronizeStatus: SyncServiceState;
+ roomMap: Record; // room id -> room
+ messageMap: Record; // room id -> message list
+ spaceMap: Record; // room id -> space
+ userMap: Record; // user id -> user
+ }>) => void;
+}
+
+export async function synchronize(client: ClientLike, listener: SynchronizeListener) {
+ client.subscribeToRoomInfo
+
+ const syncService = await client.syncService()
+ .withCrossProcessLock()
+ .finish();
+ const syncTask = syncService.state({
+ onUpdate: (state) => {
+ listener.onChange({ synchronizeStatus: state });
+ triggerDataGather(client, listener);
+ }
+ });
+ await syncService.start();
+}
+
+function toMessage(item: TimelineItemLike): Message | undefined {
+ const event = item.asEvent();
+ if (!event) {
+ return undefined;
+ }
+ if (event.content.tag !== 'MsgLike') {
+ return undefined;
+ }
+
+ const { kind } = event.content.inner.content;
+ if (kind.tag !== 'Message') {
+ return undefined;
+ }
+
+ const id = event.eventOrTransactionId.tag === 'EventId'
+ ? event.eventOrTransactionId.inner.eventId
+ : event.eventOrTransactionId.inner.transactionId;
+
+ const { msgType, ...commonContent } = kind.inner.content;
+
+ return {
+ id,
+ timestamp: new Date(Number(event.timestamp)),
+ isRead: true, // TODO: derive from event.readReceipts once read state matters
+ userId: event.sender,
+ ...commonContent,
+ ...msgType,
+ };
+}
+
+function applyDiff(items: Array, diff: TimelineDiff): Array {
+ switch (diff.tag) {
+ case 'Append':
+ return [...items, ...diff.inner.values.map(toMessage)];
+ case 'Clear':
+ return [];
+ case 'PushFront':
+ return [toMessage(diff.inner.value), ...items];
+ case 'PushBack':
+ return [...items, toMessage(diff.inner.value)];
+ case 'PopFront':
+ return items.slice(1);
+ case 'PopBack':
+ return items.slice(0, -1);
+ case 'Insert': {
+ const next = [...items];
+ next.splice(diff.inner.index, 0, toMessage(diff.inner.value));
+ return next;
+ }
+ case 'Set': {
+ const next = [...items];
+ next[diff.inner.index] = toMessage(diff.inner.value);
+ return next;
+ }
+ case 'Remove': {
+ const next = [...items];
+ next.splice(diff.inner.index, 1);
+ return next;
+ }
+ case 'Truncate':
+ return items.slice(0, diff.inner.length);
+ case 'Reset':
+ return diff.inner.values.map(toMessage);
+ default:
+ return items;
+ }
+}
+
+const subscribedRoomIds = new Set();
+
+async function subscribeToRoomTimeline(room: RoomLike, listener: SynchronizeListener) {
+ const roomId = room.id();
+ if (subscribedRoomIds.has(roomId)) {
+ return;
+ }
+ subscribedRoomIds.add(roomId);
+
+ const timeline = await room.timeline();
+ let items: Array = [];
+
+ await timeline.addListener({
+ onUpdate: (diffs) => {
+ for (const diff of diffs) {
+ items = applyDiff(items, diff);
+ }
+
+ const messageList = items.filter((message): message is Message => message !== undefined);
+ listener.onChange({ messageMap: { [roomId]: messageList } });
+ },
+ });
+
+ // timeline might be empty there we need to use pagination to get old messages
+ const maxInitialPaginationRounds = 5;
+ for (let round = 0; round < maxInitialPaginationRounds; round++) {
+ const hitStartOfTimeline = await timeline.paginateBackwards(20);
+ if (hitStartOfTimeline) {
+ break;
+ }
+ }
+}
+
+let hasRunning = false;
+async function triggerDataGather(client: ClientLike, listener: SynchronizeListener) {
+ await new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ }, 1000);
+ });
+ if (hasRunning) {
+ console.warn('ignore data gatherer cause there is already a run!');
+ return;
+ }
+ hasRunning = true;
+
+ try {
+ const spaceService = client.spaceService();
+ const allRooms = client.rooms();
+ const roomList = allRooms.filter((room) => !room.isSpace());
+ const spaceList = allRooms.filter((room) => room.isSpace());//await spaceService.joinedSpaces();
+ const roomMap: Record = {};
+ const userMap: Record = {};
+ const spaceMap: Record = {};
+
+ for (const space of spaceList) {
+ const spaceRoomList = await spaceService.spaceRoomList(space.id());
+
+ let paginationState = spaceRoomList.paginationState();
+ while (!(paginationState.tag === 'Idle' && paginationState.inner.endReached)) {
+ await spaceRoomList.paginate();
+ paginationState = spaceRoomList.paginationState();
+ }
+
+ const childRooms = spaceRoomList.rooms();
+ for (const childRoom of childRooms) {
+ spaceMap[childRoom.roomId] = space;
+ }
+ }
+
+ for (const room of roomList) {
+ roomMap[room.id()] = room;
+
+ const members = await room.members();
+ const chunkSize = 50;
+ let chunk = members.nextChunk(chunkSize);
+ while (chunk && chunk.length > 0) {
+ for (const member of chunk) {
+ if (!userMap[member.userId]) {
+ userMap[member.userId] = await client.getProfile(member.userId);
+ }
+ }
+
+ chunk = members.nextChunk(chunkSize);
+ }
+
+ await subscribeToRoomTimeline(room, listener);
+ }
+
+ listener.onChange({
+ roomMap,
+ userMap,
+ spaceMap,
+ });
+ } finally {
+ hasRunning = false;
+ }
+}
diff --git a/example/src/styles.tsx b/example/src/styles.tsx
new file mode 100644
index 0000000..5a4a300
--- /dev/null
+++ b/example/src/styles.tsx
@@ -0,0 +1,73 @@
+import { StyleSheet } from "react-native";
+
+export const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ content: {
+ padding: 40,
+ gap: 8,
+ },
+ widget: {
+ backgroundColor: '#f0f0f0',
+ padding: 10,
+ borderRadius: 8,
+ },
+ vStack: {
+ flexDirection: 'column',
+ gap: 4,
+ },
+ vSpaceCenter: {
+ alignItems: 'center',
+ },
+ vSpaceBottom: {
+ alignContent: 'flex-end',
+ },
+ hStack: {
+ flexDirection: 'row',
+ gap: 4,
+ },
+ hSpaceBetween: {
+ justifyContent: 'space-between',
+ },
+ hSpaceCenter: {
+ justifyContent: 'center',
+ },
+ input: {
+ borderWidth: 1,
+ borderColor: '#ccc',
+ padding: 8,
+ borderRadius: 4,
+ backgroundColor: '#fff',
+ },
+ picture: {
+ borderRadius: 100,
+ backgroundColor: 'gray',
+ overflow: 'hidden',
+ justifyContent: 'center',
+ alignContent: 'center',
+ },
+ pictureLarge: {
+ width: 50,
+ height: 50,
+ },
+ pictureSmall: {
+ width: 30,
+ height: 30,
+ },
+ badge: {
+ borderRadius: 100,
+ backgroundColor: 'red',
+ },
+ buttonBubble: {
+ width: 40, height: 40,
+ borderRadius: 100,
+ overflow: 'hidden',
+ backgroundColor: '#007AFF',
+ },
+ messageBubble: {
+ borderRadius: 10,
+ padding: 5,
+ backgroundColor: 'darkgray',
+ },
+});
\ No newline at end of file
diff --git a/example/src/types.ts b/example/src/types.ts
new file mode 100644
index 0000000..b2430db
--- /dev/null
+++ b/example/src/types.ts
@@ -0,0 +1,3 @@
+import type { Room } from "@unomed/react-native-matrix-sdk";
+
+export type Chat = Room;
\ No newline at end of file
diff --git a/example/src/use-matrix.tsx b/example/src/use-matrix.tsx
new file mode 100644
index 0000000..65350a7
--- /dev/null
+++ b/example/src/use-matrix.tsx
@@ -0,0 +1,105 @@
+import { createContext, useCallback, useContext, useRef, useState, type FunctionComponent, type PropsWithChildren } from "react";
+import { Session, SyncServiceState, UserProfile, type ClientLike, type Room, type RoomLike } from "@unomed/react-native-matrix-sdk";
+
+import { type Media, loadMedia as _loadMedia } from "./matrix-lib/load-media";
+import { type Credentials, login as _login } from "./matrix-lib/login";
+import { synchronize as _synchronize, type Message } from "./matrix-lib/synchronize";
+
+export { type Credentials, type Message };
+
+export interface MatrixAPI {
+ login: (credential: Credentials) => Promise;
+ synchronize: () => Promise;
+ synchronizeStatus: SyncServiceState;
+ roomMap: Record; // room id -> room
+ messageMap: Record; // room id -> message list
+ spaceMap: Record; // room id -> space
+ userMap: Record; // user id -> user
+ loadMedia: (url: string | undefined) => Promise;
+ session: Session | undefined;
+}
+
+const _useMatrix = (): MatrixAPI => {
+ const clientRef = useRef(undefined);
+ const [{
+ synchronizeStatus,
+ roomMap,
+ messageMap,
+ spaceMap,
+ session,
+ userMap,
+ }, setState] = useState({
+ synchronizeStatus: SyncServiceState.Offline,
+ roomMap: {},
+ messageMap: {},
+ spaceMap: {},
+ userMap: {},
+ session: undefined as (Session | undefined),
+ });
+
+ function getClient() {
+ if (clientRef.current) {
+ return clientRef.current;
+ }
+ throw new Error('No client available - login first!');
+ }
+
+ const login = useCallback(async (credentials: Credentials) => {
+ const loginResponse = await _login(credentials, { id: 'example-app-id', name: 'MatrixSDKExampleApp' });
+ clientRef.current = loginResponse.client;
+ console.log('finished login', loginResponse);
+ setState((old) => ({
+ ...old,
+ session: loginResponse.session,
+ }));
+ }, []);
+
+ const synchronize = useCallback(async () => {
+ const client = getClient();
+ _synchronize(client, {
+ onChange: (data) => {
+ setState((old) => ({
+ ...old,
+ ...data, // maybe need to check for actual changes
+ messageMap: { // messages are run incrementally therefor each tick might be a seperate room
+ ...old.messageMap,
+ ...data.messageMap,
+ },
+ }));
+ },
+ });
+ }, []);
+
+ const loadMedia = useCallback(async (url: string | undefined): Promise => {
+ if (!url) {
+ return undefined;
+ }
+ const client = getClient();
+ return _loadMedia(client, url);
+ }, []);
+
+ return {
+ login,
+ synchronize,
+ synchronizeStatus,
+ roomMap,
+ messageMap,
+ spaceMap,
+ userMap,
+ session,
+ loadMedia,
+ };
+}
+
+const MatrixContext = createContext(null);
+
+export const MatrixProvider: FunctionComponent = ({ children }) => {
+ const api = _useMatrix();
+ return ({children});
+};
+
+export const useMatrix = () => {
+ const ctx = useContext(MatrixContext);
+ if (!ctx) throw new Error('useMatrixContext must be used within MatrixProvider');
+ return ctx;
+};
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index b448b24..848b905 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5548,9 +5548,9 @@ __metadata:
linkType: hard
"caniuse-lite@npm:^1.0.30001663":
- version: 1.0.30001717
- resolution: "caniuse-lite@npm:1.0.30001717"
- checksum: 357fbb230d86d28c0f7005d0c19a2274059ad4f1ed419ebe8754737ec908b567c9745abf0d16eda93417f40913221adc6290eb2f0432bc5bb5364f95bd7eabfa
+ version: 1.0.30001809
+ resolution: "caniuse-lite@npm:1.0.30001809"
+ checksum: 848f2e481da9e7da0327e351bfcfdcf610058c756ceb9daa0f0a12090f84dd31962da33e62a2d8bc3774eb2ca255bdca1026431c3f47333af62f5d4e3bc71056
languageName: node
linkType: hard