feat: Add notifications
This commit is contained in:
18
electron/cqrs/queries/handlers/getMessagesSince.ts
Normal file
18
electron/cqrs/queries/handlers/getMessagesSince.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { DataSource, MoreThan } from 'typeorm';
|
||||
import { MessageEntity } from '../../../entities';
|
||||
import { GetMessagesSinceQuery } from '../../types';
|
||||
import { rowToMessage } from '../../mappers';
|
||||
|
||||
export async function handleGetMessagesSince(query: GetMessagesSinceQuery, dataSource: DataSource) {
|
||||
const repo = dataSource.getRepository(MessageEntity);
|
||||
const { roomId, sinceTimestamp } = query.payload;
|
||||
const rows = await repo.find({
|
||||
where: {
|
||||
roomId,
|
||||
timestamp: MoreThan(sinceTimestamp)
|
||||
},
|
||||
order: { timestamp: 'ASC' }
|
||||
});
|
||||
|
||||
return rows.map(rowToMessage);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
QueryTypeKey,
|
||||
Query,
|
||||
GetMessagesQuery,
|
||||
GetMessagesSinceQuery,
|
||||
GetMessageByIdQuery,
|
||||
GetReactionsForMessageQuery,
|
||||
GetUserQuery,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
GetAttachmentsForMessageQuery
|
||||
} from '../types';
|
||||
import { handleGetMessages } from './handlers/getMessages';
|
||||
import { handleGetMessagesSince } from './handlers/getMessagesSince';
|
||||
import { handleGetMessageById } from './handlers/getMessageById';
|
||||
import { handleGetReactionsForMessage } from './handlers/getReactionsForMessage';
|
||||
import { handleGetUser } from './handlers/getUser';
|
||||
@@ -27,6 +29,7 @@ import { handleGetAllAttachments } from './handlers/getAllAttachments';
|
||||
|
||||
export const buildQueryHandlers = (dataSource: DataSource): Record<QueryTypeKey, (query: Query) => Promise<unknown>> => ({
|
||||
[QueryType.GetMessages]: (query) => handleGetMessages(query as GetMessagesQuery, dataSource),
|
||||
[QueryType.GetMessagesSince]: (query) => handleGetMessagesSince(query as GetMessagesSinceQuery, dataSource),
|
||||
[QueryType.GetMessageById]: (query) => handleGetMessageById(query as GetMessageByIdQuery, dataSource),
|
||||
[QueryType.GetReactionsForMessage]: (query) => handleGetReactionsForMessage(query as GetReactionsForMessageQuery, dataSource),
|
||||
[QueryType.GetUser]: (query) => handleGetUser(query as GetUserQuery, dataSource),
|
||||
|
||||
@@ -22,6 +22,7 @@ export type CommandTypeKey = typeof CommandType[keyof typeof CommandType];
|
||||
|
||||
export const QueryType = {
|
||||
GetMessages: 'get-messages',
|
||||
GetMessagesSince: 'get-messages-since',
|
||||
GetMessageById: 'get-message-by-id',
|
||||
GetReactionsForMessage: 'get-reactions-for-message',
|
||||
GetUser: 'get-user',
|
||||
@@ -160,6 +161,7 @@ export type Command =
|
||||
| ClearAllDataCommand;
|
||||
|
||||
export interface GetMessagesQuery { type: typeof QueryType.GetMessages; payload: { roomId: string; limit?: number; offset?: number } }
|
||||
export interface GetMessagesSinceQuery { type: typeof QueryType.GetMessagesSince; payload: { roomId: string; sinceTimestamp: number } }
|
||||
export interface GetMessageByIdQuery { type: typeof QueryType.GetMessageById; payload: { messageId: string } }
|
||||
export interface GetReactionsForMessageQuery { type: typeof QueryType.GetReactionsForMessage; payload: { messageId: string } }
|
||||
export interface GetUserQuery { type: typeof QueryType.GetUser; payload: { userId: string } }
|
||||
@@ -174,6 +176,7 @@ export interface GetAllAttachmentsQuery { type: typeof QueryType.GetAllAttachmen
|
||||
|
||||
export type Query =
|
||||
| GetMessagesQuery
|
||||
| GetMessagesSinceQuery
|
||||
| GetMessageByIdQuery
|
||||
| GetReactionsForMessageQuery
|
||||
| GetUserQuery
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
desktopCapturer,
|
||||
dialog,
|
||||
ipcMain,
|
||||
Notification,
|
||||
shell
|
||||
} from 'electron';
|
||||
import * as fs from 'fs';
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
} from '../update/desktop-updater';
|
||||
import { consumePendingDeepLink } from '../app/deep-links';
|
||||
import { synchronizeAutoStartSetting } from '../app/auto-start';
|
||||
import { getMainWindow, getWindowIconPath } from '../window/create-window';
|
||||
|
||||
const DEFAULT_MIME_TYPE = 'application/octet-stream';
|
||||
const FILE_CLIPBOARD_FORMATS = [
|
||||
@@ -86,6 +88,12 @@ interface ClipboardFilePayload {
|
||||
path?: string;
|
||||
}
|
||||
|
||||
interface DesktopNotificationPayload {
|
||||
body: string;
|
||||
requestAttention?: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
function resolveLinuxDisplayServer(): string {
|
||||
if (process.platform !== 'linux') {
|
||||
return 'N/A';
|
||||
@@ -316,6 +324,69 @@ export function setupSystemHandlers(): void {
|
||||
|
||||
ipcMain.handle('get-desktop-settings', () => getDesktopSettingsSnapshot());
|
||||
|
||||
ipcMain.handle('show-desktop-notification', async (_event, payload: DesktopNotificationPayload) => {
|
||||
const title = typeof payload?.title === 'string' ? payload.title.trim() : '';
|
||||
const body = typeof payload?.body === 'string' ? payload.body : '';
|
||||
const mainWindow = getMainWindow();
|
||||
|
||||
if (!title) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Notification.isSupported()) {
|
||||
try {
|
||||
const notification = new Notification({
|
||||
title,
|
||||
body,
|
||||
icon: getWindowIconPath(),
|
||||
silent: true
|
||||
});
|
||||
|
||||
notification.on('click', () => {
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
|
||||
if (!mainWindow.isVisible()) {
|
||||
mainWindow.show();
|
||||
}
|
||||
|
||||
mainWindow.focus();
|
||||
});
|
||||
|
||||
notification.show();
|
||||
} catch {
|
||||
// Ignore notification center failures and still attempt taskbar attention.
|
||||
}
|
||||
}
|
||||
|
||||
if (payload?.requestAttention && mainWindow && (mainWindow.isMinimized() || !mainWindow.isFocused())) {
|
||||
mainWindow.flashFrame(true);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('request-window-attention', () => {
|
||||
const mainWindow = getMainWindow();
|
||||
|
||||
if (!mainWindow || (!mainWindow.isMinimized() && mainWindow.isFocused())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mainWindow.flashFrame(true);
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('clear-window-attention', () => {
|
||||
getMainWindow()?.flashFrame(false);
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('get-auto-update-state', () => getDesktopUpdateState());
|
||||
|
||||
ipcMain.handle('get-auto-update-server-health', async (_event, serverUrl: string) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ const LINUX_SCREEN_SHARE_MONITOR_AUDIO_CHUNK_CHANNEL = 'linux-screen-share-monit
|
||||
const LINUX_SCREEN_SHARE_MONITOR_AUDIO_ENDED_CHANNEL = 'linux-screen-share-monitor-audio-ended';
|
||||
const AUTO_UPDATE_STATE_CHANGED_CHANNEL = 'auto-update-state-changed';
|
||||
const DEEP_LINK_RECEIVED_CHANNEL = 'deep-link-received';
|
||||
const WINDOW_STATE_CHANGED_CHANNEL = 'window-state-changed';
|
||||
|
||||
export interface LinuxScreenShareAudioRoutingInfo {
|
||||
available: boolean;
|
||||
@@ -90,6 +91,17 @@ export interface DesktopUpdateState {
|
||||
targetVersion: string | null;
|
||||
}
|
||||
|
||||
export interface DesktopNotificationPayload {
|
||||
body: string;
|
||||
requestAttention: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface WindowStateSnapshot {
|
||||
isFocused: boolean;
|
||||
isMinimized: boolean;
|
||||
}
|
||||
|
||||
function readLinuxDisplayServer(): string {
|
||||
if (process.platform !== 'linux') {
|
||||
return 'N/A';
|
||||
@@ -132,6 +144,10 @@ export interface ElectronAPI {
|
||||
runtimeHardwareAcceleration: boolean;
|
||||
restartRequired: boolean;
|
||||
}>;
|
||||
showDesktopNotification: (payload: DesktopNotificationPayload) => Promise<boolean>;
|
||||
requestWindowAttention: () => Promise<boolean>;
|
||||
clearWindowAttention: () => Promise<boolean>;
|
||||
onWindowStateChanged: (listener: (state: WindowStateSnapshot) => void) => () => void;
|
||||
getAutoUpdateState: () => Promise<DesktopUpdateState>;
|
||||
getAutoUpdateServerHealth: (serverUrl: string) => Promise<DesktopUpdateServerHealthSnapshot>;
|
||||
configureAutoUpdateContext: (context: Partial<DesktopUpdateServerContext>) => Promise<DesktopUpdateState>;
|
||||
@@ -213,6 +229,20 @@ const electronAPI: ElectronAPI = {
|
||||
getAppDataPath: () => ipcRenderer.invoke('get-app-data-path'),
|
||||
consumePendingDeepLink: () => ipcRenderer.invoke('consume-pending-deep-link'),
|
||||
getDesktopSettings: () => ipcRenderer.invoke('get-desktop-settings'),
|
||||
showDesktopNotification: (payload) => ipcRenderer.invoke('show-desktop-notification', payload),
|
||||
requestWindowAttention: () => ipcRenderer.invoke('request-window-attention'),
|
||||
clearWindowAttention: () => ipcRenderer.invoke('clear-window-attention'),
|
||||
onWindowStateChanged: (listener) => {
|
||||
const wrappedListener = (_event: Electron.IpcRendererEvent, state: WindowStateSnapshot) => {
|
||||
listener(state);
|
||||
};
|
||||
|
||||
ipcRenderer.on(WINDOW_STATE_CHANGED_CHANNEL, wrappedListener);
|
||||
|
||||
return () => {
|
||||
ipcRenderer.removeListener(WINDOW_STATE_CHANGED_CHANNEL, wrappedListener);
|
||||
};
|
||||
},
|
||||
getAutoUpdateState: () => ipcRenderer.invoke('get-auto-update-state'),
|
||||
getAutoUpdateServerHealth: (serverUrl) => ipcRenderer.invoke('get-auto-update-server-health', serverUrl),
|
||||
configureAutoUpdateContext: (context) => ipcRenderer.invoke('configure-auto-update-context', context),
|
||||
|
||||
@@ -10,6 +10,8 @@ import * as path from 'path';
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
const WINDOW_STATE_CHANGED_CHANNEL = 'window-state-changed';
|
||||
|
||||
function getAssetPath(...segments: string[]): string {
|
||||
const basePath = app.isPackaged
|
||||
? path.join(process.resourcesPath, 'images')
|
||||
@@ -38,10 +40,23 @@ export function getDockIconPath(): string | undefined {
|
||||
return getExistingAssetPath('macos', '1024x1024.png');
|
||||
}
|
||||
|
||||
export { getWindowIconPath };
|
||||
|
||||
export function getMainWindow(): BrowserWindow | null {
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
function emitWindowState(): void {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
mainWindow.webContents.send(WINDOW_STATE_CHANGED_CHANNEL, {
|
||||
isFocused: mainWindow.isFocused(),
|
||||
isMinimized: mainWindow.isMinimized()
|
||||
});
|
||||
}
|
||||
|
||||
export async function createWindow(): Promise<void> {
|
||||
const windowIconPath = getWindowIconPath();
|
||||
|
||||
@@ -109,6 +124,25 @@ export async function createWindow(): Promise<void> {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
mainWindow.on('focus', () => {
|
||||
mainWindow?.flashFrame(false);
|
||||
emitWindowState();
|
||||
});
|
||||
|
||||
mainWindow.on('blur', () => {
|
||||
emitWindowState();
|
||||
});
|
||||
|
||||
mainWindow.on('minimize', () => {
|
||||
emitWindowState();
|
||||
});
|
||||
|
||||
mainWindow.on('restore', () => {
|
||||
emitWindowState();
|
||||
});
|
||||
|
||||
emitWindowState();
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url);
|
||||
return { action: 'deny' };
|
||||
|
||||
Reference in New Issue
Block a user