diff --git a/apps/pwa/src/vite.ts b/apps/pwa/src/vite.ts index 107e08f..962dce1 100644 --- a/apps/pwa/src/vite.ts +++ b/apps/pwa/src/vite.ts @@ -92,11 +92,6 @@ function emitIcons(): Plugin { attrs: { name: "apple-mobile-web-app-capable", content: "yes" }, injectTo: "head", }, - { - tag: "meta", - attrs: { name: "mobile-web-app-capable", content: "yes" }, - injectTo: "head", - }, { tag: "meta", attrs: { diff --git a/apps/tauri/src-tauri/Cargo.lock b/apps/tauri/src-tauri/Cargo.lock index cdc1332..d404d61 100644 --- a/apps/tauri/src-tauri/Cargo.lock +++ b/apps/tauri/src-tauri/Cargo.lock @@ -5139,7 +5139,7 @@ dependencies = [ name = "tensamin" version = "0.0.0" dependencies = [ - "base64 0.23.1", + "base64 0.22.1", "jni 0.22.4", "mtp", "reqwest", diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml index 69fbe80..2a68e95 100644 --- a/apps/tauri/src-tauri/Cargo.toml +++ b/apps/tauri/src-tauri/Cargo.toml @@ -21,7 +21,7 @@ tauri-build = { git = "https://github.com/tauri-apps/tauri", rev = "4af26a3f7f8b tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -base64 = "0.23" +base64 = "0.22" reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] } tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] } mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "a5c8d4f0c898c78351e9d54124886c86e789a22a", features = ["client", "crypto"] } diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs index 859b653..6636682 100644 --- a/apps/tauri/src-tauri/src/lib.rs +++ b/apps/tauri/src-tauri/src/lib.rs @@ -20,7 +20,6 @@ pub fn run() { accessibility_backend::accessibility_get_initial_scale, accessibility_backend::accessibility_set_initial_scale, mtp_backend::mtp_request, - mtp_backend::mtp_send_sealed_relay, mtp_backend::mtp_status, mtp_backend::mtp_store_credentials, mtp_backend::mtp_has_credentials, diff --git a/apps/tauri/src-tauri/src/mtp_backend.rs b/apps/tauri/src-tauri/src/mtp_backend.rs index 97fb983..fa28597 100644 --- a/apps/tauri/src-tauri/src/mtp_backend.rs +++ b/apps/tauri/src-tauri/src/mtp_backend.rs @@ -9,12 +9,9 @@ use base64::{ Engine as _, }; use mtp::client::{ClientConfig, MTPClient, MTPConnection, Policy, SendMode}; -use mtp::codec::{ - CommunicationType, CommunicationValue, DataType, DataValue, SealedRelayBuilder, TypeMap, -}; +use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue, TypeMap}; use mtp::crypto::{ - derive_encryption_key, AeadDecrypt, ChaCha20Poly1305, DualSigner, HybridKem, Keyring, - PublicKeyBundle, + derive_encryption_key, AeadDecrypt, ChaCha20Poly1305, HybridKem, Keyring, PublicKeyBundle, }; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; @@ -31,9 +28,6 @@ const CHAT_SECRET_SCHEME: &str = "mtp-chat-secret-kem-chacha20poly1305-hkdf-sha2 const INITIAL_SYNC_TIMEOUT: Duration = Duration::from_secs(30); const MAX_BUFFERED_INITIAL_FRAMES: usize = 1_000; const NOTIFICATION_QUEUE_CAPACITY: usize = 32; -const ROUTE_TARGET_ID_MASK: u64 = (1_u64 << 48) - 1; -const USER_ROUTE_TARGET_KIND: u64 = 0x4000_0000_0000_0000; -const IOTA_ROUTE_TARGET_KIND: u64 = 0x8000_0000_0000_0000; #[cfg(target_os = "android")] const ROOT_YE_PEM: &[u8] = b"-----BEGIN CERTIFICATE-----\n\ MIIB2TCCAWCgAwIBAgIRAKQCa6LvbHwg1AR+XmWmk4AwCgYIKoZIzj0EAwMwLjEL\n\ @@ -85,44 +79,6 @@ pub struct MtpSnapshot { pub error: Option, } -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct EncodedKeyMaterial { - value: String, - encoding: String, -} - -#[derive(Debug, Deserialize)] -#[serde(tag = "kind", rename_all = "lowercase")] -enum RelayTargetDto { - User { id: u64 }, - Iota { id: u64 }, -} - -impl RelayTargetDto { - fn wire_id(self) -> Result { - let (kind, id) = match self { - Self::User { id } => (USER_ROUTE_TARGET_KIND, id), - Self::Iota { id } => (IOTA_ROUTE_TARGET_KIND, id), - }; - if id == 0 || id > ROUTE_TARGET_ID_MASK { - return Err("relay target ID must be a non-zero 48-bit integer".into()); - } - Ok(kind | id) - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SealedRelayRequest { - type_name: String, - data: Value, - next_hop: RelayTargetDto, - final_recipient_id: u64, - metadata_recipients: Vec, - content_recipients: Vec, -} - #[derive(Clone, Serialize)] #[serde(tag = "kind", rename_all = "camelCase")] enum MtpEvent { @@ -739,88 +695,6 @@ async fn resolve_endpoint(config: &MtpConfig) -> Result<(String, String), String )) } -fn decode_key_material(value: EncodedKeyMaterial) -> Result { - let bytes = match value.encoding.as_str() { - "base64" => decode_browser_base64(&value.value)?, - "hex" => decode_sdk_bytes(&value.value)?, - encoding => return Err(format!("unsupported key material encoding: {encoding}")), - }; - let key = PublicKeyBundle::from_bytes(&bytes) - .map_err(|error| format!("invalid relay recipient public key: {error}"))?; - key.validate() - .map_err(|error| format!("invalid relay recipient public key: {error}"))?; - Ok(key) -} - -async fn send_sealed_relay(request: SealedRelayRequest) -> Result { - if request.metadata_recipients.is_empty() || request.content_recipients.is_empty() { - return Err("sealed relay requires metadata and content recipients".into()); - } - if request.final_recipient_id == 0 { - return Err("sealed relay final recipient ID must be non-zero".into()); - } - let manager = manager(); - let connection = manager - .connection - .read() - .map_err(|_| "MTP connection lock is unavailable")? - .clone() - .ok_or_else(|| "MTP is not connected".to_string())?; - let config = manager - .config - .read() - .map_err(|_| "MTP configuration lock is unavailable")? - .clone() - .ok_or_else(|| "MTP credentials are unavailable".to_string())?; - let keyring_bytes = decode_browser_base64(&config.keyring)?; - let keyring = Keyring::from_bytes(&keyring_bytes) - .map_err(|error| format!("invalid MTP keyring: {error}"))?; - let signer = DualSigner::new( - &keyring.sig_cl_secret_key, - &keyring.sig_pq_secret_key, - &keyring.sig_pq_public_key, - ) - .map_err(|error| format!("invalid MTP signing key: {error}"))?; - let next_hop_id = request.next_hop.wire_id()?; - let type_map = TypeMap::latest(); - let content = json_to_frame(&request.type_name, request.data, 1)?.into_payload(); - let metadata_recipients = request - .metadata_recipients - .into_iter() - .map(decode_key_material) - .collect::, _>>()?; - let content_recipients = request - .content_recipients - .into_iter() - .map(decode_key_material) - .collect::, _>>()?; - let created_at = mtp::common::unix_time_millis() - .map_err(|error| format!("failed to get relay timestamp: {error}"))?; - let request_id = connection.next_request_id().await?; - let message_id = format!("tensamin-relay-{created_at}-{request_id}"); - let frame = SealedRelayBuilder::new( - request.type_name, - content, - config.user_id, - request.final_recipient_id, - next_hop_id, - &signer, - ) - .message_id(message_id) - .created_at(created_at) - .metadata_recipients(metadata_recipients) - .content_recipients(content_recipients) - .type_map(&type_map) - .build() - .map_err(|error| format!("failed to build sealed relay: {error}"))?; - let response = connection - .mtp - .request(&frame, None) - .await - .map_err(|error| format!("sealed relay request failed: {error}"))?; - frame_to_json(&response) -} - async fn handle_push( generation: u64, notification_tx: &mpsc::Sender, @@ -1173,8 +1047,7 @@ mod tests { use super::{ container_value_by_name, decode_browser_base64, decode_sdk_bytes, frame_to_json, - jittered_retry_delay, json_to_frame, prepare_initial_state_ack, RelayTargetDto, - RequestIdAllocator, + jittered_retry_delay, json_to_frame, prepare_initial_state_ack, RequestIdAllocator, }; #[test] @@ -1197,20 +1070,6 @@ mod tests { assert_eq!(ids.next().unwrap(), 2); } - #[test] - fn iota_relay_target_encodes_its_namespace() { - assert_eq!( - RelayTargetDto::Iota { id: 42 }.wire_id().unwrap(), - 0x8000_0000_0000_002a - ); - } - - #[test] - fn relay_target_rejects_raw_or_out_of_range_ids() { - assert!(RelayTargetDto::Iota { id: 0 }.wire_id().is_err()); - assert!(RelayTargetDto::User { id: 1_u64 << 48 }.wire_id().is_err()); - } - #[test] fn retry_jitter_stays_within_policy_bounds() { let delay = jittered_retry_delay(std::time::Duration::from_secs(10)); @@ -1335,11 +1194,6 @@ pub async fn mtp_request(type_name: String, data: Value) -> Result Result { - send_sealed_relay(request).await -} - #[tauri::command] pub fn mtp_status() -> MtpSnapshot { manager().snapshot() diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 264fe41..8952e07 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -8,7 +8,6 @@ pnpm-debug.log* lerna-debug.log* node_modules -.mtp dist dist-ssr *.local diff --git a/apps/web/package.json b/apps/web/package.json index e6b015e..a6b3951 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -24,7 +24,6 @@ "@tensamin/chat": "workspace:*", "@tensamin/crypto": "workspace:*", "@tensamin/hotkeys": "workspace:*", - "@tensamin/markdown": "workspace:*", "@tensamin/mtp": "workspace:*", "@tensamin/notifications": "workspace:*", "@tensamin/onboarding": "workspace:*", @@ -33,7 +32,7 @@ "@tensamin/storage": "workspace:*", "@tensamin/tauri": "workspace:*", "@tensamin/tauth": "workspace:*", - "@tensamin/identity": "workspace:*", + "@tensamin/user": "workspace:*", "decimal.js-light": "^2.5.1", "eventemitter3": "^5.0.4", "lucide-react": "^1.29.0", diff --git a/apps/web/src/components/callPopout.tsx b/apps/web/src/components/callPopout.tsx deleted file mode 100644 index bbe0f91..0000000 --- a/apps/web/src/components/callPopout.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { useCall } from "@tensamin/call/state"; -import Popout from "@tensamin/call/popout"; - -export default function CallPopout() { - const active = useCall((state) => state.state !== "closed"); - - return active ? : null; -} diff --git a/apps/web/src/components/callRuntimeInit.tsx b/apps/web/src/components/callRuntimeInit.tsx deleted file mode 100644 index 296057d..0000000 --- a/apps/web/src/components/callRuntimeInit.tsx +++ /dev/null @@ -1,95 +0,0 @@ -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; -} diff --git a/apps/web/src/components/callSidebarBox.tsx b/apps/web/src/components/callSidebarBox.tsx deleted file mode 100644 index 89d7905..0000000 --- a/apps/web/src/components/callSidebarBox.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { useCall } from "@tensamin/call/state"; -import SidebarBox from "@tensamin/call/sidebarBox"; - -export default function CallSidebarBox() { - const active = useCall((state) => state.state !== "closed"); - - return active ? : null; -} diff --git a/apps/web/src/components/modals/basic.tsx b/apps/web/src/components/modals/basic.tsx index 9608f66..dc4e48a 100644 --- a/apps/web/src/components/modals/basic.tsx +++ b/apps/web/src/components/modals/basic.tsx @@ -1,4 +1,4 @@ -import type { User } from "@tensamin/identity/context"; +import type { User } from "@tensamin/user/context"; import { Avatar, AvatarImage, diff --git a/apps/web/src/components/modals/profile.tsx b/apps/web/src/components/modals/profile.tsx index f077783..a4bddb2 100644 --- a/apps/web/src/components/modals/profile.tsx +++ b/apps/web/src/components/modals/profile.tsx @@ -1,6 +1,6 @@ -import type { User } from "@tensamin/identity/context"; +import type { User } from "@tensamin/user/context"; import { Avatar, AvatarFallback, AvatarImage, Button } from "@methanium/ui"; -import Text from "@tensamin/markdown/text"; +import { Text } from "@methanium/ui/markdown"; import { ChevronDown, ChevronUp } from "lucide-react"; import { useState } from "react"; diff --git a/apps/web/src/components/navbar.tsx b/apps/web/src/components/navbar.tsx index e980a0d..122f188 100644 --- a/apps/web/src/components/navbar.tsx +++ b/apps/web/src/components/navbar.tsx @@ -14,8 +14,8 @@ import { User, } from "lucide-react"; import { useLocation, useNavigate, useSearch } from "@tanstack/react-router"; -import { useCall } from "@tensamin/call/state"; -import Wrapper from "@tensamin/identity/wrapper"; +import { joinCall, useCall } from "@tensamin/call/store"; +import Wrapper from "@tensamin/user/wrapper"; import { Skeleton } from "@methanium/ui"; import { Select, @@ -27,8 +27,7 @@ import { displayCallId } from "@tensamin/call/utils"; import { useState } from "react"; import { SidebarTrigger, useSidebar } from "@methanium/ui"; import { WindowControls as Controls } from "@methanium/ui"; -import { useSession } from "@tensamin/identity/session"; -import { joinCall } from "@tensamin/call/store"; +import { useSession } from "@tensamin/storage/session"; import Profile from "./modals/profile"; export default function Navbar({ forMobile }: { forMobile: boolean }) { @@ -140,7 +139,9 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) { {currentCalls.length === 0 ? (