Updated chat ui/ux

This commit is contained in:
Alois 2026-03-27 23:19:38 +01:00
commit 04f37be8f6
7 changed files with 146 additions and 32 deletions

View file

@ -53,7 +53,7 @@ export default function InputComponent() {
return;
}
addLiveMessage({
const reference = addLiveMessage({
height: 0,
not_encrypted: true,
timestamp: time,
@ -75,6 +75,7 @@ export default function InputComponent() {
receiver_id: currentUserId,
send_time: time,
});
reference.setFailed(true);
toast("error", "Failed to send message");
});
@ -82,7 +83,7 @@ export default function InputComponent() {
}
return (
<Card className="rounded-none rounded-t-xl border border-input/50 border-b-0 pt-0">
<Card className="rounded-none rounded-t-xl border-b-0 pt-0">
<CardHeader className="p-0 flex flex-col">
<Input
placeholder="Send a message..."
@ -93,10 +94,7 @@ export default function InputComponent() {
/>
<div className="w-full flex justify-between gap-2 p-2 pt-0">
<div className="flex gap-2">
<Button
className="w-9 h-9 p-0"
variant="ghost"
>
<Button className="w-9 h-9 p-0" variant="ghost">
<Plus size={20} />
</Button>
<div className="w-auto flex">
@ -104,16 +102,10 @@ export default function InputComponent() {
</div>
</div>
<div className="flex gap-2">
<Button
className="w-9 h-9 p-0"
variant="ghost"
>
<Button className="w-9 h-9 p-0" variant="ghost">
<Laugh size={20} />
</Button>
<Button
className="w-9 h-9 p-0"
variant="ghost"
>
<Button className="w-9 h-9 p-0" variant="ghost">
<Clapperboard size={20} />
</Button>
</div>

View file

@ -4,6 +4,13 @@ import { useChat } from "../context";
import type { RawMessage } from "../values";
import { log } from "@tensamin/shared/log";
import Text from "@tensamin/markdown/text";
import { AlertTriangle } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@tensamin/ui/cmp/tooltip";
/**
* Executes Message.
@ -11,7 +18,9 @@ import Text from "@tensamin/markdown/text";
* @returns unknown.
*/
export default function Message(props: {
message: RawMessage;
message: RawMessage & {
failed?: boolean;
};
notEncrypted?: boolean;
}) {
const { sharedSecret } = useChat();
@ -67,14 +76,24 @@ export default function Message(props: {
}, [decrypt, props.message.content, props.notEncrypted, sharedSecret]);
return (
<div className="w-full flex justify-start">
<div
className={`w-full flex justify-start ${props.message.failed && "opacity-50"}`}
>
<div
className={`animate-in fade-in duration-200 max-w-[80%] rounded-xl px-2 py-1 whitespace-pre-wrap wrap-break-word ${
className={`flex gap-1 items-center justify-center animate-in fade-in duration-200 max-w-[80%] rounded-lg px-2 py-px whitespace-pre-wrap wrap-break-word ${
props.message.sent_by_self
? "bg-primary text-primary-foreground"
: "bg-muted"
}`}
>
{props.message.failed && (
<Tooltip>
<TooltipContent>
<p>Failed to send message</p>
</TooltipContent>
<TooltipTrigger render={<AlertTriangle size={17} />} />
</Tooltip>
)}
{isReady ? <Text value={decodedContent} /> : null}
</div>
</div>

View file

@ -1,7 +1,7 @@
import * as React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useRouterState } from "@tanstack/react-router";
import type { RawMessage, RawMessages } from "./values";
import type { LiveMessage, RawMessage, RawMessages } from "./values";
import { useCrypto } from "@tensamin/crypto/context";
import { useUser } from "@tensamin/user/context";
import { useStorage } from "@tensamin/storage/context";
@ -22,9 +22,7 @@ export default function Provider(props: { children: React.ReactNode }) {
const { load } = useStorage();
const { send } = useSocket();
const [liveMessagesState, setLiveMessagesState] = React.useState<RawMessages>(
[],
);
const [liveMessagesState, setLiveMessagesState] = React.useState<LiveMessage[]>([]);
const [currentSharedSecret, setCurrentSharedSecret] = React.useState("");
const locationSearch = useRouterState({
@ -93,7 +91,30 @@ export default function Provider(props: { children: React.ReactNode }) {
);
const addLiveMessage = React.useCallback((message: RawMessage) => {
setLiveMessagesState((prev) => [...prev, message]);
const localId =
globalThis.crypto?.randomUUID?.() ??
`${Date.now()}-${Math.random().toString(36).slice(2)}`;
setLiveMessagesState((prev) => [
...prev,
{
...message,
localId,
failed: false,
},
]);
return {
setFailed: (failed: boolean) => {
setLiveMessagesState((prev) =>
prev.map((liveMessage) =>
liveMessage.localId === localId
? { ...liveMessage, failed }
: liveMessage,
),
);
},
};
}, []);
const clearLiveMessages = React.useCallback(() => {
@ -128,8 +149,10 @@ export default function Provider(props: { children: React.ReactNode }) {
type contextType = {
getMessages: (amount: number, offset: number) => Promise<RawMessages>;
liveMessages: () => RawMessages;
addLiveMessage: (message: RawMessage) => void;
liveMessages: () => LiveMessage[];
addLiveMessage: (message: RawMessage) => {
setFailed: (failed: boolean) => void;
};
clearLiveMessages: () => void;
sharedSecret: () => string;
userId: () => number;

View file

@ -6,3 +6,8 @@ export type RawMessages = z.infer<
>["messages"];
export type RawMessage = RawMessages[number];
export type LiveMessage = RawMessage & {
failed?: boolean;
localId: string;
};