Add friend request functionality and user status updates
- Implemented a modal for sending and managing friend requests, allowing users to send, accept, decline, or ignore requests. - Enhanced user status management by integrating real-time updates for online, busy, offline, and away statuses. - Updated the API and database schema to support new friend request and user status features. - Improved socket management for better connection handling and user experience. - Refactored UI components to accommodate new functionalities while maintaining consistency.
This commit is contained in:
parent
5198a12f9e
commit
45301ac52b
26 changed files with 2860 additions and 332 deletions
1
bun.lock
1
bun.lock
|
|
@ -4,6 +4,7 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "sipher",
|
"name": "sipher",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@better-fetch/fetch": "^1.1.21",
|
||||||
"@convex-dev/better-auth": "^0.10.4",
|
"@convex-dev/better-auth": "^0.10.4",
|
||||||
"@marsidev/react-turnstile": "^1.4.0",
|
"@marsidev/react-turnstile": "^1.4.0",
|
||||||
"@matrix-org/olm": "^3.2.15",
|
"@matrix-org/olm": "^3.2.15",
|
||||||
|
|
|
||||||
889
convex/_generated/api.d.ts
vendored
889
convex/_generated/api.d.ts
vendored
|
|
@ -62,22 +62,77 @@ 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?: {
|
metadata?: {
|
||||||
phrasePreference: "comforting" | "mocking" | "both";
|
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;
|
||||||
};
|
};
|
||||||
model: "user";
|
model: "user";
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
isUserSet: boolean;
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
updatedAt: number;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
model: "userStatus";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
acceptedAt?: number;
|
||||||
|
createdAt: number;
|
||||||
|
declinedAt?: number;
|
||||||
|
expiresAt?: number;
|
||||||
|
ignoredAt?: number;
|
||||||
|
method: "receive" | "send";
|
||||||
|
requestId: string;
|
||||||
|
requestTo: string;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
model: "friendRequests";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: { createdAt: number; friendId: string; userId: string };
|
||||||
|
model: "friends";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
attachments?: Array<string>;
|
||||||
|
authorId: string;
|
||||||
|
channelId: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
createdTimestamp: number;
|
||||||
|
editedAt?: string;
|
||||||
|
guildId?: string;
|
||||||
|
id: string;
|
||||||
|
inGuild?: boolean;
|
||||||
|
nonce?: string;
|
||||||
|
position?: number;
|
||||||
|
referencedMessage?: null | string | string | string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
model: "messages";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
contentType: string;
|
||||||
|
description: null | string;
|
||||||
|
ephemeral: boolean;
|
||||||
|
height?: number;
|
||||||
|
id: string;
|
||||||
|
size: number;
|
||||||
|
spoiler: boolean;
|
||||||
|
url: string;
|
||||||
|
width?: number;
|
||||||
|
};
|
||||||
|
model: "attachments";
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
data: {
|
data: {
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
|
|
@ -158,8 +213,176 @@ export declare const components: {
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "userStatus";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "userId"
|
||||||
| "status"
|
| "status"
|
||||||
| "friends"
|
| "isUserSet"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "friendRequests";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "userId"
|
||||||
|
| "requestTo"
|
||||||
|
| "method"
|
||||||
|
| "requestId"
|
||||||
|
| "createdAt"
|
||||||
|
| "expiresAt"
|
||||||
|
| "acceptedAt"
|
||||||
|
| "declinedAt"
|
||||||
|
| "ignoredAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "friends";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field: "userId" | "friendId" | "createdAt" | "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "messages";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "inGuild"
|
||||||
|
| "attachments"
|
||||||
|
| "authorId"
|
||||||
|
| "channelId"
|
||||||
|
| "content"
|
||||||
|
| "createdAt"
|
||||||
|
| "createdTimestamp"
|
||||||
|
| "editedAt"
|
||||||
|
| "guildId"
|
||||||
|
| "id"
|
||||||
|
| "nonce"
|
||||||
|
| "position"
|
||||||
|
| "referencedMessage"
|
||||||
|
| "url"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "attachments";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "contentType"
|
||||||
|
| "description"
|
||||||
|
| "ephemeral"
|
||||||
|
| "height"
|
||||||
|
| "width"
|
||||||
|
| "id"
|
||||||
|
| "size"
|
||||||
|
| "spoiler"
|
||||||
|
| "url"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -371,8 +594,176 @@ export declare const components: {
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "userStatus";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "userId"
|
||||||
| "status"
|
| "status"
|
||||||
| "friends"
|
| "isUserSet"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "friendRequests";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "userId"
|
||||||
|
| "requestTo"
|
||||||
|
| "method"
|
||||||
|
| "requestId"
|
||||||
|
| "createdAt"
|
||||||
|
| "expiresAt"
|
||||||
|
| "acceptedAt"
|
||||||
|
| "declinedAt"
|
||||||
|
| "ignoredAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "friends";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field: "userId" | "friendId" | "createdAt" | "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "messages";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "inGuild"
|
||||||
|
| "attachments"
|
||||||
|
| "authorId"
|
||||||
|
| "channelId"
|
||||||
|
| "content"
|
||||||
|
| "createdAt"
|
||||||
|
| "createdTimestamp"
|
||||||
|
| "editedAt"
|
||||||
|
| "guildId"
|
||||||
|
| "id"
|
||||||
|
| "nonce"
|
||||||
|
| "position"
|
||||||
|
| "referencedMessage"
|
||||||
|
| "url"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "attachments";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "contentType"
|
||||||
|
| "description"
|
||||||
|
| "ephemeral"
|
||||||
|
| "height"
|
||||||
|
| "width"
|
||||||
|
| "id"
|
||||||
|
| "size"
|
||||||
|
| "spoiler"
|
||||||
|
| "url"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -563,6 +954,11 @@ export declare const components: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
model:
|
model:
|
||||||
| "user"
|
| "user"
|
||||||
|
| "userStatus"
|
||||||
|
| "friendRequests"
|
||||||
|
| "friends"
|
||||||
|
| "messages"
|
||||||
|
| "attachments"
|
||||||
| "session"
|
| "session"
|
||||||
| "account"
|
| "account"
|
||||||
| "verification"
|
| "verification"
|
||||||
|
|
@ -610,6 +1006,11 @@ export declare const components: {
|
||||||
{
|
{
|
||||||
model:
|
model:
|
||||||
| "user"
|
| "user"
|
||||||
|
| "userStatus"
|
||||||
|
| "friendRequests"
|
||||||
|
| "friends"
|
||||||
|
| "messages"
|
||||||
|
| "attachments"
|
||||||
| "session"
|
| "session"
|
||||||
| "account"
|
| "account"
|
||||||
| "verification"
|
| "verification"
|
||||||
|
|
@ -654,16 +1055,11 @@ 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?: {
|
metadata?: {
|
||||||
phrasePreference: "comforting" | "mocking" | "both";
|
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;
|
||||||
|
|
@ -681,8 +1077,225 @@ export declare const components: {
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "userStatus";
|
||||||
|
update: {
|
||||||
|
isUserSet?: boolean;
|
||||||
|
status?: "online" | "busy" | "offline" | "away";
|
||||||
|
updatedAt?: number;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "userId"
|
||||||
| "status"
|
| "status"
|
||||||
| "friends"
|
| "isUserSet"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "friendRequests";
|
||||||
|
update: {
|
||||||
|
acceptedAt?: number;
|
||||||
|
createdAt?: number;
|
||||||
|
declinedAt?: number;
|
||||||
|
expiresAt?: number;
|
||||||
|
ignoredAt?: number;
|
||||||
|
method?: "receive" | "send";
|
||||||
|
requestId?: string;
|
||||||
|
requestTo?: string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "userId"
|
||||||
|
| "requestTo"
|
||||||
|
| "method"
|
||||||
|
| "requestId"
|
||||||
|
| "createdAt"
|
||||||
|
| "expiresAt"
|
||||||
|
| "acceptedAt"
|
||||||
|
| "declinedAt"
|
||||||
|
| "ignoredAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "friends";
|
||||||
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
|
friendId?: string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field: "userId" | "friendId" | "createdAt" | "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "messages";
|
||||||
|
update: {
|
||||||
|
attachments?: Array<string>;
|
||||||
|
authorId?: string;
|
||||||
|
channelId?: string;
|
||||||
|
content?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
createdTimestamp?: number;
|
||||||
|
editedAt?: string;
|
||||||
|
guildId?: string;
|
||||||
|
id?: string;
|
||||||
|
inGuild?: boolean;
|
||||||
|
nonce?: string;
|
||||||
|
position?: number;
|
||||||
|
referencedMessage?: null | string | string | string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "inGuild"
|
||||||
|
| "attachments"
|
||||||
|
| "authorId"
|
||||||
|
| "channelId"
|
||||||
|
| "content"
|
||||||
|
| "createdAt"
|
||||||
|
| "createdTimestamp"
|
||||||
|
| "editedAt"
|
||||||
|
| "guildId"
|
||||||
|
| "id"
|
||||||
|
| "nonce"
|
||||||
|
| "position"
|
||||||
|
| "referencedMessage"
|
||||||
|
| "url"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "attachments";
|
||||||
|
update: {
|
||||||
|
contentType?: string;
|
||||||
|
description?: null | string;
|
||||||
|
ephemeral?: boolean;
|
||||||
|
height?: number;
|
||||||
|
id?: string;
|
||||||
|
size?: number;
|
||||||
|
spoiler?: boolean;
|
||||||
|
url?: string;
|
||||||
|
width?: number;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "contentType"
|
||||||
|
| "description"
|
||||||
|
| "ephemeral"
|
||||||
|
| "height"
|
||||||
|
| "width"
|
||||||
|
| "id"
|
||||||
|
| "size"
|
||||||
|
| "spoiler"
|
||||||
|
| "url"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -926,16 +1539,11 @@ 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?: {
|
metadata?: {
|
||||||
phrasePreference: "comforting" | "mocking" | "both";
|
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;
|
||||||
|
|
@ -953,8 +1561,225 @@ export declare const components: {
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "userStatus";
|
||||||
|
update: {
|
||||||
|
isUserSet?: boolean;
|
||||||
|
status?: "online" | "busy" | "offline" | "away";
|
||||||
|
updatedAt?: number;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "userId"
|
||||||
| "status"
|
| "status"
|
||||||
| "friends"
|
| "isUserSet"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "friendRequests";
|
||||||
|
update: {
|
||||||
|
acceptedAt?: number;
|
||||||
|
createdAt?: number;
|
||||||
|
declinedAt?: number;
|
||||||
|
expiresAt?: number;
|
||||||
|
ignoredAt?: number;
|
||||||
|
method?: "receive" | "send";
|
||||||
|
requestId?: string;
|
||||||
|
requestTo?: string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "userId"
|
||||||
|
| "requestTo"
|
||||||
|
| "method"
|
||||||
|
| "requestId"
|
||||||
|
| "createdAt"
|
||||||
|
| "expiresAt"
|
||||||
|
| "acceptedAt"
|
||||||
|
| "declinedAt"
|
||||||
|
| "ignoredAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "friends";
|
||||||
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
|
friendId?: string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field: "userId" | "friendId" | "createdAt" | "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "messages";
|
||||||
|
update: {
|
||||||
|
attachments?: Array<string>;
|
||||||
|
authorId?: string;
|
||||||
|
channelId?: string;
|
||||||
|
content?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
createdTimestamp?: number;
|
||||||
|
editedAt?: string;
|
||||||
|
guildId?: string;
|
||||||
|
id?: string;
|
||||||
|
inGuild?: boolean;
|
||||||
|
nonce?: string;
|
||||||
|
position?: number;
|
||||||
|
referencedMessage?: null | string | string | string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "inGuild"
|
||||||
|
| "attachments"
|
||||||
|
| "authorId"
|
||||||
|
| "channelId"
|
||||||
|
| "content"
|
||||||
|
| "createdAt"
|
||||||
|
| "createdTimestamp"
|
||||||
|
| "editedAt"
|
||||||
|
| "guildId"
|
||||||
|
| "id"
|
||||||
|
| "nonce"
|
||||||
|
| "position"
|
||||||
|
| "referencedMessage"
|
||||||
|
| "url"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "attachments";
|
||||||
|
update: {
|
||||||
|
contentType?: string;
|
||||||
|
description?: null | string;
|
||||||
|
ephemeral?: boolean;
|
||||||
|
height?: number;
|
||||||
|
id?: string;
|
||||||
|
size?: number;
|
||||||
|
spoiler?: boolean;
|
||||||
|
url?: string;
|
||||||
|
width?: number;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "contentType"
|
||||||
|
| "description"
|
||||||
|
| "ephemeral"
|
||||||
|
| "height"
|
||||||
|
| "width"
|
||||||
|
| "id"
|
||||||
|
| "size"
|
||||||
|
| "spoiler"
|
||||||
|
| "url"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -1202,10 +2027,34 @@ export declare const components: {
|
||||||
};
|
};
|
||||||
user: {
|
user: {
|
||||||
index: {
|
index: {
|
||||||
|
answerFriendRequest: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ answer: "accept" | "decline" | "ignore"; requestId: string },
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
getFriendRequests: FunctionReference<"query", "internal", any, any>;
|
||||||
|
getFriends: FunctionReference<"query", "internal", any, any>;
|
||||||
|
getUserStatus: FunctionReference<"query", "internal", any, any>;
|
||||||
|
sendFriendRequest: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ username: string },
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
updateUserMetadata: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ metadata: { phrasePreference: "comforting" | "mocking" | "both" } },
|
||||||
|
any
|
||||||
|
>;
|
||||||
updateUserStatus: FunctionReference<
|
updateUserStatus: FunctionReference<
|
||||||
"mutation",
|
"mutation",
|
||||||
"internal",
|
"internal",
|
||||||
{ isUserSet: boolean; status: string },
|
{
|
||||||
|
isUserSet: boolean;
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
},
|
||||||
any
|
any
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
105
convex/auth.ts
105
convex/auth.ts
|
|
@ -3,7 +3,6 @@ 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";
|
||||||
|
|
@ -23,15 +22,6 @@ 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,
|
||||||
|
|
@ -45,45 +35,12 @@ export const createAuthOptions = (ctx: GenericCtx<DataModel>) => {
|
||||||
additionalFields: {
|
additionalFields: {
|
||||||
metadata: {
|
metadata: {
|
||||||
type: "json",
|
type: "json",
|
||||||
defaultValue: () => {
|
|
||||||
const metadata = metadataSchema.parse({
|
|
||||||
phrasePreference: "comforting",
|
|
||||||
})
|
|
||||||
|
|
||||||
return metadata.phrasePreference;
|
|
||||||
},
|
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
friends: {
|
friends: {
|
||||||
type: "string[]",
|
type: "string[]",
|
||||||
defaultValue: [],
|
|
||||||
required: false,
|
required: false,
|
||||||
index: true
|
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -103,7 +60,7 @@ export const createAuthOptions = (ctx: GenericCtx<DataModel>) => {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
oneTimeToken(),
|
oneTimeToken(),
|
||||||
openAPI()
|
openAPI(),
|
||||||
],
|
],
|
||||||
} satisfies BetterAuthOptions;
|
} satisfies BetterAuthOptions;
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +116,7 @@ export const retrieveServerOlmAccount = query({
|
||||||
|
|
||||||
export const updateUserStatus = mutation({
|
export const updateUserStatus = mutation({
|
||||||
args: {
|
args: {
|
||||||
status: v.string(),
|
status: v.union(v.literal("online"), v.literal("busy"), v.literal("offline"), v.literal("away")),
|
||||||
isUserSet: v.boolean(),
|
isUserSet: v.boolean(),
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
|
|
@ -169,3 +126,61 @@ export const updateUserStatus = mutation({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const updateUserMetadata = mutation({
|
||||||
|
args: {
|
||||||
|
metadata: v.object({
|
||||||
|
phrasePreference: v.union(v.literal("comforting"), v.literal("mocking"), v.literal("both")),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
return ctx.runMutation(components.betterAuth.user.index.updateUserMetadata, {
|
||||||
|
metadata: args.metadata,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sendFriendRequest = mutation({
|
||||||
|
args: {
|
||||||
|
username: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
return ctx.runMutation(components.betterAuth.user.index.sendFriendRequest, {
|
||||||
|
username: args.username,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const answerFriendRequest = mutation({
|
||||||
|
args: {
|
||||||
|
requestId: v.string(),
|
||||||
|
answer: v.union(v.literal("accept"), v.literal("decline"), v.literal("ignore")),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
return ctx.runMutation(components.betterAuth.user.index.answerFriendRequest, {
|
||||||
|
requestId: args.requestId,
|
||||||
|
answer: args.answer,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getFriendRequests = query({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx) => {
|
||||||
|
return ctx.runQuery(components.betterAuth.user.index.getFriendRequests)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getFriends = query({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx) => {
|
||||||
|
return ctx.runQuery(components.betterAuth.user.index.getFriends)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getUserStatus = query({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx) => {
|
||||||
|
return ctx.runQuery(components.betterAuth.user.index.getUserStatus)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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 schemas_user from "../schemas/user.js";
|
||||||
import type * as user_index from "../user/index.js";
|
import type * as user_index from "../user/index.js";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -24,6 +25,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;
|
||||||
|
"schemas/user": typeof schemas_user;
|
||||||
"user/index": typeof user_index;
|
"user/index": typeof user_index;
|
||||||
}> = anyApi as any;
|
}> = anyApi as any;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,22 +35,77 @@ 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?: {
|
metadata?: {
|
||||||
phrasePreference: "comforting" | "mocking" | "both";
|
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;
|
||||||
};
|
};
|
||||||
model: "user";
|
model: "user";
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
isUserSet: boolean;
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
updatedAt: number;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
model: "userStatus";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
acceptedAt?: number;
|
||||||
|
createdAt: number;
|
||||||
|
declinedAt?: number;
|
||||||
|
expiresAt?: number;
|
||||||
|
ignoredAt?: number;
|
||||||
|
method: "receive" | "send";
|
||||||
|
requestId: string;
|
||||||
|
requestTo: string;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
model: "friendRequests";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: { createdAt: number; friendId: string; userId: string };
|
||||||
|
model: "friends";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
attachments?: Array<string>;
|
||||||
|
authorId: string;
|
||||||
|
channelId: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
createdTimestamp: number;
|
||||||
|
editedAt?: string;
|
||||||
|
guildId?: string;
|
||||||
|
id: string;
|
||||||
|
inGuild?: boolean;
|
||||||
|
nonce?: string;
|
||||||
|
position?: number;
|
||||||
|
referencedMessage?: null | string | string | string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
model: "messages";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
contentType: string;
|
||||||
|
description: null | string;
|
||||||
|
ephemeral: boolean;
|
||||||
|
height?: number;
|
||||||
|
id: string;
|
||||||
|
size: number;
|
||||||
|
spoiler: boolean;
|
||||||
|
url: string;
|
||||||
|
width?: number;
|
||||||
|
};
|
||||||
|
model: "attachments";
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
data: {
|
data: {
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
|
|
@ -132,8 +187,176 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "userStatus";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "userId"
|
||||||
| "status"
|
| "status"
|
||||||
| "friends"
|
| "isUserSet"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "friendRequests";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "userId"
|
||||||
|
| "requestTo"
|
||||||
|
| "method"
|
||||||
|
| "requestId"
|
||||||
|
| "createdAt"
|
||||||
|
| "expiresAt"
|
||||||
|
| "acceptedAt"
|
||||||
|
| "declinedAt"
|
||||||
|
| "ignoredAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "friends";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field: "userId" | "friendId" | "createdAt" | "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "messages";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "inGuild"
|
||||||
|
| "attachments"
|
||||||
|
| "authorId"
|
||||||
|
| "channelId"
|
||||||
|
| "content"
|
||||||
|
| "createdAt"
|
||||||
|
| "createdTimestamp"
|
||||||
|
| "editedAt"
|
||||||
|
| "guildId"
|
||||||
|
| "id"
|
||||||
|
| "nonce"
|
||||||
|
| "position"
|
||||||
|
| "referencedMessage"
|
||||||
|
| "url"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "attachments";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "contentType"
|
||||||
|
| "description"
|
||||||
|
| "ephemeral"
|
||||||
|
| "height"
|
||||||
|
| "width"
|
||||||
|
| "id"
|
||||||
|
| "size"
|
||||||
|
| "spoiler"
|
||||||
|
| "url"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -346,8 +569,176 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "userStatus";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "userId"
|
||||||
| "status"
|
| "status"
|
||||||
| "friends"
|
| "isUserSet"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "friendRequests";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "userId"
|
||||||
|
| "requestTo"
|
||||||
|
| "method"
|
||||||
|
| "requestId"
|
||||||
|
| "createdAt"
|
||||||
|
| "expiresAt"
|
||||||
|
| "acceptedAt"
|
||||||
|
| "declinedAt"
|
||||||
|
| "ignoredAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "friends";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field: "userId" | "friendId" | "createdAt" | "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "messages";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "inGuild"
|
||||||
|
| "attachments"
|
||||||
|
| "authorId"
|
||||||
|
| "channelId"
|
||||||
|
| "content"
|
||||||
|
| "createdAt"
|
||||||
|
| "createdTimestamp"
|
||||||
|
| "editedAt"
|
||||||
|
| "guildId"
|
||||||
|
| "id"
|
||||||
|
| "nonce"
|
||||||
|
| "position"
|
||||||
|
| "referencedMessage"
|
||||||
|
| "url"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "attachments";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "contentType"
|
||||||
|
| "description"
|
||||||
|
| "ephemeral"
|
||||||
|
| "height"
|
||||||
|
| "width"
|
||||||
|
| "id"
|
||||||
|
| "size"
|
||||||
|
| "spoiler"
|
||||||
|
| "url"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -539,6 +930,11 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
limit?: number;
|
limit?: number;
|
||||||
model:
|
model:
|
||||||
| "user"
|
| "user"
|
||||||
|
| "userStatus"
|
||||||
|
| "friendRequests"
|
||||||
|
| "friends"
|
||||||
|
| "messages"
|
||||||
|
| "attachments"
|
||||||
| "session"
|
| "session"
|
||||||
| "account"
|
| "account"
|
||||||
| "verification"
|
| "verification"
|
||||||
|
|
@ -587,6 +983,11 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
{
|
{
|
||||||
model:
|
model:
|
||||||
| "user"
|
| "user"
|
||||||
|
| "userStatus"
|
||||||
|
| "friendRequests"
|
||||||
|
| "friends"
|
||||||
|
| "messages"
|
||||||
|
| "attachments"
|
||||||
| "session"
|
| "session"
|
||||||
| "account"
|
| "account"
|
||||||
| "verification"
|
| "verification"
|
||||||
|
|
@ -632,16 +1033,11 @@ 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?: {
|
metadata?: {
|
||||||
phrasePreference: "comforting" | "mocking" | "both";
|
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;
|
||||||
|
|
@ -659,8 +1055,225 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "userStatus";
|
||||||
|
update: {
|
||||||
|
isUserSet?: boolean;
|
||||||
|
status?: "online" | "busy" | "offline" | "away";
|
||||||
|
updatedAt?: number;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "userId"
|
||||||
| "status"
|
| "status"
|
||||||
| "friends"
|
| "isUserSet"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "friendRequests";
|
||||||
|
update: {
|
||||||
|
acceptedAt?: number;
|
||||||
|
createdAt?: number;
|
||||||
|
declinedAt?: number;
|
||||||
|
expiresAt?: number;
|
||||||
|
ignoredAt?: number;
|
||||||
|
method?: "receive" | "send";
|
||||||
|
requestId?: string;
|
||||||
|
requestTo?: string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "userId"
|
||||||
|
| "requestTo"
|
||||||
|
| "method"
|
||||||
|
| "requestId"
|
||||||
|
| "createdAt"
|
||||||
|
| "expiresAt"
|
||||||
|
| "acceptedAt"
|
||||||
|
| "declinedAt"
|
||||||
|
| "ignoredAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "friends";
|
||||||
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
|
friendId?: string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field: "userId" | "friendId" | "createdAt" | "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "messages";
|
||||||
|
update: {
|
||||||
|
attachments?: Array<string>;
|
||||||
|
authorId?: string;
|
||||||
|
channelId?: string;
|
||||||
|
content?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
createdTimestamp?: number;
|
||||||
|
editedAt?: string;
|
||||||
|
guildId?: string;
|
||||||
|
id?: string;
|
||||||
|
inGuild?: boolean;
|
||||||
|
nonce?: string;
|
||||||
|
position?: number;
|
||||||
|
referencedMessage?: null | string | string | string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "inGuild"
|
||||||
|
| "attachments"
|
||||||
|
| "authorId"
|
||||||
|
| "channelId"
|
||||||
|
| "content"
|
||||||
|
| "createdAt"
|
||||||
|
| "createdTimestamp"
|
||||||
|
| "editedAt"
|
||||||
|
| "guildId"
|
||||||
|
| "id"
|
||||||
|
| "nonce"
|
||||||
|
| "position"
|
||||||
|
| "referencedMessage"
|
||||||
|
| "url"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "attachments";
|
||||||
|
update: {
|
||||||
|
contentType?: string;
|
||||||
|
description?: null | string;
|
||||||
|
ephemeral?: boolean;
|
||||||
|
height?: number;
|
||||||
|
id?: string;
|
||||||
|
size?: number;
|
||||||
|
spoiler?: boolean;
|
||||||
|
url?: string;
|
||||||
|
width?: number;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "contentType"
|
||||||
|
| "description"
|
||||||
|
| "ephemeral"
|
||||||
|
| "height"
|
||||||
|
| "width"
|
||||||
|
| "id"
|
||||||
|
| "size"
|
||||||
|
| "spoiler"
|
||||||
|
| "url"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -905,16 +1518,11 @@ 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?: {
|
metadata?: {
|
||||||
phrasePreference: "comforting" | "mocking" | "both";
|
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;
|
||||||
|
|
@ -932,8 +1540,225 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "userStatus";
|
||||||
|
update: {
|
||||||
|
isUserSet?: boolean;
|
||||||
|
status?: "online" | "busy" | "offline" | "away";
|
||||||
|
updatedAt?: number;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "userId"
|
||||||
| "status"
|
| "status"
|
||||||
| "friends"
|
| "isUserSet"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "friendRequests";
|
||||||
|
update: {
|
||||||
|
acceptedAt?: number;
|
||||||
|
createdAt?: number;
|
||||||
|
declinedAt?: number;
|
||||||
|
expiresAt?: number;
|
||||||
|
ignoredAt?: number;
|
||||||
|
method?: "receive" | "send";
|
||||||
|
requestId?: string;
|
||||||
|
requestTo?: string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "userId"
|
||||||
|
| "requestTo"
|
||||||
|
| "method"
|
||||||
|
| "requestId"
|
||||||
|
| "createdAt"
|
||||||
|
| "expiresAt"
|
||||||
|
| "acceptedAt"
|
||||||
|
| "declinedAt"
|
||||||
|
| "ignoredAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "friends";
|
||||||
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
|
friendId?: string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field: "userId" | "friendId" | "createdAt" | "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "messages";
|
||||||
|
update: {
|
||||||
|
attachments?: Array<string>;
|
||||||
|
authorId?: string;
|
||||||
|
channelId?: string;
|
||||||
|
content?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
createdTimestamp?: number;
|
||||||
|
editedAt?: string;
|
||||||
|
guildId?: string;
|
||||||
|
id?: string;
|
||||||
|
inGuild?: boolean;
|
||||||
|
nonce?: string;
|
||||||
|
position?: number;
|
||||||
|
referencedMessage?: null | string | string | string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "inGuild"
|
||||||
|
| "attachments"
|
||||||
|
| "authorId"
|
||||||
|
| "channelId"
|
||||||
|
| "content"
|
||||||
|
| "createdAt"
|
||||||
|
| "createdTimestamp"
|
||||||
|
| "editedAt"
|
||||||
|
| "guildId"
|
||||||
|
| "id"
|
||||||
|
| "nonce"
|
||||||
|
| "position"
|
||||||
|
| "referencedMessage"
|
||||||
|
| "url"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "attachments";
|
||||||
|
update: {
|
||||||
|
contentType?: string;
|
||||||
|
description?: null | string;
|
||||||
|
ephemeral?: boolean;
|
||||||
|
height?: number;
|
||||||
|
id?: string;
|
||||||
|
size?: number;
|
||||||
|
spoiler?: boolean;
|
||||||
|
url?: string;
|
||||||
|
width?: number;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "contentType"
|
||||||
|
| "description"
|
||||||
|
| "ephemeral"
|
||||||
|
| "height"
|
||||||
|
| "width"
|
||||||
|
| "id"
|
||||||
|
| "size"
|
||||||
|
| "spoiler"
|
||||||
|
| "url"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -1184,10 +2009,43 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
};
|
};
|
||||||
user: {
|
user: {
|
||||||
index: {
|
index: {
|
||||||
|
answerFriendRequest: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ answer: "accept" | "decline" | "ignore"; requestId: string },
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
|
getFriendRequests: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
any,
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
|
getFriends: FunctionReference<"query", "internal", any, any, Name>;
|
||||||
|
getUserStatus: FunctionReference<"query", "internal", any, any, Name>;
|
||||||
|
sendFriendRequest: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ username: string },
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
|
updateUserMetadata: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ metadata: { phrasePreference: "comforting" | "mocking" | "both" } },
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
updateUserStatus: FunctionReference<
|
updateUserStatus: FunctionReference<
|
||||||
"mutation",
|
"mutation",
|
||||||
"internal",
|
"internal",
|
||||||
{ isUserSet: boolean; status: string },
|
{
|
||||||
|
isUserSet: boolean;
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
},
|
||||||
any,
|
any,
|
||||||
Name
|
Name
|
||||||
>;
|
>;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
import { Id } from "../../_generated/dataModel";
|
import { Id } from "../../_generated/dataModel";
|
||||||
import { mutation, query } from "../../_generated/server";
|
import { mutation, query } from "../_generated/server";
|
||||||
|
|
||||||
export const sendKeysToServer = mutation({
|
export const sendKeysToServer = mutation({
|
||||||
args: {
|
args: {
|
||||||
|
|
@ -16,11 +16,10 @@ export const sendKeysToServer = mutation({
|
||||||
forceInsert: v.boolean(), // if true, insert even if user already has an olm account
|
forceInsert: v.boolean(), // if true, insert even if user already has an olm account
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
console.log("sendKeysToServer", args);
|
|
||||||
// check if user already has an olm account
|
// check if user already has an olm account
|
||||||
// @ts-ignore
|
|
||||||
const olmAccount = await ctx.db.query("olmAccount").withIndex("userId", (q) => q.eq("userId", args.userId)).first();
|
const olmAccount = await ctx.db.query("olmAccount").withIndex("userId", (q) => q.eq("userId", args.userId)).first();
|
||||||
console.log("olmAccount", olmAccount);
|
|
||||||
if (olmAccount && !args.forceInsert) {
|
if (olmAccount && !args.forceInsert) {
|
||||||
throw new Error("User already has an olm account");
|
throw new Error("User already has an olm account");
|
||||||
}
|
}
|
||||||
|
|
@ -42,9 +41,8 @@ export const retrieveServerOlmAccount = query({
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const olmAccount = await ctx.db.get<"olmAccount">(args.userId as Id<"olmAccount">);
|
const olmAccount = await ctx.db.get<"olmAccount">(args.userId as Id<"olmAccount">);
|
||||||
if (olmAccount) {
|
if (olmAccount) return olmAccount;
|
||||||
return olmAccount;
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -4,33 +4,43 @@
|
||||||
|
|
||||||
import { defineSchema, defineTable } from "convex/server";
|
import { defineSchema, defineTable } from "convex/server";
|
||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
|
import { user } from "./schemas/user";
|
||||||
|
|
||||||
|
const Attachment = v.object({
|
||||||
|
contentType: v.string(), // MIME type
|
||||||
|
description: v.union(v.null(), v.string()), // Description
|
||||||
|
ephemeral: v.boolean(), // Whether the attachment is ephemeral
|
||||||
|
height: v.optional(v.number()), // Height in pixels
|
||||||
|
width: v.optional(v.number()), // Width in pixels
|
||||||
|
id: v.id("storage"), // Storage ID
|
||||||
|
size: v.number(), // Size in bytes
|
||||||
|
spoiler: v.boolean(), // Whether the attachment is a spoiler
|
||||||
|
url: v.string(), // Public URL
|
||||||
|
});
|
||||||
|
|
||||||
|
const Message = v.object({
|
||||||
|
inGuild: v.optional(v.boolean()),
|
||||||
|
attachments: v.optional(v.array(v.id("attachments"))),
|
||||||
|
authorId: v.id("user"),
|
||||||
|
channelId: v.id("channel"),
|
||||||
|
content: v.string(),
|
||||||
|
createdAt: v.string(),
|
||||||
|
createdTimestamp: v.number(),
|
||||||
|
editedAt: v.optional(v.string()),
|
||||||
|
guildId: v.optional(v.id("guild")),
|
||||||
|
id: v.string(),
|
||||||
|
nonce: v.optional(v.string()),
|
||||||
|
position: v.optional(v.number()),
|
||||||
|
referencedMessage: v.optional(
|
||||||
|
v.union(v.null(), v.id("messages"), v.id("channel"), v.id("guild")),
|
||||||
|
),
|
||||||
|
url: v.optional(v.string()),
|
||||||
|
})
|
||||||
|
|
||||||
export const tables = {
|
export const tables = {
|
||||||
user: defineTable({
|
...user,
|
||||||
name: v.string(),
|
messages: defineTable(Message),
|
||||||
email: v.string(),
|
attachments: defineTable(Attachment),
|
||||||
emailVerified: v.boolean(),
|
|
||||||
image: v.optional(v.union(v.null(), v.string())),
|
|
||||||
createdAt: v.number(),
|
|
||||||
updatedAt: v.number(),
|
|
||||||
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("status", ["status"])
|
|
||||||
.index("friends", ["friends"]),
|
|
||||||
session: defineTable({
|
session: defineTable({
|
||||||
expiresAt: v.number(),
|
expiresAt: v.number(),
|
||||||
token: v.string(),
|
token: v.string(),
|
||||||
|
|
@ -87,7 +97,9 @@ export const tables = {
|
||||||
publicKey: v.string(),
|
publicKey: v.string(),
|
||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
.index("userId", ["userId"]),
|
.index("userId", ["userId"])
|
||||||
|
.index("userId_keys", ["userId", "oneTimeKeys"])
|
||||||
|
.index("userId_identityKey", ["userId", "identityKey"]),
|
||||||
};
|
};
|
||||||
|
|
||||||
const schema = defineSchema(tables);
|
const schema = defineSchema(tables);
|
||||||
|
|
|
||||||
55
convex/betterAuth/schemas/user.ts
Normal file
55
convex/betterAuth/schemas/user.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { defineTable } from "convex/server";
|
||||||
|
import { v } from "convex/values";
|
||||||
|
|
||||||
|
export const user = {
|
||||||
|
user: defineTable({
|
||||||
|
name: v.string(),
|
||||||
|
email: v.string(),
|
||||||
|
emailVerified: v.boolean(),
|
||||||
|
image: v.optional(v.union(v.null(), v.string())),
|
||||||
|
createdAt: v.number(),
|
||||||
|
updatedAt: v.number(),
|
||||||
|
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")),
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
.index("email_name", ["email", "name"])
|
||||||
|
.index("byName", ["name"])
|
||||||
|
.index("userId", ["userId"])
|
||||||
|
.index("username", ["username"]),
|
||||||
|
userStatus: defineTable({
|
||||||
|
userId: v.id("user"),
|
||||||
|
status: v.union(v.literal("online"), v.literal("busy"), v.literal("offline"), v.literal("away")),
|
||||||
|
isUserSet: v.boolean(),
|
||||||
|
updatedAt: v.number(),
|
||||||
|
})
|
||||||
|
.index("userId", ["userId"])
|
||||||
|
.index("status", ["status"]),
|
||||||
|
friendRequests: defineTable({
|
||||||
|
userId: v.id("user"),
|
||||||
|
requestTo: v.id("user"),
|
||||||
|
method: v.union(v.literal("receive"), v.literal("send")),
|
||||||
|
requestId: v.string(),
|
||||||
|
createdAt: v.number(),
|
||||||
|
expiresAt: v.optional(v.number()),
|
||||||
|
acceptedAt: v.optional(v.number()),
|
||||||
|
declinedAt: v.optional(v.number()),
|
||||||
|
ignoredAt: v.optional(v.number()),
|
||||||
|
})
|
||||||
|
.index("userId_method", ["userId", "method"])
|
||||||
|
.index("userId", ["userId"])
|
||||||
|
.index("requestId", ["requestId"])
|
||||||
|
.index("requestTo", ["requestTo"])
|
||||||
|
.index("expiresAt", ["expiresAt"]),
|
||||||
|
friends: defineTable({
|
||||||
|
userId: v.id("user"),
|
||||||
|
friendId: v.id("user"),
|
||||||
|
createdAt: v.number(),
|
||||||
|
})
|
||||||
|
.index("userId", ["userId"])
|
||||||
|
.index("friendId", ["friendId"])
|
||||||
|
.index("userId_friendId", ["userId", "friendId"]),
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
import { Id } from "../../_generated/dataModel";
|
import { Id } from "../_generated/dataModel";
|
||||||
import { mutation } from "../../_generated/server";
|
import { mutation, MutationCtx, query, QueryCtx } from "../_generated/server";
|
||||||
|
|
||||||
export const updateUserStatus = mutation({
|
async function userValidation(ctx: MutationCtx | QueryCtx) {
|
||||||
args: {
|
|
||||||
status: v.string(),
|
|
||||||
isUserSet: v.boolean(),
|
|
||||||
},
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
const user = await ctx.auth.getUserIdentity();
|
const user = await ctx.auth.getUserIdentity();
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error("User not found");
|
throw new Error("User not found");
|
||||||
|
|
@ -18,11 +13,308 @@ export const updateUserStatus = mutation({
|
||||||
throw new Error("User not found");
|
throw new Error("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.db.patch<"user">("user", userId, {
|
return {
|
||||||
status: {
|
userId,
|
||||||
|
user,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateUserStatus = mutation({
|
||||||
|
args: {
|
||||||
|
status: v.union(v.literal("online"), v.literal("busy"), v.literal("offline"), v.literal("away")),
|
||||||
|
isUserSet: v.boolean(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
try {
|
||||||
|
const { userId } = await userValidation(ctx);
|
||||||
|
|
||||||
|
// Check if user status is already set
|
||||||
|
const userStatus = await ctx.db.query("userStatus").withIndex("userId", (q) => q.eq("userId", userId)).first();
|
||||||
|
if (userStatus) {
|
||||||
|
await ctx.db.patch(userStatus._id, {
|
||||||
status: args.status,
|
status: args.status,
|
||||||
isUserSet: args.isUserSet,
|
isUserSet: args.isUserSet,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await ctx.db.insert("userStatus", {
|
||||||
|
userId: userId,
|
||||||
|
status: args.status,
|
||||||
|
isUserSet: false,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { success: true, message: "User status updated successfully" };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating user status:", error);
|
||||||
|
throw new Error("Failed to update user status");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getUserStatus = query({
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const { userId } = await userValidation(ctx);
|
||||||
|
const userStatus = await ctx.db.query("userStatus").withIndex("userId", (q) => q.eq("userId", userId)).first();
|
||||||
|
return userStatus;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateUserMetadata = mutation({
|
||||||
|
args: {
|
||||||
|
metadata: v.object({
|
||||||
|
phrasePreference: v.union(v.literal("comforting"), v.literal("mocking"), v.literal("both")),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const { userId } = await userValidation(ctx);
|
||||||
|
|
||||||
|
return ctx.db.patch("user", userId, {
|
||||||
|
metadata: args.metadata,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const sendFriendRequest = mutation({
|
||||||
|
args: {
|
||||||
|
username: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const { userId, user: currentUser } = await userValidation(ctx);
|
||||||
|
|
||||||
|
// Find the target user
|
||||||
|
const targetUser = await ctx.db.query("user").withIndex("byName", (q) => q.eq("name", args.username)).first();
|
||||||
|
if (!targetUser) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if trying to send request to yourself
|
||||||
|
if (targetUser._id === userId) {
|
||||||
|
throw new Error("You cannot send a friend request to yourself");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already friends
|
||||||
|
const existingFriendship = await ctx.db
|
||||||
|
.query("friends")
|
||||||
|
.withIndex("userId_friendId", (q) => q.eq("userId", userId).eq("friendId", targetUser._id))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (existingFriendship) {
|
||||||
|
throw new Error("You are already friends with this user");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing requests in both directions
|
||||||
|
const existingRequests = await ctx.db
|
||||||
|
.query("friendRequests")
|
||||||
|
.filter((q) =>
|
||||||
|
q.or(
|
||||||
|
q.and(
|
||||||
|
q.eq(q.field("userId"), userId),
|
||||||
|
q.eq(q.field("requestTo"), targetUser._id)
|
||||||
|
),
|
||||||
|
q.and(
|
||||||
|
q.eq(q.field("userId"), targetUser._id),
|
||||||
|
q.eq(q.field("requestTo"), userId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter((q) => q.eq(q.field("acceptedAt"), undefined))
|
||||||
|
.filter((q) => q.eq(q.field("declinedAt"), undefined))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
const existingSentRequest = existingRequests.find(r => r.userId === userId);
|
||||||
|
const incomingRequest = existingRequests.find(r => r.userId === targetUser._id);
|
||||||
|
|
||||||
|
if (existingSentRequest) {
|
||||||
|
throw new Error("You have already sent a friend request to this user");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (incomingRequest) {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
// Auto-accept the incoming request
|
||||||
|
await ctx.db.patch(incomingRequest._id, {
|
||||||
|
acceptedAt: timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create bidirectional friendship entries
|
||||||
|
await Promise.all([
|
||||||
|
ctx.db.insert("friends", {
|
||||||
|
userId: userId,
|
||||||
|
friendId: targetUser._id,
|
||||||
|
createdAt: timestamp,
|
||||||
|
}),
|
||||||
|
ctx.db.insert("friends", {
|
||||||
|
userId: targetUser._id,
|
||||||
|
friendId: userId,
|
||||||
|
createdAt: timestamp,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Friend request accepted automatically (they had already sent you a request)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the friend request (single row)
|
||||||
|
const requestId = crypto.randomUUID();
|
||||||
|
await ctx.db.insert("friendRequests", {
|
||||||
|
userId: userId,
|
||||||
|
requestTo: targetUser._id,
|
||||||
|
method: "send",
|
||||||
|
requestId,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Friend request sent successfully",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const answerFriendRequest = mutation({
|
||||||
|
args: {
|
||||||
|
requestId: v.string(),
|
||||||
|
answer: v.union(v.literal("accept"), v.literal("decline"), v.literal("ignore")),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const { userId } = await userValidation(ctx);
|
||||||
|
|
||||||
|
// Get the friend request
|
||||||
|
const request = await ctx.db
|
||||||
|
.query("friendRequests")
|
||||||
|
.withIndex("requestId", (q) => q.eq("requestId", args.requestId))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
throw new Error("Request not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify current user is the recipient
|
||||||
|
if (request.requestTo !== userId) {
|
||||||
|
throw new Error("You are not the recipient of this request");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already answered
|
||||||
|
if (request.acceptedAt || request.declinedAt || request.ignoredAt) {
|
||||||
|
throw new Error("Request already answered");
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
// Update the request based on the answer
|
||||||
|
switch (args.answer) {
|
||||||
|
case "accept":
|
||||||
|
// Update request status
|
||||||
|
await ctx.db.patch(request._id, { acceptedAt: timestamp });
|
||||||
|
|
||||||
|
// Create bidirectional friendship entries
|
||||||
|
await Promise.all([
|
||||||
|
ctx.db.insert("friends", {
|
||||||
|
userId: userId,
|
||||||
|
friendId: request.userId,
|
||||||
|
createdAt: timestamp,
|
||||||
|
}),
|
||||||
|
ctx.db.insert("friends", {
|
||||||
|
userId: request.userId,
|
||||||
|
friendId: userId,
|
||||||
|
createdAt: timestamp,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "decline":
|
||||||
|
await ctx.db.patch(request._id, { declinedAt: timestamp });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ignore":
|
||||||
|
await ctx.db.patch(request._id, { ignoredAt: timestamp });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Friend request ${args.answer}ed successfully`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getFriendRequests = query({
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const { userId } = await userValidation(ctx);
|
||||||
|
|
||||||
|
// Get all unanswered requests involving this user (sent by them OR sent to them)
|
||||||
|
const allRequests = await ctx.db
|
||||||
|
.query("friendRequests")
|
||||||
|
.filter((q) =>
|
||||||
|
q.or(
|
||||||
|
q.eq(q.field("userId"), userId), // Requests sent by me
|
||||||
|
q.eq(q.field("requestTo"), userId) // Requests sent to me
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter((q) => q.eq(q.field("acceptedAt"), undefined))
|
||||||
|
.filter((q) => q.eq(q.field("declinedAt"), undefined))
|
||||||
|
.filter((q) => q.eq(q.field("ignoredAt"), undefined))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Transform to include method field based on perspective
|
||||||
|
const requestsWithMethod = await Promise.all(
|
||||||
|
allRequests.map(async (request) => {
|
||||||
|
const isSentByMe = request.userId === userId;
|
||||||
|
const otherUserId = isSentByMe ? request.requestTo : request.userId;
|
||||||
|
const otherUser = await ctx.db.get(otherUserId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: request.requestId,
|
||||||
|
_id: request._id,
|
||||||
|
userId: otherUserId,
|
||||||
|
username: otherUser?.username || otherUser?.displayUsername || otherUser?.name || "Unknown",
|
||||||
|
avatar: otherUser?.image || "",
|
||||||
|
createdAt: request.createdAt,
|
||||||
|
method: isSentByMe ? "send" : "receive",
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return requestsWithMethod;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getFriends = query({
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const { userId } = await userValidation(ctx);
|
||||||
|
|
||||||
|
// Get all friendships for this user
|
||||||
|
const friendships = await ctx.db
|
||||||
|
.query("friends")
|
||||||
|
.withIndex("userId", (q) => q.eq("userId", userId))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Populate friend data with relevant fields
|
||||||
|
const friends = await Promise.all(
|
||||||
|
friendships.map(async (friendship) => {
|
||||||
|
const friend = await ctx.db.get(friendship.friendId);
|
||||||
|
const friendStatus = await ctx.db.query("userStatus").withIndex("userId", (q) => q.eq("userId", friendship.friendId)).first();
|
||||||
|
if (!friend) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
_id: friend._id,
|
||||||
|
id: friend._id,
|
||||||
|
name: friend.name,
|
||||||
|
username: friend.username,
|
||||||
|
displayUsername: friend.displayUsername,
|
||||||
|
image: friend.image,
|
||||||
|
friendshipCreatedAt: friendship.createdAt,
|
||||||
|
status: friendStatus ? {
|
||||||
|
status: friendStatus.status,
|
||||||
|
isUserSet: friendStatus.isUserSet,
|
||||||
|
} : null,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return friends.filter(Boolean);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
"start:server": "NODE_ENV=development tsx src/server.ts"
|
"start:server": "NODE_ENV=development tsx src/server.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@better-fetch/fetch": "^1.1.21",
|
||||||
"@convex-dev/better-auth": "^0.10.4",
|
"@convex-dev/better-auth": "^0.10.4",
|
||||||
"@marsidev/react-turnstile": "^1.4.0",
|
"@marsidev/react-turnstile": "^1.4.0",
|
||||||
"@matrix-org/olm": "^3.2.15",
|
"@matrix-org/olm": "^3.2.15",
|
||||||
|
|
|
||||||
110
src/app/page.tsx
110
src/app/page.tsx
|
|
@ -1,5 +1,6 @@
|
||||||
"use client"
|
"use client"
|
||||||
import AppSidebar from "@/components/home";
|
import AppSidebar from "@/components/home";
|
||||||
|
import FriendRequestModal from "@/components/home/modals/friendRequest";
|
||||||
import OlmSetupDialog from "@/components/olm/olm-setup-dialog";
|
import OlmSetupDialog from "@/components/olm/olm-setup-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
@ -9,11 +10,10 @@ import { useOlmSetup } from "@/hooks/use-olm-setup";
|
||||||
import { useSocket } from "@/hooks/use-socket";
|
import { useSocket } from "@/hooks/use-socket";
|
||||||
import { authClient } from "@/lib/auth/client";
|
import { authClient } from "@/lib/auth/client";
|
||||||
import { useMutation, useQuery } from "convex/react";
|
import { useMutation, useQuery } from "convex/react";
|
||||||
import { PlusIcon, SearchIcon, UsersIcon } from "lucide-react";
|
import { PlusIcon, SearchIcon, SettingsIcon, UsersIcon } from "lucide-react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { api } from "../../convex/_generated/api";
|
import { api } from "../../convex/_generated/api";
|
||||||
|
|
||||||
const mockPhrases = [
|
const mockPhrases = [
|
||||||
"No bitches? Womp womp",
|
"No bitches? Womp womp",
|
||||||
"You're all alone",
|
"You're all alone",
|
||||||
|
|
@ -94,9 +94,9 @@ const comfortingPhrases = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { data, error, isPending } = authClient.useSession();
|
const { data, error, isPending, refetch } = authClient.useSession();
|
||||||
|
|
||||||
const [page, setPage] = useState<"friends" | "settings">("friends");
|
const [page, setPage] = useState<"friends" | "support">("friends");
|
||||||
const [currentChannel, setCurrentChannel] = useState<SiPher.Channel | null>(null);
|
const [currentChannel, setCurrentChannel] = useState<SiPher.Channel | null>(null);
|
||||||
const [openDmChannels, setOpenDmChannels] = useState<SiPher.Channel[] | []>([]);
|
const [openDmChannels, setOpenDmChannels] = useState<SiPher.Channel[] | []>([]);
|
||||||
const [availableServers, setAvailableServers] = useState<SiPher.Server[] | []>([]);
|
const [availableServers, setAvailableServers] = useState<SiPher.Server[] | []>([]);
|
||||||
|
|
@ -104,29 +104,55 @@ export default function Home() {
|
||||||
// Friends page state
|
// Friends page state
|
||||||
const [friendsPage, setFriendsPage] = useState<"all" | "available">("all");
|
const [friendsPage, setFriendsPage] = useState<"all" | "available">("all");
|
||||||
const [friendsSearch, setFriendsSearch] = useState<string>("");
|
const [friendsSearch, setFriendsSearch] = useState<string>("");
|
||||||
|
const [friendModal, setFriendModal] = useState<boolean>(false);
|
||||||
|
|
||||||
const hasServerOlm = useQuery(
|
const hasServerOlm = useQuery(
|
||||||
api.auth.retrieveServerOlmAccount,
|
api.auth.retrieveServerOlmAccount,
|
||||||
data?.user?.id ? { userId: data.user.id } : "skip"
|
data?.user?.id ? { userId: data.user.id } : "skip"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get user status from separate table
|
||||||
|
const userStatus = useQuery(api.auth.getUserStatus);
|
||||||
|
|
||||||
|
// Get friends list (reactive)
|
||||||
|
const friends = useQuery(api.auth.getFriends);
|
||||||
|
|
||||||
|
// Type for friends
|
||||||
|
type Friend = NonNullable<typeof friends>[number];
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
const updateUserMetadata = useMutation(api.auth.updateUserMetadata);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const status = data.user.status
|
const metadata = data.user.metadata
|
||||||
if (!status) return;
|
if (!metadata) {
|
||||||
|
console.debug(
|
||||||
if (status.status === "offline" && !status.isUserSet) {
|
"[Home] > User metadata set",
|
||||||
updateUserStatus({ status: "online", isUserSet: false });
|
data.user.metadata
|
||||||
|
)
|
||||||
|
updateUserMetadata({ metadata: { phrasePreference: "comforting" } });
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}, [data?.user?.id, updateUserStatus, data?.user?.status]);
|
}, [data, updateUserMetadata]);
|
||||||
|
|
||||||
// Custom hooks for socket and OLM management
|
// Custom hooks for socket and OLM management
|
||||||
const { socketStatus, socketInfo } = useSocket(data?.user?.id);
|
const { socketStatus, socketInfo, disconnect, connect } = useSocket({
|
||||||
|
user: {
|
||||||
|
id: data?.user?.id,
|
||||||
|
status: userStatus ? {
|
||||||
|
status: userStatus.status,
|
||||||
|
isUserSet: userStatus.isUserSet,
|
||||||
|
} : {
|
||||||
|
status: "offline" as const,
|
||||||
|
isUserSet: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
refetchUser: refetch
|
||||||
|
});
|
||||||
const { olmStatus, showOlmModal, setShowOlmModal, handleCreateAccount } = useOlmSetup({
|
const { olmStatus, showOlmModal, setShowOlmModal, handleCreateAccount } = useOlmSetup({
|
||||||
userId: data?.user?.id,
|
userId: data?.user?.id,
|
||||||
hasServerOlm,
|
hasServerOlm,
|
||||||
|
|
@ -140,10 +166,9 @@ export default function Home() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !data) {
|
if (error || !data) {
|
||||||
return redirect(`/auth${error ? `?error=${error.cause}` : "no-data"}`);
|
return redirect(`/auth${error ? `?error=${error.cause}` : "?error=no-data"}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const getRandomPhrase = useCallback(() => {
|
const getRandomPhrase = useCallback(() => {
|
||||||
const phrases = {
|
const phrases = {
|
||||||
comforting: comfortingPhrases,
|
comforting: comfortingPhrases,
|
||||||
|
|
@ -161,7 +186,7 @@ export default function Home() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserFloatingCard user={data.user} />
|
<UserFloatingCard user={data.user} />
|
||||||
<AppSidebar socketStatus={socketStatus} socketInfo={socketInfo}>
|
<AppSidebar socketStatus={socketStatus} socketInfo={socketInfo} disconnectSocket={disconnect} connectSocket={connect}>
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Header - fixed height and sticky */}
|
{/* 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">
|
<div className="flex items-center min-h-10 max-h-10 border-b border-border/40 sticky top-0 z-10 bg-background">
|
||||||
|
|
@ -183,6 +208,8 @@ export default function Home() {
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{/* Page title/options */}
|
{/* Page title/options */}
|
||||||
|
{
|
||||||
|
page === "friends" ? (
|
||||||
<div className="flex flex-row justify-start items-center gap-2 w-full">
|
<div className="flex flex-row justify-start items-center gap-2 w-full">
|
||||||
<div className="flex flex-row gap-2 justify-start p-2">
|
<div className="flex flex-row gap-2 justify-start p-2">
|
||||||
<UsersIcon className="size-4" />
|
<UsersIcon className="size-4" />
|
||||||
|
|
@ -196,11 +223,13 @@ export default function Home() {
|
||||||
<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")}>
|
<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
|
All Known
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" className="h-full bg-primary text-primary-foreground hover:cursor-pointer justify-start p-2 ">
|
<Button variant="ghost" className="h-full bg-primary text-primary-foreground hover:cursor-pointer justify-start p-2" onClick={() => setFriendModal(true)}>
|
||||||
Add Friend
|
Add Friend
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
{/* Content Area - Channel List + Main Content */}
|
{/* Content Area - Channel List + Main Content */}
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
|
|
@ -213,6 +242,10 @@ export default function Home() {
|
||||||
<UsersIcon className="size-4" />
|
<UsersIcon className="size-4" />
|
||||||
<span className="text-sm font-medium">Friends</span>
|
<span className="text-sm font-medium">Friends</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="ghost" className="w-full h-full hover:cursor-pointer justify-start" onClick={() => setPage("support")}>
|
||||||
|
<SettingsIcon className="size-4" />
|
||||||
|
<span className="text-sm font-medium">Settings</span>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-[calc(100%-0.8rem)] h-px bg-border/40 mx-2" />
|
<div className="w-[calc(100%-0.8rem)] h-px bg-border/40 mx-2" />
|
||||||
|
|
@ -269,19 +302,42 @@ export default function Home() {
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
friendsPage === "all" ? (
|
friendsPage === "all" ? (
|
||||||
<div className="flex items-center min-h-10 max-h-10">
|
<div className="flex flex-col items-start w-full p-2 gap-2 pt-4">
|
||||||
<span className="text-sm font-medium">All Friends</span>
|
<span className="text-sm text-start font-medium">All Friends • {friends ? friends.length : 0}</span>
|
||||||
|
{
|
||||||
|
friends && friends.length > 0 ? (
|
||||||
|
friends.map((friend: Friend) => {
|
||||||
|
if (!friend) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={friend._id} className="flex items-center min-h-10 max-h-10">
|
||||||
|
<span className="text-sm font-medium">{friend.displayUsername || friend.username || friend.name}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
{getRandomPhrase()}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-start w-full p-2 gap-2 pt-4">
|
<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>
|
<span className="text-sm text-start font-medium">Available Friends • {friends ? friends.filter((f: Friend) => f && f.status?.status !== "offline").length : 0}</span>
|
||||||
{
|
{
|
||||||
data.user.friends && data.user.friends.length > 0 ? (
|
friends && friends.length > 0 ? (
|
||||||
data.user.friends.map((friend) => (
|
friends
|
||||||
<div className="flex items-center min-h-10 max-h-10">
|
.filter((f: Friend) => f && f.status?.status !== "offline")
|
||||||
<span className="text-sm font-medium">{friend}</span>
|
.map((friend: Friend) => {
|
||||||
|
if (!friend) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={friend._id} className="flex items-center min-h-10 max-h-10">
|
||||||
|
<span className="text-sm font-medium">{friend.displayUsername || friend.username || friend.name}</span>
|
||||||
</div>
|
</div>
|
||||||
))
|
)
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
{getRandomPhrase()}
|
{getRandomPhrase()}
|
||||||
|
|
@ -293,7 +349,7 @@ export default function Home() {
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : page === "settings" ? (
|
) : page === "support" ? (
|
||||||
<div className="flex flex-col flex-1 overflow-y-auto p-4">
|
<div className="flex flex-col flex-1 overflow-y-auto p-4">
|
||||||
<div className="flex items-center min-h-10 max-h-10">
|
<div className="flex items-center min-h-10 max-h-10">
|
||||||
<span className="text-sm font-medium">Servers</span>
|
<span className="text-sm font-medium">Servers</span>
|
||||||
|
|
@ -306,6 +362,10 @@ export default function Home() {
|
||||||
</div>
|
</div>
|
||||||
</AppSidebar>
|
</AppSidebar>
|
||||||
|
|
||||||
|
<FriendRequestModal
|
||||||
|
open={friendModal}
|
||||||
|
onOpenChange={setFriendModal}
|
||||||
|
/>
|
||||||
{/* OLM Account Setup/Sync Modal */}
|
{/* OLM Account Setup/Sync Modal */}
|
||||||
<OlmSetupDialog
|
<OlmSetupDialog
|
||||||
open={showOlmModal}
|
open={showOlmModal}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { BroadcastIcon as Broadcast } from "@phosphor-icons/react";
|
import { BroadcastIcon as Broadcast } from "@phosphor-icons/react";
|
||||||
import { Activity, Clock, Globe, Radio, Zap } from "lucide-react";
|
import { Activity, Clock, Globe, LogInIcon, LogOutIcon, Radio, Zap } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||||
|
|
||||||
function formatUptime(ms: number): string {
|
function formatUptime(ms: number): string {
|
||||||
|
|
@ -19,7 +20,7 @@ function formatUptime(ms: number): string {
|
||||||
/**
|
/**
|
||||||
* Connection status indicator with popover details
|
* Connection status indicator with popover details
|
||||||
*/
|
*/
|
||||||
export default function ConnectionStatusIndicator({ socketStatus, socketInfo }: { socketStatus: SiPher.SocketStatus; socketInfo: SiPher.SocketInfo }) {
|
export default function ConnectionStatusIndicator({ socketStatus, socketInfo, disconnectSocket, connectSocket }: { socketStatus: SiPher.SocketStatus; socketInfo: SiPher.SocketInfo; disconnectSocket: () => void; connectSocket: () => void }) {
|
||||||
const [uptime, setUptime] = useState<string>("0s");
|
const [uptime, setUptime] = useState<string>("0s");
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
|
@ -188,10 +189,21 @@ export default function ConnectionStatusIndicator({ socketStatus, socketInfo }:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer hint */}
|
{/* Footer hint */}
|
||||||
<div className="px-4 py-2 border-t border-border bg-muted/30">
|
<div className="flex flex-row items-center justify-between gap-2 px-4 py-2 border-t border-border bg-muted/30">
|
||||||
<p className="text-[10px] text-muted-foreground text-center">
|
<p className="text-[10px] text-muted-foreground text-center">
|
||||||
Real-time connection via Socket.IO
|
Real-time connection via Socket.IO
|
||||||
</p>
|
</p>
|
||||||
|
<Button variant="ghost" size="icon-sm" className="hover:cursor-pointer hover:bg-transparent!" onClick={() => {
|
||||||
|
socketStatus === "connected" ? disconnectSocket() : connectSocket();
|
||||||
|
}}>
|
||||||
|
{
|
||||||
|
socketStatus === "connected" ? (
|
||||||
|
<LogOutIcon className="size-4" />
|
||||||
|
) : (
|
||||||
|
<LogInIcon className="size-4" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ const SidebarItems: SiPher.SidebarItem[] = [
|
||||||
* It also is the controller for everything on the app, including going to other pages, showing conversations and other.
|
* It also is the controller for everything on the app, including going to other pages, showing conversations and other.
|
||||||
* @param children - The children to be rendered in the sidebar inset
|
* @param children - The children to be rendered in the sidebar inset
|
||||||
*/
|
*/
|
||||||
export default function AppSidebar({ children, socketStatus, socketInfo, currentChannel }: SiPher.AppSidebarProps) {
|
export default function AppSidebar({ children, socketStatus, socketInfo, currentChannel, disconnectSocket, connectSocket }: SiPher.AppSidebarProps) {
|
||||||
const [activeItem, setActiveItem] = useState<string>("home");
|
const [activeItem, setActiveItem] = useState<string>("home");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -120,7 +120,7 @@ export default function AppSidebar({ children, socketStatus, socketInfo, current
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{/* Socket connection status */}
|
{/* Socket connection status */}
|
||||||
<ConnectionStatusIndicator socketStatus={socketStatus} socketInfo={socketInfo} />
|
<ConnectionStatusIndicator socketStatus={socketStatus} socketInfo={socketInfo} disconnectSocket={disconnectSocket} connectSocket={connectSocket} />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-9 md:hidden" /> {/* Spacer for centering on mobile */}
|
<div className="w-9 md:hidden" /> {/* Spacer for centering on mobile */}
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
305
src/components/home/modals/friendRequest.tsx
Normal file
305
src/components/home/modals/friendRequest.tsx
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import { useMutation, useQuery } from "convex/react";
|
||||||
|
import { CheckIcon, UserPlusIcon, XIcon } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { api } from "../../../../convex/_generated/api";
|
||||||
|
|
||||||
|
interface FriendRequestModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FriendRequestModal({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: FriendRequestModalProps) {
|
||||||
|
|
||||||
|
const getFriendRequests = useQuery(api.auth.getFriendRequests);
|
||||||
|
const sendFriendRequest = useMutation(api.auth.sendFriendRequest);
|
||||||
|
const answerFriendRequest = useMutation(api.auth.answerFriendRequest);
|
||||||
|
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [activeTab, setActiveTab] = useState<"send" | "pending" | "sent">("send");
|
||||||
|
const [pendingRequests, setPendingRequests] = useState<any[]>([]);
|
||||||
|
const [sentRequests, setSentRequests] = useState<any[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (getFriendRequests) {
|
||||||
|
|
||||||
|
if (!getFriendRequests || getFriendRequests.length === 0) {
|
||||||
|
console.debug("[FriendRequestModal] > Such a sad day, no friend requests found")
|
||||||
|
setPendingRequests([]);
|
||||||
|
setSentRequests([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug("[FriendRequestModal] > This guy is important, look at him with his big friend request list (¬.¬) :", getFriendRequests);
|
||||||
|
setPendingRequests(getFriendRequests.filter((request: any) => request.method === "receive"));
|
||||||
|
setSentRequests(getFriendRequests.filter((request: any) => request.method === "send"));
|
||||||
|
}
|
||||||
|
}, [getFriendRequests]);
|
||||||
|
|
||||||
|
const handleSendRequest = async () => {
|
||||||
|
if (!username.trim()) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
await sendFriendRequest({
|
||||||
|
username: username,
|
||||||
|
});
|
||||||
|
toast.success("Friend request sent successfully");
|
||||||
|
setUsername("");
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to send friend request");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAccept = async (requestId: string) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await answerFriendRequest({
|
||||||
|
requestId: requestId,
|
||||||
|
answer: "accept",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to accept request");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDecline = async (requestId: string) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await answerFriendRequest({
|
||||||
|
requestId: requestId,
|
||||||
|
answer: "decline",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to decline request");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Enter" && !isLoading) {
|
||||||
|
handleSendRequest();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTimeAgo = (timestamp: number) => {
|
||||||
|
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
||||||
|
if (seconds < 60) return "just now";
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
if (minutes < 60) return `${minutes}m ago`;
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
if (hours < 24) return `${hours}h ago`;
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
return `${days}d ago`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<UserPlusIcon className="size-5" />
|
||||||
|
Friend Requests
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Send, accept, or manage your friend requests.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex items-center gap-2 border-b border-border">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={`rounded-b-none ${activeTab === "send" ? "border-b-2 border-primary" : ""}`}
|
||||||
|
onClick={() => setActiveTab("send")}
|
||||||
|
>
|
||||||
|
Send Request
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={`rounded-b-none ${activeTab === "pending" ? "border-b-2 border-primary" : ""}`}
|
||||||
|
onClick={() => setActiveTab("pending")}
|
||||||
|
>
|
||||||
|
Pending ({pendingRequests.length})
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={`rounded-b-none ${activeTab === "sent" ? "border-b-2 border-primary" : ""}`}
|
||||||
|
onClick={() => setActiveTab("sent")}
|
||||||
|
>
|
||||||
|
Sent ({sentRequests.length})
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="min-h-[200px] max-h-[400px] overflow-y-auto">
|
||||||
|
{activeTab === "send" && (
|
||||||
|
<div className="flex flex-col gap-4 py-2">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Enter username..."
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<p className="text-sm text-destructive">{error}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleSendRequest}
|
||||||
|
disabled={!username.trim() || isLoading}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Spinner className="size-4 animate-spin mr-2" />
|
||||||
|
Sending...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<UserPlusIcon className="size-4 mr-2" />
|
||||||
|
Send Friend Request
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "pending" && (
|
||||||
|
<div className="flex flex-col gap-2 py-2">
|
||||||
|
{pendingRequests.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
No pending friend requests
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
pendingRequests.map((request) => (
|
||||||
|
<div
|
||||||
|
key={request.id}
|
||||||
|
className="flex items-center justify-between gap-3 p-3 rounded-lg border border-border hover:bg-accent/50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
|
<Avatar className="size-10 shrink-0">
|
||||||
|
<AvatarImage src={request.avatar} alt={request.username} />
|
||||||
|
<AvatarFallback className="bg-primary/20 text-primary-foreground font-semibold">
|
||||||
|
{request.username.charAt(0).toUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex flex-col min-w-0 flex-1">
|
||||||
|
<span className="text-sm font-medium truncate">
|
||||||
|
{request.username}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{formatTimeAgo(request.createdAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
<Button
|
||||||
|
size="icon-sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="size-8 text-green-500 hover:text-green-600 hover:bg-green-500/10"
|
||||||
|
onClick={() => handleAccept(request.id)}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon-sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="size-8 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||||
|
onClick={() => handleDecline(request.id)}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<XIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "sent" && (
|
||||||
|
<div className="flex flex-col gap-2 py-2">
|
||||||
|
{sentRequests.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
No sent friend requests
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
sentRequests.map((request) => (
|
||||||
|
<div
|
||||||
|
key={request.id}
|
||||||
|
className="flex items-center justify-between gap-3 p-3 rounded-lg border border-border"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
|
<Avatar className="size-10 shrink-0">
|
||||||
|
<AvatarImage src={request.avatar} alt={request.username} />
|
||||||
|
<AvatarFallback className="bg-primary/20 text-primary-foreground font-semibold">
|
||||||
|
{request.username.charAt(0).toUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex flex-col min-w-0 flex-1">
|
||||||
|
<span className="text-sm font-medium truncate">
|
||||||
|
{request.username}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Sent {formatTimeAgo(request.createdAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground shrink-0">
|
||||||
|
Pending...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
import { io, Socket } from "socket.io-client"
|
|
||||||
import { Button } from "./ui/button"
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
|
|
||||||
import { Input } from "./ui/input"
|
|
||||||
|
|
||||||
export default function SocketTest() {
|
|
||||||
const [socket, setSocket] = useState<Socket | null>(null)
|
|
||||||
const [isConnected, setIsConnected] = useState(false)
|
|
||||||
const [messages, setMessages] = useState<string[]>([])
|
|
||||||
const [inputMessage, setInputMessage] = useState("")
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Initialize Socket.IO client
|
|
||||||
const socketInstance = io()
|
|
||||||
|
|
||||||
socketInstance.on("connect", () => {
|
|
||||||
console.log("Connected to Socket.IO:", socketInstance.id)
|
|
||||||
setIsConnected(true)
|
|
||||||
setMessages(prev => [...prev, `✅ Connected: ${socketInstance.id}`])
|
|
||||||
})
|
|
||||||
|
|
||||||
socketInstance.on("disconnect", (reason) => {
|
|
||||||
console.log("Disconnected:", reason)
|
|
||||||
setIsConnected(false)
|
|
||||||
setMessages(prev => [...prev, `❌ Disconnected: ${reason}`])
|
|
||||||
})
|
|
||||||
|
|
||||||
socketInstance.on("message", (data) => {
|
|
||||||
console.log("Message received:", data)
|
|
||||||
setMessages(prev => [...prev, `📩 Received: ${data}`])
|
|
||||||
})
|
|
||||||
|
|
||||||
setSocket(socketInstance)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
socketInstance.disconnect()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const sendMessage = () => {
|
|
||||||
if (socket && inputMessage.trim()) {
|
|
||||||
socket.emit("message", inputMessage)
|
|
||||||
setMessages(prev => [...prev, `📤 Sent: ${inputMessage}`])
|
|
||||||
setInputMessage("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="w-full max-w-2xl">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Socket.IO Test Client</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Status: {isConnected ? (
|
|
||||||
<span className="text-green-600 font-semibold">🟢 Connected</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-red-600 font-semibold">🔴 Disconnected</span>
|
|
||||||
)}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter message..."
|
|
||||||
value={inputMessage}
|
|
||||||
onChange={(e) => setInputMessage(e.target.value)}
|
|
||||||
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
|
|
||||||
disabled={!isConnected}
|
|
||||||
/>
|
|
||||||
<Button onClick={sendMessage} disabled={!isConnected}>
|
|
||||||
Send
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border rounded-lg p-4 h-64 overflow-y-auto bg-muted/20">
|
|
||||||
<div className="space-y-1 font-mono text-sm">
|
|
||||||
{messages.length === 0 ? (
|
|
||||||
<p className="text-muted-foreground">No messages yet...</p>
|
|
||||||
) : (
|
|
||||||
messages.map((msg, idx) => (
|
|
||||||
<p key={idx} className="text-xs">{msg}</p>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -6,7 +6,9 @@ import {
|
||||||
GearSix,
|
GearSix,
|
||||||
MicrophoneSlash
|
MicrophoneSlash
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
|
import { useQuery } from "convex/react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { api } from "../../../../convex/_generated/api";
|
||||||
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";
|
||||||
|
|
@ -22,19 +24,21 @@ interface UserFloatingCardProps {
|
||||||
|
|
||||||
const statusColors: Record<UserStatus, string> = {
|
const statusColors: Record<UserStatus, string> = {
|
||||||
online: "bg-emerald-500",
|
online: "bg-emerald-500",
|
||||||
busy: "bg-amber-500",
|
busy: "bg-red-500",
|
||||||
away: "bg-yellow-500",
|
away: "bg-yellow-500",
|
||||||
offline: "bg-muted-foreground"
|
offline: "bg-muted-foreground"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function UserFloatingCard({
|
export default function UserFloatingCard(
|
||||||
user,
|
{ user }: 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 status = useQuery(api.auth.getUserStatus) as {
|
||||||
const activity = user.status?.activity;
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
isUserSet: boolean;
|
||||||
|
} | null;
|
||||||
|
|
||||||
// Close when clicking outside the trigger/content
|
// Close when clicking outside the trigger/content
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -113,7 +117,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",
|
||||||
status ? statusColors[status as UserStatus] : "bg-muted-foreground"
|
status ? statusColors[status.status as UserStatus] : "bg-muted-foreground"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -124,19 +128,12 @@ export default function UserFloatingCard({
|
||||||
{user.name}
|
{user.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{activity ? (
|
|
||||||
<div className="flex items-center gap-1 text-sm text-muted-foreground truncate">
|
|
||||||
<span className="text-[14px] leading-none">{"\u2022"}</span>
|
|
||||||
<span className="inline-flex items-center gap-1 text-[13px]">
|
|
||||||
<span className="text-foreground/80">{activity}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-1 text-xs text-muted-foreground/80 truncate italic">
|
<div className="flex items-center gap-1 text-xs text-muted-foreground/80 truncate italic">
|
||||||
<span className="text-[14px] leading-none">{"\u2022"}</span>
|
<span className="text-[14px] leading-none">{"\u2022"}</span>
|
||||||
<span>Activity status (coming soon)</span>
|
<span>Activity status (coming soon)</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
|
|
@ -156,10 +153,8 @@ export default function UserFloatingCard({
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex flex-col min-w-0">
|
<div className="flex flex-col min-w-0">
|
||||||
<span className="text-sm font-semibold text-foreground truncate">{user.name}</span>
|
<span className="text-sm font-semibold text-foreground truncate">{user.name}</span>
|
||||||
<span className="text-xs text-muted-foreground truncate capitalize">{status}</span>
|
<span className="text-xs text-muted-foreground truncate capitalize">{status?.status}</span>
|
||||||
<span className="text-xs text-muted-foreground truncate">
|
|
||||||
{activity ?? "Activity status (coming soon)"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,26 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useMutation } from "convex/react";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { io, Socket } from "socket.io-client";
|
import { io, Socket } from "socket.io-client";
|
||||||
|
import { api } from "../../convex/_generated/api";
|
||||||
|
|
||||||
|
interface UseSocketProps {
|
||||||
|
user: {
|
||||||
|
id?: string;
|
||||||
|
status: {
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
isUserSet: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refetchUser: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSocket({ user, refetchUser }: UseSocketProps) {
|
||||||
|
const updateUserStatus = useMutation(api.auth.updateUserStatus);
|
||||||
|
const socketRef = useRef<Socket | null>(null);
|
||||||
|
const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
export function useSocket(userId: string | undefined) {
|
|
||||||
const [socketStatus, setSocketStatus] = useState<SiPher.SocketStatus>("connecting");
|
const [socketStatus, setSocketStatus] = useState<SiPher.SocketStatus>("connecting");
|
||||||
const [socketInfo, setSocketInfo] = useState<SiPher.SocketInfo>({
|
const [socketInfo, setSocketInfo] = useState<SiPher.SocketInfo>({
|
||||||
ping: null,
|
ping: null,
|
||||||
|
|
@ -14,21 +31,81 @@ export function useSocket(userId: string | undefined) {
|
||||||
error: null
|
error: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Manual disconnect function
|
||||||
|
const disconnect = useCallback(() => {
|
||||||
|
if (socketRef.current) {
|
||||||
|
console.log("🔌 Manually disconnecting socket...");
|
||||||
|
socketRef.current.disconnect();
|
||||||
|
if (pingIntervalRef.current) {
|
||||||
|
clearInterval(pingIntervalRef.current);
|
||||||
|
pingIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
setSocketStatus("disconnected");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const connect = useCallback(() => {
|
||||||
|
if (socketRef.current) {
|
||||||
|
socketRef.current.connect();
|
||||||
|
refetchUser();
|
||||||
|
}
|
||||||
|
}, [refetchUser]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userId) return;
|
if (!user.id) return;
|
||||||
|
|
||||||
const socket: Socket = io({ withCredentials: false });
|
const socket: Socket = io({ withCredentials: false });
|
||||||
let pingInterval: NodeJS.Timeout | null = null;
|
socketRef.current = socket;
|
||||||
|
|
||||||
// Measure ping latency
|
// Measure ping latency using acknowledgment callback
|
||||||
const measurePing = () => {
|
const measurePing = () => {
|
||||||
const start = Date.now();
|
const clientTimestamp = Date.now();
|
||||||
socket.volatile.emit("ping", () => {
|
|
||||||
const latency = Date.now() - start;
|
// Use acknowledgment callback for reliable latency measurement
|
||||||
|
socket.timeout(5000).emit("ping", (err: Error, serverTimestamp: number) => {
|
||||||
|
if (err) {
|
||||||
|
console.warn("[Socket] Ping timeout or error:", err);
|
||||||
|
setSocketInfo((prev: SiPher.SocketInfo) => ({ ...prev, ping: null }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const latency = now - clientTimestamp;
|
||||||
|
console.log("[Socket] Ping latency:", latency);
|
||||||
setSocketInfo((prev: SiPher.SocketInfo) => ({ ...prev, ping: latency }));
|
setSocketInfo((prev: SiPher.SocketInfo) => ({ ...prev, ping: latency }));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function setUserDefaultStatus(
|
||||||
|
newStatus: "online" | "busy" | "offline" | "away",
|
||||||
|
oldStatus?: {
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
isUserSet: boolean;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (!oldStatus) {
|
||||||
|
console.log("🔌 User default status set to online");
|
||||||
|
updateUserStatus({ status: "online", isUserSet: false });
|
||||||
|
refetchUser();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newStatus === "offline") {
|
||||||
|
updateUserStatus({ status: newStatus, isUserSet: oldStatus.isUserSet });
|
||||||
|
refetchUser();
|
||||||
|
return;
|
||||||
|
} else if (!oldStatus.isUserSet) {
|
||||||
|
console.log("🔌 User default status set to online");
|
||||||
|
updateUserStatus({ status: newStatus, isUserSet: oldStatus.isUserSet });
|
||||||
|
refetchUser();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
updateUserStatus({ status: oldStatus.status, isUserSet: oldStatus.isUserSet });
|
||||||
|
refetchUser();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
socket.on("connect", () => {
|
socket.on("connect", () => {
|
||||||
console.log("✅ Connected to socket - Authentication successful!");
|
console.log("✅ Connected to socket - Authentication successful!");
|
||||||
setSocketStatus("connected");
|
setSocketStatus("connected");
|
||||||
|
|
@ -41,9 +118,11 @@ export function useSocket(userId: string | undefined) {
|
||||||
error: null
|
error: null
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Start ping measurement every 5 seconds
|
setUserDefaultStatus("online", user.status);
|
||||||
|
|
||||||
|
// Start ping measurement every 5 seconds for latency display
|
||||||
measurePing();
|
measurePing();
|
||||||
pingInterval = setInterval(measurePing, 5000);
|
pingIntervalRef.current = setInterval(measurePing, 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update transport when it upgrades (polling -> websocket)
|
// Update transport when it upgrades (polling -> websocket)
|
||||||
|
|
@ -53,6 +132,7 @@ export function useSocket(userId: string | undefined) {
|
||||||
|
|
||||||
socket.on("connect_error", (err) => {
|
socket.on("connect_error", (err) => {
|
||||||
console.error("❌ Socket connection error:", err.message);
|
console.error("❌ Socket connection error:", err.message);
|
||||||
|
setUserDefaultStatus("offline", user.status);
|
||||||
setSocketStatus("error");
|
setSocketStatus("error");
|
||||||
setSocketInfo((prev: SiPher.SocketInfo) => ({
|
setSocketInfo((prev: SiPher.SocketInfo) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -65,6 +145,7 @@ export function useSocket(userId: string | undefined) {
|
||||||
|
|
||||||
socket.on("disconnect", (reason) => {
|
socket.on("disconnect", (reason) => {
|
||||||
console.log("🔌 Disconnected from socket:", reason);
|
console.log("🔌 Disconnected from socket:", reason);
|
||||||
|
setUserDefaultStatus("offline", user.status);
|
||||||
setSocketStatus("disconnected");
|
setSocketStatus("disconnected");
|
||||||
setSocketInfo((prev: SiPher.SocketInfo) => ({
|
setSocketInfo((prev: SiPher.SocketInfo) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -72,7 +153,10 @@ export function useSocket(userId: string | undefined) {
|
||||||
connectedAt: null,
|
connectedAt: null,
|
||||||
error: reason
|
error: reason
|
||||||
}));
|
}));
|
||||||
if (pingInterval) clearInterval(pingInterval);
|
if (pingIntervalRef.current) {
|
||||||
|
clearInterval(pingIntervalRef.current);
|
||||||
|
pingIntervalRef.current = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle pong response for ping measurement
|
// Handle pong response for ping measurement
|
||||||
|
|
@ -81,11 +165,14 @@ export function useSocket(userId: string | undefined) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (pingInterval) clearInterval(pingInterval);
|
if (pingIntervalRef.current) {
|
||||||
|
clearInterval(pingIntervalRef.current);
|
||||||
|
pingIntervalRef.current = null;
|
||||||
|
}
|
||||||
socket.disconnect();
|
socket.disconnect();
|
||||||
};
|
};
|
||||||
}, [userId]);
|
}, [user.id, updateUserStatus]);
|
||||||
|
|
||||||
return { socketStatus, socketInfo };
|
return { socketStatus, socketInfo, disconnect, connect };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export const authClient = createAuthClient({
|
||||||
convexClient(),
|
convexClient(),
|
||||||
usernameClient(),
|
usernameClient(),
|
||||||
oneTimeTokenClient(),
|
oneTimeTokenClient(),
|
||||||
inferAdditionalFields<typeof auth>()
|
inferAdditionalFields<typeof auth>(),
|
||||||
],
|
],
|
||||||
sessionOptions: {
|
sessionOptions: {
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ interface DmMessage {
|
||||||
|
|
||||||
const dmEvent: SiPher.EventsType = {
|
const dmEvent: SiPher.EventsType = {
|
||||||
name: "dm",
|
name: "dm",
|
||||||
description: "Send a direct message to another user",
|
description: "Send a direct message to another user using the client-side encryption",
|
||||||
category: "user",
|
category: "user",
|
||||||
type: "message",
|
type: "message",
|
||||||
handler: (socket: Socket, io: SocketIOServer, data: DmMessage) => {
|
handler: (socket: Socket, io: SocketIOServer, data: DmMessage) => {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ export default {
|
||||||
handler: (socket: Socket, io: SocketIOServer, ...args: any[]) => {
|
handler: (socket: Socket, io: SocketIOServer, ...args: any[]) => {
|
||||||
console.log("Message received", args)
|
console.log("Message received", args)
|
||||||
},
|
},
|
||||||
description: "A message event",
|
description: "Send a message to a channel by using the server-side encryption",
|
||||||
category: "user",
|
category: "server",
|
||||||
type: "message"
|
type: "message"
|
||||||
} satisfies SiPher.EventsType
|
} satisfies SiPher.EventsType
|
||||||
24
src/lib/sockets/events/ping.ts
Normal file
24
src/lib/sockets/events/ping.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
/**
|
||||||
|
* @fileoverview Ping event handler for measuring latency and checking connection health
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Socket, Server as SocketIOServer } from "socket.io";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ping",
|
||||||
|
description: "Handles client ping requests and returns pong with timestamp for latency measurement",
|
||||||
|
category: "system",
|
||||||
|
type: "custom",
|
||||||
|
handler: (socket: Socket, io: SocketIOServer, callback?: (serverTimestamp: number) => void) => {
|
||||||
|
const serverTimestamp = Date.now();
|
||||||
|
|
||||||
|
// Use acknowledgment callback if provided (more reliable than emit)
|
||||||
|
if (callback && typeof callback === "function") {
|
||||||
|
callback(serverTimestamp);
|
||||||
|
} else {
|
||||||
|
// Fallback to emit if no callback
|
||||||
|
socket.emit("pong", serverTimestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} satisfies SiPher.EventsType;
|
||||||
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
* @fileoverview Socket Manager Class for handling socket connections and events at the server side.
|
* @fileoverview Socket Manager Class for handling socket connections and events at the server side.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Session, User } from "better-auth";
|
||||||
import { existsSync, readdirSync } from "fs";
|
import { existsSync, readdirSync } from "fs";
|
||||||
import type { Server as HTTPServer } from "http";
|
import type { Server as HTTPServer } from "http";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
@ -41,7 +42,11 @@ export default class SocketManager {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this.socketIo) {
|
if (!this.socketIo) {
|
||||||
this.socketIo = new SocketIOServer(nextServer)
|
this.socketIo = new SocketIOServer(nextServer, {
|
||||||
|
// Configure Socket.IO's built-in heartbeat mechanism
|
||||||
|
pingInterval: 25000, // Server sends ping every 25 seconds
|
||||||
|
pingTimeout: 60000, // Close connection if no pong received within 60 seconds
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.requireAuth) {
|
if (this.options.requireAuth) {
|
||||||
|
|
@ -54,7 +59,7 @@ export default class SocketManager {
|
||||||
|
|
||||||
this.socketIo.use(async (socket, next) => {
|
this.socketIo.use(async (socket, next) => {
|
||||||
try {
|
try {
|
||||||
let result: { user?: unknown; session?: unknown } | null = null;
|
let result: { user?: User, session?: Session } | null = null;
|
||||||
|
|
||||||
if (this.options.authMethod === "ott") {
|
if (this.options.authMethod === "ott") {
|
||||||
// OTT-based auth: client must provide token in auth object
|
// OTT-based auth: client must provide token in auth object
|
||||||
|
|
@ -94,14 +99,15 @@ export default class SocketManager {
|
||||||
return next(new Error("Authentication error: Invalid session"));
|
return next(new Error("Authentication error: Invalid session"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = result.user as { id: string; email: string; name?: string };
|
const { user, session } = result;
|
||||||
|
|
||||||
// Set socket.id to user ID for persistent identification
|
// Set socket.id to user ID for persistent identification
|
||||||
(socket as any).id = user.id;
|
// @ts-expect-error: This should be a readonly property, but IDGAF, if it breaks, it breaks :D
|
||||||
|
socket.id = user.id;
|
||||||
|
|
||||||
// Attach user and session to socket for use in event handlers
|
// Attach user and session to socket for use in event handlers
|
||||||
(socket as any).user = user;
|
socket.user = user;
|
||||||
(socket as any).session = result.session;
|
socket.session = session;
|
||||||
|
|
||||||
next();
|
next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
21
src/types/globals.d.ts
vendored
21
src/types/globals.d.ts
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Session, User } from "better-auth";
|
||||||
import { Socket, Server as SocketIOServer } from "socket.io";
|
import { Socket, Server as SocketIOServer } from "socket.io";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
@ -111,16 +112,18 @@ declare global {
|
||||||
id: string,
|
id: string,
|
||||||
system: System
|
system: System
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageEvent = {
|
|
||||||
message: {
|
|
||||||
/** Will either be a raw string or a encrypted blob, if it is a encrypted blob, the iv will be provided */
|
|
||||||
content: string,
|
|
||||||
iv?: string
|
|
||||||
},
|
|
||||||
from: SipherUser,
|
|
||||||
recipient: MessageRecipient
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add custom socket.io types
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend Socket.io types to include authenticated user data
|
||||||
|
declare module "socket.io" {
|
||||||
|
interface Socket {
|
||||||
|
/** Authenticated user from Better Auth (set after auth middleware) */
|
||||||
|
user?: User;
|
||||||
|
/** Session data from Better Auth (set after auth middleware) */
|
||||||
|
session?: Session;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
20
src/types/messages/encrypted.d.ts
vendored
Normal file
20
src/types/messages/encrypted.d.ts
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
declare global {
|
||||||
|
declare namespace SiPher.Messages.ClientEncrypted {
|
||||||
|
type EncryptedMessage = {
|
||||||
|
content: string,
|
||||||
|
iv?: string
|
||||||
|
}
|
||||||
|
type MessageEvent = {
|
||||||
|
message: {
|
||||||
|
/** Will either be a raw string or a encrypted blob, if it is a encrypted blob, the iv will be provided */
|
||||||
|
content: string,
|
||||||
|
iv?: string
|
||||||
|
},
|
||||||
|
from: SipherUser,
|
||||||
|
recipient: MessageRecipient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { }
|
||||||
|
|
||||||
24
src/types/messages/unencrypted.d.ts
vendored
Normal file
24
src/types/messages/unencrypted.d.ts
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import type { Collection, Doc } from "convex/server";
|
||||||
|
import type { DMChannel, GlobalChannel, GuildChannel, RegionalChannel } from "./channels";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
declare namespace SiPher.Messages.ServerEncrypted {
|
||||||
|
type DBMessageType = Doc<"messages">;
|
||||||
|
type DBAttachmentType = Doc<"attachments">;
|
||||||
|
type ServerEncryptedMessage = Omit<DBMessageType, "authorId" | "channelId" | "guildId"> & {
|
||||||
|
author: SipherUser,
|
||||||
|
channel: GuildChannel | RegionalChannel | GlobalChannel | DMChannel,
|
||||||
|
guild: Server | null,
|
||||||
|
attachments: Collection<string, DBAttachmentType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerEncryptedMessageEvent = {
|
||||||
|
message: DBMessageType,
|
||||||
|
from: SipherUser,
|
||||||
|
recipient: MessageRecipient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
export { };
|
||||||
|
|
||||||
2
src/types/sidebar.d.ts
vendored
2
src/types/sidebar.d.ts
vendored
|
|
@ -6,6 +6,8 @@ declare global {
|
||||||
socketStatus: SocketStatus;
|
socketStatus: SocketStatus;
|
||||||
socketInfo: SocketInfo;
|
socketInfo: SocketInfo;
|
||||||
currentChannel?: SiPher.Channel;
|
currentChannel?: SiPher.Channel;
|
||||||
|
disconnectSocket: () => void;
|
||||||
|
connectSocket: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SidebarItem {
|
interface SidebarItem {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue