(feat): improved conversation adding flow
(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:
parent
4380d664be
commit
ba59a49f98
14 changed files with 203 additions and 44 deletions
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue