Skip to content

Commit

Permalink
Merge pull request #619 from FleetAdmiralJakob/clear-chat-requests
Browse files Browse the repository at this point in the history
  • Loading branch information
FleetAdmiralJakob authored Nov 3, 2024
2 parents f19f7b4 + 51e2734 commit f8c6eb2
Show file tree
Hide file tree
Showing 10 changed files with 551 additions and 321 deletions.
4 changes: 4 additions & 0 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import type {
FunctionReference,
} from "convex/server";
import type * as chats from "../chats.js";
import type * as clearRequests from "../clearRequests.js";
import type * as crons from "../crons.js";
import type * as lib_functions from "../lib/functions.js";
import type * as lib_types from "../lib/types.js";
import type * as messages from "../messages.js";
Expand All @@ -29,6 +31,8 @@ import type * as users from "../users.js";
*/
declare const fullApi: ApiFromModules<{
chats: typeof chats;
clearRequests: typeof clearRequests;
crons: typeof crons;
"lib/functions": typeof lib_functions;
"lib/types": typeof lib_types;
messages: typeof messages;
Expand Down
94 changes: 52 additions & 42 deletions convex/chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,63 +104,73 @@ export const getChats = query({
return null;
}

return await ctx
return ctx
.table("users")
.getX("clerkId", identity.tokenIdentifier)
.edge("privateChats")
.map(async (chat) => {
const messages = await chat.edge("messages");
const sortedMessages = messages.sort(
const textMessages = await chat
.edge("messages")
.map(async (message) => {
return {
...message,
readBy: await message.edge("readBy"),
type: "message" as const,
};
});
const requests = await chat
.edge("clearRequests")
.map(async (request) => {
return {
...request,
type: `${request.status}Request` as const,
clerkId: (await ctx.table("users").getX(request.userId)).clerkId,
};
});

const allMessages = [...textMessages, ...requests];

const sortedMessages = allMessages.sort(
(a, b) => b._creationTime - a._creationTime,
);
const latestMessage = sortedMessages[0];
const readBy = latestMessage ? await latestMessage.edge("readBy") : [];

const extendedMessagesPromises = sortedMessages.map(async (message) => {
return {
...message,
readBy: await message.edge("readBy"),
deleted: message.deleted,
};
});

const extendedMessages = await Promise.all(extendedMessagesPromises);

const sortedMessagesAgain = extendedMessages.sort(
(a, b) => b._creationTime - a._creationTime,
);

let deletedCount = 0;
const firstReadMessageIndex = sortedMessagesAgain.findIndex(
(message) => {
if (message.deleted) {
deletedCount++;
}
return (
message.readBy.some(
(user) => user.clerkId === identity.tokenIdentifier,
) && !message.deleted
);
},
);

let numberOfUnreadMessages;
if (firstReadMessageIndex === -1) {
numberOfUnreadMessages = sortedMessages.length - deletedCount;
} else {
numberOfUnreadMessages = firstReadMessageIndex - deletedCount;
let firstReadMessageIndex = -1;
for (let i = 0; i < sortedMessages.length; i++) {
const message = sortedMessages[i];
if (!message) continue;

if (
(message.type === "message" && message.deleted) ||
(message.type !== "message" &&
(await ctx.table("users").getX(message.userId)).clerkId ===
identity.tokenIdentifier)
) {
deletedCount++;
}
const isReadMessage =
message.type === "message" &&
message.readBy.some(
(user) => user.clerkId === identity.tokenIdentifier,
) &&
!message.deleted;
if (isReadMessage) {
firstReadMessageIndex = i;
break;
}
}

const numberOfUnreadMessages =
firstReadMessageIndex === -1
? sortedMessages.length - deletedCount
: firstReadMessageIndex - deletedCount;

return {
...chat,
users: await chat.edge("users"),
numberOfUnreadMessages: numberOfUnreadMessages,
lastMessage: latestMessage
? {
...latestMessage,
readBy,
}
: null,
lastMessage: latestMessage,
};
});
},
Expand Down
173 changes: 173 additions & 0 deletions convex/clearRequests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { internalMutation, mutation } from "./lib/functions";
import { ConvexError, v } from "convex/values";

export const createClearRequest = mutation({
args: { chatId: v.string() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();

if (identity === null) {
console.error("Unauthenticated call to mutation");
return null;
}

const convexUser = await ctx
.table("users")
.get("clerkId", identity.tokenIdentifier);

const parsedChatId = ctx.table("privateChats").normalizeId(args.chatId);

if (!parsedChatId) {
throw new ConvexError("chatId was invalid");
}

if (!convexUser) {
throw new ConvexError(
"Mismatch between Clerk and Convex. This is an error by us.",
);
}

const usersInChat = await ctx
.table("privateChats")
.getX(parsedChatId)
.edge("users");

if (
!usersInChat.some((user) => user.clerkId === identity.tokenIdentifier)
) {
throw new ConvexError(
"UNAUTHORIZED REQUEST: User tried to create a request in a chat in which he is not in.",
);
}

const openRequests = await ctx
.table("privateChats")
.get(parsedChatId)
.edge("clearRequests")
.filter((q) => q.eq(q.field("status"), "pending"));

if (openRequests && openRequests?.length > 0) {
throw new ConvexError("There is already at least one open request.");
}

await ctx.table("clearRequests").insert({
userId: convexUser._id,
privateChatId: parsedChatId,
status: "pending",
});
},
});

export const rejectClearRequest = mutation({
args: { requestId: v.string(), chatId: v.string() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();

if (identity === null) {
console.error("Unauthenticated call to mutation");
return null;
}

const parsedRequestId = ctx
.table("clearRequests")
.normalizeId(args.requestId);

if (!parsedRequestId) {
throw new ConvexError("chatId was invalid");
}

const request = await ctx.table("clearRequests").getX(parsedRequestId);

const usersInChat = await ctx
.table("privateChats")
.getX(request.privateChatId)
.edge("users");

if (
!usersInChat.some((user) => user.clerkId === identity.tokenIdentifier)
) {
throw new ConvexError(
"UNAUTHORIZED REQUEST: User tried to reject a clear request in a chat in which he is not in.",
);
}

if ((await request.edge("user")).clerkId === identity.tokenIdentifier) {
throw new ConvexError(
"UNAUTHORIZED REQUEST: User tried to reject his own clear request.",
);
}

await request.patch({
status: "rejected",
});
},
});

export const acceptClearRequest = mutation({
args: { requestId: v.string() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();

if (identity === null) {
console.error("Unauthenticated call to mutation");
return null;
}

const parsedRequestId = ctx
.table("clearRequests")
.normalizeId(args.requestId);

if (!parsedRequestId) {
throw new ConvexError("requestId was invalid");
}

const request = await ctx.table("clearRequests").getX(parsedRequestId);

const usersInChat = await ctx
.table("privateChats")
.getX(request.privateChatId)
.edge("users");

if (
!usersInChat.some((user) => user.clerkId === identity.tokenIdentifier)
) {
throw new ConvexError(
"UNAUTHORIZED REQUEST: User tried to accept a clear request in a chat in which he is not in.",
);
}

if ((await request.edge("user")).clerkId === identity.tokenIdentifier) {
throw new ConvexError(
"UNAUTHORIZED REQUEST: User tried to accept his own clear request.",
);
}

const chat = ctx.table("privateChats").getX(request.privateChatId);
const messagesInChat = await chat.edge("messages");
const requestsInChat = await chat.edge("clearRequests");

for (const message of messagesInChat) {
await message.delete();
}

for (const request of requestsInChat) {
await request.delete();
}
},
});

export const expirePendingRequests = internalMutation({
handler: async (ctx) => {
for (const q1 of await ctx
.table("clearRequests", "by_creation_time", (q) =>
q.lte("_creationTime", Date.now() - 24 * 60 * 60 * 1000),
)
.filter((q) => q.eq(q.field("status"), "pending"))) {
if (q1) {
await q1.patch({
status: "expired",
});
}
}
},
});
12 changes: 12 additions & 0 deletions convex/crons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";

const crons = cronJobs();

crons.interval(
"expire open requests",
{ minutes: 1 },
internal.clearRequests.expirePendingRequests,
);

export default crons;
Loading

0 comments on commit f8c6eb2

Please sign in to comment.