(feat): add live messages

(fix): broken states in message component
(qol): update todos
This commit is contained in:
Alois 2026-05-01 13:40:04 +02:00
commit 2d1e2c7bf8
8 changed files with 91 additions and 30 deletions

View file

@ -146,9 +146,11 @@ function AppShell() {
<CallInit /> <CallInit />
<TAuthWrapper> <TAuthWrapper>
<AppLayout> <AppLayout>
<ChatContext>
<NotificationsProvider> <NotificationsProvider>
<Outlet /> <Outlet />
</NotificationsProvider> </NotificationsProvider>
</ChatContext>
</AppLayout> </AppLayout>
</TAuthWrapper> </TAuthWrapper>
</UserContext> </UserContext>
@ -162,14 +164,6 @@ function CallInit() {
return null; return null;
} }
function Chat() {
return (
<ChatContext>
<ChatScreen />
</ChatContext>
);
}
const rootRoute = createRootRoute({ const rootRoute = createRootRoute({
component: RootShell, component: RootShell,
}); });
@ -234,7 +228,7 @@ const homeRoute = createRoute({
const chatRoute = createRoute({ const chatRoute = createRoute({
getParentRoute: () => appRoute, getParentRoute: () => appRoute,
path: "chat", path: "chat",
component: Chat, component: ChatScreen,
validateSearch: z.object({ validateSearch: z.object({
id: z.number().optional(), id: z.number().optional(),
}), }),

View file

@ -168,6 +168,8 @@
"name": "@tensamin/notifications", "name": "@tensamin/notifications",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.11.0",
"@tensamin/chat": "workspace:*",
"@tensamin/crypto": "workspace:*", "@tensamin/crypto": "workspace:*",
"@tensamin/shared": "workspace:*", "@tensamin/shared": "workspace:*",
"@tensamin/storage": "workspace:*", "@tensamin/storage": "workspace:*",
@ -1670,6 +1672,8 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tensamin/notifications/@tauri-apps/api": ["@tauri-apps/api@2.11.0", "", {}, "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA=="],
"@tensamin/shared/sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="], "@tensamin/shared/sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="],
"@tensamin/tauri/lucide-react": ["lucide-react@1.8.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw=="], "@tensamin/tauri/lucide-react": ["lucide-react@1.8.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw=="],

View file

@ -5,4 +5,6 @@
- Avatar in the center - Avatar in the center
- Mobile - Mobile
- Call invite popup - Call invite popup
- Save call invite in `calls` array
- Sounds - Sounds
- Fullscreen button for screenshares

View file

@ -3,7 +3,7 @@ import type { RawMessage } from "../values";
import Text from "@tensamin/markdown/text"; import Text from "@tensamin/markdown/text";
import { AlertTriangle, Clock } from "lucide-react"; import { AlertTriangle, Clock } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@tensamin/ui"; import { cn, Tooltip, TooltipContent, TooltipTrigger } from "@tensamin/ui";
import { useIsMobile } from "@tensamin/ui"; import { useIsMobile } from "@tensamin/ui";
import { import {
ContextMenu, ContextMenu,
@ -20,22 +20,27 @@ function MessageComponent({
}; };
}) { }) {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const actuallyFailed = message.failed && message.message_state === "awaiting";
return ( return (
<div <div
className={`w-full flex justify-start transition-opacity duration-150 ${(message.failed || message.message_state === "awaiting") && "opacity-50"}`} className={`w-full flex justify-start transition-opacity duration-150 ${actuallyFailed || message.message_state === "awaiting" ? "opacity-50" : ""}`}
> >
<ContextMenu> <ContextMenu>
<ContextMenuTrigger <ContextMenuTrigger
render={ render={
<div <div
className={`selection-foreground flex gap-1 items-center justify-center max-w-[80%] rounded-lg px-2 py-px whitespace-pre-wrap break-all ${ className={cn(
message.sent_by_self "selection-foreground flex gap-1 items-center justify-center max-w-[80%] rounded-lg px-2 py-px whitespace-pre-wrap break-all",
? message.failed {
? "bg-destructive/75 text-destructive-foreground" "bg-(--destructive)/75 text-destructive-foreground":
: "bg-primary text-primary-foreground" actuallyFailed,
: "bg-muted" "bg-primary text-primary-foreground":
} ${isMobile && "select-none"}`} !actuallyFailed && message.sent_by_self,
"bg-muted": !message.sent_by_self,
"select-none": isMobile,
},
)}
> >
{message.failed && message.message_state === "awaiting" && ( {message.failed && message.message_state === "awaiting" && (
<Tooltip> <Tooltip>

View file

@ -12,10 +12,12 @@
"build": "tsc -p tsconfig.json --noEmit" "build": "tsc -p tsconfig.json --noEmit"
}, },
"dependencies": { "dependencies": {
"@tensamin/ttp": "workspace:*", "@tauri-apps/api": "^2.11.0",
"@tensamin/shared": "workspace:*",
"@tensamin/crypto": "workspace:*", "@tensamin/crypto": "workspace:*",
"@tensamin/shared": "workspace:*",
"@tensamin/chat": "workspace:*",
"@tensamin/storage": "workspace:*", "@tensamin/storage": "workspace:*",
"@tensamin/ttp": "workspace:*",
"@tensamin/user": "workspace:*", "@tensamin/user": "workspace:*",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",

View file

@ -1,43 +1,92 @@
import { useCrypto } from "@tensamin/crypto/context"; import { useCrypto } from "@tensamin/crypto/context";
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
import { useUser } from "@tensamin/user/context"; import { useUser } from "@tensamin/user/context";
import { useChat } from "@tensamin/chat/context";
import { useTTP } from "@tensamin/ttp"; import { useTTP } from "@tensamin/ttp";
import { createContext, useEffect, useContext } from "react"; import { createContext, useEffect, useContext } from "react";
import z from "zod"; import z from "zod";
import { toast as sonnerToast } from "sonner"; import { toast as sonnerToast } from "sonner";
import { message as messageSchema } from "@tensamin/shared/data"; import { message as messageSchema } from "@tensamin/shared/data";
import { Avatar, AvatarFallback, AvatarImage } from "@tensamin/ui";
import { isTauri } from "@tauri-apps/api/core";
export const context = createContext<contextType | undefined>(undefined); export const context = createContext<contextType | undefined>(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();
}
}
export default function Provider(props: { children: React.ReactNode }) { export default function Provider(props: { children: React.ReactNode }) {
const { subscribePush } = useTTP(); const { subscribePush } = useTTP();
const { load } = useStorage(); const { load } = useStorage();
const { get } = useUser(); const { get } = useUser();
const { decryptText, getSharedSecret } = useCrypto(); const { decryptText, getSharedSecret } = useCrypto();
const { addLiveMessage, userId } = useChat();
useEffect(() => { useEffect(() => {
subscribePush(async (ttpMessage) => { return subscribePush(async (ttpMessage) => {
if (ttpMessage.type === "message_live") { if (ttpMessage.type === "message_live") {
const { message, sender_id } = ttpMessage.data as { const { message, sender_id } = ttpMessage.data as {
message: z.infer<typeof messageSchema>; message: z.infer<typeof messageSchema>;
sender_id: number; sender_id: number;
}; };
const user = await get(sender_id);
const decryptedContent = await decryptText( const decryptedContent = await decryptText(
await getSharedSecret( await getSharedSecret(
await load("private_key"), await load("private_key"),
await get(await load("user_id")).then((data) => data.public_key), await get(await load("user_id")).then((data) => data.public_key),
await get(sender_id).then((data) => data.public_key), user.public_key,
), ),
message.content, message.content,
); );
sonnerToast(decryptedContent, { console.log(userId, sender_id, userId === sender_id);
icon: <div>A</div>, if (userId === sender_id) {
addLiveMessage({
...message,
content: decryptedContent,
sent_by_self: false,
});
return;
}
// add notification symbol to conversation cards
if (isTauri()) {
console.log("weewoo");
} else {
sonnerToast(user.display, {
classNames: {
content: "pl-4",
},
description: decryptedContent,
icon: (
<Avatar>
<AvatarImage src={user.avatar} />
<AvatarFallback>{reduceDisplay(user.display)}</AvatarFallback>
</Avatar>
),
}); });
} }
}
}); });
}, [subscribePush, decryptText, getSharedSecret, load, get]); }, [
subscribePush,
decryptText,
getSharedSecret,
load,
get,
addLiveMessage,
userId,
]);
return ( return (
<context.Provider value={{ test: () => {} }}> <context.Provider value={{ test: () => {} }}>

View file

@ -0,0 +1,3 @@
- Tauri notifications
- Mobile notifications channel setup
- Add notification symbol / outline to user modal

View file

@ -22,7 +22,9 @@ export const message = z.object({
tint: z.string().length(7).startsWith("#").optional(), tint: z.string().length(7).startsWith("#").optional(),
avatar: z.boolean().optional(), avatar: z.boolean().optional(),
display: z.boolean().optional(), display: z.boolean().optional(),
message_state: z.enum(["read", "received", "sent", "sending", "awaiting"]), // awaiting for 'internal' use message_state: z
.enum(["read", "received", "sent", "sending", "awaiting"])
.default("received"), // awaiting for 'internal' use
}); });
export const failedUser = { export const failedUser = {