Add user status management and metadata fields to authentication
- Introduced user status management with the ability to update online, busy, offline, and away statuses. - Added metadata fields for user preferences, including phrase preferences and friends list. - Updated API and database schema to accommodate new user fields. - Enhanced the authentication component to handle additional user data effectively. - Implemented hooks for socket management and OLM setup to improve user experience.
This commit is contained in:
parent
df41cf4657
commit
32168722a2
12 changed files with 633 additions and 127 deletions
46
convex/_generated/api.d.ts
vendored
46
convex/_generated/api.d.ts
vendored
|
|
@ -62,8 +62,16 @@ export declare const components: {
|
||||||
displayUsername?: null | string;
|
displayUsername?: null | string;
|
||||||
email: string;
|
email: string;
|
||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
|
friends?: Array<string>;
|
||||||
image?: null | string;
|
image?: null | string;
|
||||||
|
metadata?: {
|
||||||
|
phrasePreference: "comforting" | "mocking" | "both";
|
||||||
|
};
|
||||||
name: string;
|
name: string;
|
||||||
|
status?: {
|
||||||
|
isUserSet: boolean;
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
};
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
userId?: null | string;
|
userId?: null | string;
|
||||||
username?: null | string;
|
username?: null | string;
|
||||||
|
|
@ -149,6 +157,9 @@ export declare const components: {
|
||||||
| "userId"
|
| "userId"
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
|
| "metadata"
|
||||||
|
| "status"
|
||||||
|
| "friends"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -359,6 +370,9 @@ export declare const components: {
|
||||||
| "userId"
|
| "userId"
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
|
| "metadata"
|
||||||
|
| "status"
|
||||||
|
| "friends"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -640,8 +654,16 @@ export declare const components: {
|
||||||
displayUsername?: null | string;
|
displayUsername?: null | string;
|
||||||
email?: string;
|
email?: string;
|
||||||
emailVerified?: boolean;
|
emailVerified?: boolean;
|
||||||
|
friends?: Array<string>;
|
||||||
image?: null | string;
|
image?: null | string;
|
||||||
|
metadata?: {
|
||||||
|
phrasePreference: "comforting" | "mocking" | "both";
|
||||||
|
};
|
||||||
name?: string;
|
name?: string;
|
||||||
|
status?: {
|
||||||
|
isUserSet: boolean;
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
};
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
userId?: null | string;
|
userId?: null | string;
|
||||||
username?: null | string;
|
username?: null | string;
|
||||||
|
|
@ -658,6 +680,9 @@ export declare const components: {
|
||||||
| "userId"
|
| "userId"
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
|
| "metadata"
|
||||||
|
| "status"
|
||||||
|
| "friends"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -901,8 +926,16 @@ export declare const components: {
|
||||||
displayUsername?: null | string;
|
displayUsername?: null | string;
|
||||||
email?: string;
|
email?: string;
|
||||||
emailVerified?: boolean;
|
emailVerified?: boolean;
|
||||||
|
friends?: Array<string>;
|
||||||
image?: null | string;
|
image?: null | string;
|
||||||
|
metadata?: {
|
||||||
|
phrasePreference: "comforting" | "mocking" | "both";
|
||||||
|
};
|
||||||
name?: string;
|
name?: string;
|
||||||
|
status?: {
|
||||||
|
isUserSet: boolean;
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
};
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
userId?: null | string;
|
userId?: null | string;
|
||||||
username?: null | string;
|
username?: null | string;
|
||||||
|
|
@ -919,6 +952,9 @@ export declare const components: {
|
||||||
| "userId"
|
| "userId"
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
|
| "metadata"
|
||||||
|
| "status"
|
||||||
|
| "friends"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -1164,5 +1200,15 @@ export declare const components: {
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
user: {
|
||||||
|
index: {
|
||||||
|
updateUserStatus: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ isUserSet: boolean; status: string },
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { convex } from "@convex-dev/better-auth/plugins";
|
||||||
import { betterAuth, type BetterAuthOptions } from "better-auth";
|
import { betterAuth, type BetterAuthOptions } from "better-auth";
|
||||||
import { captcha, oneTimeToken, openAPI, username } from "better-auth/plugins";
|
import { captcha, oneTimeToken, openAPI, username } from "better-auth/plugins";
|
||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
|
import { z } from "zod";
|
||||||
import { components } from "./_generated/api";
|
import { components } from "./_generated/api";
|
||||||
import { DataModel } from "./_generated/dataModel";
|
import { DataModel } from "./_generated/dataModel";
|
||||||
import { mutation, query } from "./_generated/server";
|
import { mutation, query } from "./_generated/server";
|
||||||
|
|
@ -22,6 +23,15 @@ export const authComponent = createClient<DataModel, typeof authSchema>(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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<DataModel>) => {
|
export const createAuthOptions = (ctx: GenericCtx<DataModel>) => {
|
||||||
return {
|
return {
|
||||||
baseURL: siteUrl,
|
baseURL: siteUrl,
|
||||||
|
|
@ -31,6 +41,52 @@ export const createAuthOptions = (ctx: GenericCtx<DataModel>) => {
|
||||||
requireEmailVerification: false,
|
requireEmailVerification: false,
|
||||||
autoSignIn: true
|
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: [
|
plugins: [
|
||||||
convex({
|
convex({
|
||||||
authConfig,
|
authConfig,
|
||||||
|
|
@ -100,3 +156,16 @@ export const retrieveServerOlmAccount = query({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
import type * as adapter from "../adapter.js";
|
import type * as adapter from "../adapter.js";
|
||||||
import type * as auth from "../auth.js";
|
import type * as auth from "../auth.js";
|
||||||
import type * as olm_index from "../olm/index.js";
|
import type * as olm_index from "../olm/index.js";
|
||||||
|
import type * as user_index from "../user/index.js";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiFromModules,
|
ApiFromModules,
|
||||||
|
|
@ -23,6 +24,7 @@ const fullApi: ApiFromModules<{
|
||||||
adapter: typeof adapter;
|
adapter: typeof adapter;
|
||||||
auth: typeof auth;
|
auth: typeof auth;
|
||||||
"olm/index": typeof olm_index;
|
"olm/index": typeof olm_index;
|
||||||
|
"user/index": typeof user_index;
|
||||||
}> = anyApi as any;
|
}> = anyApi as any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,16 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
displayUsername?: null | string;
|
displayUsername?: null | string;
|
||||||
email: string;
|
email: string;
|
||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
|
friends?: Array<string>;
|
||||||
image?: null | string;
|
image?: null | string;
|
||||||
|
metadata?: {
|
||||||
|
phrasePreference: "comforting" | "mocking" | "both";
|
||||||
|
};
|
||||||
name: string;
|
name: string;
|
||||||
|
status?: {
|
||||||
|
isUserSet: boolean;
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
};
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
userId?: null | string;
|
userId?: null | string;
|
||||||
username?: null | string;
|
username?: null | string;
|
||||||
|
|
@ -123,6 +131,9 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| "userId"
|
| "userId"
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
|
| "metadata"
|
||||||
|
| "status"
|
||||||
|
| "friends"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -334,6 +345,9 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| "userId"
|
| "userId"
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
|
| "metadata"
|
||||||
|
| "status"
|
||||||
|
| "friends"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -618,8 +632,16 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
displayUsername?: null | string;
|
displayUsername?: null | string;
|
||||||
email?: string;
|
email?: string;
|
||||||
emailVerified?: boolean;
|
emailVerified?: boolean;
|
||||||
|
friends?: Array<string>;
|
||||||
image?: null | string;
|
image?: null | string;
|
||||||
|
metadata?: {
|
||||||
|
phrasePreference: "comforting" | "mocking" | "both";
|
||||||
|
};
|
||||||
name?: string;
|
name?: string;
|
||||||
|
status?: {
|
||||||
|
isUserSet: boolean;
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
};
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
userId?: null | string;
|
userId?: null | string;
|
||||||
username?: null | string;
|
username?: null | string;
|
||||||
|
|
@ -636,6 +658,9 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| "userId"
|
| "userId"
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
|
| "metadata"
|
||||||
|
| "status"
|
||||||
|
| "friends"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -880,8 +905,16 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
displayUsername?: null | string;
|
displayUsername?: null | string;
|
||||||
email?: string;
|
email?: string;
|
||||||
emailVerified?: boolean;
|
emailVerified?: boolean;
|
||||||
|
friends?: Array<string>;
|
||||||
image?: null | string;
|
image?: null | string;
|
||||||
|
metadata?: {
|
||||||
|
phrasePreference: "comforting" | "mocking" | "both";
|
||||||
|
};
|
||||||
name?: string;
|
name?: string;
|
||||||
|
status?: {
|
||||||
|
isUserSet: boolean;
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
};
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
userId?: null | string;
|
userId?: null | string;
|
||||||
username?: null | string;
|
username?: null | string;
|
||||||
|
|
@ -898,6 +931,9 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| "userId"
|
| "userId"
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
|
| "metadata"
|
||||||
|
| "status"
|
||||||
|
| "friends"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -1146,4 +1182,15 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
user: {
|
||||||
|
index: {
|
||||||
|
updateUserStatus: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ isUserSet: boolean; status: string },
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,21 @@ export const tables = {
|
||||||
userId: v.optional(v.union(v.null(), v.string())),
|
userId: v.optional(v.union(v.null(), v.string())),
|
||||||
username: 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())),
|
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("email_name", ["email", "name"])
|
||||||
.index("name", ["name"])
|
.index("name", ["name"])
|
||||||
.index("userId", ["userId"])
|
.index("userId", ["userId"])
|
||||||
.index("username", ["username"]),
|
.index("username", ["username"])
|
||||||
|
.index("status", ["status"])
|
||||||
|
.index("friends", ["friends"]),
|
||||||
session: defineTable({
|
session: defineTable({
|
||||||
expiresAt: v.number(),
|
expiresAt: v.number(),
|
||||||
token: v.string(),
|
token: v.string(),
|
||||||
|
|
|
||||||
28
convex/betterAuth/user/index.ts
Normal file
28
convex/betterAuth/user/index.ts
Normal file
|
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
382
src/app/page.tsx
382
src/app/page.tsx
|
|
@ -1,29 +1,109 @@
|
||||||
"use client"
|
"use client"
|
||||||
import AppSidebar from "@/components/home";
|
import AppSidebar from "@/components/home";
|
||||||
import OlmSetupDialog from "@/components/olm/olm-setup-dialog";
|
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 { Spinner } from "@/components/ui/spinner";
|
||||||
import UserFloatingCard from "@/components/ui/user/floating-card";
|
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 { authClient } from "@/lib/auth/client";
|
||||||
import { checkOlmStatus as checkOlmStatusUtil, handleOlmAccountCreation } from "@/lib/olm";
|
|
||||||
import { useMutation, useQuery } from "convex/react";
|
import { useMutation, useQuery } from "convex/react";
|
||||||
|
import { PlusIcon, SearchIcon, UsersIcon } from "lucide-react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { io, Socket } from "socket.io-client";
|
|
||||||
import { api } from "../../convex/_generated/api";
|
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() {
|
export default function Home() {
|
||||||
const { data, error, isPending } = authClient.useSession();
|
const { data, error, isPending } = authClient.useSession();
|
||||||
const [socketStatus, setSocketStatus] = useState<SiPher.SocketStatus>("connecting");
|
|
||||||
const [socketInfo, setSocketInfo] = useState<SiPher.SocketInfo>({
|
const [page, setPage] = useState<"friends" | "settings">("friends");
|
||||||
ping: null,
|
const [currentChannel, setCurrentChannel] = useState<SiPher.Channel | null>(null);
|
||||||
transport: null,
|
const [openDmChannels, setOpenDmChannels] = useState<SiPher.Channel[] | []>([]);
|
||||||
connectedAt: null,
|
const [availableServers, setAvailableServers] = useState<SiPher.Server[] | []>([]);
|
||||||
socketId: null,
|
|
||||||
serverUrl: null,
|
// Friends page state
|
||||||
error: null
|
const [friendsPage, setFriendsPage] = useState<"all" | "available">("all");
|
||||||
});
|
const [friendsSearch, setFriendsSearch] = useState<string>("");
|
||||||
const [olmStatus, setOlmStatus] = useState<SiPher.OlmStatus>("checking");
|
|
||||||
const [showOlmModal, setShowOlmModal] = useState(false);
|
|
||||||
|
|
||||||
const hasServerOlm = useQuery(
|
const hasServerOlm = useQuery(
|
||||||
api.auth.retrieveServerOlmAccount,
|
api.auth.retrieveServerOlmAccount,
|
||||||
|
|
@ -33,111 +113,25 @@ export default function Home() {
|
||||||
// Mutation for sending keys to server
|
// Mutation for sending keys to server
|
||||||
const sendKeysToServer = useMutation(api.auth.sendKeysToServer);
|
const sendKeysToServer = useMutation(api.auth.sendKeysToServer);
|
||||||
|
|
||||||
|
const updateUserStatus = useMutation(api.auth.updateUserStatus);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const socket: Socket = io({ withCredentials: false });
|
const status = data.user.status
|
||||||
let pingInterval: NodeJS.Timeout | null = null;
|
if (!status) return;
|
||||||
|
|
||||||
// Measure ping latency
|
if (status.status === "offline" && !status.isUserSet) {
|
||||||
const measurePing = () => {
|
updateUserStatus({ status: "online", isUserSet: false });
|
||||||
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<void> {
|
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
}
|
}, [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) {
|
if (isPending) {
|
||||||
return <div className="flex items-center justify-center h-screen w-full bg-background">
|
return <div className="flex items-center justify-center h-screen w-full bg-background">
|
||||||
|
|
@ -149,11 +143,167 @@ export default function Home() {
|
||||||
return redirect(`/auth${error ? `?error=${error.cause}` : "no-data"}`);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserFloatingCard user={data.user} />
|
<UserFloatingCard user={data.user} />
|
||||||
<AppSidebar socketStatus={socketStatus} socketInfo={socketInfo}>
|
<AppSidebar socketStatus={socketStatus} socketInfo={socketInfo}>
|
||||||
<></>
|
<div className="flex flex-col h-full">
|
||||||
|
{/* Header - fixed height and sticky */}
|
||||||
|
<div className="flex items-center min-h-10 max-h-10 border-b border-border/40 sticky top-0 z-10 bg-background">
|
||||||
|
{/* SCS or DM Selector */}
|
||||||
|
<div className="flex justify-center items-center gap-2 max-w-72 min-w-72 border-r h-10 border-border/40">
|
||||||
|
{
|
||||||
|
// If the current channel is none or a DM, we show a search bar
|
||||||
|
!currentChannel || currentChannel.type === SiPher.ChannelType.DM ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-[calc(100%-2rem)] h-3/4 rounded-lg hover:cursor-pointer"
|
||||||
|
>
|
||||||
|
<SearchIcon className="size-4" />
|
||||||
|
<span className="text-sm font-medium">Search for a Server or DM</span>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm font-medium">{currentChannel.name}</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{/* Page title/options */}
|
||||||
|
<div className="flex flex-row justify-start items-center gap-2 w-full">
|
||||||
|
<div className="flex flex-row gap-2 justify-start p-2">
|
||||||
|
<UsersIcon className="size-4" />
|
||||||
|
<span className="text-sm font-medium">Friends</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium">•</span>
|
||||||
|
<div className="flex flex-row gap-2 h-full">
|
||||||
|
<Button variant="ghost" disabled={friendsPage === "available"} className={`h-full hover:cursor-pointer justify-start p-2 ${friendsPage === "available" ? "bg-primary text-primary-foreground" : ""}`} onClick={() => setFriendsPage("available")}>
|
||||||
|
Available
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" disabled={friendsPage === "all"} className={`h-full hover:cursor-pointer justify-start p-2 ${friendsPage === "all" ? "bg-primary text-primary-foreground" : ""}`} onClick={() => setFriendsPage("all")}>
|
||||||
|
All Known
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" className="h-full bg-primary text-primary-foreground hover:cursor-pointer justify-start p-2 ">
|
||||||
|
Add Friend
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Content Area - Channel List + Main Content */}
|
||||||
|
<div className="flex flex-1 overflow-hidden">
|
||||||
|
{/* Channel List */}
|
||||||
|
<div className="flex flex-col shrink-0 max-w-72 min-w-72 border-r border-border/40">
|
||||||
|
{/* Channel List Header - sticky top */}
|
||||||
|
<div className="flex justify-center items-center min-h-10 max-h-50 bg-background">
|
||||||
|
<div className="flex flex-col justify-start items-start p-1 gap-2 w-full">
|
||||||
|
<Button variant="ghost" className="w-full h-full hover:cursor-pointer justify-start" onClick={() => setPage("friends")}>
|
||||||
|
<UsersIcon className="size-4" />
|
||||||
|
<span className="text-sm font-medium">Friends</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-[calc(100%-0.8rem)] h-px bg-border/40 mx-2" />
|
||||||
|
{/* Channel List */}
|
||||||
|
<div className="flex flex-col flex-1 overflow-y-auto">
|
||||||
|
{/* Channel List Item */}
|
||||||
|
<div className="flex items-center min-h-10 max-h-10">
|
||||||
|
{
|
||||||
|
currentChannel && currentChannel.type === SiPher.ChannelType.DM || !currentChannel ? (
|
||||||
|
<div className="flex flex-col items-center min-h-10 max-h-10 w-full">
|
||||||
|
<div className="flex items-center w-full justify-between p-2 select-none">
|
||||||
|
<span className="text-xs font-semibold text-muted-foreground">Direct Messages</span>
|
||||||
|
<Button variant="ghost" size="icon-sm" className="hover:cursor-pointer hover:bg-transparent!">
|
||||||
|
<PlusIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
openDmChannels.length > 0 ? (
|
||||||
|
openDmChannels.map((channel) => (
|
||||||
|
<Button variant="ghost" size="icon-sm" className="hover:cursor-pointer hover:bg-transparent!">
|
||||||
|
<span className="text-sm font-medium">{channel.name}</span>
|
||||||
|
</Button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center min-h-10 max-h-10">
|
||||||
|
<span className="text-xs font-medium text-muted-foreground text-center text-wrap">
|
||||||
|
{getRandomPhrase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center min-h-10 max-h-10">
|
||||||
|
<span className="text-sm font-medium">No channels</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex flex-col flex-1 overflow-hidden">
|
||||||
|
{
|
||||||
|
page === "friends" ? (
|
||||||
|
<div className="flex flex-col flex-1 overflow-y-auto p-4">
|
||||||
|
<div className="flex flex-col items-center min-h-10 max-h-10">
|
||||||
|
<Input
|
||||||
|
placeholder="Search for a friend..."
|
||||||
|
value={friendsSearch}
|
||||||
|
onChange={(e) => setFriendsSearch(e.target.value)}
|
||||||
|
className="w-full min-h-10 sticky top-0"
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
friendsPage === "all" ? (
|
||||||
|
<div className="flex items-center min-h-10 max-h-10">
|
||||||
|
<span className="text-sm font-medium">All Friends</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-start w-full p-2 gap-2 pt-4">
|
||||||
|
<span className="text-sm text-start font-medium">Available Friends • {data.user.friends && data.user.friends.length > 0 ? data.user.friends.length : 0}</span>
|
||||||
|
{
|
||||||
|
data.user.friends && data.user.friends.length > 0 ? (
|
||||||
|
data.user.friends.map((friend) => (
|
||||||
|
<div className="flex items-center min-h-10 max-h-10">
|
||||||
|
<span className="text-sm font-medium">{friend}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
{getRandomPhrase()}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : page === "settings" ? (
|
||||||
|
<div className="flex flex-col flex-1 overflow-y-auto p-4">
|
||||||
|
<div className="flex items-center min-h-10 max-h-10">
|
||||||
|
<span className="text-sm font-medium">Servers</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</AppSidebar>
|
</AppSidebar>
|
||||||
|
|
||||||
{/* OLM Account Setup/Sync Modal */}
|
{/* OLM Account Setup/Sync Modal */}
|
||||||
|
|
@ -161,7 +311,7 @@ export default function Home() {
|
||||||
open={showOlmModal}
|
open={showOlmModal}
|
||||||
onOpenChange={setShowOlmModal}
|
onOpenChange={setShowOlmModal}
|
||||||
olmStatus={olmStatus}
|
olmStatus={olmStatus}
|
||||||
onCreateAccount={handleCreateOlmAccount}
|
onCreateAccount={handleCreateAccount}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,9 @@ export default function OlmSetupDialog({
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
<DialogFooter className="gap-2">
|
<DialogFooter className="gap-2">
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
Import Keys from Backup
|
||||||
|
</Button>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -6,36 +6,35 @@ import {
|
||||||
GearSix,
|
GearSix,
|
||||||
MicrophoneSlash
|
MicrophoneSlash
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import { User } from "better-auth";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "../avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "../avatar";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "../hover-card";
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from "../hover-card";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip";
|
||||||
|
|
||||||
type UserStatus = "online" | "idle" | "dnd" | "offline";
|
type UserStatus = "online" | "busy" | "offline" | "away";
|
||||||
|
|
||||||
interface UserFloatingCardProps {
|
interface UserFloatingCardProps {
|
||||||
user: User;
|
user: any; // Too lazy to type the user type
|
||||||
status?: UserStatus;
|
status?: UserStatus;
|
||||||
activity?: string;
|
activity?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusColors: Record<UserStatus, string> = {
|
const statusColors: Record<UserStatus, string> = {
|
||||||
online: "bg-emerald-500",
|
online: "bg-emerald-500",
|
||||||
idle: "bg-amber-500",
|
busy: "bg-amber-500",
|
||||||
dnd: "bg-red-500",
|
away: "bg-yellow-500",
|
||||||
offline: "bg-muted-foreground"
|
offline: "bg-muted-foreground"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function UserFloatingCard({
|
export default function UserFloatingCard({
|
||||||
user,
|
user,
|
||||||
status = "online",
|
|
||||||
activity
|
|
||||||
}: UserFloatingCardProps) {
|
}: UserFloatingCardProps) {
|
||||||
const [cardOpen, setCardOpen] = useState(false);
|
const [cardOpen, setCardOpen] = useState(false);
|
||||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const status = user.status?.status;
|
||||||
|
const activity = user.status?.activity;
|
||||||
|
|
||||||
// Close when clicking outside the trigger/content
|
// Close when clicking outside the trigger/content
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -114,7 +113,7 @@ export default function UserFloatingCard({
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute -bottom-0.5 -right-0.5 size-3.5 rounded-full border-[3px] border-secondary",
|
"absolute -bottom-0.5 -right-0.5 size-3.5 rounded-full border-[3px] border-secondary",
|
||||||
statusColors[status]
|
status ? statusColors[status as UserStatus] : "bg-muted-foreground"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
59
src/hooks/use-olm-setup.ts
Normal file
59
src/hooks/use-olm-setup.ts
Normal file
|
|
@ -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<SiPher.OlmStatus>("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<void> => {
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
91
src/hooks/use-socket.ts
Normal file
91
src/hooks/use-socket.ts
Normal file
|
|
@ -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<SiPher.SocketStatus>("connecting");
|
||||||
|
const [socketInfo, setSocketInfo] = useState<SiPher.SocketInfo>({
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import { convexClient } from "@convex-dev/better-auth/client/plugins";
|
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 { createAuthClient } from "better-auth/react";
|
||||||
|
import { auth } from "../../../convex/betterAuth/auth";
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
plugins: [
|
plugins: [
|
||||||
convexClient(),
|
convexClient(),
|
||||||
usernameClient(),
|
usernameClient(),
|
||||||
oneTimeTokenClient()
|
oneTimeTokenClient(),
|
||||||
|
inferAdditionalFields<typeof auth>()
|
||||||
],
|
],
|
||||||
sessionOptions: {
|
sessionOptions: {
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue