(feat): add live messages
(fix): broken states in message component (qol): update todos
This commit is contained in:
parent
c99b5940b8
commit
2d1e2c7bf8
8 changed files with 91 additions and 30 deletions
|
|
@ -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(),
|
||||
}),
|
||||
|
|
|
|||
4
bun.lock
4
bun.lock
|
|
@ -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=="],
|
||||
|
|
|
|||
|
|
@ -5,4 +5,6 @@
|
|||
- Avatar in the center
|
||||
- Mobile
|
||||
- Call invite popup
|
||||
- Save call invite in `calls` array
|
||||
- Sounds
|
||||
- Fullscreen button for screenshares
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: () => {} }}>
|
||||
|
|
|
|||
3
packages/notifications/todo.md
Normal file
3
packages/notifications/todo.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
- Tauri notifications
|
||||
- Mobile notifications channel setup
|
||||
- Add notification symbol / outline to user modal
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue