feat: implement Paarrot API for Electron integration with navigation and IPC handlers
This commit is contained in:
418
src/app/paarrot-api.ts
Normal file
418
src/app/paarrot-api.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
/**
|
||||
* Paarrot API Handler
|
||||
*
|
||||
* This module handles API actions from the Electron API server.
|
||||
* It receives actions via IPC and executes them on the Matrix client.
|
||||
*
|
||||
* To use this, import and call initPaarrotAPI() early in your app initialization.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { getHomeRoomPath, getDirectRoomPath, getSpaceRoomPath } from './pages/pathUtils';
|
||||
import { getCanonicalAliasOrRoomId, isRoomId } from './utils/matrix';
|
||||
import { getCallService } from './features/call/useCall';
|
||||
|
||||
/**
|
||||
* Global reference to the router navigate function
|
||||
* This is set by the router and used to navigate programmatically
|
||||
*/
|
||||
let navigateFunction: ((path: string) => void) | null = null;
|
||||
|
||||
/**
|
||||
* Set the navigate function for programmatic navigation
|
||||
* Call this from your router setup
|
||||
*/
|
||||
export function setPaarrotNavigate(navigate: (path: string) => void) {
|
||||
navigateFunction = navigate;
|
||||
console.log('Paarrot API: Navigate function registered');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the Paarrot API handler
|
||||
* This should be called once during app startup
|
||||
*/
|
||||
export function initPaarrotAPI(matrixClient: MatrixClient) {
|
||||
// Check if we're running in Electron
|
||||
if (!(window as any).electron?.api?.onAction) {
|
||||
console.log('Paarrot API: Not running in Electron, skipping API initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Paarrot API: Initializing API handler');
|
||||
|
||||
// Listen for API actions from the Electron API server
|
||||
(window as any).electron.api.onAction(async (action: {
|
||||
action: string;
|
||||
params: any;
|
||||
responseChannel: string;
|
||||
}) => {
|
||||
console.log('Paarrot API: Received action:', action.action, action.params);
|
||||
|
||||
try {
|
||||
let result;
|
||||
|
||||
switch (action.action) {
|
||||
case 'get-status':
|
||||
result = await getStatus(matrixClient);
|
||||
break;
|
||||
|
||||
case 'toggle-mute':
|
||||
result = await toggleMute(matrixClient);
|
||||
break;
|
||||
|
||||
case 'set-mute':
|
||||
result = await setMute(matrixClient, action.params.muted);
|
||||
break;
|
||||
|
||||
case 'toggle-deafen':
|
||||
result = await toggleDeafen(matrixClient);
|
||||
break;
|
||||
|
||||
case 'set-deafen':
|
||||
result = await setDeafen(matrixClient, action.params.deafened);
|
||||
break;
|
||||
|
||||
case 'change-channel':
|
||||
result = await changeChannel(matrixClient, action.params.roomId);
|
||||
break;
|
||||
|
||||
case 'get-channels':
|
||||
result = await getChannels(matrixClient);
|
||||
break;
|
||||
|
||||
case 'send-message':
|
||||
result = await sendMessage(matrixClient, action.params.roomId, action.params.message);
|
||||
break;
|
||||
|
||||
case 'send-message-current':
|
||||
result = await sendMessageCurrent(matrixClient, action.params.message);
|
||||
break;
|
||||
|
||||
case 'get-current-room':
|
||||
result = await getCurrentRoom(matrixClient);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown action: ${action.action}`);
|
||||
}
|
||||
|
||||
// Send success response back
|
||||
(window as any).electron.api.sendResponse(action.responseChannel, {
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
|
||||
console.log('Paarrot API: Action completed successfully:', action.action);
|
||||
} catch (error: any) {
|
||||
console.error('Paarrot API: Action failed:', action.action, error);
|
||||
|
||||
// Send error response back
|
||||
(window as any).electron.api.sendResponse(action.responseChannel, {
|
||||
success: false,
|
||||
error: error.message || 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Paarrot API: Handler initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current app status
|
||||
*/
|
||||
async function getStatus(matrixClient: MatrixClient) {
|
||||
const currentRoom = getCurrentRoomId();
|
||||
|
||||
// Get actual mute/deafen state from CallService if available
|
||||
const callService = getCallService();
|
||||
const activeCall = callService?.getActiveCall();
|
||||
|
||||
return {
|
||||
muted: activeCall?.isMuted ?? false,
|
||||
deafened: activeCall?.isDeafened ?? false,
|
||||
currentRoom,
|
||||
connected: matrixClient.isInitialSyncComplete() || false,
|
||||
userId: matrixClient.getUserId(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle microphone mute
|
||||
*/
|
||||
async function toggleMute(matrixClient: MatrixClient) {
|
||||
const callService = getCallService();
|
||||
|
||||
if (!callService) {
|
||||
return {
|
||||
muted: false,
|
||||
message: 'CallService not initialized - no active call',
|
||||
};
|
||||
}
|
||||
|
||||
const activeCall = callService.getActiveCall();
|
||||
if (!activeCall) {
|
||||
return {
|
||||
muted: false,
|
||||
message: 'No active call to mute/unmute',
|
||||
};
|
||||
}
|
||||
|
||||
// Toggle mute state
|
||||
const newMuteState = callService.toggleMute();
|
||||
|
||||
return {
|
||||
muted: newMuteState,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set microphone mute state
|
||||
*/
|
||||
async function setMute(matrixClient: MatrixClient, muted: boolean) {
|
||||
const callService = getCallService();
|
||||
|
||||
if (!callService) {
|
||||
return {
|
||||
muted,
|
||||
message: 'CallService not initialized - no active call',
|
||||
};
|
||||
}
|
||||
|
||||
const activeCall = callService.getActiveCall();
|
||||
if (!activeCall) {
|
||||
return {
|
||||
muted,
|
||||
message: 'No active call to mute/unmute',
|
||||
};
|
||||
}
|
||||
|
||||
// Set mute to desired state if it's different
|
||||
if (activeCall.isMuted !== muted) {
|
||||
callService.toggleMute();
|
||||
}
|
||||
|
||||
return {
|
||||
muted,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle deafen (mute speakers)
|
||||
*/
|
||||
async function toggleDeafen(matrixClient: MatrixClient) {
|
||||
const callService = getCallService();
|
||||
|
||||
if (!callService) {
|
||||
return {
|
||||
deafened: false,
|
||||
message: 'CallService not initialized - no active call',
|
||||
};
|
||||
}
|
||||
|
||||
const activeCall = callService.getActiveCall();
|
||||
if (!activeCall) {
|
||||
return {
|
||||
deafened: false,
|
||||
message: 'No active call to deafen/undeafen',
|
||||
};
|
||||
}
|
||||
|
||||
// Toggle deafen state
|
||||
const newDeafenState = callService.toggleDeafen();
|
||||
|
||||
return {
|
||||
deafened: newDeafenState,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set deafen state
|
||||
*/
|
||||
async function setDeafen(matrixClient: MatrixClient, deafened: boolean) {
|
||||
const callService = getCallService();
|
||||
|
||||
if (!callService) {
|
||||
return {
|
||||
deafened,
|
||||
message: 'CallService not initialized - no active call',
|
||||
};
|
||||
}
|
||||
|
||||
const activeCall = callService.getActiveCall();
|
||||
if (!activeCall) {
|
||||
return {
|
||||
deafened,
|
||||
message: 'No active call to deafen/undeafen',
|
||||
};
|
||||
}
|
||||
|
||||
// Set deafen to desired state if it's different
|
||||
if (activeCall.isDeafened !== deafened) {
|
||||
callService.toggleDeafen();
|
||||
}
|
||||
|
||||
return {
|
||||
deafened,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Change to a different room/channel
|
||||
*/
|
||||
async function changeChannel(matrixClient: any, roomId: string) {
|
||||
const room = matrixClient?.getRoom(roomId);
|
||||
if (!room) {
|
||||
throw new Error(`Room not found: ${roomId}`);
|
||||
}
|
||||
|
||||
if (!navigateFunction) {
|
||||
throw new Error('Navigate function not registered. Call setPaarrotNavigate from your router setup.');
|
||||
}
|
||||
|
||||
// Determine the appropriate path based on room type
|
||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(matrixClient, roomId);
|
||||
let path: string;
|
||||
|
||||
// Check if it's a direct message
|
||||
const isDirect = room.guessDMUserId() !== null;
|
||||
|
||||
if (isDirect) {
|
||||
// Navigate to direct message
|
||||
path = getDirectRoomPath(roomIdOrAlias);
|
||||
} else if (room.isSpaceRoom()) {
|
||||
// Navigate to space
|
||||
path = getSpaceRoomPath(roomIdOrAlias, roomIdOrAlias);
|
||||
} else {
|
||||
// Navigate to regular room in home
|
||||
path = getHomeRoomPath(roomIdOrAlias);
|
||||
}
|
||||
|
||||
// Navigate to the room
|
||||
navigateFunction(path);
|
||||
|
||||
return {
|
||||
roomId,
|
||||
roomName: room.name || 'Unnamed Room',
|
||||
path,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of rooms/channels
|
||||
*/
|
||||
async function getChannels(matrixClient: any) {
|
||||
const rooms = matrixClient?.getRooms() || [];
|
||||
|
||||
return rooms
|
||||
.filter((room: any) => !room.isSpaceRoom())
|
||||
.map((room: any) => ({
|
||||
roomId: room.roomId,
|
||||
name: room.name || 'Unnamed Room',
|
||||
isDirect: room.getMyMembership() === 'invite' ? false : room.guessDMUserId() !== null,
|
||||
avatar: room.getMxcAvatarUrl() || null,
|
||||
}))
|
||||
.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to a specific room
|
||||
*/
|
||||
async function sendMessage(matrixClient: any, roomId: string, message: string) {
|
||||
const room = matrixClient?.getRoom(roomId);
|
||||
if (!room) {
|
||||
throw new Error(`Room not found: ${roomId}`);
|
||||
}
|
||||
|
||||
const content = {
|
||||
msgtype: 'm.text',
|
||||
body: message,
|
||||
};
|
||||
|
||||
const result = await matrixClient.sendMessage(roomId, content);
|
||||
|
||||
return {
|
||||
eventId: result.event_id,
|
||||
roomId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the current room
|
||||
*/
|
||||
async function sendMessageCurrent(matrixClient: any, message: string) {
|
||||
const currentRoomId = getCurrentRoomId();
|
||||
if (!currentRoomId) {
|
||||
throw new Error('No room currently active');
|
||||
}
|
||||
|
||||
return sendMessage(matrixClient, currentRoomId, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about the current room
|
||||
*/
|
||||
async function getCurrentRoom(matrixClient: any) {
|
||||
const currentRoomId = getCurrentRoomId();
|
||||
if (!currentRoomId) {
|
||||
throw new Error('No room currently active');
|
||||
}
|
||||
|
||||
const room = matrixClient?.getRoom(currentRoomId);
|
||||
if (!room) {
|
||||
throw new Error(`Current room not found: ${currentRoomId}`);
|
||||
}
|
||||
|
||||
return {
|
||||
roomId: room.roomId,
|
||||
name: room.name || 'Unnamed Room',
|
||||
avatar: room.getMxcAvatarUrl() || null,
|
||||
isDirect: room.guessDMUserId() !== null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently active room ID
|
||||
* Extracts room ID from the current URL pathname
|
||||
*/
|
||||
function getCurrentRoomId(): string | null {
|
||||
try {
|
||||
// Get the pathname from URL
|
||||
// Support both normal routing and hash routing
|
||||
let pathname = window.location.pathname;
|
||||
|
||||
// If using hash router, parse from hash
|
||||
if (window.location.hash) {
|
||||
const hashMatch = window.location.hash.match(/#(.+)/);
|
||||
if (hashMatch) {
|
||||
pathname = hashMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
// URL patterns:
|
||||
// /home/:roomIdOrAlias/:eventId?/
|
||||
// /direct/:roomIdOrAlias/:eventId?/
|
||||
// /:spaceIdOrAlias/:roomIdOrAlias/:eventId?/
|
||||
|
||||
// Try to match room patterns
|
||||
const patterns = [
|
||||
/^\/home\/([^/]+)/, // /home/:roomIdOrAlias
|
||||
/^\/direct\/([^/]+)/, // /direct/:roomIdOrAlias
|
||||
/^\/[^/]+\/([^/]+)/, // /:spaceIdOrAlias/:roomIdOrAlias (space rooms)
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = pathname.match(pattern);
|
||||
if (match && match[1]) {
|
||||
// Decode URI component in case the room ID was encoded
|
||||
return decodeURIComponent(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Paarrot API: Error getting current room ID:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default { initPaarrotAPI };
|
||||
Reference in New Issue
Block a user