Skip to content

Commit

Permalink
create or get conversation get member, get messages, endpoint hooks c…
Browse files Browse the repository at this point in the history
…reated and integrated , get member id hook, and 1:1 convo dm started, threads properly working now with validation
  • Loading branch information
Diivvuu committed Oct 16, 2024
1 parent 7dad002 commit 6f8cc51
Show file tree
Hide file tree
Showing 16 changed files with 634 additions and 9 deletions.
2 changes: 2 additions & 0 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
} from "convex/server";
import type * as auth from "../auth.js";
import type * as channels from "../channels.js";
import type * as conversations from "../conversations.js";
import type * as http from "../http.js";
import type * as members from "../members.js";
import type * as messages from "../messages.js";
Expand All @@ -36,6 +37,7 @@ import type * as workspaces from "../workspaces.js";
declare const fullApi: ApiFromModules<{
auth: typeof auth;
channels: typeof channels;
conversations: typeof conversations;
http: typeof http;
members: typeof members;
messages: typeof messages;
Expand Down
54 changes: 54 additions & 0 deletions convex/conversations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { v } from "convex/values";
import { mutation } from "./_generated/server";
import { getAuthUserId } from "@convex-dev/auth/server";

export const createOrGet = mutation({
args: {
workspaceId: v.id("workspaces"),
memberId: v.id("members"),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Unauthorized");

const currentMember = await ctx.db
.query("members")
.withIndex("by_workspace_id_user_id", (q) =>
q.eq("workspaceId", args.workspaceId).eq("userId", userId)
)
.unique();

const otherMember = await ctx.db.get(args.memberId);
if (!currentMember || !otherMember) throw new Error("Member not found");

const existingConversation = await ctx.db
.query("conversations")
.filter((q) => q.eq(q.field("workspaceId"), args.workspaceId))
.filter((q) =>
q.or(
q.and(
q.eq(q.field("memberOneId"), currentMember._id),
q.eq(q.field("memberTwoId"), otherMember._id)
),
q.and(
q.eq(q.field("memberOneId"), otherMember._id),
q.eq(q.field("memberTwoId"), currentMember._id)
)
)
)
.unique();

if (existingConversation) return existingConversation._id;

const conversationId = await ctx.db.insert("conversations", {
workspaceId: args.workspaceId,
memberOneId: currentMember._id,
memberTwoId: otherMember._id,
});

const conversation = await ctx.db.get(conversationId);
if (!conversation) throw new Error("Conversation not found");

return conversationId;
},
});
30 changes: 30 additions & 0 deletions convex/members.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,36 @@ const populateUser = (ctx: QueryCtx, id: Id<"users">) => {
return ctx.db.get(id);
};

export const getById = query({
args: {
id: v.id("members"),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Unauthorized");

const member = await ctx.db.get(args.id);
if (!member) return null;

//this tells is if user is part of that workspace or not
const currentMember = await ctx.db
.query("members")
.withIndex("by_workspace_id_user_id", (q) =>
q.eq("workspaceId", member.workspaceId).eq("userId", userId)
)
.unique();
if (!currentMember) return null;

const user = await populateUser(ctx, member.userId);
if (!user) return null;

return {
...member,
user,
};
},
});

export const get = query({
args: { workspaceId: v.id("workspaces") },
handler: async (ctx, args) => {
Expand Down
62 changes: 62 additions & 0 deletions convex/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,68 @@ export const update = mutation({
},
});

export const getById = query({
args: {
id: v.id("messages"),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Unauthorized");

const message = await ctx.db.get(args.id);
if (!message) return null;

//who is trying to access the message
const currentMember = await getMember(ctx, message.workspaceId, userId);
if (!currentMember) return null;

//this member wrote the message
const member = await populateMember(ctx, message.memberId);
if (!member) return null;

const user = await populateUser(ctx, member.userId);
if (!user) return null;

const reactions = await populateReactions(ctx, message._id);
const reactionsWithCounts = reactions.map((reaction) => {
return {
...reaction,
count: reactions.filter((r) => r.value === reaction.value).length,
};
});
const dedupedReactions = reactionsWithCounts.reduce(
(acc, reaction) => {
const existingReaction = acc.find((r) => r.value === reaction.value);
if (existingReaction) {
existingReaction.memberIds = Array.from(
new Set([...existingReaction.memberIds, reaction.memberId])
);
} else {
acc.push({ ...reaction, memberIds: [reaction.memberId] });
}
return acc;
},
[] as (Doc<"reactions"> & {
count: number;
memberIds: Id<"members">[];
})[]
);
const reactionsWithoutMemberIdProperty = dedupedReactions.map(
({ memberId, ...rest }) => rest
);

return {
...message,
image: message.image
? await ctx.storage.getUrl(message.image)
: undefined,
user,
member,
reactions: reactionsWithoutMemberIdProperty,
};
},
});

export const get = query({
args: {
channelId: v.optional(v.id("channels")),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useGetChannel } from "@/features/channels/api/use-get-channel";
import { useChannelId } from "@/hooks/use-channel-id";

import { Loader, TriangleAlert } from "lucide-react";
import { Header } from "./Header";
import { Header } from "./header";
import { ChatInput } from "./chat-input";
import { useGetMessages } from "@/features/messages/api/use-get-messages";
import { MessageList } from "@/components/message-list";
Expand Down
39 changes: 39 additions & 0 deletions src/app/workspace/[workspaceId]/member/[memberId]/conversation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useMemberId } from "@/hooks/use-member-id";
import { Id } from "../../../../../../convex/_generated/dataModel";
import { useGetMember } from "@/features/members/api/use-get-member";
import { useGetMessages } from "@/features/messages/api/use-get-messages";
import { Loader } from "lucide-react";
import { Header } from "./header";

interface ConversationProps {
id: Id<"conversations">;
}

export const Conversations = ({ id }: ConversationProps) => {
const memberId = useMemberId();

const { data: member, isLoading: memberLoading } = useGetMember({
id: memberId,
});
console.log(member);
const { results, status, loadMore } = useGetMessages({
conversationId: id,
});

if (memberLoading || status === "LoadingFirstPage")
return (
<div className="h-full flex justify-center items-center">
<Loader className="size-6 animate-spin text-muted-foreground" />
</div>
);

return (
<div className="flex flex-col h-full">
<Header
memberName={member?.user.name}
memberImage={member?.user.image}
onClick={() => {}}
/>
</div>
);
};
30 changes: 30 additions & 0 deletions src/app/workspace/[workspaceId]/member/[memberId]/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { FaChevronDown } from "react-icons/fa";

interface HeaderProps {
memberName?: string;
memberImage?: string;
onClick?: () => void;
}

export const Header = ({ memberName, memberImage, onClick }: HeaderProps) => {
const avatarFallback = memberName?.charAt(0).toUpperCase();
return (
<div className="bg-white border-b h-[49px] flex items-center px-4 overflow-hidden">
<Button
variant="ghost"
className="text-lg font-semibold px-2 overflow-hidden w-auto"
size="sm"
onClick={onClick}
>
<Avatar className="size-6 mr-2">
<AvatarImage src={memberImage} />
<AvatarFallback>{avatarFallback}</AvatarFallback>
</Avatar>
<span className="truncate">{memberName}</span>
<FaChevronDown className="size-2.5 ml-2" />
</Button>
</div>
);
};
56 changes: 56 additions & 0 deletions src/app/workspace/[workspaceId]/member/[memberId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client";

import { useCreateOrGetConversation } from "@/features/conversations/api/use-create-or-get-conversation";
import { useMemberId } from "@/hooks/use-member-id";
import { useWorkspaceId } from "@/hooks/use-workspace-id";
import { AlertTriangle, Loader } from "lucide-react";
import { useEffect, useState } from "react";
import { Id } from "../../../../../../convex/_generated/dataModel";
import { toast } from "sonner";
import { Conversations } from "./conversation";

const MemberIdPage = () => {
const workspaceId = useWorkspaceId();
const memberId = useMemberId();

const [conversationId, setConversationId] =
useState<Id<"conversations"> | null>(null);

const { mutate, isPending } = useCreateOrGetConversation();

useEffect(() => {
mutate(
{
workspaceId,
memberId,
},
{
onSuccess(data) {
setConversationId(data);
},
onError(error) {
toast.error("Failed to create or get conversation.");
},
}
);
}, [memberId, workspaceId, mutate]);
if (isPending)
return (
<div className="h-full flex items-center justify-center">
<Loader className="size-6 animate-spin text-muted-foreground" />
</div>
);

if (!conversationId)
return (
<div className="h-full flex flex-col gap-y-2 items-center justify-center">
<AlertTriangle className="size-6 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
Conversation not found
</span>
</div>
);
return <Conversations id = {conversationId}/>;
};

export default MemberIdPage;
2 changes: 1 addition & 1 deletion src/components/message-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useWorkspaceId } from "@/hooks/use-workspace-id";
import { useCurrentMember } from "@/features/members/api/use-current-member";
import { Loader } from "lucide-react";

const TIME_THRESHOLD = 5;
const TIME_THRESHOLD = 20;
interface MessageListProps {
memberName?: string;
memberImage?: string;
Expand Down
4 changes: 2 additions & 2 deletions src/components/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const Renderer = dynamic(() => import("@/components/renderer"), { ssr: false });
const Editor = dynamic(() => import("@/components/editor"), { ssr: false });
interface MessageProps {
id: Id<"messages">;
memberId: Id<"members">;
memberId: Id<"members"> ;
authorImage?: string;
authorName?: string;
isAuthor: boolean;
Expand All @@ -31,7 +31,7 @@ interface MessageProps {
memberIds: Id<"members">[];
}
>;
body: Doc<"messages">["body"];
body: Doc<"messages">["body"] ;
image: string | null | undefined;
createdAt: Doc<"messages">["_creationTime"];
updatedAt: Doc<"messages">["updatedAt"];
Expand Down
Loading

0 comments on commit 6f8cc51

Please sign in to comment.