@@ -19,7 +19,14 @@ export async function POST(req: NextRequest) {
|
|||||||
{ status: StatusCodes.BAD_REQUEST }
|
{ status: StatusCodes.BAD_REQUEST }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!username || !display_name || !avatar_url)
|
if (
|
||||||
|
!username ||
|
||||||
|
!display_name ||
|
||||||
|
avatar_url === undefined ||
|
||||||
|
avatar_url === null ||
|
||||||
|
node_address === undefined ||
|
||||||
|
node_address === null
|
||||||
|
)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
message:
|
message:
|
||||||
|
|||||||
+22
-33
@@ -5,10 +5,10 @@ import { supabase } from "../supa";
|
|||||||
/**
|
/**
|
||||||
* POST /api/user
|
* POST /api/user
|
||||||
* Register new user
|
* Register new user
|
||||||
* Body: { name, username, email, password }
|
* Body: { username, email, password }
|
||||||
*/
|
*/
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
const { name, username, email, password } = await req.json();
|
const { username, email, password } = await req.json();
|
||||||
|
|
||||||
if (!email || !password || !username)
|
if (!email || !password || !username)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -28,29 +28,35 @@ export async function POST(req: NextRequest) {
|
|||||||
{ status: StatusCodes.CONFLICT }
|
{ status: StatusCodes.CONFLICT }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: authData, error: authError } =
|
const { data: authData, error: authError } = await supabase.auth.signUp({
|
||||||
await supabase.auth.admin.createUser({
|
email,
|
||||||
email,
|
password,
|
||||||
password,
|
});
|
||||||
email_confirm: true,
|
|
||||||
user_metadata: { name, username },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (authError)
|
if (authError)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: authError.message },
|
{ message: authError.message },
|
||||||
{ status: StatusCodes.BAD_REQUEST }
|
{ status: StatusCodes.INTERNAL_SERVER_ERROR }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!authData.user)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "User is null" },
|
||||||
|
{ status: StatusCodes.INTERNAL_SERVER_ERROR }
|
||||||
);
|
);
|
||||||
|
|
||||||
await supabase.from("profiles").insert({
|
await supabase.from("profiles").insert({
|
||||||
id: authData.user.id,
|
id: authData.user.id,
|
||||||
name,
|
|
||||||
username,
|
username,
|
||||||
|
display_name: username,
|
||||||
|
avatar_url: "",
|
||||||
|
node_address: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
message: "ok",
|
message: "User registered. Please check your email to verify your account.",
|
||||||
user_id: authData.user.id,
|
user_id: authData.user?.id,
|
||||||
|
email_sent: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,30 +67,14 @@ export async function POST(req: NextRequest) {
|
|||||||
*/
|
*/
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
const params = new URL(req.url).searchParams;
|
const params = new URL(req.url).searchParams;
|
||||||
const [username, password] = [params.get("username"), params.get("password")];
|
const [email, password] = [params.get("email"), params.get("password")];
|
||||||
|
|
||||||
if (!username || !password)
|
if (!email || !password)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: "Params username and password are required" },
|
{ message: "Params email and password are required" },
|
||||||
{ status: StatusCodes.BAD_REQUEST }
|
{ status: StatusCodes.BAD_REQUEST }
|
||||||
);
|
);
|
||||||
|
|
||||||
let email = username;
|
|
||||||
if (!username.includes("@")) {
|
|
||||||
const { data } = await supabase
|
|
||||||
.from("profiles")
|
|
||||||
.select("email")
|
|
||||||
.eq("email", username)
|
|
||||||
.maybeSingle();
|
|
||||||
email = data?.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!email)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ message: "Invalid username" },
|
|
||||||
{ status: StatusCodes.UNAUTHORIZED }
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data, error } = await supabase.auth.signInWithPassword({
|
const { data, error } = await supabase.auth.signInWithPassword({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
@@ -104,7 +94,6 @@ export async function GET(req: NextRequest) {
|
|||||||
user_id: session?.user?.id,
|
user_id: session?.user?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Optionally set cookie
|
|
||||||
res.cookies.set("token", session?.access_token ?? "", { httpOnly: true });
|
res.cookies.set("token", session?.access_token ?? "", { httpOnly: true });
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { supabase } from "../supa";
|
||||||
|
import { StatusCodes } from "http-status-codes";
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
const email = req.nextUrl.searchParams.get("email");
|
||||||
|
const password = req.nextUrl.searchParams.get("password");
|
||||||
|
|
||||||
|
if (!email || !password)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Missing email or password" },
|
||||||
|
{ status: StatusCodes.BAD_REQUEST }
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, error } = await supabase.auth.signInWithPassword({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: error.message },
|
||||||
|
{ status: StatusCodes.INTERNAL_SERVER_ERROR }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ verified: false, message: "User not found" },
|
||||||
|
{ status: StatusCodes.NOT_FOUND }
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = NextResponse.json({
|
||||||
|
verified: !!data.user.email_confirmed_at,
|
||||||
|
user_id: data.user.id,
|
||||||
|
access_token: data.session.access_token,
|
||||||
|
refresh_token: data.session.refresh_token,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
@@ -2,16 +2,20 @@
|
|||||||
|
|
||||||
import AppSidebar from "@/components/Sidebar";
|
import AppSidebar from "@/components/Sidebar";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { useIsMobile } from "@/hooks/is-mobile";
|
||||||
import useApp from "@/hooks/use-app";
|
import useApp from "@/hooks/use-app";
|
||||||
|
|
||||||
export default function Chat() {
|
export default function Chat() {
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppSidebar app={app}>
|
<AppSidebar app={app}>
|
||||||
<Card className="bg-background w-full">
|
<Card className="bg-background w-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Direct Messages</CardTitle>
|
<CardTitle onClick={() => isMobile && app.setSidebarOpen(true)}>
|
||||||
|
Direct Messages
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent></CardContent>
|
<CardContent></CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export default function Verify() {
|
||||||
|
const [email, setEmail] = useState<string>();
|
||||||
|
const router = useRouter();
|
||||||
|
const [verified, setVerified] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const email = Cookies.get("verify_temp_email");
|
||||||
|
setEmail(email);
|
||||||
|
const password = Cookies.get("verify_temp_password");
|
||||||
|
|
||||||
|
const i = setInterval(async () => {
|
||||||
|
const { data } = await axios.get("/api/verify", {
|
||||||
|
params: { email, password },
|
||||||
|
});
|
||||||
|
const response = data as {
|
||||||
|
verified: boolean;
|
||||||
|
user_id: string;
|
||||||
|
access_token?: string;
|
||||||
|
refresh_token?: string;
|
||||||
|
};
|
||||||
|
if (response.verified) {
|
||||||
|
clearInterval(i);
|
||||||
|
if (response.access_token) {
|
||||||
|
setVerified(true);
|
||||||
|
toast.info("Email verified, redirecting...");
|
||||||
|
Cookies.set("token", response.access_token);
|
||||||
|
Cookies.remove("verify_temp_email");
|
||||||
|
Cookies.remove("verify_temp_password");
|
||||||
|
setTimeout(() => router.push("/chat"), 500);
|
||||||
|
} else {
|
||||||
|
console.log(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 8000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-screen flex items-center">
|
||||||
|
<Card className="w-md m-auto">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-3xl mx-auto">Verify Email</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{verified ? (
|
||||||
|
"Successfully verified"
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
We sent you a verification email at {email}. Please verify to
|
||||||
|
continue.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="gap-3 flex flex-col">
|
||||||
|
Current status: {verified ? "Verified" : "Unverified"}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "./ui/dropdown-menu";
|
} from "./ui/dropdown-menu";
|
||||||
import { ChevronLeftIcon, SmilePlusIcon } from "lucide-react";
|
import { ChevronLeftIcon, SendIcon, SmilePlusIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { format, isToday, isYesterday } from "date-fns";
|
import { format, isToday, isYesterday } from "date-fns";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
@@ -19,6 +19,8 @@ import { UserProfile } from "@/hooks/get-user";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import ProfilePicture from "./ProfilePicture";
|
import ProfilePicture from "./ProfilePicture";
|
||||||
import App from "@/types/app";
|
import App from "@/types/app";
|
||||||
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
|
import { Textarea } from "./ui/textarea";
|
||||||
|
|
||||||
function MessageContainer({ message, app }: { message: Message; app: App }) {
|
function MessageContainer({ message, app }: { message: Message; app: App }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -41,14 +43,14 @@ function MessageContainer({ message, app }: { message: Message; app: App }) {
|
|||||||
url={app.profiles[message.from]?.avatar_url}
|
url={app.profiles[message.from]?.avatar_url}
|
||||||
name={String(
|
name={String(
|
||||||
app.profiles[message.from]
|
app.profiles[message.from]
|
||||||
? app.profiles[message.from].display_name
|
? app.profiles[message.from]?.display_name
|
||||||
: message.from
|
: message.from
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col flex-1">
|
<div className="flex flex-col flex-1">
|
||||||
<span className="font-bold flex gap-1 items-center">
|
<span className="font-bold flex gap-1 items-center">
|
||||||
{app.profiles[message.from]
|
{app.profiles[message.from]
|
||||||
? app.profiles[message.from].display_name
|
? app.profiles[message.from]?.display_name
|
||||||
: message.from}
|
: message.from}
|
||||||
|
|
||||||
<p className="text-neutral-500 text-xs">
|
<p className="text-neutral-500 text-xs">
|
||||||
@@ -142,13 +144,15 @@ export default function MessageBox({
|
|||||||
messages: Message[];
|
messages: Message[];
|
||||||
}) {
|
}) {
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const inputRef = useRef<any>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-svh w-full max-h-svh flex flex-col pb-5 pl-5 gap-5">
|
<div className="h-svh w-full max-h-svh flex flex-col pb-5 pl-5 gap-5">
|
||||||
{channelName && (
|
{channelName && (
|
||||||
<header className="h-12 py-4 flex items-center border-b text-sm font-semibold">
|
<header className="h-12 py-4 flex items-center border-b text-sm font-semibold">
|
||||||
<span
|
<span
|
||||||
onClick={() => app.setSidebarOpen(true)}
|
onClick={() => isMobile && app.setSidebarOpen(true)}
|
||||||
className="cursor-pointer flex"
|
className="cursor-pointer flex"
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon className="w-5 h-5" />
|
<ChevronLeftIcon className="w-5 h-5" />
|
||||||
@@ -156,27 +160,70 @@ export default function MessageBox({
|
|||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col-reverse overflow-y-scroll">
|
<div className="mt-auto flex flex-col-reverse overflow-y-scroll">
|
||||||
{messages.toReversed().map((msg) => (
|
{messages.toReversed().map((msg) => (
|
||||||
<MessageContainer key={msg.id} message={msg} app={app} />
|
<MessageContainer key={msg.id} message={msg} app={app} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<footer className="flex gap-3 pr-5 w-full mt-auto">
|
<footer className="flex gap-3 pr-5 w-full h-max">
|
||||||
<Input
|
{text.includes("\n") ? (
|
||||||
className=""
|
<Textarea
|
||||||
placeholder="Type a message..."
|
ref={inputRef}
|
||||||
value={text}
|
className="resize-none"
|
||||||
onChange={(e) => setText(e.target.value)}
|
placeholder="Type a message..."
|
||||||
onKeyDown={(e) => {
|
value={text}
|
||||||
if (e.key === "Enter") {
|
onChange={(e) => {
|
||||||
sendMessage(text);
|
if (!e.target.value.includes("\n") && text.includes("\n")) {
|
||||||
setText("");
|
setTimeout(() => {
|
||||||
}
|
if (inputRef.current) {
|
||||||
}}
|
inputRef.current.selectionStart = text.length + 1;
|
||||||
/>
|
inputRef.current.selectionEnd = text.length + 1;
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
setText(e.target.value);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
if (!e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage(text);
|
||||||
|
setText("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
placeholder="Type a message..."
|
||||||
|
ref={inputRef}
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
setText((p) => p + "\n");
|
||||||
|
setTimeout(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.selectionStart = text.length + 1;
|
||||||
|
inputRef.current.selectionEnd = text.length + 1;
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
} else {
|
||||||
|
sendMessage(text.trim());
|
||||||
|
setText("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="secondary">
|
<Button variant="secondary" className="hidden md:block">
|
||||||
<SmilePlusIcon />
|
<SmilePlusIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -184,10 +231,18 @@ export default function MessageBox({
|
|||||||
<DropdownMenuContent className="">
|
<DropdownMenuContent className="">
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
onEmojiClick={(e) => setText(text + e.emoji)}
|
onEmojiClick={(e) => setText(text + e.emoji)}
|
||||||
theme={emoji.Theme.DARK}
|
theme={
|
||||||
|
app.clientSettings.theme === "dark"
|
||||||
|
? emoji.Theme.DARK
|
||||||
|
: emoji.Theme.LIGHT
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
|
<Button variant="secondary">
|
||||||
|
<SendIcon />
|
||||||
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export default function ProfilePicture({
|
|||||||
url,
|
url,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string | undefined;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Avatar className="h-8 w-8">
|
<Avatar className="h-8 w-8">
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import Link from "next/link";
|
|||||||
import { get, Response } from "@/lib/request";
|
import { get, Response } from "@/lib/request";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import App from "@/types/app";
|
import App from "@/types/app";
|
||||||
|
import { ProfileSettings } from "@/types/settings";
|
||||||
|
|
||||||
export default function AppSidebar({
|
export default function AppSidebar({
|
||||||
children,
|
children,
|
||||||
@@ -106,9 +107,7 @@ export default function AppSidebar({
|
|||||||
];
|
];
|
||||||
|
|
||||||
setServers(updatedServers);
|
setServers(updatedServers);
|
||||||
Cookies.set("servers", updatedServers.map((x) => x.join("@")).join(","), {
|
Cookies.set("servers", updatedServers.map((x) => x.join("@")).join(","));
|
||||||
expires: 7,
|
|
||||||
});
|
|
||||||
setNewServer({ name: "", ip: "" });
|
setNewServer({ name: "", ip: "" });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -203,7 +202,7 @@ export default function AppSidebar({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<footer className="mt-auto">
|
<footer className="mt-auto">
|
||||||
<SettingsDialog />
|
<SettingsDialog profile={app.profile as ProfileSettings} />
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -245,6 +244,7 @@ export default function AppSidebar({
|
|||||||
?.avatar_url || ""
|
?.avatar_url || ""
|
||||||
}
|
}
|
||||||
key={id}
|
key={id}
|
||||||
|
app={app}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@@ -258,6 +258,7 @@ export default function AppSidebar({
|
|||||||
id={app.profile?.id || "me"}
|
id={app.profile?.id || "me"}
|
||||||
avatar={app.profile?.avatar_url || ""}
|
avatar={app.profile?.avatar_url || ""}
|
||||||
status="online"
|
status="online"
|
||||||
|
app={app}
|
||||||
settings
|
settings
|
||||||
/>
|
/>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -268,7 +269,7 @@ export default function AppSidebar({
|
|||||||
className="w-full h-full overflow-x-hidden"
|
className="w-full h-full overflow-x-hidden"
|
||||||
onClick={() => app.setSidebarOpen(false)}
|
onClick={() => app.setSidebarOpen(false)}
|
||||||
>
|
>
|
||||||
<div className="overflow-x-hidden min-w-max flex-shrink-0">
|
<div className="overflow-x-hidden min-w-screen flex-shrink-0">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -285,12 +286,14 @@ function DMItem({
|
|||||||
avatar,
|
avatar,
|
||||||
status,
|
status,
|
||||||
settings,
|
settings,
|
||||||
|
app,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
status: string;
|
status: string;
|
||||||
settings?: boolean;
|
settings?: boolean;
|
||||||
|
app: App;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
@@ -306,6 +309,7 @@ function DMItem({
|
|||||||
<SettingsDialog
|
<SettingsDialog
|
||||||
className="ml-auto w-7 h-7 p-0 flex items-center justify-center"
|
className="ml-auto w-7 h-7 p-0 flex items-center justify-center"
|
||||||
tab="profile"
|
tab="profile"
|
||||||
|
profile={app.profile as ProfileSettings}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -334,10 +338,12 @@ function ChannelIcon({ kind }: { kind: "text" | "voice" }) {
|
|||||||
|
|
||||||
function getUser(
|
function getUser(
|
||||||
id: string,
|
id: string,
|
||||||
userList: StringMap<UserProfile>,
|
userList: StringMap<UserProfile | null>,
|
||||||
setUserList: React.Dispatch<React.SetStateAction<StringMap<UserProfile>>>
|
setUserList: React.Dispatch<
|
||||||
|
React.SetStateAction<StringMap<UserProfile | null>>
|
||||||
|
>
|
||||||
) {
|
) {
|
||||||
if (userList[id]) return userList[id];
|
if (userList[id] !== undefined) return userList[id];
|
||||||
|
|
||||||
const onResponse = (res: Response<UserProfile>) => {
|
const onResponse = (res: Response<UserProfile>) => {
|
||||||
setUserList((prev) => {
|
setUserList((prev) => {
|
||||||
@@ -348,5 +354,10 @@ function getUser(
|
|||||||
|
|
||||||
get(`/api/profile/?id=${id}`, onResponse)
|
get(`/api/profile/?id=${id}`, onResponse)
|
||||||
?.then(onResponse)
|
?.then(onResponse)
|
||||||
.catch((e) => {});
|
.catch((e) => {
|
||||||
|
setUserList((prev) => {
|
||||||
|
prev[id] = null;
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Cookies from "js-cookie";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { CardContent, CardHeader, CardTitle } from "../ui/card";
|
import { CardContent, CardHeader, CardTitle } from "../ui/card";
|
||||||
import { Label } from "../ui/label";
|
import { Label } from "../ui/label";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
export default function CreateAccount() {
|
export default function CreateAccount() {
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
@@ -20,7 +21,6 @@ export default function CreateAccount() {
|
|||||||
const [feedback, setFeedback] = useState<
|
const [feedback, setFeedback] = useState<
|
||||||
undefined | { kind: "error" | "info"; message: string }
|
undefined | { kind: "error" | "info"; message: string }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const validateInput = ():
|
const validateInput = ():
|
||||||
@@ -36,7 +36,7 @@ export default function CreateAccount() {
|
|||||||
return {
|
return {
|
||||||
kind: "error",
|
kind: "error",
|
||||||
message:
|
message:
|
||||||
"Username must be 3–20 characters and only letters, numbers, or underscores.",
|
"Username must be 3-20 characters and only letters, numbers, or underscores.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (password.length < 6) {
|
if (password.length < 6) {
|
||||||
@@ -67,11 +67,31 @@ export default function CreateAccount() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await axios.post("/api/user", { username, password, email });
|
const { data } = await axios.post("/api/user", {
|
||||||
Cookies.set("token", (res.data as any).token);
|
username,
|
||||||
|
password,
|
||||||
setFeedback({ kind: "info", message: "Account created! Redirecting..." });
|
email,
|
||||||
setTimeout(() => router.push("/chat"), 1000);
|
});
|
||||||
|
const response = data as {
|
||||||
|
message: string;
|
||||||
|
email_sent: boolean;
|
||||||
|
user_id: string;
|
||||||
|
};
|
||||||
|
if (response.email_sent) {
|
||||||
|
toast.info(
|
||||||
|
"We sent you a verification email. Please verify to continue."
|
||||||
|
);
|
||||||
|
setFeedback({
|
||||||
|
message:
|
||||||
|
"We sent you a verification email. Please verify to continue.",
|
||||||
|
kind: "info",
|
||||||
|
});
|
||||||
|
Cookies.set("verify_temp_email", email);
|
||||||
|
Cookies.set("verify_temp_password", password);
|
||||||
|
router.push(`/verify`);
|
||||||
|
} else {
|
||||||
|
setFeedback({ message: response.message, kind: "error" });
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setFeedback({
|
setFeedback({
|
||||||
kind: "error",
|
kind: "error",
|
||||||
|
|||||||
@@ -6,18 +6,10 @@ import axios from "axios";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Label } from "@/components/ui/label";
|
import { CardContent, CardHeader, CardTitle } from "../ui/card";
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "../ui/card";
|
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [username, setUsername] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [feedback, setFeedback] = useState<
|
const [feedback, setFeedback] = useState<
|
||||||
undefined | { kind: "error" | "info"; message: string }
|
undefined | { kind: "error" | "info"; message: string }
|
||||||
@@ -29,7 +21,7 @@ export default function Login() {
|
|||||||
setFeedback(undefined);
|
setFeedback(undefined);
|
||||||
try {
|
try {
|
||||||
const res = await axios.get("/api/user", {
|
const res = await axios.get("/api/user", {
|
||||||
params: { username, password },
|
params: { email, password },
|
||||||
});
|
});
|
||||||
|
|
||||||
Cookies.set("token", (res.data as any).token);
|
Cookies.set("token", (res.data as any).token);
|
||||||
@@ -57,7 +49,7 @@ export default function Login() {
|
|||||||
<CardContent className="gap-3 flex flex-col">
|
<CardContent className="gap-3 flex flex-col">
|
||||||
<span>
|
<span>
|
||||||
Email
|
Email
|
||||||
<Input onChange={(e) => setUsername(e.target.value)} type="email" />
|
<Input onChange={(e) => setEmail(e.target.value)} type="email" />
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Password
|
Password
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useClientSettings } from "@/hooks/use-settings";
|
|
||||||
import { setClientSettings } from "@/lib/clientSettings";
|
import { setClientSettings } from "@/lib/clientSettings";
|
||||||
import SettingSchema from "./SettingSchema";
|
import SettingSchema from "./SettingSchema";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
@@ -11,19 +10,20 @@ import {
|
|||||||
CLIENT_SCHEMA,
|
CLIENT_SCHEMA,
|
||||||
ProfileSettings,
|
ProfileSettings,
|
||||||
} from "@/types/settings";
|
} from "@/types/settings";
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { useClientSettings } from "@/hooks/use-settings";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function Settings({
|
export default function Settings({
|
||||||
tab,
|
tab,
|
||||||
profileSettings,
|
profileSettings: ps,
|
||||||
setProfileSettings,
|
|
||||||
}: {
|
}: {
|
||||||
tab?: string;
|
tab?: string;
|
||||||
profileSettings: ProfileSettings;
|
profileSettings: ProfileSettings;
|
||||||
setProfileSettings: React.Dispatch<React.SetStateAction<ProfileSettings>>;
|
|
||||||
}) {
|
}) {
|
||||||
const [settings, setSettings] = useClientSettings();
|
const [settings, setSettings] = useClientSettings();
|
||||||
|
const [profileSettings, setProfileSettings] = useState<ProfileSettings>(ps);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Button } from "../ui/button";
|
|||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
@@ -11,18 +10,17 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import Settings from "./Settings";
|
import Settings from "./Settings";
|
||||||
import { ProfileSettings } from "@/types/settings";
|
import { ProfileSettings } from "@/types/settings";
|
||||||
import { useProfileSettings } from "@/hooks/use-settings";
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function SettingsDialog({
|
export default function SettingsDialog({
|
||||||
className,
|
className,
|
||||||
tab,
|
tab,
|
||||||
|
profile,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
tab?: string;
|
tab?: string;
|
||||||
profile?: ProfileSettings;
|
profile: ProfileSettings;
|
||||||
}) {
|
}) {
|
||||||
const [profileSettings, setProfileSettings] = useProfileSettings();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -41,11 +39,7 @@ export default function SettingsDialog({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Settings
|
<Settings tab={tab} profileSettings={profile} />
|
||||||
tab={tab}
|
|
||||||
profileSettings={profileSettings}
|
|
||||||
setProfileSettings={setProfileSettings}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -17,12 +17,6 @@ export default function useUser() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchUser() {
|
async function fetchUser() {
|
||||||
const token = Cookies.get("token");
|
|
||||||
if (!token) {
|
|
||||||
router.push("/login");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setUser((await get("/api/profile")).data);
|
setUser((await get("/api/profile")).data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function useIsMobile() {
|
||||||
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaQuery = window.matchMedia("(max-width: 767px)");
|
||||||
|
const handleChange = () => setIsMobile(mediaQuery.matches);
|
||||||
|
|
||||||
|
// Set initial value
|
||||||
|
handleChange();
|
||||||
|
|
||||||
|
// Listen for changes
|
||||||
|
mediaQuery.addEventListener("change", handleChange);
|
||||||
|
|
||||||
|
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return isMobile;
|
||||||
|
}
|
||||||
@@ -3,16 +3,20 @@ import { useRef, useState } from "react";
|
|||||||
import { useMessages } from "./use-messages";
|
import { useMessages } from "./use-messages";
|
||||||
import { StringMap } from "@/types/typeUtils";
|
import { StringMap } from "@/types/typeUtils";
|
||||||
import useUser, { UserProfile } from "./get-user";
|
import useUser, { UserProfile } from "./get-user";
|
||||||
|
import { useClientSettings } from "./use-settings";
|
||||||
|
|
||||||
export default function useApp(): App {
|
export default function useApp(): App {
|
||||||
const node = useRef<WebSocket | null>(null);
|
const node = useRef<WebSocket | null>(null);
|
||||||
const [profiles, setProfiles] = useState<StringMap<UserProfile>>({});
|
const [profiles, setProfiles] = useState<StringMap<UserProfile | null>>({});
|
||||||
const { messages, addMessage } = useMessages();
|
const { messages, addMessage } = useMessages();
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
const profile = useUser();
|
const profile = useUser();
|
||||||
|
const [clientSettings, setClientSettings] = useClientSettings();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
|
clientSettings,
|
||||||
|
setClientSettings,
|
||||||
profile,
|
profile,
|
||||||
profiles,
|
profiles,
|
||||||
setProfiles,
|
setProfiles,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { defaultSettings, getClientSettings } from "@/lib/clientSettings";
|
import { defaultSettings, getClientSettings } from "@/lib/clientSettings";
|
||||||
import { ProfileSettings, ClientSettings } from "@/types/settings";
|
import { ClientSettings } from "@/types/settings";
|
||||||
import axios from "axios";
|
|
||||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||||
|
|
||||||
export function useClientSettings(): [
|
export function useClientSettings(): [
|
||||||
@@ -17,30 +16,3 @@ export function useClientSettings(): [
|
|||||||
|
|
||||||
return [settings, setSettings];
|
return [settings, setSettings];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useProfileSettings(): [
|
|
||||||
ProfileSettings,
|
|
||||||
Dispatch<SetStateAction<ProfileSettings>>
|
|
||||||
] {
|
|
||||||
const [settings, setSettings] = useState<ProfileSettings>({
|
|
||||||
node_address: "",
|
|
||||||
username: "",
|
|
||||||
display_name: "",
|
|
||||||
avatar_url: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchProfileSettings() {
|
|
||||||
try {
|
|
||||||
const res = await axios.get("/api/profile");
|
|
||||||
setSettings(res.data as ProfileSettings);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to fetch profile settings:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchProfileSettings();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return [settings, setSettings];
|
|
||||||
}
|
|
||||||
|
|||||||
+6
-2
@@ -2,6 +2,7 @@ import { Dispatch, RefObject, SetStateAction } from "react";
|
|||||||
import { StringMap } from "./typeUtils";
|
import { StringMap } from "./typeUtils";
|
||||||
import { UserProfile } from "@/hooks/get-user";
|
import { UserProfile } from "@/hooks/get-user";
|
||||||
import { Message } from "./types";
|
import { Message } from "./types";
|
||||||
|
import { ClientSettings } from "./settings";
|
||||||
|
|
||||||
export type SetState<T> = Dispatch<SetStateAction<T>>;
|
export type SetState<T> = Dispatch<SetStateAction<T>>;
|
||||||
|
|
||||||
@@ -9,8 +10,11 @@ export default interface App {
|
|||||||
node: RefObject<WebSocket | null>;
|
node: RefObject<WebSocket | null>;
|
||||||
profile: UserProfile | null;
|
profile: UserProfile | null;
|
||||||
|
|
||||||
profiles: StringMap<UserProfile>;
|
clientSettings: ClientSettings;
|
||||||
setProfiles: SetState<StringMap<UserProfile>>;
|
setClientSettings: SetState<ClientSettings>;
|
||||||
|
|
||||||
|
profiles: StringMap<UserProfile | null>;
|
||||||
|
setProfiles: SetState<StringMap<UserProfile | null>>;
|
||||||
|
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
addMessage: (v: Message) => void;
|
addMessage: (v: Message) => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user