Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix all problems with the request of clearing a chat #619

Merged
merged 10 commits into from
Nov 3, 2024
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