Merge pull request #22 from kleo-dev/target-node

Target node
This commit is contained in:
Klesti Selimaj
2025-10-17 16:06:26 +02:00
committed by GitHub
8 changed files with 131 additions and 77 deletions
+18 -19
View File
@@ -71,33 +71,32 @@ export async function POST(req: NextRequest) {
*/
export async function GET(req: NextRequest) {
const url = new URL(req.url);
const query_id = url.searchParams.get("id");
const id = url.searchParams.get("id");
const username = url.searchParams.get("username");
const token = req.cookies.get("token")?.value;
if (!query_id && !token)
if (!token && !id && !username)
return NextResponse.json(
{ message: "Query arg should be token or id" },
{ message: "Query arg should be token or id or username" },
{ status: StatusCodes.BAD_REQUEST }
);
let user_id = query_id;
if (!user_id && token) {
const {
data: { user },
} = await supabase.auth.getUser(token);
user_id = user?.id ?? null;
}
if (!user_id)
return NextResponse.json(
{ message: "Invalid token" },
{ status: StatusCodes.BAD_REQUEST }
);
const { data: profile, error } = await supabase
const { data: profile, error } = id
? await supabase
.from("profiles")
.select("id, username, display_name, avatar_url, node_address")
.eq("id", user_id)
.eq("id", id)
.maybeSingle()
: username
? await supabase
.from("profiles")
.select("id, username, display_name, avatar_url, node_address")
.eq("username", username)
.maybeSingle()
: await supabase
.from("profiles")
.select("id, username, display_name, avatar_url, node_address")
.eq("id", (await supabase.auth.getUser(token)).data.user?.id)
.maybeSingle();
if (error)
+21 -1
View File
@@ -4,10 +4,30 @@ import MessageBox from "@/components/MessageBox";
import AppSidebar from "@/components/Sidebar";
import { useParams } from "next/navigation";
import useApp from "@/hooks/use-app";
import { useEffect, useRef } from "react";
import auth from "@/lib/auth";
export default function DMs() {
const { id } = useParams<{ id: string }>();
const app = useApp();
const targetNode = useRef<WebSocket | null>(null);
useEffect(() => {
async function connectTargetNode() {
const target = await app.getUserById(id);
if (target) {
auth(
target.node_address,
targetNode,
() => {},
app.addMessage,
() => {}
);
}
}
connectTargetNode().then(() => {});
}, [id]);
return (
<AppSidebar app={app}>
@@ -20,7 +40,7 @@ export default function DMs() {
(m.from === id && m.channel_id === app.profile?.id)
)}
sendMessage={(message) => {
app.node.current?.send(
targetNode.current?.send(
JSON.stringify({
type: "send_message",
params: {
+48 -40
View File
@@ -37,6 +37,7 @@ import { get, Response } from "@/lib/request";
import { cn } from "@/lib/utils";
import App from "@/types/app";
import { ProfileSettings } from "@/types/settings";
import axios from "axios";
export default function AppSidebar({
children,
@@ -53,7 +54,9 @@ export default function AppSidebar({
}>) {
const [servers, setServers] = useState<[string, string][]>([]);
const [newServer, setNewServer] = useState({ name: "", ip: "" });
const [query, setQuery] = useState("");
const router = useRouter();
const [filteredDms, setFilteredDms] = useState<UserProfile[]>([]);
const dms = Array.from(
new Set(
@@ -63,6 +66,21 @@ export default function AppSidebar({
)
);
useEffect(() => {
const load = async () => {
const users = await Promise.all(dms.map((id) => app.getUserById(id)));
setFilteredDms(
users.filter(
(user) =>
user !== null &&
(user?.username.toLowerCase().includes(query.toLowerCase()) ||
user?.id.toLowerCase().includes(query.toLowerCase()))
) as UserProfile[]
);
};
load();
}, [dms]);
useEffect(() => {
const s = Cookies.get("servers")?.split(",");
if (s) setServers(s.map((x) => x.trim().split("@") as [string, string]));
@@ -213,12 +231,16 @@ export default function AppSidebar({
<h2>{server.name}</h2>
</div>
) : (
<div className="p-3 border-b flex items-center gap-2">
<Search className="h-4 w-4 text-muted-foreground" />
<div className="p-3 border-b flex items-center">
<div className="relative w-full">
<Input
placeholder="Find or start a conversation"
className="h-8"
className="pr-8 h-8"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<Search className="absolute right-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
</div>
</div>
)}
<ScrollArea className="flex-1">
@@ -231,22 +253,34 @@ export default function AppSidebar({
</>
) : (
<>
{dms.map((id) => (
{filteredDms.map((user) => (
<DMItem
name={
getUser(id, app.profiles, app.setProfiles)
?.display_name || id
}
id={id}
name={user.display_name}
id={user.id}
status="online"
avatar={
getUser(id, app.profiles, app.setProfiles)
?.avatar_url || ""
}
key={id}
avatar={user.avatar_url}
key={user.id}
app={app}
/>
))}
{filteredDms.length === 0 && query.trim() !== "" ? (
<div className="px-3 py-2">
<Button
variant="ghost"
className="w-full justify-start gap-2 text-sm"
onClick={async () => {
const response = await axios.get("/api/profile", {
params: { username: query },
});
const user = response.data as UserProfile;
router.push(`/chat/${user.id}`);
}}
>
<Plus className="h-4 w-4" />
Start new DM with “{query}”
</Button>
</div>
) : null}
</>
)}
</div>
@@ -335,29 +369,3 @@ function ChannelIcon({ kind }: { kind: "text" | "voice" }) {
return null;
}
}
function getUser(
id: string,
userList: StringMap<UserProfile | null>,
setUserList: React.Dispatch<
React.SetStateAction<StringMap<UserProfile | null>>
>
) {
if (userList[id] !== undefined) return userList[id];
const onResponse = (res: Response<UserProfile>) => {
setUserList((prev) => {
prev[id] = res.data as UserProfile;
return prev;
});
};
get(`/api/profile/?id=${id}`, onResponse)
?.then(onResponse)
.catch((e) => {
setUserList((prev) => {
prev[id] = null;
return prev;
});
});
}
+2
View File
@@ -58,6 +58,8 @@ export default function CreateAccount() {
};
const signup = async () => {
localStorage.removeItem("chat-messages");
localStorage.removeItem("server-messages");
setFeedback(undefined);
const validationError = validateInput();
if (validationError) {
+2
View File
@@ -18,6 +18,8 @@ export default function Login() {
const router = useRouter();
const login = async () => {
localStorage.removeItem("chat-messages");
localStorage.removeItem("server-messages");
setFeedback(undefined);
try {
const res = await axios.get("/api/user", {
+19
View File
@@ -4,6 +4,7 @@ import { useMessages } from "./use-messages";
import { StringMap } from "@/types/typeUtils";
import useUser, { UserProfile } from "./get-user";
import { useClientSettings } from "./use-settings";
import { get, Response } from "@/lib/request";
export default function useApp(): App {
const node = useRef<WebSocket | null>(null);
@@ -24,5 +25,23 @@ export default function useApp(): App {
addMessage,
sidebarOpen,
setSidebarOpen,
getUserById: async (id: string) => {
if (profiles[id] !== undefined) return profiles[id];
const onResponse = (res: Response<UserProfile>) => {
setProfiles((prev) => {
prev[id] = res.data as UserProfile;
return prev;
});
};
try {
const user = await get(`/api/profile/?id=${id}`, onResponse);
onResponse(user);
return user.data;
} catch {
return null;
}
},
};
}
+13 -11
View File
@@ -1,19 +1,21 @@
import * as React from "react"
import * as React from "react";
const MOBILE_BREAKPOINT = 768
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile
return !!isMobile;
}
+2
View File
@@ -21,4 +21,6 @@ export default interface App {
sidebarOpen: boolean;
setSidebarOpen: SetState<boolean>;
getUserById: (userId: string) => Promise<UserProfile | null>;
}