Skip to content

Commit

Permalink
image upload in convex diles db added, messages create and get endpoi…
Browse files Browse the repository at this point in the history
…nt (including populating messages) with hooks and integration
  • Loading branch information
Diivvuu committed Oct 14, 2024
1 parent cc746ce commit 1285d58
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 20 deletions.
2 changes: 2 additions & 0 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type * as channels from "../channels.js";
import type * as http from "../http.js";
import type * as members from "../members.js";
import type * as messages from "../messages.js";
import type * as upload from "../upload.js";
import type * as users from "../users.js";
import type * as workspaces from "../workspaces.js";

Expand All @@ -37,6 +38,7 @@ declare const fullApi: ApiFromModules<{
http: typeof http;
members: typeof members;
messages: typeof messages;
upload: typeof upload;
users: typeof users;
workspaces: typeof workspaces;
}>;
Expand Down
103 changes: 99 additions & 4 deletions convex/messages.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,60 @@
import { v } from "convex/values";
import { mutation, QueryCtx } from "./_generated/server";
import { mutation, query, QueryCtx } from "./_generated/server";
import { getAuthUserId } from "@convex-dev/auth/server";
import { Id } from "./_generated/dataModel";
import { timeStamp } from "console";
import { paginationOptsValidator } from "convex/server";

const populateThread = async (ctx: QueryCtx, messageId: Id<"messages">) => {
const messages = await ctx.db
.query("messages")
.withIndex("by_parent_message_id", (q) =>
q.eq("parentMessageId", messageId)
)
.collect();

//if there is any message with parent message then it will come in replies only
if (messages.length === 0) {
return {
count: 0,
image: undefined,
timeStamp: 0,
};
}

const lastMessage = messages[messages.length - 1];
const lastMessageMember = await populateMember(ctx, lastMessage.memberId);

if (!lastMessageMember) {
return {
count: 0,
image: undefined,
timeStamp: 0,
};
}

const lastMessageUser = await populateUser(ctx, lastMessageMember.userId);
return {
count: messages.length,
image: lastMessageUser?.image,
timeStamp: lastMessage._creationTime,
};
};

const populateReactions = (ctx: QueryCtx, messageId: Id<"messages">) => {
return ctx.db
.query("reactions")
.withIndex("by_message_id", (q) => q.eq("messageId", messageId))
.collect();
};

const populateUser = (ctx: QueryCtx, userId: Id<"users">) => {
return ctx.db.get(userId);
};

const populateMember = (ctx: QueryCtx, memberId: Id<"members">) => {
return ctx.db.get(memberId);
};

const getMember = async (
ctx: QueryCtx,
Expand All @@ -16,14 +69,48 @@ const getMember = async (
.unique();
};

export const get = query({
args: {
channelId: v.optional(v.id("channels")),
conversationId: v.optional(v.id("conversations")),
parentMessageId: v.optional(v.id("messages")),
paginationOpts: paginationOptsValidator,
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Unauthorized");

let _conversationId = args.conversationId;
if (!args.conversationId && !args.channelId && args.parentMessageId) {
const parentMessage = await ctx.db.get(args.parentMessageId);
if (!parentMessage) throw new Error("Parent message not found");

_conversationId = parentMessage.conversationId;
}

const results = await ctx.db
.query("messages")
.withIndex("by_channel_id_parent_message_id_conversation_id", (q) =>
q
.eq("channelId", args.channelId)
.eq("parentMessageId", args.parentMessageId)
.eq("conversationId", _conversationId)
)
.order("desc")
.paginate(args.paginationOpts);

return results;
},
});

export const create = mutation({
args: {
body: v.string(),
image: v.optional(v.id("_storage")),
workspaceId: v.id("workspaces"),
channelId: v.optional(v.id("channels")),
parentMessageId: v.optional(v.id("messages")),
//todo : add convo id
conversationId: v.optional(v.id("conversations")),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
Expand All @@ -32,12 +119,20 @@ export const create = mutation({
const member = await getMember(ctx, args.workspaceId, userId);
if (!member) throw new Error("Unauthorized");

//todo : handle convo id
let _conversationId = args.conversationId;

//this is only possible when in thread (i.e. conversation)
if (!args.conversationId && !args.channelId && args.parentMessageId) {
const parentMessage = await ctx.db.get(args.parentMessageId);
if (!parentMessage) throw new Error("Parent message not found");
_conversationId = parentMessage.conversationId;
}

const messageId = await ctx.db.insert("messages", {
memberId: member._id,
body: args.body,
image: args.image,
channeld: args.channelId,
channelId: args.channelId,
workspaceId: args.workspaceId,
parentMessageId: args.parentMessageId,
updatedAt: Date.now(),
Expand Down
31 changes: 28 additions & 3 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,41 @@ const schema = defineSchema({
workspaceId: v.id("workspaces"),
}).index("by_workspace_id", ["workspaceId"]),

conversations: defineTable({
workspaceId: v.id("workspaces"),
memberOneId: v.id("members"),
memberTwoId: v.id("members"),
}).index("by_workspace_id", ["workspaceId"]),

messages: defineTable({
body: v.string(),
image: v.optional(v.id("_storage")),
memberId: v.id("members"),
workspaceId: v.id("workspaces"),
channeld: v.optional(v.id("channels")),
channelId: v.optional(v.id("channels")),
parentMessageId: v.optional(v.id("messages")),
//todo add convo id
conversationId: v.optional(v.id("conversations")),
updatedAt: v.number(),
}),
})
.index("by_workspace_id", ["workspaceId"])
.index("by_member_id", ["memberId"])
.index("by_channel_id", ["channelId"])
.index("by_conversation_id", ["conversationId"])
.index("by_parent_message_id", ["parentMessageId"])
.index("by_channel_id_parent_message_id_conversation_id", [
"channelId",
"parentMessageId",
"conversationId",
]),
reactions: defineTable({
workspaceId: v.id("workspaces"),
messageId: v.id("messages"),
memberId: v.id("messages"),
value: v.string(),
})
.index("by_workspace_id", ["workspaceId"])
.index("by_message_id", ["messageId"])
.index("by_member_id", ["memberId"]),
});

export default schema;
5 changes: 5 additions & 0 deletions convex/upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { mutation } from "./_generated/server";

export const generateUploadURL = mutation(async (ctx) => {
return await ctx.storage.generateUploadUrl();
});
49 changes: 40 additions & 9 deletions src/app/workspace/[workspaceId]/channel/[channelId]/chat-input.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { useCreateMessage } from "@/features/messages/api/use-create-messages";
import { useGenerateUploadUrl } from "@/features/upload/api/use-generate-upload-url";
import { useChannelId } from "@/hooks/use-channel-id";
import { useWorkspaceId } from "@/hooks/use-workspace-id";
import dynamic from "next/dynamic";
import Quill from "quill";
import { useRef, useState } from "react";
import { toast } from "sonner";
import { Id } from "../../../../../../convex/_generated/dataModel";

const Editor = dynamic(() => import("@/components/editor"), { ssr: false });

interface ChatInputProps {
placeholder: string;
}

type CreateMessageValues = {
channelId: Id<"channels">;
workspaceId: Id<"workspaces">;
body: string;
image?: Id<"_storage"> | undefined;
};

export const ChatInput = ({ placeholder }: ChatInputProps) => {
const [editorKey, setEditorKey] = useState(0);
const [pending, setPending] = useState(false);
Expand All @@ -20,6 +29,8 @@ export const ChatInput = ({ placeholder }: ChatInputProps) => {

const workspaceId = useWorkspaceId();
const channelId = useChannelId();

const { mutate: generateUploadUrl } = useGenerateUploadUrl();
const { mutate: createMessage } = useCreateMessage();

const handleSubmit = async ({
Expand All @@ -32,23 +43,43 @@ export const ChatInput = ({ placeholder }: ChatInputProps) => {
console.log(body, image);
try {
setPending(true);
await createMessage(
{
workspaceId,
channelId,
body,
},
{ throwError: true }
);
editorRef?.current?.enable(false);

const values: CreateMessageValues = {
channelId,
workspaceId,
body,
image: undefined,
};

if (image) {
const url = await generateUploadUrl({}, { throwError: true });

if (!url) throw new Error("Url not found");

const result = await fetch(url, {
method: "POST",
headers: { "Content-Type": image.type },
body: image,
});

if (!result.ok) throw new Error("Failed to upload image");

const { storageId } = await result.json();

values.image = storageId;
}

await createMessage(values, { throwError: true });

setEditorKey((prevKey) => prevKey + 1);
} catch (error) {
toast.error("Failed to send message");
} finally {
setPending(false);
editorRef?.current?.enable(true);
}
};

return (
<div className="px-5 w-full">
<Editor
Expand Down
1 change: 0 additions & 1 deletion src/components/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ const Editor = ({
const editorContainer = container.appendChild(
container.ownerDocument.createElement("div")
);
console.log(container, editorContainer);
const options: QuillOptions = {
theme: "snow",
placeholder: placeholderRef.current,
Expand Down
2 changes: 0 additions & 2 deletions src/components/emoji-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ export const EmojiPopover = ({
};

const onSelect = (emoji: any) => {
console.log("selected", emoji);
onEmojiSelect(emoji);
console.log("sent");
setPopoverOpen(false);

setTimeout(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/features/messages/api/use-create-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type RequestType = {
workspaceId: Id<"workspaces">;
channelId?: Id<"channels">;
parentMessageId?: Id<"messages">;
//todo : add convoId
conversationId?: Id<"conversations">;
};
type ResponseType = Id<"messages"> | null;

Expand Down
35 changes: 35 additions & 0 deletions src/features/messages/api/use-get-messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { usePaginatedQuery } from "convex/react";
import { api } from "../../../../convex/_generated/api";
import { Id } from "../../../../convex/_generated/dataModel";

const BATCH_SIZE = 20;
interface UseGetMessageProps {
channelId?: Id<"channels">;
conversationId?: Id<"conversations">;
parentMessageId?: Id<"messages">;
}

export type GetMessageReturnType =
(typeof api.messages.get._returnType)["page"];

export const useGetMessages = ({
channelId,
conversationId,
parentMessageId,
}: UseGetMessageProps) => {
const { results, status, loadMore } = usePaginatedQuery(
api.messages.get,
{
channelId,
conversationId,
parentMessageId,
},
{ initialNumItems: BATCH_SIZE }
);

return {
results,
status,
loadMore: () => loadMore(BATCH_SIZE),
};
};
Loading

0 comments on commit 1285d58

Please sign in to comment.