Compare commits

...
Author SHA1 Message Date
be80e2f387 (fix): actually fix outdated tag recognition
All checks were successful
/ build-web (push) Successful in 1m13s
/ build-desktop (push) Successful in 12m1s
/ build-mobile (push) Successful in 16m47s
/ release (push) Successful in 22s
2026-05-25 00:58:49 +02:00
15138a30da (fix): outdated tag recognition
All checks were successful
/ build-web (push) Successful in 1m12s
/ build-desktop (push) Successful in 11m50s
/ build-mobile (push) Successful in 16m13s
/ release (push) Successful in 26s
2026-05-25 00:05:48 +02:00
37338f12fb (feat): rename productName to have a uppercase T
Some checks failed
/ build-web (push) Successful in 1m23s
/ release (push) Has been cancelled
/ build-desktop (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-05-25 00:00:33 +02:00
edfb037864 (fix): lowercase name on android
Some checks failed
/ build-desktop (push) Has been cancelled
/ release (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-05-25 00:00:10 +02:00
d16f260a3a (fix): small desktop app bugs
Some checks failed
/ build-web (push) Successful in 1m12s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (push) Has been cancelled
2026-05-24 23:52:33 +02:00
b0cdf0adf2 (fix): actually fix android icons
All checks were successful
/ build-web (push) Successful in 1m11s
/ build-desktop (push) Successful in 11m40s
/ build-mobile (push) Successful in 16m14s
/ release (push) Successful in 28s
2026-05-24 22:24:38 +02:00
0c933e4890 (fix): messages at the top overlapping
All checks were successful
/ build-web (push) Successful in 1m17s
/ build-desktop (push) Successful in 12m24s
/ build-mobile (push) Successful in 17m15s
/ release (push) Successful in 23s
2026-05-24 21:30:42 +02:00
78f354d0d3 (fix): android icons
All checks were successful
/ build-web (push) Successful in 1m22s
/ build-desktop (push) Successful in 12m30s
/ build-mobile (push) Successful in 17m20s
/ release (push) Successful in 24s
2026-05-24 20:50:28 +02:00
d297282320 (fix): chat message measurement
Some checks failed
/ build-web (push) Successful in 1m16s
/ build-desktop (push) Successful in 12m24s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
(qol): update todos
2026-05-24 20:37:57 +02:00
7ea3b32286 (feat): add native notifications
All checks were successful
/ build-web (push) Successful in 1m9s
/ build-desktop (push) Successful in 11m16s
/ build-mobile (push) Successful in 15m50s
/ release (push) Successful in 23s
(feat): add navbar profile popover
(fix): chat input box click area to slim
(qol): format
2026-05-24 18:31:27 +02:00
50 changed files with 204 additions and 84 deletions

View file

@ -168,7 +168,7 @@ jobs:
test -d releases test -d releases
find releases -type f | grep -q . find releases -type f | grep -q .
LATEST_PROD_TAG="$(git tag --list '*-prod' --sort=-v:refname | head -n 1 || true)" LATEST_PROD_TAG="$(git for-each-ref refs/tags --sort=-creatordate --format='%(refname:short)' | awk '!/-/' | head -n 1 || true)"
if [ -n "$LATEST_PROD_TAG" ]; then if [ -n "$LATEST_PROD_TAG" ]; then
RAW_LOG="$(git log "$LATEST_PROD_TAG"..HEAD --pretty=format:'- %s')" RAW_LOG="$(git log "$LATEST_PROD_TAG"..HEAD --pretty=format:'- %s')"

BIN
apps/tauri/android.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
apps/tauri/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

8
apps/tauri/logo.json Normal file
View file

@ -0,0 +1,8 @@
{
"default": "./logo.svg",
"android_fg": "./android.png",
"android_bg": "./background.png",
"android_fg_scale": 100,
"android_monochrome": "./monochrome.png"
}

BIN
apps/tauri/monochrome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -30,7 +30,7 @@
"build:desktop:raw": "tauri build", "build:desktop:raw": "tauri build",
"dev:desktop": "if command -v nix >/dev/null 2>&1; then nix develop --command bun dev:desktop:raw; else bun dev:desktop:raw; fi", "dev:desktop": "if command -v nix >/dev/null 2>&1; then nix develop --command bun dev:desktop:raw; else bun dev:desktop:raw; fi",
"build:desktop": "if command -v nix >/dev/null 2>&1; then nix develop --command bun build:desktop:raw; else bun build:desktop:raw; fi", "build:desktop": "if command -v nix >/dev/null 2>&1; then nix develop --command bun build:desktop:raw; else bun build:desktop:raw; fi",
"gen-icons": "tauri icon ./logo.svg", "gen-icons": "tauri icon ./logo.json",
"format": "bunx prettier --write .", "format": "bunx prettier --write .",
"lint": "eslint src" "lint": "eslint src"
}, },

View file

@ -12,9 +12,7 @@ const tauriConfigPath = path.resolve(__dirname, "./src-tauri/tauri.conf.json");
const isUnrender = process.argv.includes("--unrender"); const isUnrender = process.argv.includes("--unrender");
// Version Source // Version Source
const packageJson = JSON.parse( const packageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, "utf8"));
fs.readFileSync(rootPackageJsonPath, "utf8")
);
const packageVersion: string = packageJson.version; const packageVersion: string = packageJson.version;
@ -22,9 +20,7 @@ if (!packageVersion && !isUnrender) {
throw new Error("No version found in package.json"); throw new Error("No version found in package.json");
} }
const targetVersion = isUnrender const targetVersion = isUnrender ? PLACEHOLDER_VERSION : packageVersion;
? PLACEHOLDER_VERSION
: packageVersion;
// Helpers // Helpers
function updateCargoToml(content: string): string { 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"); throw new Error("Could not find version field in Cargo.toml");
} }
return content.replace( return content.replace(regex, `version = "${targetVersion}"`);
regex,
`version = "${targetVersion}"`
);
} }
function updateTauriConfig(content: string): string { 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"); throw new Error("Could not find version field in tauri.conf.json");
} }
return content.replace( return content.replace(regex, `"version": "${targetVersion}"`);
regex,
`"version": "${targetVersion}"`
);
} }
// Update Cargo.toml // Update Cargo.toml

View file

@ -4838,7 +4838,7 @@ dependencies = [
[[package]] [[package]]
name = "tensamin" name = "tensamin"
version = "0.1.0" version = "0.0.0"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"image 0.25.10", "image 0.25.10",

View file

@ -1,5 +1,5 @@
[package] [package]
name = "Tensamin" name = "tensamin"
version = "0.0.0" version = "0.0.0"
description = "Privacy focused messanger" description = "Privacy focused messanger"
authors = ["methanium"] authors = ["methanium"]

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@mipmap/ic_launcher_background"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon> </adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before After
Before After

View file

@ -1,4 +1,4 @@
<resources> <resources>
<string name="app_name">tensamin</string> <string name="app_name">Tensamin</string>
<string name="main_activity_title">tensamin</string> <string name="main_activity_title">Tensamin</string>
</resources> </resources>

Binary file not shown.

View file

@ -7,6 +7,7 @@ import {
} from "react"; } from "react";
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"; import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link";
import { isTauri } from "@tauri-apps/api/core"; import { isTauri } from "@tauri-apps/api/core";
import { useIsMobile } from "@tensamin/ui";
type DeeplinkContextValue = { type DeeplinkContextValue = {
deeplinks: readonly string[]; deeplinks: readonly string[];
@ -31,10 +32,10 @@ export default function DeeplinkProvider({
children: ReactNode; children: ReactNode;
}) { }) {
const [deeplinks, setDeeplinks] = useState<string[]>([]); const [deeplinks, setDeeplinks] = useState<string[]>([]);
const isTauriEnv = isTauri(); const isMobile = useIsMobile();
useEffect(() => { useEffect(() => {
if (!isTauriEnv) return; if (!isTauri() || !isMobile) return;
let mounted = true; let mounted = true;
let unlisten: (() => void) | undefined; let unlisten: (() => void) | undefined;
@ -57,7 +58,7 @@ export default function DeeplinkProvider({
mounted = false; mounted = false;
unlisten?.(); unlisten?.();
}; };
}, [isTauriEnv]); }, [isMobile]);
return ( return (
<deeplinkContext.Provider <deeplinkContext.Provider

View file

@ -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 (
<div className="flex flex-col gap-2">
<div className="flex gap-2 items-center">
<Avatar className="size-10">
<AvatarImage src={user.avatar} />
<AvatarFallback className="text-lg">
{user.display.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<p className="text-lg font-semibold">{user.display}</p>
<p className="text-muted-foreground">{user.username}</p>
</div>
</div>
<Text value={user.about || ""} />
<div className="flex flex-col">
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
iota: {user.iota_id}
</p>
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
user: {user.user_id}
</p>
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
pub key: {user.public_key}
</p>
</div>
</div>
);
}

View file

@ -1,7 +1,8 @@
import { Button } from "@tensamin/ui"; import { Button, Popover, PopoverContent, PopoverTrigger } from "@tensamin/ui";
import { import {
ArrowLeft, ArrowLeft,
ChevronDown, ChevronDown,
ChevronUp,
House, House,
Phone, Phone,
Settings, Settings,
@ -17,6 +18,7 @@ import { useState } from "react";
import { SidebarTrigger, useSidebar } from "@tensamin/ui"; import { SidebarTrigger, useSidebar } from "@tensamin/ui";
import { WindowControls as Controls } from "@tensamin/ui"; import { WindowControls as Controls } from "@tensamin/ui";
import { useSession } from "@tensamin/storage/session"; import { useSession } from "@tensamin/storage/session";
import Profile from "./modals/profile";
export default function Navbar({ forMobile }: { forMobile: boolean }) { export default function Navbar({ forMobile }: { forMobile: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
@ -33,6 +35,8 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
const [selectOpen, setSelectOpen] = useState(false); const [selectOpen, setSelectOpen] = useState(false);
const [userInfoOpen, setUserInfoOpen] = useState(false);
return ( return (
<div <div
data-tauri-drag-region data-tauri-drag-region
@ -69,6 +73,10 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
<Wrapper <Wrapper
userId={id} userId={id}
component={(user) => ( component={(user) => (
<>
<Popover open={userInfoOpen} onOpenChange={setUserInfoOpen}>
<PopoverTrigger
render={
<Button <Button
variant="link" variant="link"
className="px-0! text-foreground" className="px-0! text-foreground"
@ -77,8 +85,15 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
}} }}
> >
<p className="font-medium text-md">{user?.display}</p> <p className="font-medium text-md">{user?.display}</p>
<ChevronDown /> {userInfoOpen ? <ChevronUp /> : <ChevronDown />}
</Button> </Button>
}
/>
<PopoverContent side="bottom">
<Profile user={user} />
</PopoverContent>
</Popover>
</>
)} )}
loading={<Skeleton className="ml-3 w-40 h-5" />} loading={<Skeleton className="ml-3 w-40 h-5" />}
/> />
@ -141,7 +156,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
)} )}
</> </>
)} )}
<Controls className="mr-2 ring-border" /> <Controls className="mr-2" />
</div> </div>
</div> </div>
); );

View file

@ -1,4 +1,4 @@
import { Button, cn } from "@tensamin/ui"; import { Button, cn, useIsMobile } from "@tensamin/ui";
import { Input } from "@tensamin/ui"; import { Input } from "@tensamin/ui";
import { Label } from "@tensamin/ui"; import { Label } from "@tensamin/ui";
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
@ -84,6 +84,7 @@ function parseTuFileContent(rawFileContent: string): {
} }
export default function Form() { export default function Form() {
const isMobile = useIsMobile();
const uploadRef = React.useRef<HTMLInputElement | null>(null); const uploadRef = React.useRef<HTMLInputElement | null>(null);
const [isDragging, setIsDragging] = React.useState(false); const [isDragging, setIsDragging] = React.useState(false);
const { save } = useStorage(); const { save } = useStorage();
@ -266,7 +267,7 @@ export default function Form() {
return ( return (
<div className="relative flex md:flex-row flex-col gap-15"> <div className="relative flex md:flex-row flex-col gap-15">
{isTauri() ? ( {isTauri() && isMobile ? (
<> <>
<QrCodeScanner <QrCodeScanner
onData={async (data) => { onData={async (data) => {

View file

@ -171,6 +171,7 @@
"name": "@tensamin/notifications", "name": "@tensamin/notifications",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@tanstack/react-router": "^1.169.1",
"@tauri-apps/api": "^2.11.0", "@tauri-apps/api": "^2.11.0",
"@tensamin/chat": "workspace:*", "@tensamin/chat": "workspace:*",
"@tensamin/crypto": "workspace:*", "@tensamin/crypto": "workspace:*",

View file

@ -295,7 +295,9 @@ async function updateLocalParticipantAttributes(
screenSharePreviewLength: attributes.screenSharePreview?.length ?? 0, screenSharePreviewLength: attributes.screenSharePreview?.length ?? 0,
}); });
await getRoom().localParticipant.setAttributes(attributes).catch((error) => { await getRoom()
.localParticipant.setAttributes(attributes)
.catch((error) => {
log(1, "call", "red", "Failed to update local participant attributes", { log(1, "call", "red", "Failed to update local participant attributes", {
attributes: Object.keys(attributes), attributes: Object.keys(attributes),
error, error,
@ -795,7 +797,9 @@ export async function connect(callId: string) {
syncAllRemoteTrackSubscriptions(); syncAllRemoteTrackSubscriptions();
await getRoom().localParticipant.setMicrophoneEnabled(true).catch((error) => { await getRoom()
.localParticipant.setMicrophoneEnabled(true)
.catch((error) => {
log(1, "call", "red", "Failed to enable microphone", error); log(1, "call", "red", "Failed to enable microphone", error);
toast("error", "Failed to enable microphone."); toast("error", "Failed to enable microphone.");
throw error; throw error;

View file

@ -11,4 +11,3 @@
- Desktop-App screenshares - Desktop-App screenshares
- Context menus - Context menus
- Popout Window - Popout Window
- If micGated=true & isSpeaking=false for 5 seconds show banner with mic detection

View file

@ -97,6 +97,7 @@ export default function InputComponent({
> >
<CardHeader className="relative p-0 flex flex-col"> <CardHeader className="relative p-0 flex flex-col">
<Input <Input
className="w-full"
paddingY="13px" paddingY="13px"
paddingX="13px" paddingX="13px"
placeholder="Send a message..." placeholder="Send a message..."

View file

@ -8,6 +8,7 @@ import Message from "./components/message";
import { PAGE_SIZE } from "./values"; import { PAGE_SIZE } from "./values";
import { useIsMobile } from "@tensamin/ui"; import { useIsMobile } from "@tensamin/ui";
import { Loader2 } from "lucide-react";
function getDistanceFromBottom(element: HTMLDivElement) { function getDistanceFromBottom(element: HTMLDivElement) {
return element.scrollHeight - (element.scrollTop + element.clientHeight); return element.scrollHeight - (element.scrollTop + element.clientHeight);
@ -101,12 +102,6 @@ export default function Screen() {
const virtualRowCount = const virtualRowCount =
messages.length + (shouldShowConversationStart ? 1 : 0); messages.length + (shouldShowConversationStart ? 1 : 0);
const messagesRef = React.useRef(messages);
React.useEffect(() => {
messagesRef.current = messages;
}, [messages]);
const getItemKey = React.useCallback( const getItemKey = React.useCallback(
(index: number) => { (index: number) => {
if (shouldShowConversationStart && index === 0) { if (shouldShowConversationStart && index === 0) {
@ -114,9 +109,9 @@ export default function Screen() {
} }
const messageIndex = shouldShowConversationStart ? index - 1 : index; const messageIndex = shouldShowConversationStart ? index - 1 : index;
return messagesRef.current[messageIndex]?.send_time ?? index; return messages[messageIndex]?.send_time ?? index;
}, },
[shouldShowConversationStart], [shouldShowConversationStart, messages],
); );
const setMeasurementRef = React.useCallback( const setMeasurementRef = React.useCallback(
@ -360,9 +355,38 @@ export default function Screen() {
return; return;
} }
let rafId: number;
let stableFrames = 0;
let lastScrollHeight = 0;
const settleToBottom = () => {
if (!scrollRef.current || hasScrolledToBottomInitially) {
return;
}
scrollRef.current.scrollTop = scrollRef.current.scrollHeight; scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
const currentScrollHeight = scrollRef.current.scrollHeight;
if (currentScrollHeight === lastScrollHeight) {
stableFrames++;
} else {
stableFrames = 0;
lastScrollHeight = currentScrollHeight;
}
if (stableFrames >= 2) {
isAtBottomRef.current = true; isAtBottomRef.current = true;
setHasScrolledToBottomInitially(true); setHasScrolledToBottomInitially(true);
} else {
rafId = requestAnimationFrame(settleToBottom);
}
};
settleToBottom();
return () => {
cancelAnimationFrame(rafId);
};
}, [hasScrolledToBottomInitially, virtualRowCount]); }, [hasScrolledToBottomInitially, virtualRowCount]);
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
@ -442,6 +466,19 @@ export default function Screen() {
}} }}
onScroll={handleContainerScroll} onScroll={handleContainerScroll}
> >
{messagesQuery.isPending && (
<div className="flex items-center justify-center pt-10">
<p className="text-foreground/45 flex gap-1 items-center justify-center">
<Loader2 size={18} className="animate-spin" />
Loading...
</p>
</div>
)}
{messagesQuery.isFetchingNextPage && (
<div className="flex items-center justify-center pt-3 pb-1">
<Loader2 size={16} className="animate-spin text-foreground/45" />
</div>
)}
<div <div
className="relative w-full" className="relative w-full"
style={{ style={{

1
packages/chat/todo.md Normal file
View file

@ -0,0 +1 @@
- Add context menu to messages

View file

@ -12,6 +12,7 @@
"build": "tsc -p tsconfig.json --noEmit" "build": "tsc -p tsconfig.json --noEmit"
}, },
"dependencies": { "dependencies": {
"@tanstack/react-router": "^1.169.1",
"@tauri-apps/api": "^2.11.0", "@tauri-apps/api": "^2.11.0",
"@tensamin/crypto": "workspace:*", "@tensamin/crypto": "workspace:*",
"@tensamin/shared": "workspace:*", "@tensamin/shared": "workspace:*",

View file

@ -10,16 +10,17 @@ import { message as messageSchema } from "@tensamin/shared/data";
import { Avatar, AvatarFallback, AvatarImage } from "@tensamin/ui"; import { Avatar, AvatarFallback, AvatarImage } from "@tensamin/ui";
import { isTauri } from "@tauri-apps/api/core"; import { isTauri } from "@tauri-apps/api/core";
import { useSession } from "@tensamin/storage/session"; import { useSession } from "@tensamin/storage/session";
import { useNavigate } from "@tanstack/react-router";
export const context = createContext<contextType | undefined>(undefined); export const context = createContext<contextType | undefined>(undefined);
function reduceDisplay(display: string) { async function requestNotificationPermission() {
const words = display.split(" "); if (!("Notification" in window)) return false;
if (words.length === 1) {
return display.slice(0, 2).toUpperCase(); if (Notification.permission === "granted") return true;
} else {
return words[0].charAt(0).toUpperCase() + words[1].charAt(0).toUpperCase(); const permission = await Notification.requestPermission();
} return permission === "granted";
} }
export default function Provider(props: { children: React.ReactNode }) { 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 { decryptText, getSharedSecret } = useCrypto();
const { addLiveMessage, userId } = useChat(); const { addLiveMessage, userId } = useChat();
const { moveUserIdToTop } = useSession(); const { moveUserIdToTop } = useSession();
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
return subscribePush(async (ttpMessage) => { return subscribePush(async (ttpMessage) => {
@ -54,7 +56,7 @@ export default function Provider(props: { children: React.ReactNode }) {
(await load("settings.read_confirmations")) && (await load("settings.read_confirmations")) &&
userId === sender_id userId === sender_id
) { ) {
await send( void send(
"message_state", "message_state",
{ {
message_state: "read", message_state: "read",
@ -64,7 +66,7 @@ export default function Provider(props: { children: React.ReactNode }) {
}, },
); );
} else { } else {
await send( void send(
"message_state", "message_state",
{ {
message_state: "received", message_state: "received",
@ -91,6 +93,26 @@ export default function Provider(props: { children: React.ReactNode }) {
if (isTauri()) { if (isTauri()) {
console.log("weewoo"); console.log("weewoo");
} else {
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 { } else {
sonnerToast(user.display, { sonnerToast(user.display, {
classNames: { classNames: {
@ -100,12 +122,15 @@ export default function Provider(props: { children: React.ReactNode }) {
icon: ( icon: (
<Avatar> <Avatar>
<AvatarImage src={user.avatar} /> <AvatarImage src={user.avatar} />
<AvatarFallback>{reduceDisplay(user.display)}</AvatarFallback> <AvatarFallback>
{user.display.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar> </Avatar>
), ),
}); });
} }
} }
}
}); });
}, [ }, [
subscribePush, subscribePush,
@ -117,6 +142,7 @@ export default function Provider(props: { children: React.ReactNode }) {
userId, userId,
moveUserIdToTop, moveUserIdToTop,
send, send,
navigate,
]); ]);
return ( return (

View file

@ -257,7 +257,7 @@ export function Provider(props: {
setErrorDescription( setErrorDescription(
"You could try to restart your Iota, check for updates or check your network connection.", "You could try to restart your Iota, check for updates or check your network connection.",
); );
}) });
}, [connected, subscribePush]); }, [connected, subscribePush]);
useEffect(() => { useEffect(() => {