TTP -> MTP, A lot of other stuff #21

Merged
alois merged 81 commits from dev into main 2026-07-21 11:57:33 +03:00
10 changed files with 205 additions and 15 deletions
Showing only changes of commit 943203eeaf - Show all commits

(feat): improve tray icon
All checks were successful
/ build-web (push) Successful in 7m20s
/ build-desktop (linux) (push) Successful in 11m59s
/ build-mobile (push) Successful in 19m4s
/ release (push) Successful in 3m0s

(qol): update todo
Alois 2026-07-11 01:45:26 +02:00

View file

@ -58,8 +58,12 @@
"to": "web" "to": "web"
}, },
{ {
"from": "build/icons/icon.png", "from": "build/icons",
"to": "icons/icon.png" "to": "icons",
"filter": [
"32x32.png",
"icon.png"
]
} }
], ],
"linux": { "linux": {

View file

@ -12,10 +12,11 @@ import {
import { checkForUpdates } from "./updates.js"; import { checkForUpdates } from "./updates.js";
import { import {
ipcChannels, ipcChannels,
type DesktopCallStatus,
type DesktopScreenShareAudioOutput, type DesktopScreenShareAudioOutput,
type DesktopScreenShareCapabilities, type DesktopScreenShareCapabilities,
} from "../shared/ipc.js"; } from "../shared/ipc.js";
import { initTray } from "./tray.js"; import { initTray, setTrayCallStatus } from "./tray.js";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const verbose = process.argv.includes("--verbose"); const verbose = process.argv.includes("--verbose");
@ -189,6 +190,25 @@ function registerIpc() {
); );
ipcMain.handle(ipcChannels.getVersion, () => app.getVersion()); ipcMain.handle(ipcChannels.getVersion, () => app.getVersion());
ipcMain.handle(ipcChannels.checkForUpdates, checkForUpdates); ipcMain.handle(ipcChannels.checkForUpdates, checkForUpdates);
ipcMain.handle(ipcChannels.setCallStatus, (_event, status: unknown) => {
if (
typeof status !== "object" ||
status === null ||
typeof (status as DesktopCallStatus).inCall !== "boolean" ||
typeof (status as DesktopCallStatus).speaking !== "boolean" ||
((status as DesktopCallStatus).iconDataUrl !== undefined &&
(typeof (status as DesktopCallStatus).iconDataUrl !== "string" ||
!(status as DesktopCallStatus).iconDataUrl?.startsWith(
"data:image/png;base64,",
) ||
(status as DesktopCallStatus).iconDataUrl!.length > 16_384))
) {
throw new Error("Invalid call status.");
}
const { inCall, iconDataUrl } = status as DesktopCallStatus;
setTrayCallStatus(inCall, iconDataUrl);
});
ipcMain.handle(ipcChannels.minimizeWindow, () => { ipcMain.handle(ipcChannels.minimizeWindow, () => {
verboseLog("window:minimize"); verboseLog("window:minimize");
mainWindow?.minimize(); mainWindow?.minimize();
@ -311,7 +331,7 @@ async function start() {
verboseLog("app ready"); verboseLog("app ready");
registerIpc(); registerIpc();
registerDisplayMediaHandler(); registerDisplayMediaHandler();
initTray(); initTray(() => mainWindow);
await createWindow(); await createWindow();
} }

View file

@ -1,4 +1,4 @@
import { Tray, Menu } from "electron"; import { app, BrowserWindow, Menu, nativeImage, Tray } from "electron";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@ -7,19 +7,56 @@ let tray: Tray | null = null;
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
export function initTray() { function getTrayIconPath(filename: string) {
const iconPath = path.join(__dirname, "../../build/icons/32x32.png"); if (app.isPackaged) return path.join(process.resourcesPath, "icons", filename);
return path.resolve(__dirname, "../../build/icons", filename);
}
tray = new Tray(iconPath); // keep reference alive export function setTrayCallStatus(
inCall: boolean,
iconDataUrl?: string,
) {
if (!tray) return;
if (inCall && iconDataUrl) {
const image = nativeImage.createFromDataURL(iconDataUrl);
if (!image.isEmpty()) {
tray.setImage(image);
return;
}
}
tray.setImage(getTrayIconPath("32x32.png"));
}
export function initTray(getMainWindow: () => BrowserWindow | null) {
tray = new Tray(getTrayIconPath("32x32.png")); // keep reference alive
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ label: "Restart", type: "normal" }, {
{ label: "Quit", type: "normal" }, label: "Restart",
type: "normal",
click: () => {
app.relaunch();
app.quit();
},
},
{ label: "Quit", type: "normal", click: () => app.quit() },
]); ]);
tray.setContextMenu(contextMenu); tray.setContextMenu(contextMenu);
tray.on("click", (event) => { tray.on("click", () => {
console.log(event); const mainWindow = getMainWindow();
if (!mainWindow) return;
if (mainWindow.isVisible()) {
mainWindow.hide();
return;
}
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.show();
mainWindow.focus();
}); });
} }

View file

@ -1,5 +1,9 @@
import { contextBridge, ipcRenderer } from "electron"; import { contextBridge, ipcRenderer } from "electron";
import { ipcChannels, type DesktopScreenShareSource } from "../shared/ipc.js"; import {
ipcChannels,
type DesktopCallStatus,
type DesktopScreenShareSource,
} from "../shared/ipc.js";
function windowAction(channel: string) { function windowAction(channel: string) {
return () => ipcRenderer.invoke(channel); return () => ipcRenderer.invoke(channel);
@ -27,6 +31,21 @@ const desktopApi = {
updates: { updates: {
checkForUpdates: () => ipcRenderer.invoke(ipcChannels.checkForUpdates), checkForUpdates: () => ipcRenderer.invoke(ipcChannels.checkForUpdates),
}, },
call: {
setStatus: (status: DesktopCallStatus) => {
if (
typeof status?.inCall !== "boolean" ||
typeof status?.speaking !== "boolean" ||
(status.iconDataUrl !== undefined &&
(typeof status.iconDataUrl !== "string" ||
!status.iconDataUrl.startsWith("data:image/png;base64,")))
) {
return Promise.reject(new Error("Invalid call status."));
}
return ipcRenderer.invoke(ipcChannels.setCallStatus, status);
},
},
window: { window: {
minimize: () => windowAction(ipcChannels.minimizeWindow), minimize: () => windowAction(ipcChannels.minimizeWindow),
maximize: () => windowAction(ipcChannels.maximizeWindow), maximize: () => windowAction(ipcChannels.maximizeWindow),

View file

@ -20,6 +20,12 @@ export type DesktopScreenShareCapabilities = {
hasReliableSystemAudio: boolean; hasReliableSystemAudio: boolean;
}; };
export type DesktopCallStatus = {
inCall: boolean;
speaking: boolean;
iconDataUrl?: string;
};
export type ReleaseArtifact = { export type ReleaseArtifact = {
name: string; name: string;
platform: string; platform: string;
@ -55,4 +61,5 @@ export const ipcChannels = {
closeWindow: "window:close", closeWindow: "window:close",
getVersion: "app:getVersion", getVersion: "app:getVersion",
checkForUpdates: "updates:checkForUpdates", checkForUpdates: "updates:checkForUpdates",
setCallStatus: "call:setStatus",
} as const; } as const;

View file

@ -23,7 +23,8 @@ import Login from "@/routes/screens/login";
import CallPopout from "@tensamin/call/popout"; import CallPopout from "@tensamin/call/popout";
import ChatContext from "@tensamin/chat/context"; import ChatContext from "@tensamin/chat/context";
import { useInitializeCall } from "@tensamin/call/store"; import { useCall, useInitializeCall } from "@tensamin/call/store";
import { useIsSpeaking } from "@tensamin/call/speakingState";
import { Provider as MTPProvider } from "@tensamin/mtp"; import { Provider as MTPProvider } from "@tensamin/mtp";
import UserProvider from "@tensamin/user/context"; import UserProvider from "@tensamin/user/context";
import DeeplinkContext from "@tensamin/tauri/deeplinkHandler"; import DeeplinkContext from "@tensamin/tauri/deeplinkHandler";
@ -274,8 +275,94 @@ function AppShell() {
); );
} }
function createCallTrayIcon(color: string, speaking: boolean) {
const canvas = document.createElement("canvas");
canvas.width = 32;
canvas.height = 32;
const context = canvas.getContext("2d");
if (!context) return undefined;
context.globalAlpha = speaking ? 1 : 0.55;
context.fillStyle = color;
context.beginPath();
context.arc(16, 16, 13, 0, Math.PI * 2);
context.fill();
if (speaking) {
context.globalAlpha = 0.3;
context.fillStyle = "#ffffff";
context.fill();
}
return canvas.toDataURL("image/png");
}
function CallInit() { function CallInit() {
return useInitializeCall(); useInitializeCall();
const { load } = useStorage();
const {
themeColor,
themePalette,
themePrimaryColor,
themePolarity,
themeTint,
themeCustomCss,
} = useTheme();
const [localUserId, setLocalUserId] = useState(-1);
const [primaryColor, setPrimaryColor] = useState("");
const inCall = useCall((state) => state.state === "open");
const speaking = useIsSpeaking(localUserId);
useEffect(() => {
let active = true;
load("user_id").then((userId) => {
if (active) setLocalUserId(userId);
});
return () => {
active = false;
};
}, [load]);
useEffect(() => {
const frame = requestAnimationFrame(() => {
setPrimaryColor(
getComputedStyle(document.documentElement)
.getPropertyValue("--primary")
.trim(),
);
});
return () => cancelAnimationFrame(frame);
}, [
themeColor,
themeCustomCss,
themePalette,
themePolarity,
themePrimaryColor,
themeTint,
]);
useEffect(() => {
const iconDataUrl = primaryColor
? createCallTrayIcon(primaryColor, speaking)
: undefined;
void window.tensaminDesktop?.call
?.setStatus?.({
inCall,
speaking: inCall && speaking,
iconDataUrl: inCall ? iconDataUrl : undefined,
})
.catch((error: unknown) => {
console.error("Failed to update desktop call status", error);
});
}, [inCall, primaryColor, speaking]);
return null;
} }
const rootRoute = createRootRoute({ const rootRoute = createRootRoute({

View file

@ -5,6 +5,7 @@
"type": "module", "type": "module",
"exports": { "exports": {
"./store": "./src/store.tsx", "./store": "./src/store.tsx",
"./speakingState": "./src/speakingState.ts",
"./screen": "./src/screen.tsx", "./screen": "./src/screen.tsx",
"./utils": "./src/utils.ts", "./utils": "./src/utils.ts",
"./sidebarBox": "./src/components/sidebarBox.tsx", "./sidebarBox": "./src/components/sidebarBox.tsx",

View file

@ -50,6 +50,13 @@ declare global {
>; >;
selectScreenShareSource?: (sourceId: string) => Promise<boolean>; selectScreenShareSource?: (sourceId: string) => Promise<boolean>;
}; };
call?: {
setStatus?: (status: {
inCall: boolean;
speaking: boolean;
iconDataUrl?: string;
}) => Promise<void>;
};
}; };
} }
} }

View file

@ -31,6 +31,13 @@ type ElectronDesktopApi = {
>; >;
selectScreenShareSource?: (sourceId: string) => Promise<boolean>; selectScreenShareSource?: (sourceId: string) => Promise<boolean>;
}; };
call?: {
setStatus?: (status: {
inCall: boolean;
speaking: boolean;
iconDataUrl?: string;
}) => Promise<void>;
};
}; };
declare global { declare global {

View file

@ -1,4 +1,5 @@
- Move legal to extra onboarding package - Move legal to extra onboarding package
- Add a bunch of tests - Add a bunch of tests
- Add packages/cache/ to handle caching - Add packages/cache/ to handle caching
- Add packages/hotkeys/
- Full accessability - Full accessability