nativescript-matrix-sdk
Version:
Native Matrix SDK integration for NativeScript
1,512 lines (1,304 loc) • 113 kB
text/typescript
/// <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}`);