From 7ea3b322863b54a24a6820015061e65e5b6bf61a Mon Sep 17 00:00:00 2001 From: Alois Date: Sun, 24 May 2026 18:31:27 +0200 Subject: [PATCH] (feat): add native notifications (feat): add navbar profile popover (fix): chat input box click area to slim (qol): format --- apps/tauri/render-version.ts | 20 ++----- apps/web/src/components/modals/profile.tsx | 34 +++++++++++ apps/web/src/components/navbar.tsx | 37 ++++++++---- bun.lock | 1 + packages/call/src/store.tsx | 26 +++++---- packages/chat/src/components/input.tsx | 1 + packages/notifications/package.json | 1 + packages/notifications/src/context.tsx | 68 +++++++++++++++------- packages/ttp/src/context.tsx | 2 +- 9 files changed, 131 insertions(+), 59 deletions(-) create mode 100644 apps/web/src/components/modals/profile.tsx diff --git a/apps/tauri/render-version.ts b/apps/tauri/render-version.ts index 96f1c10..0bd3fca 100644 --- a/apps/tauri/render-version.ts +++ b/apps/tauri/render-version.ts @@ -12,9 +12,7 @@ const tauriConfigPath = path.resolve(__dirname, "./src-tauri/tauri.conf.json"); const isUnrender = process.argv.includes("--unrender"); // Version Source -const packageJson = JSON.parse( - fs.readFileSync(rootPackageJsonPath, "utf8") -); +const packageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, "utf8")); const packageVersion: string = packageJson.version; @@ -22,9 +20,7 @@ if (!packageVersion && !isUnrender) { throw new Error("No version found in package.json"); } -const targetVersion = isUnrender - ? PLACEHOLDER_VERSION - : packageVersion; +const targetVersion = isUnrender ? PLACEHOLDER_VERSION : packageVersion; // Helpers function updateCargoToml(content: string): string { @@ -34,10 +30,7 @@ function updateCargoToml(content: string): string { throw new Error("Could not find version field in Cargo.toml"); } - return content.replace( - regex, - `version = "${targetVersion}"` - ); + return content.replace(regex, `version = "${targetVersion}"`); } function updateTauriConfig(content: string): string { @@ -47,10 +40,7 @@ function updateTauriConfig(content: string): string { throw new Error("Could not find version field in tauri.conf.json"); } - return content.replace( - regex, - `"version": "${targetVersion}"` - ); + return content.replace(regex, `"version": "${targetVersion}"`); } // Update Cargo.toml @@ -72,4 +62,4 @@ if (isUnrender) { console.log(`Unrendered versions back to ${PLACEHOLDER_VERSION}`); } else { console.log(`Rendered version ${targetVersion}`); -} \ No newline at end of file +} diff --git a/apps/web/src/components/modals/profile.tsx b/apps/web/src/components/modals/profile.tsx new file mode 100644 index 0000000..6a31e36 --- /dev/null +++ b/apps/web/src/components/modals/profile.tsx @@ -0,0 +1,34 @@ +import type { User } from "@tensamin/user/context"; +import { Avatar, AvatarFallback, AvatarImage } from "@tensamin/ui"; +import Text from "@tensamin/markdown/text"; + +export default function Profile({ user }: { user: User }) { + return ( +
+
+ + + + {user.display.slice(0, 2).toUpperCase()} + + +
+

{user.display}

+

{user.username}

+
+
+ +
+

+ iota: {user.iota_id} +

+

+ user: {user.user_id} +

+

+ pub key: {user.public_key} +

+
+
+ ); +} diff --git a/apps/web/src/components/navbar.tsx b/apps/web/src/components/navbar.tsx index 9e3ec9d..57f9d24 100644 --- a/apps/web/src/components/navbar.tsx +++ b/apps/web/src/components/navbar.tsx @@ -1,7 +1,8 @@ -import { Button } from "@tensamin/ui"; +import { Button, Popover, PopoverContent, PopoverTrigger } from "@tensamin/ui"; import { ArrowLeft, ChevronDown, + ChevronUp, House, Phone, Settings, @@ -17,6 +18,7 @@ import { useState } from "react"; import { SidebarTrigger, useSidebar } from "@tensamin/ui"; import { WindowControls as Controls } from "@tensamin/ui"; import { useSession } from "@tensamin/storage/session"; +import Profile from "./modals/profile"; export default function Navbar({ forMobile }: { forMobile: boolean }) { const navigate = useNavigate(); @@ -33,6 +35,8 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) { const [selectOpen, setSelectOpen] = useState(false); + const [userInfoOpen, setUserInfoOpen] = useState(false); + return (
( - + <> + + +

{user?.display}

+ {userInfoOpen ? : } + + } + /> + + + +
+ )} loading={} /> diff --git a/bun.lock b/bun.lock index a15bdbe..1f1b34a 100644 --- a/bun.lock +++ b/bun.lock @@ -171,6 +171,7 @@ "name": "@tensamin/notifications", "version": "0.0.0", "dependencies": { + "@tanstack/react-router": "^1.169.1", "@tauri-apps/api": "^2.11.0", "@tensamin/chat": "workspace:*", "@tensamin/crypto": "workspace:*", diff --git a/packages/call/src/store.tsx b/packages/call/src/store.tsx index 5b49603..5398e70 100644 --- a/packages/call/src/store.tsx +++ b/packages/call/src/store.tsx @@ -295,13 +295,15 @@ async function updateLocalParticipantAttributes( screenSharePreviewLength: attributes.screenSharePreview?.length ?? 0, }); - await getRoom().localParticipant.setAttributes(attributes).catch((error) => { - log(1, "call", "red", "Failed to update local participant attributes", { - attributes: Object.keys(attributes), - error, + await getRoom() + .localParticipant.setAttributes(attributes) + .catch((error) => { + log(1, "call", "red", "Failed to update local participant attributes", { + attributes: Object.keys(attributes), + error, + }); + throw error; }); - throw error; - }); log(2, "call", "purple", "Updated local participant attributes", { attributeKeys: Object.keys(attributes), @@ -795,11 +797,13 @@ export async function connect(callId: string) { syncAllRemoteTrackSubscriptions(); - await getRoom().localParticipant.setMicrophoneEnabled(true).catch((error) => { - log(1, "call", "red", "Failed to enable microphone", error); - toast("error", "Failed to enable microphone."); - throw error; - }); + await getRoom() + .localParticipant.setMicrophoneEnabled(true) + .catch((error) => { + log(1, "call", "red", "Failed to enable microphone", error); + toast("error", "Failed to enable microphone."); + throw error; + }); syncParticipantState(); } diff --git a/packages/chat/src/components/input.tsx b/packages/chat/src/components/input.tsx index b2aef9e..cf74848 100644 --- a/packages/chat/src/components/input.tsx +++ b/packages/chat/src/components/input.tsx @@ -97,6 +97,7 @@ export default function InputComponent({ > (undefined); -function reduceDisplay(display: string) { - const words = display.split(" "); - if (words.length === 1) { - return display.slice(0, 2).toUpperCase(); - } else { - return words[0].charAt(0).toUpperCase() + words[1].charAt(0).toUpperCase(); - } +async function requestNotificationPermission() { + if (!("Notification" in window)) return false; + + if (Notification.permission === "granted") return true; + + const permission = await Notification.requestPermission(); + return permission === "granted"; } export default function Provider(props: { children: React.ReactNode }) { @@ -29,6 +30,7 @@ export default function Provider(props: { children: React.ReactNode }) { const { decryptText, getSharedSecret } = useCrypto(); const { addLiveMessage, userId } = useChat(); const { moveUserIdToTop } = useSession(); + const navigate = useNavigate(); useEffect(() => { return subscribePush(async (ttpMessage) => { @@ -54,7 +56,7 @@ export default function Provider(props: { children: React.ReactNode }) { (await load("settings.read_confirmations")) && userId === sender_id ) { - await send( + void send( "message_state", { message_state: "read", @@ -64,7 +66,7 @@ export default function Provider(props: { children: React.ReactNode }) { }, ); } else { - await send( + void send( "message_state", { message_state: "received", @@ -92,18 +94,41 @@ export default function Provider(props: { children: React.ReactNode }) { if (isTauri()) { console.log("weewoo"); } else { - sonnerToast(user.display, { - classNames: { - content: "pl-4", - }, - description: decryptedContent, - icon: ( - - - {reduceDisplay(user.display)} - - ), - }); + const hasPermissions = await requestNotificationPermission(); + + if (hasPermissions) { + const notification = new Notification(user.display, { + body: decryptedContent, + icon: user.avatar || user.display.slice(0, 2).toUpperCase(), + badge: user.avatar || user.display.slice(0, 2).toUpperCase(), + tag: `message-${user.user_id}`, + silent: true, + }); + + notification.onclick = () => { + window.focus(); + navigate({ + to: `/chat?id=${user.user_id}`, + }); + + notification.close(); + }; + } else { + sonnerToast(user.display, { + classNames: { + content: "pl-4", + }, + description: decryptedContent, + icon: ( + + + + {user.display.slice(0, 2).toUpperCase()} + + + ), + }); + } } } }); @@ -117,6 +142,7 @@ export default function Provider(props: { children: React.ReactNode }) { userId, moveUserIdToTop, send, + navigate, ]); return ( diff --git a/packages/ttp/src/context.tsx b/packages/ttp/src/context.tsx index 2f8fc40..d33313d 100644 --- a/packages/ttp/src/context.tsx +++ b/packages/ttp/src/context.tsx @@ -257,7 +257,7 @@ export function Provider(props: { setErrorDescription( "You could try to restart your Iota, check for updates or check your network connection.", ); - }) + }); }, [connected, subscribePush]); useEffect(() => {