diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 84da040..b17e83b 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -62,8 +62,16 @@ export declare const components: { displayUsername?: null | string; email: string; emailVerified: boolean; + friends?: Array; image?: null | string; + metadata?: { + phrasePreference: "comforting" | "mocking" | "both"; + }; name: string; + status?: { + isUserSet: boolean; + status: "online" | "busy" | "offline" | "away"; + }; updatedAt: number; userId?: null | string; username?: null | string; @@ -149,6 +157,9 @@ export declare const components: { | "userId" | "username" | "displayUsername" + | "metadata" + | "status" + | "friends" | "_id"; operator?: | "lt" @@ -359,6 +370,9 @@ export declare const components: { | "userId" | "username" | "displayUsername" + | "metadata" + | "status" + | "friends" | "_id"; operator?: | "lt" @@ -640,8 +654,16 @@ export declare const components: { displayUsername?: null | string; email?: string; emailVerified?: boolean; + friends?: Array; image?: null | string; + metadata?: { + phrasePreference: "comforting" | "mocking" | "both"; + }; name?: string; + status?: { + isUserSet: boolean; + status: "online" | "busy" | "offline" | "away"; + }; updatedAt?: number; userId?: null | string; username?: null | string; @@ -658,6 +680,9 @@ export declare const components: { | "userId" | "username" | "displayUsername" + | "metadata" + | "status" + | "friends" | "_id"; operator?: | "lt" @@ -901,8 +926,16 @@ export declare const components: { displayUsername?: null | string; email?: string; emailVerified?: boolean; + friends?: Array; image?: null | string; + metadata?: { + phrasePreference: "comforting" | "mocking" | "both"; + }; name?: string; + status?: { + isUserSet: boolean; + status: "online" | "busy" | "offline" | "away"; + }; updatedAt?: number; userId?: null | string; username?: null | string; @@ -919,6 +952,9 @@ export declare const components: { | "userId" | "username" | "displayUsername" + | "metadata" + | "status" + | "friends" | "_id"; operator?: | "lt" @@ -1164,5 +1200,15 @@ export declare const components: { >; }; }; + user: { + index: { + updateUserStatus: FunctionReference< + "mutation", + "internal", + { isUserSet: boolean; status: string }, + any + >; + }; + }; }; }; diff --git a/convex/auth.ts b/convex/auth.ts index 0a02f8b..0aa79ec 100644 --- a/convex/auth.ts +++ b/convex/auth.ts @@ -3,6 +3,7 @@ import { convex } from "@convex-dev/better-auth/plugins"; import { betterAuth, type BetterAuthOptions } from "better-auth"; import { captcha, oneTimeToken, openAPI, username } from "better-auth/plugins"; import { v } from "convex/values"; +import { z } from "zod"; import { components } from "./_generated/api"; import { DataModel } from "./_generated/dataModel"; import { mutation, query } from "./_generated/server"; @@ -22,6 +23,15 @@ export const authComponent = createClient( } ); +const metadataSchema = z.object({ + phrasePreference: z.enum(["comforting", "mocking", "both"]), +}) + +const statusSchema = z.object({ + status: z.enum(["online", "busy", "offline", "away"]), + isUserSet: z.boolean(), +}); + export const createAuthOptions = (ctx: GenericCtx) => { return { baseURL: siteUrl, @@ -31,6 +41,52 @@ export const createAuthOptions = (ctx: GenericCtx) => { requireEmailVerification: false, autoSignIn: true }, + user: { + additionalFields: { + metadata: { + type: "json", + defaultValue: () => { + const metadata = metadataSchema.parse({ + phrasePreference: "comforting", + }) + + return metadata.phrasePreference; + }, + required: false, + }, + friends: { + type: "string[]", + defaultValue: [], + required: false, + index: true + }, + status: { + type: "json", + defaultValue: () => { + return { + status: "offline", + isUserSet: false, + } + }, + required: false, + index: true, + transform: { + input: (status) => { + return statusSchema.safeParse(status).success ? status : { + status: "offline", + isUserSet: false, + }; + }, + output: (status) => { + return statusSchema.safeParse(status).success ? status : { + status: "offline", + isUserSet: false, + }; + } + } + } + }, + }, plugins: [ convex({ authConfig, @@ -99,4 +155,17 @@ export const retrieveServerOlmAccount = query({ userId: args.userId, }); }, +}); + +export const updateUserStatus = mutation({ + args: { + status: v.string(), + isUserSet: v.boolean(), + }, + handler: async (ctx, args) => { + return ctx.runMutation(components.betterAuth.user.index.updateUserStatus, { + status: args.status, + isUserSet: args.isUserSet, + }); + }, }); \ No newline at end of file diff --git a/convex/betterAuth/_generated/api.ts b/convex/betterAuth/_generated/api.ts index 09121a7..fd792d3 100644 --- a/convex/betterAuth/_generated/api.ts +++ b/convex/betterAuth/_generated/api.ts @@ -11,6 +11,7 @@ import type * as adapter from "../adapter.js"; import type * as auth from "../auth.js"; import type * as olm_index from "../olm/index.js"; +import type * as user_index from "../user/index.js"; import type { ApiFromModules, @@ -23,6 +24,7 @@ const fullApi: ApiFromModules<{ adapter: typeof adapter; auth: typeof auth; "olm/index": typeof olm_index; + "user/index": typeof user_index; }> = anyApi as any; /** diff --git a/convex/betterAuth/_generated/component.ts b/convex/betterAuth/_generated/component.ts index e048b38..8ba446b 100644 --- a/convex/betterAuth/_generated/component.ts +++ b/convex/betterAuth/_generated/component.ts @@ -35,8 +35,16 @@ export type ComponentApi = displayUsername?: null | string; email: string; emailVerified: boolean; + friends?: Array; image?: null | string; + metadata?: { + phrasePreference: "comforting" | "mocking" | "both"; + }; name: string; + status?: { + isUserSet: boolean; + status: "online" | "busy" | "offline" | "away"; + }; updatedAt: number; userId?: null | string; username?: null | string; @@ -123,6 +131,9 @@ export type ComponentApi = | "userId" | "username" | "displayUsername" + | "metadata" + | "status" + | "friends" | "_id"; operator?: | "lt" @@ -334,6 +345,9 @@ export type ComponentApi = | "userId" | "username" | "displayUsername" + | "metadata" + | "status" + | "friends" | "_id"; operator?: | "lt" @@ -618,8 +632,16 @@ export type ComponentApi = displayUsername?: null | string; email?: string; emailVerified?: boolean; + friends?: Array; image?: null | string; + metadata?: { + phrasePreference: "comforting" | "mocking" | "both"; + }; name?: string; + status?: { + isUserSet: boolean; + status: "online" | "busy" | "offline" | "away"; + }; updatedAt?: number; userId?: null | string; username?: null | string; @@ -636,6 +658,9 @@ export type ComponentApi = | "userId" | "username" | "displayUsername" + | "metadata" + | "status" + | "friends" | "_id"; operator?: | "lt" @@ -880,8 +905,16 @@ export type ComponentApi = displayUsername?: null | string; email?: string; emailVerified?: boolean; + friends?: Array; image?: null | string; + metadata?: { + phrasePreference: "comforting" | "mocking" | "both"; + }; name?: string; + status?: { + isUserSet: boolean; + status: "online" | "busy" | "offline" | "away"; + }; updatedAt?: number; userId?: null | string; username?: null | string; @@ -898,6 +931,9 @@ export type ComponentApi = | "userId" | "username" | "displayUsername" + | "metadata" + | "status" + | "friends" | "_id"; operator?: | "lt" @@ -1146,4 +1182,15 @@ export type ComponentApi = >; }; }; + user: { + index: { + updateUserStatus: FunctionReference< + "mutation", + "internal", + { isUserSet: boolean; status: string }, + any, + Name + >; + }; + }; }; diff --git a/convex/betterAuth/schema.ts b/convex/betterAuth/schema.ts index dd32843..c444520 100644 --- a/convex/betterAuth/schema.ts +++ b/convex/betterAuth/schema.ts @@ -16,11 +16,21 @@ export const tables = { userId: v.optional(v.union(v.null(), v.string())), username: v.optional(v.union(v.null(), v.string())), displayUsername: v.optional(v.union(v.null(), v.string())), + metadata: v.optional(v.object({ + phrasePreference: v.union(v.literal("comforting"), v.literal("mocking"), v.literal("both")), + })), + status: v.optional(v.object({ + status: v.union(v.literal("online"), v.literal("busy"), v.literal("offline"), v.literal("away")), + isUserSet: v.boolean(), + })), + friends: v.optional(v.array(v.string())), }) .index("email_name", ["email", "name"]) .index("name", ["name"]) .index("userId", ["userId"]) - .index("username", ["username"]), + .index("username", ["username"]) + .index("status", ["status"]) + .index("friends", ["friends"]), session: defineTable({ expiresAt: v.number(), token: v.string(), diff --git a/convex/betterAuth/user/index.ts b/convex/betterAuth/user/index.ts new file mode 100644 index 0000000..3ad2f3d --- /dev/null +++ b/convex/betterAuth/user/index.ts @@ -0,0 +1,28 @@ +import { v } from "convex/values"; +import { Id } from "../../_generated/dataModel"; +import { mutation } from "../../_generated/server"; + +export const updateUserStatus = mutation({ + args: { + status: v.string(), + isUserSet: v.boolean(), + }, + handler: async (ctx, args) => { + const user = await ctx.auth.getUserIdentity(); + if (!user) { + throw new Error("User not found"); + } + + const userId = ctx.db.normalizeId("user", user.subject as string) as Id<"user">; + if (!userId) { + throw new Error("User not found"); + } + + return ctx.db.patch<"user">("user", userId, { + status: { + status: args.status, + isUserSet: args.isUserSet, + }, + }); + }, +}); \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 20ceab7..13a67b3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,29 +1,109 @@ "use client" import AppSidebar from "@/components/home"; import OlmSetupDialog from "@/components/olm/olm-setup-dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/spinner"; import UserFloatingCard from "@/components/ui/user/floating-card"; +import { useOlmSetup } from "@/hooks/use-olm-setup"; +import { useSocket } from "@/hooks/use-socket"; import { authClient } from "@/lib/auth/client"; -import { checkOlmStatus as checkOlmStatusUtil, handleOlmAccountCreation } from "@/lib/olm"; import { useMutation, useQuery } from "convex/react"; +import { PlusIcon, SearchIcon, UsersIcon } from "lucide-react"; import { redirect } from "next/navigation"; -import { useEffect, useState } from "react"; -import { io, Socket } from "socket.io-client"; +import { useCallback, useEffect, useState } from "react"; import { api } from "../../convex/_generated/api"; +const mockPhrases = [ + "No bitches? Womp womp", + "You're all alone", + "No friends? Damn", + "Oh look, a spiderweb!", + "You must be bored, go make some friends", + "DMs drier than the Sahara", + "Echo echo... anyone there?", + "Your inbox called, it's collecting dust", + "Even the bots won't slide in", + "Social life on life support", + "Crickets in the chat", + "Zero notifications? Skill issue", + "This is the quietest room on the internet", + "Go outside, the graphics are better", + "Loneliness speedrun any%", + "Your DMs look like a ghost town", + "Population: You", + "Unread messages: 0 (forever)", + "Bro really out here talking to himself", + "The void stares back", + "Touch grass detected: false", + "Friends list looking minimalist", + "Inbox so empty it has an echo", + "No one loves you... yet", + "Slide into someone's DMs instead of staring at none", +]; + +const comfortingPhrases = [ + "Quiet inbox today—just a little peace and quiet", + "Empty DMs mean more time for you", + "Even when it's silent here, you're never truly alone", + "Sometimes the best company is your own thoughts", + "Take a deep breath—this calm won't last forever", + "Your worth isn't measured by notifications", + "The right people will show up exactly when they're meant to", + "God is with you in the silence, just like always", + "'Be still, and know that I am God' – Psalm 46:10", + "An empty inbox is just a blank page waiting for new stories", + "Enjoy the quiet while it lasts—life gets loud again soon", + "You're building strength in these quiet moments", + "Real connections can't be rushed; they're coming", + "In the stillness, you can hear your own heart clearest", + "'I am with you always' – Matthew 28:20", + "No rush—good things take time", + "This is your moment to recharge without distractions", + "Loneliness is temporary; connection is inevitable", + "God's presence fills every empty space", + "Silence isn't empty—it's full of possibility", + "You're exactly where you need to be right now", + "The best conversations often start after a little quiet", + "Peaceful DMs = a peaceful mind", + "Don't worry, someone is thinking of you right now", + "You're not alone, we're all here for you", + "Trust the process, even if it's slow and painful", + "Someone out there is thinking of messaging you... any second now", + "You're loved more than you know, messages or not", + "Silence is a rare gift in such a noisy world", + "No notifications means no demands on your energy today", + "God is working behind the scenes on your behalf", + "Your value exists completely outside of this app", + "Take this moment to simply be, rather than do", + "The right message will arrive at the perfect time", + "You are safe, loved, and held in this silence", + "Let the quiet wash over you like a gentle wave", + "He knows the desires of your heart—have faith", + "A quiet screen is just an invitation to look up", + "True connection starts with being comfortable within yourself", + "Your soul needs this rest more than a quick reply", + "Someone, somewhere, is grateful that you exist today", + "Prayers travel much further than any direct message can", + "You are preserving your peace for something better", + "God's timing is rarely early, but never late", + "Use this time to love yourself a little harder", + "The world is loud, but your space is peaceful", + "You don't need a buzz in your pocket to matter", + "Rest easy, the right people are finding their way" +]; + export default function Home() { const { data, error, isPending } = authClient.useSession(); - const [socketStatus, setSocketStatus] = useState("connecting"); - const [socketInfo, setSocketInfo] = useState({ - ping: null, - transport: null, - connectedAt: null, - socketId: null, - serverUrl: null, - error: null - }); - const [olmStatus, setOlmStatus] = useState("checking"); - const [showOlmModal, setShowOlmModal] = useState(false); + + const [page, setPage] = useState<"friends" | "settings">("friends"); + const [currentChannel, setCurrentChannel] = useState(null); + const [openDmChannels, setOpenDmChannels] = useState([]); + const [availableServers, setAvailableServers] = useState([]); + + // Friends page state + const [friendsPage, setFriendsPage] = useState<"all" | "available">("all"); + const [friendsSearch, setFriendsSearch] = useState(""); const hasServerOlm = useQuery( api.auth.retrieveServerOlmAccount, @@ -33,111 +113,25 @@ export default function Home() { // Mutation for sending keys to server const sendKeysToServer = useMutation(api.auth.sendKeysToServer); + const updateUserStatus = useMutation(api.auth.updateUserStatus); useEffect(() => { if (!data) return; - const socket: Socket = io({ withCredentials: false }); - let pingInterval: NodeJS.Timeout | null = null; + const status = data.user.status + if (!status) return; - // Measure ping latency - const measurePing = () => { - const start = Date.now(); - socket.volatile.emit("ping", () => { - const latency = Date.now() - start; - setSocketInfo((prev: SiPher.SocketInfo) => ({ ...prev, ping: latency })); - }); - }; - - socket.on("connect", () => { - console.log("✅ Connected to socket - Authentication successful!"); - setSocketStatus("connected"); - setSocketInfo((prev: SiPher.SocketInfo) => ({ - ...prev, - connectedAt: Date.now(), - socketId: socket.id || null, - serverUrl: window.location.origin, - transport: socket.io.engine?.transport?.name || "unknown", - error: null - })); - - // Start ping measurement every 5 seconds - measurePing(); - pingInterval = setInterval(measurePing, 5000); - }); - - // Update transport when it upgrades (polling -> websocket) - socket.io.engine?.on("upgrade", (transport) => { - setSocketInfo((prev: SiPher.SocketInfo) => ({ ...prev, transport: transport.name })); - }); - - socket.on("connect_error", (err) => { - console.error("❌ Socket connection error:", err.message); - setSocketStatus("error"); - setSocketInfo((prev: SiPher.SocketInfo) => ({ - ...prev, - error: err.message, - ping: null, - connectedAt: null, - socketId: null - })); - }); - - socket.on("disconnect", (reason) => { - console.log("🔌 Disconnected from socket:", reason); - setSocketStatus("disconnected"); - setSocketInfo((prev: SiPher.SocketInfo) => ({ - ...prev, - ping: null, - connectedAt: null, - error: reason - })); - if (pingInterval) clearInterval(pingInterval); - }); - - // Handle pong response for ping measurement - socket.on("pong", () => { - // Handled in measurePing callback - }); - - return () => { - if (pingInterval) clearInterval(pingInterval); - socket.disconnect(); - }; - }, [data]); - - useEffect(() => { - if (!data || hasServerOlm === undefined) return; - - const checkStatus = async () => { - const status = await checkOlmStatusUtil(data.user.id, hasServerOlm); - setOlmStatus(status); - - if (status === "not_setup" || status === "mismatched") { - setShowOlmModal(true); - } - }; - - checkStatus(); - }, [data, hasServerOlm]); - - async function handleCreateOlmAccount(password: string): Promise { - if (!data || !password.trim()) return; - - setOlmStatus("creating"); - const success = await handleOlmAccountCreation( - data.user.id, - password, - sendKeysToServer, - olmStatus === "mismatched" - ); - - if (success) { - setOlmStatus("synced"); - setShowOlmModal(false); - } else { - setOlmStatus("not_setup"); + if (status.status === "offline" && !status.isUserSet) { + updateUserStatus({ status: "online", isUserSet: false }); } - } + }, [data?.user?.id, updateUserStatus, data?.user?.status]); + + // Custom hooks for socket and OLM management + const { socketStatus, socketInfo } = useSocket(data?.user?.id); + const { olmStatus, showOlmModal, setShowOlmModal, handleCreateAccount } = useOlmSetup({ + userId: data?.user?.id, + hasServerOlm, + sendKeysToServer + }); if (isPending) { return
@@ -149,11 +143,167 @@ export default function Home() { return redirect(`/auth${error ? `?error=${error.cause}` : "no-data"}`); } + + const getRandomPhrase = useCallback(() => { + const phrases = { + comforting: comfortingPhrases, + mocking: mockPhrases, + both: [...comfortingPhrases, ...mockPhrases] + } + + const preference = data.user.metadata?.phrasePreference as keyof typeof phrases; + + if (!preference) return comfortingPhrases[Math.floor(Math.random() * comfortingPhrases.length)]; + + return phrases[preference][Math.floor(Math.random() * phrases[preference].length)]; + }, [data.user.metadata?.phrasePreference]); + return ( <> - <> +
+ {/* Header - fixed height and sticky */} +
+ {/* SCS or DM Selector */} +
+ { + // If the current channel is none or a DM, we show a search bar + !currentChannel || currentChannel.type === SiPher.ChannelType.DM ? ( + + ) : ( + {currentChannel.name} + ) + } +
+ {/* Page title/options */} +
+
+ + Friends +
+ +
+ + + +
+
+
+ {/* Content Area - Channel List + Main Content */} +
+ {/* Channel List */} +
+ {/* Channel List Header - sticky top */} +
+
+ +
+
+
+ {/* Channel List */} +
+ {/* Channel List Item */} +
+ { + currentChannel && currentChannel.type === SiPher.ChannelType.DM || !currentChannel ? ( +
+
+ Direct Messages + +
+ { + openDmChannels.length > 0 ? ( + openDmChannels.map((channel) => ( + + )) + ) : ( +
+ + {getRandomPhrase()} + +
+ ) + } +
+ ) : ( +
+ No channels +
+ ) + } +
+
+
+ + {/* Main Content */} +
+ { + page === "friends" ? ( +
+
+ setFriendsSearch(e.target.value)} + className="w-full min-h-10 sticky top-0" + /> + { + friendsPage === "all" ? ( +
+ All Friends +
+ ) : ( +
+ Available Friends • {data.user.friends && data.user.friends.length > 0 ? data.user.friends.length : 0} + { + data.user.friends && data.user.friends.length > 0 ? ( + data.user.friends.map((friend) => ( +
+ {friend} +
+ )) + ) : ( + + {getRandomPhrase()} + + ) + } +
+ ) + } +
+
+ ) : page === "settings" ? ( +
+
+ Servers +
+
+ ) : null + } +
+
+
{/* OLM Account Setup/Sync Modal */} @@ -161,7 +311,7 @@ export default function Home() { open={showOlmModal} onOpenChange={setShowOlmModal} olmStatus={olmStatus} - onCreateAccount={handleCreateOlmAccount} + onCreateAccount={handleCreateAccount} /> ) diff --git a/src/components/olm/olm-setup-dialog.tsx b/src/components/olm/olm-setup-dialog.tsx index bede4cd..d05cdbf 100644 --- a/src/components/olm/olm-setup-dialog.tsx +++ b/src/components/olm/olm-setup-dialog.tsx @@ -95,6 +95,9 @@ export default function OlmSetupDialog({ onKeyDown={handleKeyDown} /> + diff --git a/src/components/ui/user/floating-card.tsx b/src/components/ui/user/floating-card.tsx index 7603328..8369d9b 100644 --- a/src/components/ui/user/floating-card.tsx +++ b/src/components/ui/user/floating-card.tsx @@ -6,36 +6,35 @@ import { GearSix, MicrophoneSlash } from "@phosphor-icons/react"; -import { User } from "better-auth"; import { useEffect, useRef, useState } from "react"; import { Avatar, AvatarFallback, AvatarImage } from "../avatar"; import { Button } from "../button"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "../hover-card"; import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip"; -type UserStatus = "online" | "idle" | "dnd" | "offline"; +type UserStatus = "online" | "busy" | "offline" | "away"; interface UserFloatingCardProps { - user: User; + user: any; // Too lazy to type the user type status?: UserStatus; activity?: string; } const statusColors: Record = { online: "bg-emerald-500", - idle: "bg-amber-500", - dnd: "bg-red-500", + busy: "bg-amber-500", + away: "bg-yellow-500", offline: "bg-muted-foreground" }; export default function UserFloatingCard({ user, - status = "online", - activity }: UserFloatingCardProps) { const [cardOpen, setCardOpen] = useState(false); const triggerRef = useRef(null); const contentRef = useRef(null); + const status = user.status?.status; + const activity = user.status?.activity; // Close when clicking outside the trigger/content useEffect(() => { @@ -114,7 +113,7 @@ export default function UserFloatingCard({
diff --git a/src/hooks/use-olm-setup.ts b/src/hooks/use-olm-setup.ts new file mode 100644 index 0000000..a39adb2 --- /dev/null +++ b/src/hooks/use-olm-setup.ts @@ -0,0 +1,59 @@ +"use client" + +import { checkOlmStatus, handleOlmAccountCreation, SendKeysToServerFn } from "@/lib/olm"; +import { useEffect, useState } from "react"; + +interface UseOlmSetupOptions { + userId: string | undefined; + hasServerOlm: boolean | undefined; + sendKeysToServer: SendKeysToServerFn; +} + +export function useOlmSetup({ userId, hasServerOlm, sendKeysToServer }: UseOlmSetupOptions) { + const [olmStatus, setOlmStatus] = useState("checking"); + const [showOlmModal, setShowOlmModal] = useState(false); + + // Check OLM status when user data and server status are available + useEffect(() => { + if (!userId || hasServerOlm === undefined) return; + + const checkStatus = async () => { + const status = await checkOlmStatus(userId, hasServerOlm); + setOlmStatus(status); + + if (status === "not_setup" || status === "mismatched") { + setShowOlmModal(true); + } + }; + + checkStatus(); + }, [userId, hasServerOlm]); + + // Handle OLM account creation + const handleCreateAccount = async (password: string): Promise => { + if (!userId || !password.trim()) return; + + setOlmStatus("creating"); + const success = await handleOlmAccountCreation( + userId, + password, + sendKeysToServer, + olmStatus === "mismatched" + ); + + if (success) { + setOlmStatus("synced"); + setShowOlmModal(false); + } else { + setOlmStatus("not_setup"); + } + }; + + return { + olmStatus, + showOlmModal, + setShowOlmModal, + handleCreateAccount + }; +} + diff --git a/src/hooks/use-socket.ts b/src/hooks/use-socket.ts new file mode 100644 index 0000000..c88e7ef --- /dev/null +++ b/src/hooks/use-socket.ts @@ -0,0 +1,91 @@ +"use client" + +import { useEffect, useState } from "react"; +import { io, Socket } from "socket.io-client"; + +export function useSocket(userId: string | undefined) { + const [socketStatus, setSocketStatus] = useState("connecting"); + const [socketInfo, setSocketInfo] = useState({ + ping: null, + transport: null, + connectedAt: null, + socketId: null, + serverUrl: null, + error: null + }); + + useEffect(() => { + if (!userId) return; + + const socket: Socket = io({ withCredentials: false }); + let pingInterval: NodeJS.Timeout | null = null; + + // Measure ping latency + const measurePing = () => { + const start = Date.now(); + socket.volatile.emit("ping", () => { + const latency = Date.now() - start; + setSocketInfo((prev: SiPher.SocketInfo) => ({ ...prev, ping: latency })); + }); + }; + + socket.on("connect", () => { + console.log("✅ Connected to socket - Authentication successful!"); + setSocketStatus("connected"); + setSocketInfo((prev: SiPher.SocketInfo) => ({ + ...prev, + connectedAt: Date.now(), + socketId: socket.id || null, + serverUrl: window.location.origin, + transport: socket.io.engine?.transport?.name || "unknown", + error: null + })); + + // Start ping measurement every 5 seconds + measurePing(); + pingInterval = setInterval(measurePing, 5000); + }); + + // Update transport when it upgrades (polling -> websocket) + socket.io.engine?.on("upgrade", (transport) => { + setSocketInfo((prev: SiPher.SocketInfo) => ({ ...prev, transport: transport.name })); + }); + + socket.on("connect_error", (err) => { + console.error("❌ Socket connection error:", err.message); + setSocketStatus("error"); + setSocketInfo((prev: SiPher.SocketInfo) => ({ + ...prev, + error: err.message, + ping: null, + connectedAt: null, + socketId: null + })); + }); + + socket.on("disconnect", (reason) => { + console.log("🔌 Disconnected from socket:", reason); + setSocketStatus("disconnected"); + setSocketInfo((prev: SiPher.SocketInfo) => ({ + ...prev, + ping: null, + connectedAt: null, + error: reason + })); + if (pingInterval) clearInterval(pingInterval); + }); + + // Handle pong response for ping measurement + socket.on("pong", () => { + // Handled in measurePing callback + }); + + return () => { + if (pingInterval) clearInterval(pingInterval); + socket.disconnect(); + }; + }, [userId]); + + return { socketStatus, socketInfo }; +} + diff --git a/src/lib/auth/client.ts b/src/lib/auth/client.ts index 3d5d4fb..c2c057a 100644 --- a/src/lib/auth/client.ts +++ b/src/lib/auth/client.ts @@ -1,12 +1,14 @@ import { convexClient } from "@convex-dev/better-auth/client/plugins"; -import { oneTimeTokenClient, usernameClient } from "better-auth/client/plugins"; +import { inferAdditionalFields, oneTimeTokenClient, usernameClient } from "better-auth/client/plugins"; import { createAuthClient } from "better-auth/react"; +import { auth } from "../../../convex/betterAuth/auth"; export const authClient = createAuthClient({ plugins: [ convexClient(), usernameClient(), - oneTimeTokenClient() + oneTimeTokenClient(), + inferAdditionalFields() ], sessionOptions: { refetchOnWindowFocus: false,