UNPKG

nativescript-matrix-sdk

Version:

Native Matrix SDK integration for NativeScript

1,512 lines (1,304 loc) 113 kB
/// <reference path="./matrix-types.d.ts" /> import { MatrixMessage, MatrixEventType, MatrixClient, MatrixServer, MatrixChat, MessageOptions, MessageResult, SendMessageOptions, PaginationToken, MatrixEventData, MatrixEventListener, FileSendOptions, TransferProgress, ReadReceipt, PresenceStatus, UserPresence, DeviceVerificationStatus, DeviceKeyInfo, KeyExportFormat, KeyBackupInfo, CrossSigningInfo, RoomEncryptionAlgorithm, MatrixReaction } from '../../index.d'; import { MessageTransactionStatus, IPendingTransaction, IRetryOptions } from '../../common/interfaces'; import { MatrixError, MatrixErrorType } from '../../src/errors'; import { Logger } from '../../src/logger'; import { Application, isAndroid, File } from '@nativescript/core'; import { TransactionManagerAndroid } from './transaction-manager.android'; // Declare global variables needed for Android declare const global: { android: any; }; // Try to load Matrix Android SDK let Matrix: any; // Check if Matrix Android SDK is available const hasMatrixSDK = () => { try { if (global.android && isAndroid) { // Get classes from Java namespace Matrix = org.matrix.android.sdk.api.Matrix; return true; } return false; } catch (error) { Logger.error('[MatrixAndroidClient] Error checking Matrix Android SDK:', error); return false; } }; // Track if we should use mock implementation const useMockImplementation = !hasMatrixSDK(); // Initialize Matrix Android SDK if available try { if (hasMatrixSDK()) { Logger.info('[MatrixAndroidClient] Successfully loaded Matrix Android SDK classes'); } else { Logger.warn('[MatrixAndroidClient] Matrix Android SDK classes not found. Using mock implementation.'); } } catch (error) { Logger.error('[MatrixAndroidClient] Error initializing Matrix Android SDK:', error); } /** * Matrix Android Client implementation * This will use the native Matrix Android SDK if available, * otherwise falls back to a mock implementation */ export class MatrixAndroidClient implements MatrixClient { private nativeClient: any; // Matrix Session private userId: string | null = null; private homeserverUrl: string | null = null; private isInitialized = false; private useMockImplementation = useMockImplementation; // Event handling properties private eventListeners: Map<MatrixEventType, Set<MatrixEventListener>> = new Map(); private isListening = false; private roomListeners: Map<string, any> = new Map(); private syncStateListener: any = null; private syncStatusListener: any = null; private presenceListener: any = null; // Listener for presence updates private reconnectTimeoutId: any = null; private lastSyncTime = 0; private transactionManager: TransactionManagerAndroid; constructor() { this.transactionManager = new TransactionManagerAndroid(this); } /** * Initialize the Matrix client * @param homeserverUrl - URL of the Matrix homeserver * @param accessToken - User's access token */ async initialize(homeserverUrl: string, accessToken: string): Promise<void> { try { this.homeserverUrl = homeserverUrl; if (this.useMockImplementation) { // Mock implementation for testing or when native SDK is not available Logger.info(`[MatrixAndroidClient] Using mock implementation with homeserver: ${homeserverUrl}`); this.userId = "@user:matrix.org"; // Placeholder this.isInitialized = true; return; } // Real implementation using native Android SDK // Parse URL for uri creation const uri = android.net.Uri.parse(homeserverUrl); // Create credentials const credentials = new org.matrix.android.sdk.api.auth.data.Credentials(); credentials.accessToken = accessToken; credentials.userId = ""; // Will be set after initialization // Create the Matrix session const context = Application.android.context; const matrix = Matrix.getInstance(context); // Initialize the session this.nativeClient = matrix.authenticateWith(credentials); this.nativeClient.open(); // Get user ID this.userId = this.nativeClient.getMyUserId(); if (!this.userId) { throw MatrixError.initialization('Failed to get user ID', undefined, { homeserverUrl }); } credentials.userId = this.userId; // Update credentials // Wait for sync to complete with a proper listener await this.waitForInitialSync(); Logger.info(`[MatrixAndroidClient] Successfully initialized with user ID: ${this.userId}`); this.isInitialized = true; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`[MatrixAndroidClient] Initialization error: ${errorMessage}`); throw MatrixError.initialization( `Failed to initialize Matrix Android client: ${errorMessage}`, error instanceof Error ? error : undefined, { homeserverUrl } ); } } /** * Wait for initial sync completion using proper listener * @returns Promise that resolves when sync is complete */ private async waitForInitialSync(): Promise<void> { return new Promise<void>((resolve) => { try { // Try to use the proper sync listener const syncStatusService = this.nativeClient.getSyncStatusService(); if (syncStatusService) { // Note: Using any type for Java class references due to TypeScript limitations // @ts-ignore - TypeScript doesn't understand Java class hierarchies const listener = new org.matrix.android.sdk.api.session.sync.SyncStatusListener({ onSyncStatus: (status: any) => { // Check if the status is READY if (status && status.toString().includes('READY')) { // Clean up listener syncStatusService.removeSyncStatusListener(listener); Logger.info('[MatrixAndroidClient] Sync completed via listener'); resolve(); } } }); syncStatusService.addSyncStatusListener(listener); // Set a fallback timeout in case the listener doesn't fire setTimeout(() => { try { syncStatusService.removeSyncStatusListener(listener); } catch (e) { // Ignore cleanup errors } Logger.warn('[MatrixAndroidClient] Sync listener timed out, continuing anyway'); resolve(); }, 10000); } else { // Fallback if sync service not available Logger.warn('[MatrixAndroidClient] SyncStatusService not available, using timeout'); setTimeout(() => resolve(), 3000); } } catch (error) { // Fallback if anything fails with the listener Logger.warn('[MatrixAndroidClient] Error setting up sync listener, using timeout'); setTimeout(() => resolve(), 3000); } }); } /** * Login with username and password * @param homeserverUrl - URL of the Matrix homeserver * @param username - Username or Matrix ID * @param password - User password * @param deviceName - Name for this device * @returns Promise that resolves with user ID and access token */ async login(homeserverUrl: string, username: string, password: string, deviceName?: string): Promise<{userId: string, accessToken: string}> { try { Logger.info(`[MatrixAndroidClient] Logging in with username: ${username} on server: ${homeserverUrl}`); if (this.useMockImplementation) { // Mock implementation for testing Logger.info(`[MatrixAndroidClient] Using mock login implementation`); this.homeserverUrl = homeserverUrl; this.userId = `@${username}:${new URL(homeserverUrl).hostname}`; const mockToken = `mock_token_${Date.now()}`; this.isInitialized = true; return { userId: this.userId, accessToken: mockToken }; } // Real implementation using the Matrix Android SDK const context = Application.android.context; const matrix = Matrix.getInstance(context); // Create authentication service const authService = matrix.getAuthenticationService(); // Prepare login params const loginParams = new org.matrix.android.sdk.api.auth.LoginParams.Builder() .login(username) .password(password) .deviceDisplayName(deviceName || "NativeScript Android Client") .build(); // Perform login and get credentials const credentials = await new Promise<any>((resolve, reject) => { // Use type assertion to bypass TypeScript type checking const callback = { onSuccess: (data) => { resolve(data); }, onFailure: (error) => { reject(new Error(error.getMessage())); } }; authService.login(loginParams, callback as any); }); // Update client with the obtained credentials this.userId = credentials.userId; this.homeserverUrl = homeserverUrl; // Initialize the Matrix session this.nativeClient = matrix.authenticateWith(credentials); this.nativeClient.open(); // Wait for sync to complete with a proper listener await this.waitForInitialSync(); this.isInitialized = true; Logger.info(`[MatrixAndroidClient] Successfully logged in as: ${credentials.userId}`); return { userId: credentials.userId, accessToken: credentials.accessToken }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; Logger.error(`[MatrixAndroidClient] Login error: ${errorMessage}`); throw MatrixError.authentication( `Failed to login: ${errorMessage}`, error instanceof Error ? error : undefined, { homeserverUrl, username } ); } } /** * Login with SSO * @param homeserverUrl - URL of the Matrix homeserver * @param callbackUrl - URL to redirect to after successful SSO authentication * @returns Promise that resolves with user ID and access token */ async loginWithSSO(homeserverUrl: string, callbackUrl: string): Promise<{userId: string, accessToken: string}> { try { Logger.info(`[MatrixAndroidClient] Initiating SSO login on server: ${homeserverUrl}`); if (this.useMockImplementation) { // Mock implementation for testing Logger.info(`[MatrixAndroidClient] Using mock SSO login implementation`); this.homeserverUrl = homeserverUrl; this.userId = `@sso_user:${new URL(homeserverUrl).hostname}`; const mockToken = `mock_sso_token_${Date.now()}`; this.isInitialized = true; return { userId: this.userId, accessToken: mockToken }; } // Real implementation using the Matrix Android SDK const context = Application.android.context; const matrix = Matrix.getInstance(context); // Create authentication service const authService = matrix.getAuthenticationService(); // Get login types from homeserver const loginFlows = await new Promise<any>((resolve, reject) => { // Use type assertion to bypass TypeScript type checking const callback = { onSuccess: (data) => { resolve(data); }, onFailure: (error) => { reject(new Error(error.getMessage())); } }; authService.getLoginFlows(homeserverUrl, callback as any); }); // Check if SSO login is supported let ssoSupported = false; for (let i = 0; i < loginFlows.size(); i++) { const flow = loginFlows.get(i); if (flow.type === 'm.login.sso') { ssoSupported = true; break; } } if (!ssoSupported) { throw MatrixError.authentication('SSO login not supported by this homeserver', undefined, { homeserverUrl }); } // Generate SSO URL const loginWizard = authService.getLoginWizard(); const ssoUrl = loginWizard.getSsoUrl(callbackUrl, null, null); // Return the SSO URL to be opened in a WebView // Note: The actual token exchange should be handled when receiving the redirect to callbackUrl return { userId: '', // Will be set after redirect completes accessToken: '', // Will be set after redirect completes ssoUrl // Additional property to be handled by the app } as any; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; Logger.error(`[MatrixAndroidClient] SSO Login error: ${errorMessage}`); throw MatrixError.authentication( `Failed to initiate SSO login: ${errorMessage}`, error instanceof Error ? error : undefined, { homeserverUrl } ); } } /** * Register a new user * @param homeserverUrl - URL of the Matrix homeserver * @param username - Desired username * @param password - Desired password * @param deviceName - Name for this device * @returns Promise that resolves with user ID and access token */ async register(homeserverUrl: string, username: string, password: string, deviceName?: string): Promise<{userId: string, accessToken: string}> { try { Logger.info(`[MatrixAndroidClient] Registering user: ${username} on server: ${homeserverUrl}`); if (this.useMockImplementation) { // Mock implementation for testing Logger.info(`[MatrixAndroidClient] Using mock registration implementation`); this.homeserverUrl = homeserverUrl; this.userId = `@${username}:${new URL(homeserverUrl).hostname}`; const mockToken = `mock_reg_token_${Date.now()}`; this.isInitialized = true; return { userId: this.userId, accessToken: mockToken }; } // Real implementation using the Matrix Android SDK const context = Application.android.context; const matrix = Matrix.getInstance(context); // Create authentication service const authService = matrix.getAuthenticationService(); // Prepare registration params const registrationParams = new org.matrix.android.sdk.api.auth.RegistrationParams.Builder() .username(username) .password(password) .initialDeviceDisplayName(deviceName || "NativeScript Android Client") .build(); // Perform registration and get credentials const credentials = await new Promise<any>((resolve, reject) => { // Use type assertion to bypass TypeScript type checking const callback = { onSuccess: (data) => { resolve(data); }, onFailure: (error) => { reject(new Error(error.getMessage())); } }; authService.register(registrationParams, callback as any); }); // Update client with the obtained credentials this.userId = credentials.userId; this.homeserverUrl = homeserverUrl; // Initialize the Matrix session this.nativeClient = matrix.authenticateWith(credentials); this.nativeClient.open(); // Wait for sync to complete with a proper listener await this.waitForInitialSync(); this.isInitialized = true; Logger.info(`[MatrixAndroidClient] Successfully registered as: ${credentials.userId}`); return { userId: credentials.userId, accessToken: credentials.accessToken }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; Logger.error(`[MatrixAndroidClient] Registration error: ${errorMessage}`); throw MatrixError.authentication( `Failed to register: ${errorMessage}`, error instanceof Error ? error : undefined, { homeserverUrl, username } ); } } /** * Get the current user ID */ getUserId(): string | null { return this.userId; } /** * Get available servers/homeservers */ async getServers(): Promise<MatrixServer[]> { this.checkInitialized(); // Matrix doesn't have a direct equivalent to Discord servers // For simplicity, we'll create a default "home" server if (this.useMockImplementation) { // Return a placeholder server return [{ id: 'home', name: 'Home', icon: '🏠', description: 'Your Matrix homeserver', memberCount: 1, isPrivate: false, createdAt: new Date(), updatedAt: new Date() }]; } try { // Get the homeserver domain const domain = new URL(this.homeserverUrl || '').hostname; // Return a single server representing the Matrix homeserver return [{ id: domain, name: domain, icon: '🔷', description: 'Your Matrix homeserver', memberCount: 0, // We don't have this information isPrivate: false, createdAt: new Date(), updatedAt: new Date() }]; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`[MatrixAndroidClient] Error getting servers: ${errorMessage}`); throw MatrixError.network( `Failed to get Matrix servers: ${errorMessage}`, error instanceof Error ? error : undefined ); } } /** * Get chats for a specific server */ async getChats(serverId: string): Promise<MatrixChat[]> { this.checkInitialized(); if (this.useMockImplementation) { // Return placeholder chats return [{ id: '!room1:matrix.org', serverId, type: 'room', name: 'General Chat', avatar: '#', lastMessage: 'Welcome to Matrix!', lastMessageAt: new Date(), unread: 0, participants: ['@user:matrix.org'], isArchived: false, isPinned: false }]; } try { // Get all rooms using the Room List API const roomSummaries = await this.nativeClient.getRoomSummaries( new org.matrix.android.sdk.api.session.room.model.RoomSummaryQueryParams.Builder().build() ); // Convert to our chat type const chats: MatrixChat[] = []; const roomSummaryCount = roomSummaries.size(); for (let i = 0; i < roomSummaryCount; i++) { const summary = roomSummaries.get(i); // Determine if it's a direct chat const isDirect = summary.isDirect; const roomType = isDirect ? 'direct' : 'room'; // Get basic info const roomId = summary.getRoomId(); const roomName = summary.getDisplayName() || 'Unnamed Room'; // Get last message info (simplified) const lastMsg = summary.getLatestPreviewableEvent(); const lastMessage = lastMsg ? this.getEventText(lastMsg) : 'No messages'; const lastMessageAt = lastMsg ? new Date(lastMsg.getOriginServerTs()) : new Date(); // Get unread count const unreadCount = summary.getNotificationCount(); chats.push({ id: roomId, serverId, type: roomType, name: roomName, avatar: isDirect ? '👤' : '#', lastMessage, lastMessageAt, unread: unreadCount, participants: [], // Would need to get room members isArchived: false, isPinned: false }); } return chats; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`[MatrixAndroidClient] Error getting chats: ${errorMessage}`); throw MatrixError.room( `Failed to get chats: ${errorMessage}`, error instanceof Error ? error : undefined, { serverId } ); } } /** * Get messages for a specific chat * @param chatId - ID of the chat to fetch messages from * @param options - Options for retrieving messages * @returns List of messages in the chat and pagination token */ async getMessages(chatId: string, options?: MessageOptions): Promise<MessageResult> { this.checkInitialized(); Logger.info(`[MatrixAndroidClient] Getting messages for chat: ${chatId}`); // Set default options const limit = options?.limit || 20; const direction = options?.direction || 'backward'; const fromToken = options?.fromToken || null; try { if (this.useMockImplementation) { // Mock implementation for testing const messages: MatrixMessage[] = []; const now = new Date(); for (let i = 0; i < limit; i++) { const messageDate = new Date(now.getTime() - (i * 60000)); // 1 minute apart messages.push({ id: `mock_msg_${chatId}_${i}_${Date.now()}`, chatId, sender: { id: i % 3 === 0 ? this.userId || "@user:matrix.org" : `@user${i}:matrix.org`, name: i % 3 === 0 ? "Me" : `User ${i}`, avatar: '' }, content: `This is mock message ${i} in chat ${chatId}`, contentType: 'text', reactions: [], createdAt: messageDate, status: MessageTransactionStatus.SENT }); } // Return mock pagination token return { messages, nextToken: { token: `mock_token_${Date.now()}`, hasMore: messages.length >= limit // Pretend there are more if we returned the requested limit } }; } // Get the room const room = this.nativeClient.getRoom(chatId); if (!room) { throw MatrixError.room(`Room not found: ${chatId}`); } // Get the timeline service const timelineService = room.getTimelineService(); // Build pagination parameters const timelineParams = new org.matrix.android.sdk.api.session.room.timeline.TimelineParams.Builder() .limit(limit) .buildAround(fromToken); // Set direction const timelineDirection = direction === 'forward' ? org.matrix.android.sdk.api.session.room.timeline.Direction.FORWARDS : org.matrix.android.sdk.api.session.room.timeline.Direction.BACKWARDS; // Get timeline events with pagination const timelineEvents = await new Promise<any>((resolve, reject) => { // Use type assertion to bypass TypeScript type checking const callback = { onSuccess: (events: any[], nextToken: string) => { resolve({ events, nextToken }); }, onFailure: (error: any) => { reject(new Error(error.getMessage())); } }; timelineService.getTimelineEvents(timelineParams, timelineDirection, callback as any); }); // Parse the messages const messages: MatrixMessage[] = []; const events = timelineEvents.events; // Extract the pagination token const nextToken: PaginationToken = { token: timelineEvents.nextToken || "", hasMore: timelineEvents.nextToken !== null }; // Process events into messages for (let i = 0; i < events.size(); i++) { const event = events.get(i); // Skip non-message events if (event.type !== "m.room.message") { continue; } // Get event content const content = event.contentJson; // Get sender information const senderId = event.senderId; const sender = room.getMember(senderId); const senderName = sender ? sender.displayName : senderId; const senderAvatar = sender ? sender.avatarUrl : ''; // Create the message object const message: MatrixMessage = { id: event.eventId, chatId, sender: { id: senderId, name: senderName, avatar: senderAvatar }, content: content.getString("body") || "", contentType: this.convertContentType(content.getString("msgtype")), reactions: await this.getReactionsForEvent(room, event.eventId), createdAt: new Date(event.originServerTs), status: MessageTransactionStatus.SENT }; messages.push(message); } return { messages, nextToken }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; Logger.error(`[MatrixAndroidClient] Error getting messages: ${errorMessage}`); throw MatrixError.room( `Failed to get messages for room ${chatId}: ${errorMessage}`, error instanceof Error ? error : undefined, { chatId } ); } } /** * Helper to convert Matrix message type to SDK message type */ private convertContentType(matrixType: string): 'text' | 'image' | 'file' | 'system' { switch (matrixType) { case 'm.text': case 'm.notice': return 'text'; case 'm.image': return 'image'; case 'm.file': return 'file'; default: return 'text'; } } /** * Helper to get reactions for an event */ private async getReactionsForEvent(room: any, eventId: string): Promise<MatrixReaction[]> { try { // Get the reaction service const reactionService = room.getReactionService(); // Get all reaction events for the message const reactionEvents = reactionService.getAllReactionEvents(eventId); const reactions: MatrixReaction[] = []; const reactionMap = new Map<string, {count: number, users: string[]}>(); // Process reaction events for (let i = 0; i < reactionEvents.size(); i++) { const reactionEvent = reactionEvents.get(i); const content = reactionEvent.getContent(); // Skip invalid reaction events if (!content.has("m.relates_to") || !content.getAsJsonObject("m.relates_to").has("key")) { continue; } const relatesTo = content.getAsJsonObject("m.relates_to"); const emoji = relatesTo.get("key").getAsString(); const userId = reactionEvent.getSender(); // Group by emoji if (!reactionMap.has(emoji)) { reactionMap.set(emoji, {count: 0, users: []}); } const data = reactionMap.get(emoji)!; data.count++; data.users.push(userId); } // Convert map to array reactionMap.forEach((data, emoji) => { reactions.push({ emoji, count: data.count, users: data.users }); }); return reactions; } catch (error) { // Just return empty array on error Logger.warn(`[MatrixAndroidClient] Error getting reactions: ${error}`); return []; } } /** * Send a message to a chat */ async sendMessage(chatId: string, content: string): Promise<MatrixMessage> { this.checkInitialized(); if (this.useMockImplementation) { // Return mock sent message Logger.info(`[MatrixAndroidClient] Mock sending message to ${chatId}: ${content}`); return { id: `$msg-${Date.now()}`, chatId, sender: { id: this.userId || '@user:matrix.org', name: 'Me', avatar: '👤' }, content, contentType: 'text', reactions: [], createdAt: new Date(), status: MessageTransactionStatus.SENT }; } try { // Get the room const room = this.nativeClient.getRoom(chatId); if (!room) { throw MatrixError.room( `Room not found: ${chatId}`, undefined, { chatId } ); } // Get room service const roomService = room.getRoomService(); // Send message const eventId = await roomService.sendTextMessage(content); // Create message object to return return { id: eventId, chatId, sender: { id: this.userId || '@unknown', name: 'Me', avatar: '👤' }, content, contentType: 'text', reactions: [], createdAt: new Date(), status: MessageTransactionStatus.SENT }; } catch (error) { // If this is already a MatrixError, just rethrow it if (error instanceof MatrixError) { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`[MatrixAndroidClient] Error sending message: ${errorMessage}`); throw MatrixError.event( `Failed to send message: ${errorMessage}`, error instanceof Error ? error : undefined, { chatId } ); } } /** * Add a reaction to a message */ async addReaction(chatId: string, messageId: string, reaction: string): Promise<void> { this.checkInitialized(); if (this.useMockImplementation) { Logger.info(`[MatrixAndroidClient] Mock adding reaction ${reaction} to message ${messageId} in chat ${chatId}`); return; } try { // Get the room const room = this.nativeClient.getRoom(chatId); if (!room) { throw MatrixError.room( `Room not found: ${chatId}`, undefined, { chatId } ); } // Get reaction service const reactionService = room.getReactionService(); // Send reaction await reactionService.sendReaction(messageId, reaction); Logger.info(`[MatrixAndroidClient] Successfully sent reaction ${reaction} to message ${messageId}`); } catch (error) { // If this is already a MatrixError, just rethrow it if (error instanceof MatrixError) { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`[MatrixAndroidClient] Error adding reaction: ${errorMessage}`); throw MatrixError.event( `Failed to add reaction: ${errorMessage}`, error instanceof Error ? error : undefined, { chatId, messageId, reaction } ); } } /** * Join a chat room */ async joinChat(chatId: string): Promise<void> { this.checkInitialized(); if (this.useMockImplementation) { Logger.info(`[MatrixAndroidClient] Mock joining chat ${chatId}`); return; } try { // Get room service const roomService = this.nativeClient.getRoomService(); // Join room await roomService.joinRoom(chatId); Logger.info(`[MatrixAndroidClient] Successfully joined room ${chatId}`); } catch (error) { // If this is already a MatrixError, just rethrow it if (error instanceof MatrixError) { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`[MatrixAndroidClient] Error joining room: ${errorMessage}`); throw MatrixError.room( `Failed to join room: ${errorMessage}`, error instanceof Error ? error : undefined, { chatId } ); } } /** * Leave a chat room */ async leaveChat(chatId: string): Promise<void> { this.checkInitialized(); if (this.useMockImplementation) { Logger.info(`[MatrixAndroidClient] Mock leaving chat ${chatId}`); return; } try { // Get room service const roomService = this.nativeClient.getRoomService(); // Leave room await roomService.leaveRoom(chatId); Logger.info(`[MatrixAndroidClient] Successfully left room ${chatId}`); } catch (error) { // If this is already a MatrixError, just rethrow it if (error instanceof MatrixError) { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`[MatrixAndroidClient] Error leaving room: ${errorMessage}`); throw MatrixError.room( `Failed to leave room: ${errorMessage}`, error instanceof Error ? error : undefined, { chatId } ); } } /** * Check if the client is initialized * @private */ private checkInitialized(): void { if (!this.isInitialized) { throw MatrixError.initialization( 'Matrix client not initialized. Call initialize() first.', undefined, { useMockImplementation: this.useMockImplementation } ); } } /** * Helper to extract text from an event * @private */ private getEventText(event: any): string { try { const content = event.getClearContent(); if (content && content.has('body')) { return content.get('body'); } return 'No message content'; } catch (error) { return 'Unable to get message content'; } } /** * Clean up resources when client is no longer needed * Important for memory management */ async cleanup(): Promise<void> { if (!this.isInitialized || this.useMockImplementation) { return; } try { Logger.info('[MatrixAndroidClient] Cleaning up resources'); if (this.nativeClient) { // Sign out and close the session - using default params await this.nativeClient.signOut(); // Clear references to native objects this.nativeClient = null; } this.isInitialized = false; this.userId = null; } catch (error) { Logger.error('[MatrixAndroidClient] Error during cleanup:', error); } } /** * Enable end-to-end encryption for a room * @param roomId - ID of the room to enable encryption for */ async enableEncryption(roomId: string): Promise<void> { this.checkInitialized(); if (this.useMockImplementation) { Logger.info(`[MatrixAndroidClient] Mock enabling encryption for room ${roomId}`); return; } try { // Get the room const room = this.nativeClient.getRoom(roomId); if (!room) { throw MatrixError.room( `Room not found: ${roomId}`, undefined, { roomId } ); } // Get crypto service const cryptoService = this.nativeClient.getCryptoService(); if (!cryptoService) { throw MatrixError.encryption( 'Crypto service not available', undefined, { roomId } ); } // Enable encryption for the room await cryptoService.enableEncryptionInRoom(roomId); Logger.info(`[MatrixAndroidClient] Successfully enabled encryption for room ${roomId}`); } catch (error) { // If this is already a MatrixError, just rethrow it if (error instanceof MatrixError) { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`[MatrixAndroidClient] Error enabling encryption: ${errorMessage}`); throw MatrixError.encryption( `Failed to enable encryption for room ${roomId}: ${errorMessage}`, error instanceof Error ? error : undefined, { roomId } ); } } /** * Check if a room has encryption enabled * @param roomId - ID of the room to check * @returns Whether encryption is enabled */ async isEncryptionEnabled(roomId: string): Promise<boolean> { this.checkInitialized(); if (this.useMockImplementation) { Logger.info(`[MatrixAndroidClient] Mock checking encryption for room ${roomId}`); return false; } try { // Get the room const room = this.nativeClient.getRoom(roomId); if (!room) { throw MatrixError.room( `Room not found: ${roomId}`, undefined, { roomId } ); } // Get room summary const roomSummary = room.getRoomSummary(); if (!roomSummary) { return false; } // Check if encryption is enabled return roomSummary.isEncrypted(); } catch (error) { // If this is already a MatrixError, just rethrow it if (error instanceof MatrixError) { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`[MatrixAndroidClient] Error checking encryption status: ${errorMessage}`); throw MatrixError.encryption( `Failed to check encryption status for room ${roomId}: ${errorMessage}`, error instanceof Error ? error : undefined, { roomId } ); } } /** * Add an event listener * @param eventType Type of event to listen for * @param listener Callback function to handle the event */ addEventListener(eventType: MatrixEventType, listener: MatrixEventListener): void { Logger.debug(`[MatrixAndroidClient] Adding event listener for ${eventType}`); if (!this.eventListeners.has(eventType)) { this.eventListeners.set(eventType, new Set()); } const listeners = this.eventListeners.get(eventType); listeners?.add(listener); } /** * Remove an event listener * @param eventType Type of event to stop listening for * @param listener Callback function to remove */ removeEventListener(eventType: MatrixEventType, listener: MatrixEventListener): void { Logger.debug(`[MatrixAndroidClient] Removing event listener for ${eventType}`); const listeners = this.eventListeners.get(eventType); if (listeners) { listeners.delete(listener); if (listeners.size === 0) { this.eventListeners.delete(eventType); } } } /** * Start listening for events */ async startListening(): Promise<void> { this.checkInitialized(); if (this.isListening) { Logger.debug('[MatrixAndroidClient] Already listening for events'); return; } if (this.useMockImplementation) { Logger.warn('[MatrixAndroidClient] Using mock implementation for startListening'); this.isListening = true; return; } try { Logger.info('[MatrixAndroidClient] Starting event listeners'); // Set up the room event listener this.setupRoomEventListener(); // Listen for sync state changes this.setupSyncListener(); // Start automatic reconnection if sync fails this.startSyncWatchdog(); this.isListening = true; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`[MatrixAndroidClient] Error starting listeners: ${errorMessage}`); throw new MatrixError( `Failed to start event listeners: ${errorMessage}`, MatrixErrorType.SYNC, error instanceof Error ? error : undefined ); } } /** * Stop listening for events */ async stopListening(): Promise<void> { if (!this.isListening || this.useMockImplementation) { this.isListening = false; return; } try { Logger.info('[MatrixAndroidClient] Stopping event listeners'); // Stop sync watchdog if (this.reconnectTimeoutId) { clearTimeout(this.reconnectTimeoutId); this.reconnectTimeoutId = null; } // Clean up room listeners for (const [roomId, listener] of this.roomListeners.entries()) { try { const room = this.nativeClient.getRoom(roomId); if (room) { const timelineService = room.getTimelineService(); if (timelineService && listener) { timelineService.removeListener(listener); } } } catch (err) { Logger.warn(`[MatrixAndroidClient] Error removing listener for room ${roomId}: ${err}`); } } // Remove sync listeners try { const syncService = this.nativeClient.getSyncService(); if (syncService && this.syncStateListener) { syncService.removeSyncStateListener(this.syncStateListener); this.syncStateListener = null; } const syncStatusService = this.nativeClient.getSyncStatusService(); if (syncStatusService && this.syncStatusListener) { syncStatusService.removeSyncStatusListener(this.syncStatusListener); this.syncStatusListener = null; } } catch (err) { Logger.warn(`[MatrixAndroidClient] Error removing sync listeners: ${err}`); } this.roomListeners.clear(); this.isListening = false; } catch (error) { Logger.error('[MatrixAndroidClient] Error stopping listeners:', error); } } /** * Start a watchdog to monitor sync state and reconnect if necessary */ private startSyncWatchdog(): void { // Clear any existing watchdog if (this.reconnectTimeoutId) { clearTimeout(this.reconnectTimeoutId); this.reconnectTimeoutId = null; } // Check sync state every minute const checkInterval = 60000; // 1 minute this.reconnectTimeoutId = setTimeout(async () => { try { // Check if we've received events in the last 2 minutes const now = Date.now(); const syncTimeout = 2 * 60 * 1000; // 2 minutes if (now - this.lastSyncTime > syncTimeout) { Logger.warn('[MatrixAndroidClient] No sync events received recently, triggering reconnection'); // Try to restart sync await this.restartSync(); } // Continue the watchdog this.startSyncWatchdog(); } catch (error) { Logger.error('[MatrixAndroidClient] Error in sync watchdog:', error); // Continue the watchdog even if there was an error this.startSyncWatchdog(); } }, checkInterval); } /** * Restart the sync process */ private async restartSync(): Promise<void> { try { // Get sync service const syncService = this.nativeClient.getSyncService(); if (!syncService) return; Logger.info('[MatrixAndroidClient] Restarting sync'); // Stop and restart sync await syncService.stopSync(); // Wait a moment before restarting await new Promise(resolve => setTimeout(resolve, 1000)); // Start sync again syncService.startSync(true); // true = start a new sync from scratch Logger.info('[MatrixAndroidClient] Sync restarted'); } catch (error) { Logger.error('[MatrixAndroidClient] Error restarting sync:', error); } } /** * Set up listeners for room events */ private setupRoomEventListener(): void { try { const roomService = this.nativeClient.getRoomService(); if (!roomService) return; // Note: Using any type for Java class references due to TypeScript limitations // @ts-ignore - TypeScript doesn't understand Java class hierarchies const roomListener = new org.matrix.android.sdk.api.session.room.RoomListener({ onRoomUpdated: (roomId: string) => { const room = this.nativeClient.getRoom(roomId); if (!room) return; // Add timeline listener for this room if not already listening if (!this.roomListeners.has(roomId)) { this.setupRoomTimelineListener(roomId, room); } // Get room details if available const roomSummary = room.getRoomSummary(); // Dispatch room state change event this.dispatchEvent(MatrixEventType.ROOM_STATE_CHANGED, { roomId, roomState: { updatedAt: new Date(), name: roomSummary?.getDisplayName ? roomSummary.getDisplayName() : undefined, membersCount: roomSummary?.getJoinedMembersCount ? roomSummary.getJoinedMembersCount() : undefined } }); } }); roomService.addListener(roomListener); // Set up listeners for existing rooms const rooms = this.nativeClient.getRoomSummaries( new org.matrix.android.sdk.api.session.room.model.RoomSummaryQueryParams.Builder().build() ); for (let i = 0; i < rooms.size(); i++) { const roomSummary = rooms.get(i); const roomId = roomSummary.getRoomId(); const room = this.nativeClient.getRoom(roomId); if (room) { this.setupRoomTimelineListener(roomId, room); } } } catch (error) { Logger.error('[MatrixAndroidClient] Error setting up room listener:', error); } } /** * Set up timeline listener for a specific room */ private setupRoomTimelineListener(roomId: string, room: any): void { try { // Remove any existing listener for this room const existingListener = this.roomListeners.get(roomId); if (existingListener) { try { const timelineService = room.getTimelineService(); if (timelineService) { timelineService.removeListener(existingListener); } } catch (err) { Logger.warn(`[MatrixAndroidClient] Error removing existing timeline listener for room ${roomId}: ${err}`); } this.roomListeners.delete(roomId); } const timelineService = room.getTimelineService(); if (!timelineService) return; // Note: Using any type for Java class references due to TypeScript limitations // @ts-ignore - TypeScript doesn't understand Java class hierarchies const timelineListener = new org.matrix.android.sdk.api.session.room.timeline.TimelineListener({ onTimelineUpdated: (snapshot: any) => { try { // Update last sync time this.lastSyncTime = Date.now(); // Handle new events this.processTimelineEvents(roomId, snapshot); } catch (err) { Logger.error(`[MatrixAndroidClient] Error in timeline listener for room ${roomId}:`, err); } } }); // Store listener for cleanup this.roomListeners.set(roomId, timelineListener); // Add the listener to the timeline service timelineService.addListener(timelineListener); // Add listeners for typing events and read receipts // Check if there's a typing service if (typeof room.getTypingService === 'function') { try { const typingService = room.getTypingService(); if (typingService) { // Using 'any' type to avoid TypeScript errors with Android Java classes // @ts-ignore const typingListener: any = new java.lang.Object({ onTypingStateChanged: (users: any) => { const typingUsers: string[] = []; if (users) { const size = users.size(); for (let i = 0; i < size; i++) { const userId = users.get(i); if (userId && userId !== this.userId) { typingUsers.push(userId); } } } // Dispatch typing event this.dispatchEvent(MatrixEventType.TYPING, { roomId, timestamp: new Date(), typing: { users: typingUsers, isTyping: typingUsers.length > 0 } }); Logger.debug(`[MatrixAndroidClient] Typing notification for room ${roomId}: ${typingUsers.join(', ')}`); } }); // Add the listener - using any type to bypass TypeScript limitations typingService.addTypingServiceListener(typingListener); Logger.debug(`[MatrixAndroidClient] Added typing listener for room ${roomId}`); } } catch (error) { Logger.error(`[MatrixAndroidClient] Error setting up typing listener for room ${roomId}:`, error); } } // Check if there's a read receipt service if (typeof room.getReadReceiptService === 'function') { try { const readService = room.getReadReceiptService(); if (readService) { // Using 'any' type to avoid TypeScript errors with Android Java classes // @ts-ignore const readReceiptListener: any = new java.lang.Object({ onReadReceiptUpdated: (userId: string, eventId: string, timestamp: number) => { if (userId && eventId && userId !== this.userId) { // Dispatch read receipt event this.dispatchEvent(MatrixEventType.READ_RECEIPT, { roomId, timestamp: new Date(), readReceipt: { userId, messageId: eventId, timestamp: new Date(timestamp) } }); Logger.debug(`[MatrixAndroidClient] Read receipt for room ${roomId}: ${userId} read message ${eventId}`);