diff --git a/.forgejo/workflows/dependency-builds.yml b/.forgejo/workflows/dependency-builds.yml new file mode 100644 index 0000000..f208570 --- /dev/null +++ b/.forgejo/workflows/dependency-builds.yml @@ -0,0 +1,49 @@ +name: Dependency builds + +on: + pull_request: + +env: + NIX_CONFIG: experimental-features = nix-command flakes + FORGEJO_TOKEN: "" + GITHUB_TOKEN: "" + +jobs: + web: + if: ${{ github.actor == 'renovate' }} + name: Build web + runs-on: nixos + steps: + - run: nix profile add nixpkgs#nodejs_24 + - uses: https://data.forgejo.org/actions/checkout@v4 + with: + persist-credentials: false + - run: nix develop .#electron --command pnpm install --frozen-lockfile + - run: nix develop .#electron --command pnpm run build:packages + - run: nix develop .#electron --command pnpm run build:web + + desktop: + if: ${{ github.actor == 'renovate' }} + name: Build desktop + runs-on: nixos + steps: + - run: nix profile add nixpkgs#nodejs_24 + - uses: https://data.forgejo.org/actions/checkout@v4 + with: + persist-credentials: false + - run: nix develop .#electron --command pnpm install --frozen-lockfile + - run: nix develop .#electron --command pnpm run build:packages + - run: nix develop .#electron --command pnpm run build:desktop + + mobile: + if: ${{ github.actor == 'renovate' }} + name: Build mobile + runs-on: nixos + steps: + - run: nix profile add nixpkgs#nodejs_24 + - uses: https://data.forgejo.org/actions/checkout@v4 + with: + persist-credentials: false + - run: nix develop .#tauri --command pnpm install --frozen-lockfile + - run: nix develop .#tauri --command pnpm run build:packages + - run: nix develop .#tauri --command pnpm --dir apps/tauri run build:mobile:ci \ No newline at end of file diff --git a/apps/electron/package.json b/apps/electron/package.json index ea0cd5f..560ed34 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -28,12 +28,11 @@ "validate:raw": "pnpm run build && pnpm run package:linux:raw", "validate": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run validate:raw; else pnpm run validate:raw; fi" }, - "dependencies": {}, "devDependencies": { - "@types/node": "^25.9.1", - "electron": "^39.2.7", - "electron-builder": "^26.0.12", - "esbuild": "^0.28.0", + "@types/node": "^26.1.2", + "electron": "^43.3.0", + "electron-builder": "^26.15.3", + "esbuild": "^0.28.1", "typescript": "~6.0.3" }, "build": { diff --git a/apps/tauri/package.json b/apps/tauri/package.json index 27be94c..7b7e5a7 100644 --- a/apps/tauri/package.json +++ b/apps/tauri/package.json @@ -17,6 +17,7 @@ "dev:mobile:raw": "tauri android dev --host ${TAURI_DEV_HOST:-127.0.0.1}", "start-adb:mobile:raw": "adb devices", "build:mobile:raw": "tauri android build", + "build:mobile:ci": "node render-version.ts && trap 'node render-version.ts --unrender' EXIT && tauri android build --debug", "dev:mobile": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#tauri --command pnpm run dev:mobile:raw; else pnpm run dev:mobile:raw; fi", "start-adb:mobile": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#tauri --command pnpm run start-adb:mobile:raw; else pnpm run start-adb:mobile:raw; fi", "build:mobile": "node render-version.ts && trap 'node render-version.ts --unrender' EXIT && if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#tauri --command pnpm run build:mobile:raw; else pnpm run build:mobile:raw; fi", @@ -25,18 +26,16 @@ "lint": "eslint src" }, "dependencies": { - "@tauri-apps/api": "^2", - "@tauri-apps/plugin-barcode-scanner": "~2", - "@tauri-apps/plugin-deep-link": "~2", - "@tauri-apps/plugin-log": "~2", - "@tauri-apps/plugin-notification": "~2", - "@tensamin/shared": "workspace:*", "@methanium/ui": "*", - "react": "^19.2.0", - "react-dom": "^19.2.0" + "@tauri-apps/api": "^2.11.1", + "@tauri-apps/plugin-barcode-scanner": "~2.4.5", + "@tauri-apps/plugin-deep-link": "~2.4.9", + "@tensamin/shared": "workspace:*", + "react": "^19.2.8", + "react-dom": "^19.2.8" }, "devDependencies": { - "@tauri-apps/cli": "^2", - "@types/node": "^25.9.1" + "@tauri-apps/cli": "^2.11.4", + "@types/node": "^26.1.2" } } diff --git a/apps/tauri/src-tauri/src/mtp_backend.rs b/apps/tauri/src-tauri/src/mtp_backend.rs index b1cd753..09fa249 100644 --- a/apps/tauri/src-tauri/src/mtp_backend.rs +++ b/apps/tauri/src-tauri/src/mtp_backend.rs @@ -923,28 +923,33 @@ mod android { use std::sync::OnceLock; use jni::{ - objects::{GlobalRef, JClass, JObject, JString, JValue}, - JNIEnv, JavaVM, + jni_sig, jni_str, + objects::{Global, JClass, JObject, JString, JValue}, + Env, EnvUnowned, JavaVM, }; use serde_json::Value; pub struct Host { vm: JavaVM, - context: GlobalRef, - bridge: GlobalRef, + context: Global>, + bridge: Global>, } static HOST: OnceLock = OnceLock::new(); - pub fn attach(env: &mut JNIEnv, context: JObject) -> Result<(), String> { + pub fn attach(env: &mut Env, context: JObject) -> Result<(), String> { if HOST.get().is_some() { return Ok(()); } let class = env - .find_class("net/tensamin/client/NativeMtpBridge") + .find_class(jni_str!("net/tensamin/client/NativeMtpBridge")) .map_err(|e| e.to_string())?; let bridge = env - .get_static_field(class, "INSTANCE", "Lnet/tensamin/client/NativeMtpBridge;") + .get_static_field( + class, + jni_str!("INSTANCE"), + jni_sig!("Lnet/tensamin/client/NativeMtpBridge;"), + ) .and_then(|value| value.l()) .map_err(|e| e.to_string())?; HOST.set(Host { @@ -955,15 +960,11 @@ mod android { .map_err(|_| "Android MTP host is already attached".to_string()) } - fn with_env( - call: impl FnOnce(&mut JNIEnv, &Host) -> Result, - ) -> Result { + fn with_env(call: impl FnOnce(&mut Env, &Host) -> Result) -> Result { let host = HOST.get().ok_or("Android MTP host is not attached")?; - let mut env = host - .vm - .attach_current_thread_as_daemon() - .map_err(|e| e.to_string())?; - call(&mut env, host) + host.vm + .attach_current_thread(|env| Ok::<_, jni::errors::Error>(call(env, host))) + .map_err(|e| e.to_string())? } pub fn store_config(config: &str) -> Result<(), String> { @@ -971,8 +972,8 @@ mod android { let value = env.new_string(config).map_err(|e| e.to_string())?; env.call_method( host.bridge.as_obj(), - "storeConfig", - "(Landroid/content/Context;Ljava/lang/String;)V", + jni_str!("storeConfig"), + jni_sig!("(Landroid/content/Context;Ljava/lang/String;)V"), &[ JValue::Object(host.context.as_obj()), JValue::Object(&value), @@ -987,8 +988,8 @@ mod android { with_env(|env, host| { env.call_method( host.bridge.as_obj(), - "hasConfig", - "(Landroid/content/Context;)Z", + jni_str!("hasConfig"), + jni_sig!("(Landroid/content/Context;)Z"), &[JValue::Object(host.context.as_obj())], ) .and_then(|value| value.z()) @@ -1000,8 +1001,8 @@ mod android { with_env(|env, host| { env.call_method( host.bridge.as_obj(), - "setServiceEnabled", - "(Landroid/content/Context;Z)V", + jni_str!("setServiceEnabled"), + jni_sig!("(Landroid/content/Context;Z)V"), &[ JValue::Object(host.context.as_obj()), JValue::Bool(enabled.into()), @@ -1017,8 +1018,8 @@ mod android { let status = env.new_string(status).map_err(|e| e.to_string())?; env.call_method( host.bridge.as_obj(), - "updateServiceStatus", - "(Landroid/content/Context;Ljava/lang/String;)V", + jni_str!("updateServiceStatus"), + jni_sig!("(Landroid/content/Context;Ljava/lang/String;)V"), &[ JValue::Object(host.context.as_obj()), JValue::Object(&status), @@ -1043,8 +1044,8 @@ mod android { .map_err(|e| e.to_string())?; env.call_method( host.bridge.as_obj(), - "postMessageNotification", - "(Landroid/content/Context;JLjava/lang/String;Ljava/lang/String;[B)V", + jni_str!("postMessageNotification"), + jni_sig!("(Landroid/content/Context;JLjava/lang/String;Ljava/lang/String;[B)V"), &[ JValue::Object(host.context.as_obj()), JValue::Long(sender_id as i64), @@ -1062,8 +1063,8 @@ mod android { with_env(|env, host| { env.call_method( host.bridge.as_obj(), - "cancelMessageNotification", - "(Landroid/content/Context;J)V", + jni_str!("cancelMessageNotification"), + jni_sig!("(Landroid/content/Context;J)V"), &[ JValue::Object(host.context.as_obj()), JValue::Long(sender_id as i64), @@ -1078,8 +1079,8 @@ mod android { with_env(|env, host| { env.call_method( host.bridge.as_obj(), - "isIgnoringBatteryOptimizations", - "(Landroid/content/Context;)Z", + jni_str!("isIgnoringBatteryOptimizations"), + jni_sig!("(Landroid/content/Context;)Z"), &[JValue::Object(host.context.as_obj())], ) .and_then(|value| value.z()) @@ -1091,8 +1092,8 @@ mod android { with_env(|env, host| { env.call_method( host.bridge.as_obj(), - "requestBatteryExemption", - "(Landroid/content/Context;)V", + jni_str!("requestBatteryExemption"), + jni_sig!("(Landroid/content/Context;)V"), &[JValue::Object(host.context.as_obj())], ) .map_err(|e| e.to_string())?; @@ -1101,32 +1102,36 @@ mod android { } #[no_mangle] - pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeAttach( - mut env: JNIEnv, + pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeAttach<'caller>( + mut env: EnvUnowned<'caller>, _class: JClass, - context: JObject, + context: JObject<'caller>, ) { - let _ = - std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| attach(&mut env, context))); + let _ = env.with_env(|env| { + let _ = attach(env, context); + Ok::<_, jni::errors::Error>(()) + }); } #[no_mangle] - pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeStart( - mut env: JNIEnv, + pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeStart<'caller>( + mut env: EnvUnowned<'caller>, _class: JClass, - config: JString, + config: JString<'caller>, ) { - let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let config: String = env.get_string(&config).map_err(|e| e.to_string())?.into(); - let config = serde_json::from_str(&config).map_err(|e| e.to_string())?; - super::manager().configure_and_start(config); - Ok::<_, String>(()) - })); + let _ = env.with_env(|env| { + if let Ok(config) = config.mutf8_chars(env) { + if let Ok(config) = serde_json::from_str(config.to_str().as_ref()) { + super::manager().configure_and_start(config); + } + } + Ok::<_, jni::errors::Error>(()) + }); } #[no_mangle] pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeStop( - _env: JNIEnv, + _env: EnvUnowned, _class: JClass, ) { let _ = std::panic::catch_unwind(|| super::manager().stop()); @@ -1134,31 +1139,34 @@ mod android { #[no_mangle] pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeSetUiState( - _env: JNIEnv, + _env: EnvUnowned, _class: JClass, visible: jni::sys::jboolean, ) { - super::manager().set_ui_visible(visible != 0); + super::manager().set_ui_visible(visible); } #[no_mangle] - pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeLog( - mut env: JNIEnv, + pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeLog<'caller>( + mut env: EnvUnowned<'caller>, _class: JClass, level: jni::sys::jint, - message: JString, - details: JString, + message: JString<'caller>, + details: JString<'caller>, ) { - let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let message: String = env.get_string(&message).map_err(|e| e.to_string())?.into(); - let details: String = env.get_string(&details).map_err(|e| e.to_string())?.into(); - super::manager().log( - level.clamp(0, 3) as u8, - message, - (!details.is_empty()).then(|| Value::String(details)), - ); - Ok::<_, String>(()) - })); + let _ = env.with_env(|env| { + if let (Ok(message), Ok(details)) = (message.mutf8_chars(env), details.mutf8_chars(env)) + { + let message = message.to_str(); + let details = details.to_str(); + super::manager().log( + level.clamp(0, 3) as u8, + message.into_owned(), + (!details.is_empty()).then(|| Value::String(details.into_owned())), + ); + } + Ok::<_, jni::errors::Error>(()) + }); } } diff --git a/apps/web/package.json b/apps/web/package.json index 3c175b1..e6c6bf8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,135 +12,51 @@ "preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .." }, "dependencies": { - "@base-ui/react": "^1.0.0", - "@base-ui/utils": "0.3.2", - "@babel/runtime": "^7.29.2", - "@fontsource-variable/inter": "^5.2.8", - "@fontsource-variable/public-sans": "^5.2.7", - "@floating-ui/core": "^1.7.0", - "@floating-ui/dom": "^1.7.0", - "@floating-ui/react-dom": "^2.1.8", - "@floating-ui/utils": "^0.2.11", - "@radix-ui/primitive": "^1.1.0", - "@radix-ui/react-compose-refs": "^1.1.1", - "@radix-ui/react-context": "^1.1.4", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-dismissable-layer": "^1.1.11", - "@radix-ui/react-focus-guards": "^1.1.4", - "@radix-ui/react-focus-scope": "^1.1.11", - "@radix-ui/react-id": "^1.1.0", - "@radix-ui/react-portal": "^1.1.13", - "@radix-ui/react-presence": "^1.1.6", - "@radix-ui/react-primitive": "^2.0.2", - "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-use-callback-ref": "^1.1.1", - "@radix-ui/react-use-controllable-state": "^1.2.3", - "@radix-ui/react-use-effect-event": "^0.0.5", - "@radix-ui/react-use-layout-effect": "^1.1.0", - "@reduxjs/toolkit": "^2.0.0", - "@tailwindcss/vite": "^4.2.4", - "@tanstack/devtools-event-client": "^0.5.0", - "@tanstack/query-core": "^5.0.0", - "@tanstack/react-router": "^1.169.1", - "@tanstack/history": "1.162.0", - "@tanstack/react-store": "^0.11.0", - "@tanstack/router-core": "^1.169.1", - "@tanstack/store": "^0.11.0", - "@tanstack/virtual-core": "^3.13.24", - "@tanstack/react-virtual": "^3.13.24", - "@tauri-apps/api": "^2", - "@tensamin/call": "workspace:*", + "@fontsource-variable/public-sans": "^5.3.0", + "@methanium/ui": "*", + "@tailwindcss/vite": "^4.3.3", + "@tanstack/react-router": "^1.170.21", + "@tanstack/react-virtual": "^3.14.9", + "@tauri-apps/api": "^2.11.1", "@tensamin/cache": "workspace:*", + "@tensamin/call": "workspace:*", "@tensamin/chat": "workspace:*", "@tensamin/crypto": "workspace:*", "@tensamin/hotkeys": "workspace:*", - "@tensamin/shared": "workspace:*", - "@tensamin/settings": "workspace:*", - "@tensamin/storage": "workspace:*", - "@tensamin/tauri": "workspace:*", - "@tensamin/mtp": "workspace:*", - "@tensamin/tauth": "workspace:*", "@tensamin/markdown": "workspace:*", - "@methanium/ui": "*", - "@tensamin/user": "workspace:*", + "@tensamin/mtp": "workspace:*", "@tensamin/notifications": "workspace:*", "@tensamin/onboarding": "workspace:*", - "aria-hidden": "^1.2.4", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "cookie-es": "^3.0.0", - "d3-array": "^3.1.6", - "d3-color": "^3.1.0", - "d3-ease": "^3.0.1", - "d3-format": "^3.1.0", - "d3-interpolate": "^3.0.1", - "d3-path": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-time-format": "^4.1.0", - "d3-timer": "^3.0.1", - "date-fns": "^4.4.0", + "@tensamin/settings": "workspace:*", + "@tensamin/shared": "workspace:*", + "@tensamin/storage": "workspace:*", + "@tensamin/tauri": "workspace:*", + "@tensamin/tauth": "workspace:*", + "@tensamin/user": "workspace:*", "decimal.js-light": "^2.5.1", - "detect-node-es": "^1.1.0", - "dijkstrajs": "^1.0.1", - "embla-carousel": "8.6.0", - "embla-carousel-react": "^8.6.0", - "embla-carousel-reactive-utils": "8.6.0", - "es-toolkit": "^1.39.3", - "eventemitter3": "^5.0.1", - "get-nonce": "^1.0.1", - "immer": "^10.1.1", - "input-otp": "^1.4.2", - "internmap": "^2.0.3", - "tw-animate-css": "^1.4.0", - "lucide-react": "^1.14.0", - "next-themes": "^0.4.6", - "pngjs": "^5.0.0", - "qrcode": "^1.5.4", - "react": "^19.2.0", - "react-day-picker": "^10.0.1", - "react-dom": "^19.2.0", - "react-is": "^19.0.0", - "react-redux": "^9.0.0", - "react-remove-scroll": "^2.7.2", - "react-remove-scroll-bar": "^2.3.7", - "react-resizable-panels": "^4.11.2", - "react-style-singleton": "^2.2.3", - "recharts": "3.10.1", - "redux": "^5.0.0", - "redux-thunk": "^3.1.0", - "reselect": "5.2.0", - "scheduler": "^0.27.0", - "seroval": "^1.5.4", - "seroval-plugins": "^1.5.4", - "shadcn": "^4.11.0", - "sonner": "^2.0.7", - "tailwindcss": "^4.2.4", - "tailwind-merge": "^3.6.0", + "eventemitter3": "^5.0.4", + "lucide-react": "^1.29.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-is": "^19.2.8", + "react-redux": "^9.3.0", "tailwind-scrollbar-hide": "^4.0.0", - "tiny-invariant": "^1.3.3", - "tslib": "^2.8.1", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3", - "use-sync-external-store": "^1.2.2", - "vaul": "^1.1.2", - "victory-vendor": "^37.0.2", - "yargs": "^15.3.1", - "zod": "^4.3.6" + "tailwindcss": "^4.3.3", + "tw-animate-css": "^1.4.0", + "use-sync-external-store": "^1.6.0", + "zod": "^4.4.3" }, "devDependencies": { "@eslint/js": "^10.0.1", "@types/qrcode": "^1.5.6", - "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2", - "@vitejs/plugin-react": "^6.0.1", - "esbuild": "^0.28.0", - "eslint": "^10.0.3", - "globals": "^17.4.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.0.5", + "esbuild": "^0.28.1", + "eslint": "^10.8.0", + "globals": "^17.9.0", "typescript": "~6.0.3", - "typescript-eslint": "^8.57.0", - "vite": "^8.0.10" + "typescript-eslint": "^8.66.0", + "vite": "^8.2.1" } } diff --git a/apps/web/src/components/sidebar.tsx b/apps/web/src/components/sidebar.tsx index 10baf34..7a7c58b 100644 --- a/apps/web/src/components/sidebar.tsx +++ b/apps/web/src/components/sidebar.tsx @@ -36,23 +36,19 @@ import { useShowMobileNavbar } from "@/routes/app/useShowMobileNavbar"; import { Ellipsis, Check } from "lucide-react"; import { useState } from "react"; import type { User } from "@tensamin/user/context"; -import type z from "zod"; -import { mtp } from "@tensamin/shared/data"; +import { mtp, userPresencePreferenceSchema } from "@tensamin/shared/data"; import { useMTP } from "@tensamin/mtp"; +import { + onlineStatusLabels, + onlineStatusOptions, + type OnlineStatus, +} from "./status-options"; -type OnlineStatus = z.infer; - -const onlineStatusLabels: Record = { - user_online: "Online", - user_offline: "Offline", - user_dnd: "Do not disturb", - user_idle: "Idle", - user_wc: "Away", - user_borked: "Borked", - iota_offline: "Iota offline", - iota_online: "Iota online", - iota_borked: "Iota borked", -}; +function accountPreference(status: User["OnlineStatus"]): OnlineStatus { + return userPresencePreferenceSchema.safeParse(status).success + ? (status as OnlineStatus) + : "user_online"; +} function StatusDialog({ user, @@ -87,7 +83,7 @@ function StatusDialog({ onOpenChange={(nextOpen) => { if (!nextOpen) { setDraftStatus(user.Status ?? ""); - setDraftOnlineStatus(user.OnlineStatus); + setDraftOnlineStatus(accountPreference(user.OnlineStatus)); setErrorMessage(""); setSaveSucceeded(false); } @@ -126,10 +122,11 @@ function StatusDialog({ - Online - Offline - Idle - Do not disturb + {onlineStatusOptions.map((option) => ( + + {option.label} + + ))} @@ -140,12 +137,9 @@ function StatusDialog({ Cancel} />