perf(vite): add dynamic imports

This commit is contained in:
Alois 2026-08-31 20:48:32 +02:00
commit 85921a0a08
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
29 changed files with 470 additions and 326 deletions

View file

@ -92,6 +92,11 @@ function emitIcons(): Plugin {
attrs: { name: "apple-mobile-web-app-capable", content: "yes" }, attrs: { name: "apple-mobile-web-app-capable", content: "yes" },
injectTo: "head", injectTo: "head",
}, },
{
tag: "meta",
attrs: { name: "mobile-web-app-capable", content: "yes" },
injectTo: "head",
},
{ {
tag: "meta", tag: "meta",
attrs: { attrs: {

View file

@ -24,6 +24,7 @@
"@tensamin/chat": "workspace:*", "@tensamin/chat": "workspace:*",
"@tensamin/crypto": "workspace:*", "@tensamin/crypto": "workspace:*",
"@tensamin/hotkeys": "workspace:*", "@tensamin/hotkeys": "workspace:*",
"@tensamin/markdown": "workspace:*",
"@tensamin/mtp": "workspace:*", "@tensamin/mtp": "workspace:*",
"@tensamin/notifications": "workspace:*", "@tensamin/notifications": "workspace:*",
"@tensamin/onboarding": "workspace:*", "@tensamin/onboarding": "workspace:*",

View file

@ -0,0 +1,15 @@
import { lazy, Suspense } from "react";
import { useCall } from "@tensamin/call/state";
const Popout = lazy(() => import("@tensamin/call/popout"));
export default function CallPopout() {
const active = useCall((state) => state.state !== "closed");
return active ? (
<Suspense fallback={null}>
<Popout />
</Suspense>
) : null;
}

View file

@ -0,0 +1,95 @@
import { useEffect, useState } from "react";
import { useTheme } from "@methanium/ui";
import { useIsSpeaking } from "@tensamin/call/speakingState";
import { useCall, useInitializeCall } from "@tensamin/call/store";
import { useStorage } from "@tensamin/storage/context";
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");
}
export default function CallRuntimeInit() {
const callInvitePopup = 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 callInvitePopup;
}

View file

@ -0,0 +1,15 @@
import { lazy, Suspense } from "react";
import { useCall } from "@tensamin/call/state";
const SidebarBox = lazy(() => import("@tensamin/call/sidebarBox"));
export default function CallSidebarBox() {
const active = useCall((state) => state.state !== "closed");
return active ? (
<Suspense fallback={null}>
<SidebarBox />
</Suspense>
) : null;
}

View file

@ -1,6 +1,6 @@
import type { User } from "@tensamin/identity/context"; import type { User } from "@tensamin/identity/context";
import { Avatar, AvatarFallback, AvatarImage, Button } from "@methanium/ui"; import { Avatar, AvatarFallback, AvatarImage, Button } from "@methanium/ui";
import { Text } from "@methanium/ui/markdown"; import Text from "@tensamin/markdown/text";
import { ChevronDown, ChevronUp } from "lucide-react"; import { ChevronDown, ChevronUp } from "lucide-react";
import { useState } from "react"; import { useState } from "react";

View file

@ -14,7 +14,7 @@ import {
User, User,
} from "lucide-react"; } from "lucide-react";
import { useLocation, useNavigate, useSearch } from "@tanstack/react-router"; import { useLocation, useNavigate, useSearch } from "@tanstack/react-router";
import { joinCall, useCall } from "@tensamin/call/store"; import { useCall } from "@tensamin/call/state";
import Wrapper from "@tensamin/identity/wrapper"; import Wrapper from "@tensamin/identity/wrapper";
import { Skeleton } from "@methanium/ui"; import { Skeleton } from "@methanium/ui";
import { import {
@ -140,7 +140,9 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
<Button <Button
disabled={callState !== "closed"} disabled={callState !== "closed"}
onClick={() => { onClick={() => {
void joinCall(id); void import("@tensamin/call/store").then(({ joinCall }) =>
joinCall(id),
);
}} }}
className="w-9 h-9! aspect-square rounded-lg" className="w-9 h-9! aspect-square rounded-lg"
variant="outline" variant="outline"
@ -151,10 +153,12 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
<Button <Button
disabled={callState !== "closed"} disabled={callState !== "closed"}
onClick={() => { onClick={() => {
void joinCall( void import("@tensamin/call/store").then(({ joinCall }) =>
id, joinCall(
currentCalls[0].CallSecret, id,
currentCalls[0].CallId, currentCalls[0].CallSecret,
currentCalls[0].CallId,
),
); );
}} }}
className="w-9 h-9! aspect-square rounded-lg" className="w-9 h-9! aspect-square rounded-lg"
@ -182,7 +186,10 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
value={call.CallId} value={call.CallId}
key={call.CallId} key={call.CallId}
onSelect={() => { onSelect={() => {
void joinCall(id, call.CallSecret, call.CallId); void import("@tensamin/call/store").then(
({ joinCall }) =>
joinCall(id, call.CallSecret, call.CallId),
);
}} }}
> >
{displayCallId(call.CallId)} {displayCallId(call.CallId)}

View file

@ -0,0 +1,15 @@
import { Loader2 } from "lucide-react";
export default function RouteLoader() {
return (
<div
className="flex h-full min-h-40 w-full items-center justify-center bg-background"
role="status"
>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="size-5 animate-spin" />
<span>Loading...</span>
</div>
</div>
);
}

View file

@ -1,5 +1,6 @@
import Wrapper from "@tensamin/identity/wrapper"; import Wrapper from "@tensamin/identity/wrapper";
import { Basic, Loading } from "./modals/basic"; import { Basic, Loading } from "./modals/basic";
import CallSidebarBox from "./callSidebarBox";
import List from "@/features/conversation/list/body"; import List from "@/features/conversation/list/body";
import { import {
@ -30,7 +31,6 @@ import {
import { useIsMobile } from "@methanium/ui"; import { useIsMobile } from "@methanium/ui";
import { MobileNavbar } from "./navbar"; import { MobileNavbar } from "./navbar";
import SidebarBox from "@tensamin/call/sidebarBox";
import { useShowMobileNavbar } from "@/routes/app/useShowMobileNavbar"; import { useShowMobileNavbar } from "@/routes/app/useShowMobileNavbar";
import { Ellipsis, Check } from "lucide-react"; import { Ellipsis, Check } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
@ -285,7 +285,7 @@ export default function Sidebar() {
)} )}
{!isMobile && ( {!isMobile && (
<SidebarFooter> <SidebarFooter>
<SidebarBox /> <CallSidebarBox />
</SidebarFooter> </SidebarFooter>
)} )}
</> </>

View file

@ -12,45 +12,40 @@ import "./index.css";
import "@methanium/ui/index.css"; import "@methanium/ui/index.css";
import NotFound from "@/routes/404"; import NotFound from "@/routes/404";
import RouteLoader from "@/components/routeLoader";
import AppLayout from "@/routes/app/layout";
import { createSettingsRoute } from "@tensamin/settings"; import { createSettingsRoute } from "@tensamin/settings";
import OnboardingGate from "@tensamin/onboarding";
import Home from "@/routes/app/home"; import DeeplinkContext from "@tensamin/tauri/deeplinkHandler";
import ChatScreen from "@tensamin/chat/screen";
import CallScreen from "@tensamin/call/screen";
import Login from "@/routes/screens/login";
import ChatContext from "@tensamin/chat/context";
import { useCall, useInitializeCall } from "@tensamin/call/store";
import { useIsSpeaking } from "@tensamin/call/speakingState";
import { Provider as MTPProvider } from "@tensamin/mtp";
import UserProvider from "@tensamin/identity/context";
import DeeplinkContext, { useDeeplinks } from "@tensamin/tauri/deeplinkHandler";
import NotificationsProvider from "@tensamin/notifications/context";
import PwaRuntime from "@tensamin/pwa/runtime"; import PwaRuntime from "@tensamin/pwa/runtime";
import TAuthWrapper from "@tensamin/tauth/context";
import { ErrorScreen, ThemeProvider, useTheme } from "@methanium/ui"; import { ErrorScreen, ThemeProvider, useTheme } from "@methanium/ui";
import z from "zod"; import z from "zod";
import { useEffect, useRef, useState, type ReactNode } from "react"; import {
lazy,
Suspense,
useEffect,
useRef,
useState,
type ReactNode,
} from "react";
import Storage from "@tensamin/storage/context"; import Storage from "@tensamin/storage/context";
import Session from "@tensamin/storage/session";
import Crypto from "@tensamin/crypto/context";
import DesktopMediaProvider from "@tensamin/shared/desktopMedia";
import { log } from "@tensamin/shared/log"; import { log } from "@tensamin/shared/log";
import CacheSync from "@tensamin/cache/sync";
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
import { useLocation, useNavigate } from "@tanstack/react-router"; import { useLocation, useNavigate } from "@tanstack/react-router";
import { useIsMobile, Toaster, TooltipProvider } from "@methanium/ui"; import { useIsMobile, Toaster, TooltipProvider } from "@methanium/ui";
import { HotkeysProvider } from "@tensamin/hotkeys"; import { HotkeysProvider } from "@tensamin/hotkeys";
const Home = lazy(() => import("@/routes/app/home"));
const ChatScreen = lazy(() => import("@tensamin/chat/screen"));
const CallScreen = lazy(() => import("@tensamin/call/screen"));
const Login = lazy(() => import("@/routes/screens/login"));
const appShellImport = import("@/routes/app/shell");
const AppShell = lazy(() => appShellImport);
const wrapper = document.getElementById("root"); const wrapper = document.getElementById("root");
if (!wrapper) { if (!wrapper) {
@ -108,7 +103,7 @@ function LoginWrapper({ children }: { children: ReactNode }) {
}, [load, location.pathname, navigate, secureStorage]); }, [load, location.pathname, navigate, secureStorage]);
if (loggedIn !== true && location.pathname !== "/login") { if (loggedIn !== true && location.pathname !== "/login") {
return null; return <RouteLoader />;
} }
return children; return children;
@ -265,7 +260,9 @@ function RootShell() {
<HotkeysProvider> <HotkeysProvider>
<ThemeStorageBridge /> <ThemeStorageBridge />
<LoginWrapper> <LoginWrapper>
<Outlet /> <Suspense fallback={<RouteLoader />}>
<Outlet />
</Suspense>
</LoginWrapper> </LoginWrapper>
</HotkeysProvider> </HotkeysProvider>
</Storage> </Storage>
@ -276,155 +273,6 @@ function RootShell() {
); );
} }
function AppShell() {
return (
<OnboardingGate>
<Crypto>
<DesktopMediaProvider>
<MTPProvider>
<CacheSync />
<DeeplinkNavigator />
<Session>
<UserProvider>
<CallInit />
<TAuthWrapper>
<AppLayout>
<ChatContext>
<NotificationsProvider>
<Outlet />
</NotificationsProvider>
</ChatContext>
</AppLayout>
</TAuthWrapper>
</UserProvider>
</Session>
</MTPProvider>
</DesktopMediaProvider>
</Crypto>
</OnboardingGate>
);
}
function DeeplinkNavigator() {
const { deeplinks } = useDeeplinks();
const navigate = useNavigate();
const handledCount = useRef(0);
useEffect(() => {
const links = deeplinks.slice(handledCount.current);
handledCount.current = deeplinks.length;
for (const link of links) {
try {
const url = new URL(link);
const id = Number(url.searchParams.get("id"));
if (
url.protocol === "tensamin:" &&
url.hostname === "chat" &&
Number.isSafeInteger(id) &&
id > 0
) {
void navigate({ to: "/chat", search: { id } });
}
} catch {
// Ignore malformed URLs delivered by the platform.
}
}
}, [deeplinks, navigate]);
return null;
}
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() {
const callInvitePopup = 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 callInvitePopup;
}
const rootRoute = createRootRoute({ const rootRoute = createRootRoute({
component: RootShell, component: RootShell,
errorComponent: ({ error }: { error: Error }) => ( errorComponent: ({ error }: { error: Error }) => (

View file

@ -2,8 +2,8 @@ import { type ReactNode } from "react";
import Sidebar from "@/components/sidebar"; import Sidebar from "@/components/sidebar";
import Navbar, { MobileNavbar } from "@/components/navbar"; import Navbar, { MobileNavbar } from "@/components/navbar";
import CallPopout from "@/components/callPopout";
import { useShowMobileNavbar } from "./useShowMobileNavbar"; import { useShowMobileNavbar } from "./useShowMobileNavbar";
import CallPopout from "@tensamin/call/popout";
import { useIsMobile, cn, SidebarProvider } from "@methanium/ui"; import { useIsMobile, cn, SidebarProvider } from "@methanium/ui";

View file

@ -0,0 +1,78 @@
import { Suspense, useEffect, useRef } from "react";
import { Outlet, useNavigate } from "@tanstack/react-router";
import RouteLoader from "@/components/routeLoader";
import AppLayout from "./layout";
import CallInit from "@/components/callRuntimeInit";
import CacheSync from "@tensamin/cache/sync";
import ChatContext from "@tensamin/chat/context";
import Crypto from "@tensamin/crypto/context";
import UserProvider from "@tensamin/identity/context";
import { Provider as MTPProvider } from "@tensamin/mtp";
import NotificationsProvider from "@tensamin/notifications/context";
import OnboardingGate from "@tensamin/onboarding";
import DesktopMediaProvider from "@tensamin/shared/desktopMedia";
import Session from "@tensamin/storage/session";
import { useDeeplinks } from "@tensamin/tauri/deeplinkHandler";
import TAuthWrapper from "@tensamin/tauth/context";
function DeeplinkNavigator() {
const { deeplinks } = useDeeplinks();
const navigate = useNavigate();
const handledCount = useRef(0);
useEffect(() => {
const links = deeplinks.slice(handledCount.current);
handledCount.current = deeplinks.length;
for (const link of links) {
try {
const url = new URL(link);
const id = Number(url.searchParams.get("id"));
if (
url.protocol === "tensamin:" &&
url.hostname === "chat" &&
Number.isSafeInteger(id) &&
id > 0
) {
void navigate({ to: "/chat", search: { id } });
}
} catch {
// Ignore malformed URLs delivered by the platform.
}
}
}, [deeplinks, navigate]);
return null;
}
export default function AppShell() {
return (
<OnboardingGate>
<Crypto>
<DesktopMediaProvider>
<MTPProvider>
<CacheSync />
<DeeplinkNavigator />
<Session>
<UserProvider>
<CallInit />
<TAuthWrapper>
<AppLayout>
<ChatContext>
<NotificationsProvider>
<Suspense fallback={<RouteLoader />}>
<Outlet />
</Suspense>
</NotificationsProvider>
</ChatContext>
</AppLayout>
</TAuthWrapper>
</UserProvider>
</Session>
</MTPProvider>
</DesktopMediaProvider>
</Crypto>
</OnboardingGate>
);
}

View file

@ -124,6 +124,35 @@ export default defineConfig({
build: { build: {
minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false, minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
sourcemap: !!process.env.TAURI_ENV_DEBUG, sourcemap: !!process.env.TAURI_ENV_DEBUG,
chunkSizeWarningLimit: 550,
rolldownOptions: {
output: {
codeSplitting: {
groups: [
{
name: "codemirror-core",
test: /node_modules\/.pnpm\/@codemirror\+(?:state|view|language)@/,
priority: 20,
},
{
name: "codemirror-editor",
test: /node_modules\/.pnpm\/@codemirror\+(?:autocomplete|commands|lang-markdown)@|node_modules\/.pnpm\/codemirror@/,
priority: 20,
},
{
name: "livekit-client",
test: /node_modules\/.pnpm\/livekit-client@/,
priority: 20,
},
{
name: "livekit-support",
test: /node_modules\/.pnpm\/@livekit\+/,
priority: 20,
},
],
},
},
},
}, },
plugins: [ plugins: [
...tensaminPwa(), ...tensaminPwa(),

View file

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

View file

@ -0,0 +1,93 @@
import type { RefObject } from "react";
import { create } from "zustand";
import type { LocalMediaShareSession } from "./mediaShare/controller";
export type CallView = "preview" | "focused" | "grid";
export type WrappedCallSecret = {
secretId: string;
versionNumber: number;
encryptedSecret: Uint8Array;
kemCiphertext: Uint8Array;
wrappingScheme: string;
};
export type CallRuntime = {
navigate: (options: {
to: string;
search?: Record<string, unknown>;
}) => Promise<void>;
send: (
type: string,
data: Record<string, unknown>,
) => Promise<{ data: unknown }>;
load: (key: string) => Promise<unknown>;
getPublicKey: (userId: number) => Promise<string>;
};
export const useCall = create<{
state: "closed" | "closing" | "connecting" | "open" | "encrypting";
view: CallView;
invitedUserId: number | null;
callId: string | null;
incomingCallInvite: {
callId: string;
callSecret: WrappedCallSecret;
senderId: number;
} | null;
callSecret: string | null;
livekitToken: string | null;
currentCallData: { UserIds: number[]; exists: boolean } | null;
deaf: boolean;
micEnabled: boolean;
cameraEnabled: boolean;
screenShareEnabled: boolean;
screenShareSession: LocalMediaShareSession | null;
cameraSession: LocalMediaShareSession | null;
disabledCameraParticipantIds: number[];
focusedParticipantId: number | null;
focusedParticipantType: "user" | "stream" | null;
usersInFocusedViewHidden: boolean;
watchedStreamParticipantIds: number[];
pendingWatchedParticipantIds: number[];
activeScreenShareParticipantIds: number[];
isEncrypted: boolean;
ownCallSecretInvitePending: boolean;
callIsFullscreen: boolean;
callIsPopout: boolean;
layoutVersion: number;
screenRef: RefObject<HTMLDivElement | null> | null;
runtime: CallRuntime | null;
lastFocusedParticipantId: number | null;
}>(() => ({
state: "closed",
view: "preview",
invitedUserId: null,
callId: null,
incomingCallInvite: null,
callSecret: null,
livekitToken: null,
currentCallData: null,
deaf: false,
micEnabled: false,
cameraEnabled: false,
screenShareEnabled: false,
screenShareSession: null,
cameraSession: null,
disabledCameraParticipantIds: [],
focusedParticipantId: null,
focusedParticipantType: null,
usersInFocusedViewHidden: false,
watchedStreamParticipantIds: [],
pendingWatchedParticipantIds: [],
activeScreenShareParticipantIds: [],
isEncrypted: false,
ownCallSecretInvitePending: false,
callIsFullscreen: false,
callIsPopout: false,
layoutVersion: 0,
screenRef: null,
runtime: null,
lastFocusedParticipantId: null,
}));

View file

@ -1,5 +1,4 @@
import { useCallback, useEffect, useMemo, useRef } from "react"; import { useCallback, useEffect, useMemo, useRef } from "react";
import { create } from "zustand";
import { useLocation, useNavigate } from "@tanstack/react-router"; import { useLocation, useNavigate } from "@tanstack/react-router";
import { useMTP } from "@tensamin/mtp"; import { useMTP } from "@tensamin/mtp";
import { log, toast } from "@tensamin/shared/log"; import { log, toast } from "@tensamin/shared/log";
@ -33,7 +32,6 @@ import {
import z from "zod"; import z from "zod";
import { import {
createMediaShareController, createMediaShareController,
type LocalMediaShareSession,
} from "./mediaShare/controller"; } from "./mediaShare/controller";
import type { MediaShareRequest } from "./mediaShare"; import type { MediaShareRequest } from "./mediaShare";
import { import {
@ -41,6 +39,14 @@ import {
disposeSpeakingDetector, disposeSpeakingDetector,
} from "./speakingIndicator"; } from "./speakingIndicator";
import InvitePopup from "./components/invitePopup"; import InvitePopup from "./components/invitePopup";
import {
useCall,
type CallRuntime as Runtime,
type CallView,
type WrappedCallSecret,
} from "./state";
export { useCall } from "./state";
// logging // logging
setLogExtension( setLogExtension(
@ -51,19 +57,9 @@ setLogExtension(
getLogger("tensamin"), getLogger("tensamin"),
); );
type CallView = "preview" | "focused" | "grid";
type ProtocolCallSecret = NonNullable< type ProtocolCallSecret = NonNullable<
z.infer<typeof mtp.CallInvite.response>["CallSecret"] z.infer<typeof mtp.CallInvite.response>["CallSecret"]
>; >;
type WrappedCallSecret = {
secretId: string;
versionNumber: number;
encryptedSecret: Uint8Array;
kemCiphertext: Uint8Array;
wrappingScheme: string;
};
type CurrentCallData =
(z.infer<typeof mtp.CallData.response> & { exists: boolean }) | null;
type SendFn = ( type SendFn = (
type: string, type: string,
@ -72,16 +68,6 @@ type SendFn = (
type LoadFn = (key: string) => Promise<unknown>; type LoadFn = (key: string) => Promise<unknown>;
type RemoteVideoTrackSelector = Track.Kind | Track.Source; type RemoteVideoTrackSelector = Track.Kind | Track.Source;
type Runtime = {
navigate: (options: {
to: string;
search?: Record<string, unknown>;
}) => Promise<void>;
send: SendFn;
load: LoadFn;
getPublicKey: (userId: number) => Promise<string>;
};
let _keyProvider: ExternalE2EEKeyProvider | null = null; let _keyProvider: ExternalE2EEKeyProvider | null = null;
let _e2eeWorker: Worker | null = null; let _e2eeWorker: Worker | null = null;
let _room: Room | null = null; let _room: Room | null = null;
@ -632,7 +618,7 @@ export function setCallId(callId: string | null) {
// Cache server call metadata used by the preview screen. // Cache server call metadata used by the preview screen.
export function setCurrentCallData( export function setCurrentCallData(
currentCallData: CurrentCallData & { exists: boolean }, currentCallData: { UserIds: number[]; exists: boolean },
) { ) {
useCall.setState({ currentCallData }); useCall.setState({ currentCallData });
} }
@ -1153,72 +1139,6 @@ async function ensureNoiseFilter(
} }
} }
export const useCall = create<{
state: "closed" | "closing" | "connecting" | "open" | "encrypting";
view: CallView;
invitedUserId: number | null;
callId: string | null;
incomingCallInvite: {
callId: string;
callSecret: WrappedCallSecret;
senderId: number;
} | null;
callSecret: string | null;
livekitToken: string | null;
currentCallData: CurrentCallData;
deaf: boolean;
micEnabled: boolean;
cameraEnabled: boolean;
screenShareEnabled: boolean;
screenShareSession: LocalMediaShareSession | null;
cameraSession: LocalMediaShareSession | null;
disabledCameraParticipantIds: number[];
focusedParticipantId: number | null;
focusedParticipantType: "user" | "stream" | null;
usersInFocusedViewHidden: boolean;
watchedStreamParticipantIds: number[];
pendingWatchedParticipantIds: number[];
activeScreenShareParticipantIds: number[];
isEncrypted: boolean;
ownCallSecretInvitePending: boolean;
callIsFullscreen: boolean;
callIsPopout: boolean;
layoutVersion: number;
screenRef: React.RefObject<HTMLDivElement | null> | null;
runtime: Runtime | null;
lastFocusedParticipantId: number | null;
}>(() => ({
state: "closed",
view: "preview",
invitedUserId: null,
callId: null,
incomingCallInvite: null,
callSecret: null,
livekitToken: null,
currentCallData: null,
deaf: false,
micEnabled: false,
cameraEnabled: false,
screenShareEnabled: false,
screenShareSession: null,
cameraSession: null,
disabledCameraParticipantIds: [],
focusedParticipantId: null,
focusedParticipantType: null,
usersInFocusedViewHidden: false,
watchedStreamParticipantIds: [],
pendingWatchedParticipantIds: [],
activeScreenShareParticipantIds: [],
isEncrypted: false,
ownCallSecretInvitePending: false,
callIsFullscreen: false,
callIsPopout: false,
layoutVersion: 0,
screenRef: null,
runtime: null,
lastFocusedParticipantId: null,
}));
// Register app-level call listeners and wire React dependencies into the store. // Register app-level call listeners and wire React dependencies into the store.
export function useInitializeCall() { export function useInitializeCall() {
const navigate = useNavigate(); const navigate = useNavigate();

View file

@ -24,6 +24,7 @@
"@tensamin/cache": "workspace:*", "@tensamin/cache": "workspace:*",
"@tensamin/crypto": "workspace:*", "@tensamin/crypto": "workspace:*",
"@tensamin/hotkeys": "workspace:*", "@tensamin/hotkeys": "workspace:*",
"@tensamin/markdown": "workspace:*",
"@tensamin/mtp": "workspace:*", "@tensamin/mtp": "workspace:*",
"@tensamin/shared": "workspace:*", "@tensamin/shared": "workspace:*",
"@tensamin/storage": "workspace:*", "@tensamin/storage": "workspace:*",

View file

@ -1,5 +1,5 @@
import { Button } from "@methanium/ui"; import { Button } from "@methanium/ui";
import { Emoji } from "@methanium/ui/markdown"; import Emoji from "@tensamin/markdown/emoji";
import { getRecentEmojis, useEmojiRanks } from "./emojiRanks"; import { getRecentEmojis, useEmojiRanks } from "./emojiRanks";
export default function EmojiPicker({ export default function EmojiPicker({

View file

@ -1,6 +1,6 @@
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { normalizeShortcode } from "@methanium/ui/markdown"; import { normalizeShortcode } from "@tensamin/markdown/emoji";
const RANKS_CHANGED_EVENT = "tensamin-reaction-ranks-changed"; const RANKS_CHANGED_EVENT = "tensamin-reaction-ranks-changed";
let recordQueue = Promise.resolve(); let recordQueue = Promise.resolve();

View file

@ -1,4 +1,4 @@
import { Input, type InputController } from "@methanium/ui/markdown"; import Input, { type InputController } from "@tensamin/markdown/input";
import { import {
Card, Card,
CardHeader, CardHeader,
@ -10,7 +10,14 @@ import {
PopoverTrigger, PopoverTrigger,
} from "@methanium/ui"; } from "@methanium/ui";
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
import React, { useCallback, useEffect, useState, useRef } from "react"; import React, {
lazy,
Suspense,
useCallback,
useEffect,
useState,
useRef,
} from "react";
import { Button } from "@methanium/ui"; import { Button } from "@methanium/ui";
import { Plus, Laugh, FileVideo, SendHorizonal } from "lucide-react"; import { Plus, Laugh, FileVideo, SendHorizonal } from "lucide-react";
@ -21,14 +28,15 @@ import { cn, useIsMobile } from "@methanium/ui";
import { encryptChatText } from "@tensamin/crypto/chatSecret"; import { encryptChatText } from "@tensamin/crypto/chatSecret";
import { useSession } from "@tensamin/storage/session"; import { useSession } from "@tensamin/storage/session";
import EmojiPicker from "./emoji/emojiPicker";
import { useEmojiRanks, useRecordEmojiUse } from "./emoji/emojiRanks"; import { useEmojiRanks, useRecordEmojiUse } from "./emoji/emojiRanks";
import GifPicker from "./media/gifPicker";
import ReplyBox from "./replyBox"; import ReplyBox from "./replyBox";
import { useHotkey } from "@tensamin/hotkeys"; import { useHotkey } from "@tensamin/hotkeys";
import { editLastMessageHotkey } from "../hotkeys"; import { editLastMessageHotkey } from "../hotkeys";
import { useUser } from "@tensamin/identity/context"; import { useUser } from "@tensamin/identity/context";
const EmojiPicker = lazy(() => import("./emoji/emojiPicker"));
const GifPicker = lazy(() => import("./media/gifPicker"));
export default function InputComponent({ export default function InputComponent({
value, value,
setValue, setValue,
@ -351,13 +359,15 @@ export default function InputComponent({
)} )}
/> />
<PopoverContent className="w-auto p-0"> <PopoverContent className="w-auto p-0">
<EmojiPicker <Suspense fallback={null}>
onSelect={(shortcode) => { <EmojiPicker
setValue(`${value}${shortcode} `); onSelect={(shortcode) => {
recordUse(shortcode); setValue(`${value}${shortcode} `);
setEmojiPopoverOpen(false); recordUse(shortcode);
}} setEmojiPopoverOpen(false);
/> }}
/>
</Suspense>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{isMobile ? ( {isMobile ? (
@ -371,12 +381,14 @@ export default function InputComponent({
</DrawerTrigger> </DrawerTrigger>
<DrawerContent className="h-[80dvh]"> <DrawerContent className="h-[80dvh]">
<div className="min-h-0 flex-1 p-3"> <div className="min-h-0 flex-1 p-3">
<GifPicker <Suspense fallback={null}>
onSelect={(url) => { <GifPicker
void handleSubmit(url, true); onSelect={(url) => {
setGifPopoverOpen(false); void handleSubmit(url, true);
}} setGifPopoverOpen(false);
/> }}
/>
</Suspense>
</div> </div>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
@ -408,14 +420,16 @@ export default function InputComponent({
className="absolute left-0 top-0 z-10 h-4 w-4 cursor-nwse-resize" className="absolute left-0 top-0 z-10 h-4 w-4 cursor-nwse-resize"
onPointerDown={handleGifPopoverResizeStart} onPointerDown={handleGifPopoverResizeStart}
/> />
<GifPicker <Suspense fallback={null}>
resizeHeight={gifPopoverSize?.height} <GifPicker
resizeWidth={gifPopoverSize?.width} resizeHeight={gifPopoverSize?.height}
onSelect={(url) => { resizeWidth={gifPopoverSize?.width}
void handleSubmit(url, true); onSelect={(url) => {
setGifPopoverOpen(false); void handleSubmit(url, true);
}} setGifPopoverOpen(false);
/> }}
/>
</Suspense>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
)} )}

View file

@ -1,4 +1,4 @@
import { Text } from "@methanium/ui/markdown"; import Text from "@tensamin/markdown/text";
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
import { import {
Avatar, Avatar,

View file

@ -19,7 +19,9 @@ import { useMTP } from "@tensamin/mtp";
import { getMessage, useChat } from "../context"; import { getMessage, useChat } from "../context";
import { decryptChatText, encryptChatText } from "@tensamin/crypto/chatSecret"; import { decryptChatText, encryptChatText } from "@tensamin/crypto/chatSecret";
import { log, toast } from "@tensamin/shared/log"; import { log, toast } from "@tensamin/shared/log";
import { Emoji, Input, normalizeShortcode, Text } from "@methanium/ui/markdown"; import Emoji, { normalizeShortcode } from "@tensamin/markdown/emoji";
import Input from "@tensamin/markdown/input";
import Text from "@tensamin/markdown/text";
import { useRecordEmojiUse } from "./emoji/emojiRanks"; import { useRecordEmojiUse } from "./emoji/emojiRanks";
import ReplyBox from "./replyBox"; import ReplyBox from "./replyBox";
import { useHotkey } from "@tensamin/hotkeys"; import { useHotkey } from "@tensamin/hotkeys";

View file

@ -44,7 +44,7 @@ import type {
Ref, Ref,
} from "react"; } from "react";
import { useChat } from "../context"; import { useChat } from "../context";
import { Emoji } from "@methanium/ui/markdown"; import Emoji from "@tensamin/markdown/emoji";
import EmojiPicker from "./emoji/emojiPicker"; import EmojiPicker from "./emoji/emojiPicker";
import { getRecentEmojis, useEmojiRanks } from "./emoji/emojiRanks"; import { getRecentEmojis, useEmojiRanks } from "./emoji/emojiRanks";

View file

@ -6,7 +6,7 @@ import {
cn, cn,
Skeleton, Skeleton,
} from "@methanium/ui"; } from "@methanium/ui";
import { Text } from "@methanium/ui/markdown"; import Text from "@tensamin/markdown/text";
import type { SelectedUser } from "@tensamin/identity/context"; import type { SelectedUser } from "@tensamin/identity/context";
import Wrapper from "@tensamin/identity/wrapper"; import Wrapper from "@tensamin/identity/wrapper";
import { Forward, X } from "lucide-react"; import { Forward, X } from "lucide-react";

View file

@ -9,6 +9,7 @@ import {
export type TextProps = { export type TextProps = {
value: string; value: string;
fontSize?: CSSProperties["fontSize"]; fontSize?: CSSProperties["fontSize"];
showEditedIndicator?: boolean;
}; };
/** /**
@ -25,6 +26,9 @@ export default function Text(props: TextProps) {
return ( return (
<div className="tm-md-root" style={{ fontSize: props.fontSize }}> <div className="tm-md-root" style={{ fontSize: props.fontSize }}>
{renderedBlocks} {renderedBlocks}
{props.showEditedIndicator && (
<span className="ml-1 text-xs text-muted-foreground">(edited)</span>
)}
</div> </div>
); );
} }

View file

@ -17,6 +17,7 @@
"@tauri-apps/api": "^2.11.1", "@tauri-apps/api": "^2.11.1",
"@tensamin/cache": "workspace:*", "@tensamin/cache": "workspace:*",
"@tensamin/hotkeys": "workspace:*", "@tensamin/hotkeys": "workspace:*",
"@tensamin/markdown": "workspace:*",
"@tensamin/mtp": "workspace:*", "@tensamin/mtp": "workspace:*",
"@tensamin/shared": "workspace:*", "@tensamin/shared": "workspace:*",
"@tensamin/storage": "workspace:*", "@tensamin/storage": "workspace:*",

View file

@ -1,29 +1,20 @@
import Accessibility from "./pages/accessibility"; import { lazy } from "react";
import Cache from "./pages/cache";
import Call from "./pages/call";
import Chat from "./pages/chat";
import Index from "./pages/index";
import Licenses from "./pages/licenses";
import Profile from "./pages/profile";
import Security from "./pages/security";
import Theme from "./pages/theme";
import Hotkeys from "./pages/hotkeys";
import { settingsNavigation } from "./navigation"; import { settingsNavigation } from "./navigation";
const pageComponents = { const pageComponents = {
profile: Profile, profile: lazy(() => import("./pages/profile")),
security: Security, security: lazy(() => import("./pages/security")),
chat: Chat, chat: lazy(() => import("./pages/chat")),
call: Call, call: lazy(() => import("./pages/call")),
cache: Cache, cache: lazy(() => import("./pages/cache")),
theme: Theme, theme: lazy(() => import("./pages/theme")),
accessibility: Accessibility, accessibility: lazy(() => import("./pages/accessibility")),
hotkeys: Hotkeys, hotkeys: lazy(() => import("./pages/hotkeys")),
licenses: Licenses, licenses: lazy(() => import("./pages/licenses")),
} as const; } as const;
export const settingsPages = [ export const settingsPages = [
{ path: "/", component: Index }, { path: "/", component: lazy(() => import("./pages/index")) },
...settingsNavigation.map((page) => ({ ...settingsNavigation.map((page) => ({
...page, ...page,
component: pageComponents[page.path], component: pageComponents[page.path],

View file

@ -1,4 +1,4 @@
import { Input as MDInput } from "@methanium/ui/markdown"; import MDInput from "@tensamin/markdown/input";
import { useMTP } from "@tensamin/mtp"; import { useMTP } from "@tensamin/mtp";
import { mtp } from "@tensamin/shared/data"; import { mtp } from "@tensamin/shared/data";
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";

9
pnpm-lock.yaml generated
View file

@ -201,6 +201,9 @@ importers:
'@tensamin/identity': '@tensamin/identity':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/identity version: link:../../packages/identity
'@tensamin/markdown':
specifier: workspace:*
version: link:../../packages/markdown
'@tensamin/mtp': '@tensamin/mtp':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/mtp version: link:../../packages/mtp
@ -403,6 +406,9 @@ importers:
'@tensamin/identity': '@tensamin/identity':
specifier: workspace:* specifier: workspace:*
version: link:../identity version: link:../identity
'@tensamin/markdown':
specifier: workspace:*
version: link:../markdown
'@tensamin/mtp': '@tensamin/mtp':
specifier: workspace:* specifier: workspace:*
version: link:../mtp version: link:../mtp
@ -639,6 +645,9 @@ importers:
'@tensamin/identity': '@tensamin/identity':
specifier: workspace:* specifier: workspace:*
version: link:../identity version: link:../identity
'@tensamin/markdown':
specifier: workspace:*
version: link:../markdown
'@tensamin/mtp': '@tensamin/mtp':
specifier: workspace:* specifier: workspace:*
version: link:../mtp version: link:../mtp