Compare commits

..
Author SHA1 Message Date
1a71e2ddbf Merge pull request '(feat): version dump for testing' (#8) from dev into main
All checks were successful
/ build-web (push) Successful in 1m10s
/ build-desktop (push) Successful in 11m21s
/ build-mobile (push) Successful in 15m54s
/ release (push) Successful in 22s
Reviewed-on: #8
2026-05-24 18:00:32 +02:00
21274991f3 Merge pull request '(feat): added version render script for apps like obtainium' (#7) from dev into main
All checks were successful
/ build-web (push) Successful in 1m9s
/ build-desktop (push) Successful in 11m6s
/ build-mobile (push) Successful in 15m37s
/ release (push) Successful in 22s
Reviewed-on: #7
2026-05-23 18:09:17 +02:00
955b4d53da Merge pull request '(feat): version bump needed for new ttp version' (#4) from dev into main
All checks were successful
/ build-web (push) Successful in 1m7s
/ build-desktop (push) Successful in 17m48s
/ build-mobile (push) Successful in 27m28s
/ release (push) Successful in 32s
Reviewed-on: #4
2026-05-21 21:31:55 +02:00
9f483ce8d3 Merge pull request '(feat): good call progress, mandatory update due to ttp changes' (#3) from dev into main
All checks were successful
/ build-web (push) Successful in 3m17s
/ build-desktop (push) Successful in 20m0s
/ build-mobile (push) Successful in 28m4s
/ release (push) Successful in 22s
Reviewed-on: #3
2026-05-20 22:11:21 +02:00
8b1b34db6a Merge pull request '(feat): various workflow improvements, mobile sidebar improvements' (#2) from dev into main
All checks were successful
/ build-web (push) Successful in 1m9s
/ build-desktop (push) Successful in 11m24s
/ build-mobile (push) Successful in 15m56s
/ release (push) Successful in 22s
Reviewed-on: #2
2026-05-14 15:24:51 +02:00
50 changed files with 84 additions and 204 deletions

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 MiB

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View file

@ -30,7 +30,7 @@
"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",
"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.json",
"gen-icons": "tauri icon ./logo.svg",
"format": "bunx prettier --write .",
"lint": "eslint src"
},

View file

@ -12,7 +12,9 @@ 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;
@ -20,7 +22,9 @@ 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 {
@ -30,7 +34,10 @@ 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 {
@ -40,7 +47,10 @@ 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
@ -62,4 +72,4 @@ if (isUnrender) {
console.log(`Unrendered versions back to ${PLACEHOLDER_VERSION}`);
} else {
console.log(`Rendered version ${targetVersion}`);
}
}

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

View file

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

Binary file not shown.

View file

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

View file

@ -1,34 +0,0 @@
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,8 +1,7 @@
import { Button, Popover, PopoverContent, PopoverTrigger } from "@tensamin/ui";
import { Button } from "@tensamin/ui";
import {
ArrowLeft,
ChevronDown,
ChevronUp,
House,
Phone,
Settings,
@ -18,7 +17,6 @@ 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();
@ -35,8 +33,6 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
const [selectOpen, setSelectOpen] = useState(false);
const [userInfoOpen, setUserInfoOpen] = useState(false);
return (
<div
data-tauri-drag-region
@ -73,27 +69,16 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
<Wrapper
userId={id}
component={(user) => (
<>
<Popover open={userInfoOpen} onOpenChange={setUserInfoOpen}>
<PopoverTrigger
render={
<Button
variant="link"
className="px-0! text-foreground"
style={{
textDecorationLine: "none",
}}
>
<p className="font-medium text-md">{user?.display}</p>
{userInfoOpen ? <ChevronUp /> : <ChevronDown />}
</Button>
}
/>
<PopoverContent side="bottom">
<Profile user={user} />
</PopoverContent>
</Popover>
</>
<Button
variant="link"
className="px-0! text-foreground"
style={{
textDecorationLine: "none",
}}
>
<p className="font-medium text-md">{user?.display}</p>
<ChevronDown />
</Button>
)}
loading={<Skeleton className="ml-3 w-40 h-5" />}
/>
@ -156,7 +141,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
)}
</>
)}
<Controls className="mr-2" />
<Controls className="mr-2 ring-border" />
</div>
</div>
);

View file

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

View file

@ -171,7 +171,6 @@
"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:*",

View file

@ -295,15 +295,13 @@ 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,
});
throw 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;
});
log(2, "call", "purple", "Updated local participant attributes", {
attributeKeys: Object.keys(attributes),
@ -797,13 +795,11 @@ 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();
}

View file

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

View file

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

View file

@ -8,7 +8,6 @@ import Message from "./components/message";
import { PAGE_SIZE } from "./values";
import { useIsMobile } from "@tensamin/ui";
import { Loader2 } from "lucide-react";
function getDistanceFromBottom(element: HTMLDivElement) {
return element.scrollHeight - (element.scrollTop + element.clientHeight);
@ -102,6 +101,12 @@ export default function Screen() {
const virtualRowCount =
messages.length + (shouldShowConversationStart ? 1 : 0);
const messagesRef = React.useRef(messages);
React.useEffect(() => {
messagesRef.current = messages;
}, [messages]);
const getItemKey = React.useCallback(
(index: number) => {
if (shouldShowConversationStart && index === 0) {
@ -109,9 +114,9 @@ export default function Screen() {
}
const messageIndex = shouldShowConversationStart ? index - 1 : index;
return messages[messageIndex]?.send_time ?? index;
return messagesRef.current[messageIndex]?.send_time ?? index;
},
[shouldShowConversationStart, messages],
[shouldShowConversationStart],
);
const setMeasurementRef = React.useCallback(
@ -355,38 +360,9 @@ export default function Screen() {
return;
}
let rafId: number;
let stableFrames = 0;
let lastScrollHeight = 0;
const settleToBottom = () => {
if (!scrollRef.current || hasScrolledToBottomInitially) {
return;
}
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;
setHasScrolledToBottomInitially(true);
} else {
rafId = requestAnimationFrame(settleToBottom);
}
};
settleToBottom();
return () => {
cancelAnimationFrame(rafId);
};
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
isAtBottomRef.current = true;
setHasScrolledToBottomInitially(true);
}, [hasScrolledToBottomInitially, virtualRowCount]);
React.useLayoutEffect(() => {
@ -466,19 +442,6 @@ export default function Screen() {
}}
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
className="relative w-full"
style={{

View file

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

View file

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

View file

@ -10,17 +10,16 @@ import { message as messageSchema } from "@tensamin/shared/data";
import { Avatar, AvatarFallback, AvatarImage } from "@tensamin/ui";
import { isTauri } from "@tauri-apps/api/core";
import { useSession } from "@tensamin/storage/session";
import { useNavigate } from "@tanstack/react-router";
export const context = createContext<contextType | undefined>(undefined);
async function requestNotificationPermission() {
if (!("Notification" in window)) return false;
if (Notification.permission === "granted") return true;
const permission = await Notification.requestPermission();
return permission === "granted";
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();
}
}
export default function Provider(props: { children: React.ReactNode }) {
@ -30,7 +29,6 @@ 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) => {
@ -56,7 +54,7 @@ export default function Provider(props: { children: React.ReactNode }) {
(await load("settings.read_confirmations")) &&
userId === sender_id
) {
void send(
await send(
"message_state",
{
message_state: "read",
@ -66,7 +64,7 @@ export default function Provider(props: { children: React.ReactNode }) {
},
);
} else {
void send(
await send(
"message_state",
{
message_state: "received",
@ -94,41 +92,18 @@ export default function Provider(props: { children: React.ReactNode }) {
if (isTauri()) {
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 {
sonnerToast(user.display, {
classNames: {
content: "pl-4",
},
description: decryptedContent,
icon: (
<Avatar>
<AvatarImage src={user.avatar} />
<AvatarFallback>
{user.display.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
),
});
}
sonnerToast(user.display, {
classNames: {
content: "pl-4",
},
description: decryptedContent,
icon: (
<Avatar>
<AvatarImage src={user.avatar} />
<AvatarFallback>{reduceDisplay(user.display)}</AvatarFallback>
</Avatar>
),
});
}
}
});
@ -142,7 +117,6 @@ export default function Provider(props: { children: React.ReactNode }) {
userId,
moveUserIdToTop,
send,
navigate,
]);
return (

View file

@ -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(() => {