(feat): improved conversation adding flow
All checks were successful
/ build-web (push) Successful in 1m8s
/ build-desktop (push) Successful in 11m11s
/ build-mobile (push) Successful in 15m40s
/ release (push) Successful in 21s

(feat): visual tweaks for user modal
(feat): now using full width for profile settings page on mobile
(fix): profile not saving with empty avatar
(chore): bump version due to prod release not getting created
(chore): update licenses
This commit is contained in:
Alois 2026-05-21 12:15:37 +02:00
commit ba59a49f98
14 changed files with 203 additions and 44 deletions

View file

@ -31,12 +31,12 @@ export function Basic({
<Tooltip>
<TooltipTrigger
render={
<div className="absolute -bottom-0.75 -right-0.75 z-10 flex h-4 w-4 items-center justify-center rounded-full bg-card">
<div className="absolute -bottom-0.5 -right-0.5 z-10 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-card">
<div
style={{
backgroundColor: getStatusColor(user.online_status),
}}
className="h-2.5 w-2.5 rounded-full"
className="h-2.25 w-2.25 rounded-full"
/>
</div>
}
@ -52,7 +52,7 @@ export function Basic({
<div className="flex flex-col gap-1 w-full items-start justify-center text-[15px]">
<p>{user.display}</p>
</div>
<div className="pr-2">{extra}</div>
<div className="pr-1">{extra}</div>
</CardHeader>
</Card>
);

View file

@ -23,7 +23,6 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
const currentCalls = calls.filter((call) =>
call.call_members.some((member) => member === id),
);
//const currentCalls = ["leck", "schleck", "und", "eck"];
const [selectOpen, setSelectOpen] = useState(false);

View file

@ -220,7 +220,7 @@ export default function Sidebar() {
<button
type="button"
aria-label="Open profile menu"
className="inline-flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
className="cursor-pointer inline-flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
>
<Ellipsis />
</button>

View file

@ -76,6 +76,16 @@ export default function List() {
</div>
);
})}
<p
className="text-center text-sm px-2 text-muted-foreground transition-all duration-200 ease-in-out"
hidden={contacts.length !== 0 || category !== "conversations"}
style={{
opacity:
contacts.length === 0 && category === "conversations" ? 1 : 0,
}}
>
Get started by adding a conversation
</p>
</div>
</div>
</div>

View file

@ -11,12 +11,12 @@ import {
useIsMobile,
} from "@tensamin/ui";
import z from "zod";
import { toast } from "@tensamin/shared/log";
import { useTTP } from "@tensamin/ttp";
import { useState } from "react";
import { Loader2 } from "lucide-react";
import { isTauri } from "@tauri-apps/api/core";
import { useStorage } from "@tensamin/storage/context";
import { useSession } from "@tensamin/storage/session";
// The page
export default function Page() {
@ -43,11 +43,16 @@ export default function Page() {
// Add Conversation Button Component
function AddConversationButton() {
const { send } = useTTP();
const { contacts, insertContact } = useSession();
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [error, setError] = useState<string | null>(null);
function submit(username: string | null) {
async function submit(username: string | null) {
if (loading) return;
setError(null);
// username check
const schema = z
.string()
.min(1, "Username is too short")
@ -56,26 +61,50 @@ function AddConversationButton() {
const result = schema.safeParse(username?.toLowerCase().trim());
if (!result.success) {
toast("error", result.error.issues[0].message);
setError(result.error.issues[0].message);
return;
}
// user existence check
const user = await send("get_user_data", {
username: result.data,
})
.then((data) => {
if (data.data.user_id === 0) {
throw new Error();
}
return data;
})
.catch(() => {
setError("User not found");
return;
});
if (!user) return;
// alrady added check
if (contacts.some((contact) => contact.user_id === user.data.user_id)) {
setError("Conversation already exists");
return;
}
// add the conv
const timeout = setTimeout(() => setLoading(true), 500);
send("add_conversation", {
chat_partner_name: result.data,
})
.then(() => {
toast("success", "Conversation added");
insertContact(user.data.user_id);
setOpen(false);
})
.catch((error) => {
if (String(error).includes("error_not_found")) {
toast("error", "User not found");
setError("User not found");
return;
}
toast("error", "Failed to add conversation, check console for details");
console.error("Failed to add conversation", error);
setError(String(error));
})
.finally(() => {
clearTimeout(timeout);
@ -84,7 +113,15 @@ function AddConversationButton() {
}
return (
<Dialog>
<Dialog
open={open}
onOpenChange={(value) => {
if (value) {
setError(null);
}
setOpen(value);
}}
>
<DialogTrigger render={<Button>Add Conversation</Button>} />
<DialogContent>
<DialogHeader>
@ -110,7 +147,10 @@ function AddConversationButton() {
name="username"
placeholder="Enter username..."
/>
<div className="flex w-full justify-end gap-1">
<div className="flex w-full justify-end items-center gap-1">
{error && (
<p className="text-sm text-destructive w-full">{error}</p>
)}
<DialogClose render={<Button variant="outline">Cancel</Button>} />
<Button disabled={loading} type="submit">
{loading && <Loader2 className="animate-spin" />} Continue

View file

@ -4,7 +4,9 @@ import {
AvatarFallback,
AvatarImage,
Button,
cn,
Input,
useIsMobile,
} from "@tensamin/ui";
import { useUser, type User } from "@tensamin/user/context";
import { useEffect, useRef, useState } from "react";
@ -83,6 +85,8 @@ export default function Page() {
}
};
const isMobile = useIsMobile();
return currentUser ? (
<>
<input
@ -93,7 +97,7 @@ export default function Page() {
}
type="file"
/>
<div className="flex flex-col gap-5 w-80">
<div className={cn("flex flex-col gap-5", isMobile ? "w-full" : "w-80")}>
<div className="flex items-center gap-3">
<Avatar className="size-14">
<AvatarImage src={effectiveAvatar} />
@ -121,7 +125,7 @@ export default function Page() {
</Button>
</div>
<p className="text-sm text-muted-foreground">
GIFs are supported in decentralised mode or with Tensamin Premium
GIFs are supported in decentralised mode or with Tensamin Premium.
<br />
Maximum file size is 16mb.
</p>
@ -162,13 +166,16 @@ export default function Page() {
/>
<Button
onClick={async () => {
const { avatar, ...draftUsersWithoutAvatar } = draftUser;
const payload = {
...draftUser,
avatar:
typeof draftUser.avatar === "string" &&
draftUser.avatar.startsWith("data:")
? (draftUser.avatar.split(",", 2)[1] ?? "")
: draftUser.avatar,
...draftUsersWithoutAvatar,
...(typeof avatar === "string"
? {
avatar: avatar.startsWith("data:")
? (avatar.split(",", 2)[1] ?? "")
: avatar,
}
: {}),
};
const validation = ttp.change_user_data.request.safeParse(payload);