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

View file

@ -168,6 +168,8 @@
"name": "@tensamin/notifications",
"version": "0.0.0",
"dependencies": {
"@tauri-apps/api": "^2.11.0",
"@tensamin/chat": "workspace:*",
"@tensamin/crypto": "workspace:*",
"@tensamin/shared": "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=="],
"@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/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
- Mobile
- Call invite popup
- Save call invite in `calls` array
- Sounds
- Fullscreen button for screenshares

View file

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

View file

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

View file

@ -1,43 +1,92 @@
import { useCrypto } from "@tensamin/crypto/context";
import { useStorage } from "@tensamin/storage/context";
import { useUser } from "@tensamin/user/context";
import { useChat } from "@tensamin/chat/context";
import { useTTP } from "@tensamin/ttp";
import { createContext, useEffect, useContext } from "react";
import z from "zod";
import { toast as sonnerToast } from "sonner";
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);
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 }) {
const { subscribePush } = useTTP();
const { load } = useStorage();
const { get } = useUser();
const { decryptText, getSharedSecret } = useCrypto();
const { addLiveMessage, userId } = useChat();
useEffect(() => {
subscribePush(async (ttpMessage) => {
return subscribePush(async (ttpMessage) => {
if (ttpMessage.type === "message_live") {
const { message, sender_id } = ttpMessage.data as {
message: z.infer<typeof messageSchema>;
sender_id: number;
};
const user = await get(sender_id);
const decryptedContent = await decryptText(
await getSharedSecret(
await load("private_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,
);
sonnerToast(decryptedContent, {
icon: <div>A</div>,
});
console.log(userId, sender_id, userId === sender_id);
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 (
<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(),
avatar: 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 = {