Skip to content

Commit

Permalink
feat: edit messages
Browse files Browse the repository at this point in the history
  • Loading branch information
FleetAdmiralJakob committed Nov 5, 2024
1 parent e782e60 commit 90da0d6
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 27 deletions.
38 changes: 34 additions & 4 deletions convex/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,15 @@ export const createMessage = mutation({
);
}

if (args.content.trim() === "") throw new Error("Post cannot be empty");
if (args.content.trim() === "") throw new Error("Message cannot be empty");

await ctx.table("messages").insert({
userId: convexUser._id,
privateChatId: parsedChatId,
content: args.content.trim(),
deleted: false,
readBy: [convexUser._id],
modified: false,
});
},
});
Expand All @@ -121,16 +122,17 @@ export const deleteMessage = mutation({
}

const message = await ctx.table("messages").getX(parsedMessageId);
const chatId = message.privateChatId;
const chat = await ctx.table("privateChats").getX(chatId);
const usersInChat = await chat.edge("users");

if ((await message.edge("user")).clerkId !== identity.tokenIdentifier) {
throw new ConvexError(
"UNAUTHORIZED REQUEST: User tried to delete a message from another person.",
);
}

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

await message.patch({
content: "",
deleted: true,
Expand Down Expand Up @@ -177,3 +179,31 @@ export const markMessageRead = mutation({
return { success: true };
},
});

export const editMessage = mutation({
args: { messageId: v.id("messages"), newContent: v.string() },
handler: async (ctx, args) => {
if (args.newContent.trim() === "")
throw new Error("Message cannot be empty");

const identity = await ctx.auth.getUserIdentity();

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

const message = await ctx.table("messages").getX(args.messageId);

if ((await message.edge("user")).clerkId !== identity.tokenIdentifier) {
throw new ConvexError(
"UNAUTHORIZED REQUEST: User tried to edit a message from another person.",
);
}

await message.patch({
content: args.newContent.trim(),
modified: true,
});
},
});
1 change: 1 addition & 0 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const schema = defineEntSchema({
messages: defineEnt({})
.field("content", v.string())
.field("deleted", v.boolean(), { default: false })
.field("modified", v.boolean(), { default: false })
.edge("privateChat")
.edge("user")
.edges("readBy", {
Expand Down
152 changes: 134 additions & 18 deletions src/app/(internal-sites)/chats/[chatId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
Plus,
SendHorizontal,
Video,
X,
} from "lucide-react";
import { useRouter } from "next/navigation";
import React, { use, useCallback, useEffect, useRef, useState } from "react";
Expand Down Expand Up @@ -82,7 +83,7 @@ const useScrollBehavior = (
const { scrollTop, scrollHeight, clientHeight } = messagesEndRef.current;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;

// Consider "near bottom" if within 100px of bottom
// Consider "near bottom" if within 100 px of the bottom
const nearBottom = distanceFromBottom < 100;
setIsNearBottom(nearBottom);
}
Expand Down Expand Up @@ -121,13 +122,16 @@ export default function Page(props: { params: Promise<{ chatId: string }> }) {
const params = use(props.params);
const [progress, setProgress] = React.useState(13);

const [editingMessageId, setEditingMessageId] =
useState<Id<"messages"> | null>(null);

const [selectedMessageId, setSelectedMessageId] = useState<string | null>(
null,
);

const router = useRouter();

React.useEffect(() => {
useEffect(() => {
const timer = setTimeout(() => setProgress(66), 500);
return () => clearTimeout(timer);
}, []);
Expand Down Expand Up @@ -159,6 +163,7 @@ export default function Page(props: { params: Promise<{ chatId: string }> }) {
from: userInfo.data,
readBy: [userInfo.data],
sent: false,
modified: false,
};
localStore.setQuery(api.messages.getMessages, { chatId }, [
...(Array.isArray(existingMessages) ? existingMessages : []),
Expand All @@ -184,6 +189,65 @@ export default function Page(props: { params: Promise<{ chatId: string }> }) {
}
});

const editMessage = useMutation(
api.messages.editMessage,
).withOptimisticUpdate((localStore, args) => {
const chatId: Id<"privateChats"> = params.chatId as Id<"privateChats">;
const newContent = args.newContent;

const existingMessages = localStore.getQuery(api.messages.getMessages, {
chatId,
});
const existingChats = localStore.getQuery(api.chats.getChats);
// If we've loaded the api.messages.getMessages and api.chats.getChats query, push an optimistic message
// onto the lists.
if (existingMessages && existingChats) {
localStore.setQuery(
api.messages.getMessages,
{ chatId },
existingMessages.map((message) => {
if (message.type === "message" && message._id === args.messageId) {
return {
...message,
content: newContent,
modified: true,
};
} else {
return message;
}
}),
);

const lastMessage = existingChats?.find(
(chat) => chat._id === chatId,
)?.lastMessage;

if (
lastMessage?._id === args.messageId &&
lastMessage.type === "message"
) {
localStore.setQuery(
api.chats.getChats,
{},
existingChats.map((chat) => {
if (chat._id === chatId) {
return {
...chat,
lastMessage: {
...lastMessage,
newContent,
modified: true,
},
};
} else {
return chat;
}
}),
);
}
}
});

const userInfo = useQueryWithStatus(api.users.getUserData, {});

const messages = useQueryWithStatus(api.messages.getMessages, {
Expand All @@ -207,9 +271,9 @@ export default function Page(props: { params: Promise<{ chatId: string }> }) {
}
}, [userInfo, messages, chatInfo, router]);

const is2xlOrmore = useMediaQuery({ query: "(max-width: 1537px)" });
const maxSize = is2xlOrmore ? 50 : 60;
const minSize = is2xlOrmore ? 45 : 30;
const is2xlOrMore = useMediaQuery({ query: "(max-width: 1537px)" });
const maxSize = is2xlOrMore ? 50 : 60;
const minSize = is2xlOrMore ? 45 : 30;

const textMessageForm = useForm<z.infer<typeof textMessageSchema>>({
resolver: zodResolver(textMessageSchema),
Expand All @@ -221,6 +285,21 @@ export default function Page(props: { params: Promise<{ chatId: string }> }) {
const [animationInput, setAnimationInput] = useState(true);

const formRef = useRef<HTMLFormElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);

useEffect(() => {
if (editingMessageId) {
const message = messages.data?.find((message) => {
return message._id === editingMessageId;
});
if (message && message.type === "message") {
setInputValue(message.content);
inputRef.current?.focus();
} else {
console.error("Message not found");
}
}
}, [editingMessageId, messages.data]);

const [inputValue, setInputValue] = useState("");

Expand All @@ -231,7 +310,26 @@ export default function Page(props: { params: Promise<{ chatId: string }> }) {
async function onTextMessageFormSubmit(
values: z.infer<typeof textMessageSchema>,
) {
void sendMessage({ content: values.message, chatId: params.chatId });
if (editingMessageId) {
const message = messages.data?.find((message) => {
return message._id === editingMessageId;
});

if (
!(
message?.type === "message" &&
message.content === values.message.trim()
)
) {
void editMessage({
newContent: values.message,
messageId: editingMessageId,
});
}
setEditingMessageId(null);
} else {
void sendMessage({ content: values.message, chatId: params.chatId });
}
textMessageForm.reset();
setInputValue("");
scrollToBottom();
Expand Down Expand Up @@ -283,7 +381,7 @@ export default function Page(props: { params: Promise<{ chatId: string }> }) {
<div className="flex h-20 w-full items-center justify-between bg-primary py-6">
<div className="text-lg lg:hidden">
<ChevronLeft
className="ml-2 mr-1"
className="ml-2 mr-1 cursor-pointer"
onClick={() => {
router.back();
}}
Expand Down Expand Up @@ -371,6 +469,7 @@ export default function Page(props: { params: Promise<{ chatId: string }> }) {
<Message
selectedMessageId={selectedMessageId}
setSelectedMessageId={setSelectedMessageId}
setEditingMessageId={setEditingMessageId}
message={message}
/>
</React.Fragment>
Expand Down Expand Up @@ -403,10 +502,10 @@ export default function Page(props: { params: Promise<{ chatId: string }> }) {
</div>

<div className="flex h-28 w-full items-center justify-start bg-primary p-4 pb-10 lg:h-24 lg:pb-4">
<div className="flex w-full justify-between">
<div className="flex w-full justify-between gap-8">
<Form {...textMessageForm}>
<form
className="w-10/12"
className="w-full"
ref={formRef}
onSubmit={textMessageForm.handleSubmit(
onTextMessageFormSubmit,
Expand All @@ -422,14 +521,17 @@ export default function Page(props: { params: Promise<{ chatId: string }> }) {
control={textMessageForm.control}
render={({ field }) => (
<Input
className="ml-4 h-11 w-10/12 rounded-2xl border-2 border-secondary-foreground bg-secondary p-2 lg:h-16"
className="h-11 w-full rounded-2xl border-2 border-secondary-foreground bg-secondary p-2 lg:h-16"
placeholder="Message ..."
value={inputValue}
onChange={(e) => {
handleChange(e);
field.onChange(e);
}}
ref={field.ref}
ref={(e) => {
field.ref(e);
inputRef.current = e;
}}
/>
)}
/>
Expand All @@ -438,13 +540,26 @@ export default function Page(props: { params: Promise<{ chatId: string }> }) {
/>
</form>
</Form>
<div className="flex items-center">
<div className="flex items-center gap-8">
<Mic
className={cn(
"mx-4 h-11 w-11 cursor-pointer rounded-sm border-2 border-secondary-foreground bg-primary p-2 lg:h-14 lg:w-14 lg:p-3",
{ "hidden lg:flex": inputValue != "" },
"h-11 w-11 cursor-pointer rounded-sm border-2 border-secondary-foreground bg-primary p-2 lg:h-14 lg:w-14 lg:p-3",
{ hidden: inputValue !== "" },
)}
/>

<X
className={cn(
"h-11 w-11 cursor-pointer rounded-sm border-2 border-secondary-foreground bg-primary p-2 lg:h-14 lg:w-14 lg:p-3",
{ hidden: editingMessageId === null },
)}
onClick={() => {
setEditingMessageId(null);
textMessageForm.reset();
setInputValue("");
}}
/>

<SendHorizontal
onClick={(e) => {
setAnimationInput(!animationInput);
Expand All @@ -453,14 +568,15 @@ export default function Page(props: { params: Promise<{ chatId: string }> }) {
);
}}
className={cn(
"mx-4 h-11 w-11 cursor-pointer rounded-sm border-2 border-secondary-foreground bg-primary p-2 lg:hidden lg:h-14 lg:w-14 lg:p-3",
{ hidden: inputValue == "" },
"h-11 w-11 cursor-pointer rounded-sm border-2 border-secondary-foreground bg-primary p-2 lg:h-14 lg:w-14 lg:p-3",
{ hidden: inputValue === "" },
)}
/>

<Plus
className={cn(
"mx-4 h-11 w-11 cursor-pointer rounded-sm border-2 border-secondary-foreground bg-primary p-2 lg:h-14 lg:w-14 lg:p-3",
{ "hidden lg:flex": inputValue != "" },
"h-11 w-11 cursor-pointer rounded-sm border-2 border-secondary-foreground bg-primary p-2 lg:h-14 lg:w-14 lg:p-3",
{ hidden: inputValue !== "" },
)}
onClick={menuClick}
/>
Expand Down
Loading

0 comments on commit 90da0d6

Please sign in to comment.