Skip to content

Commit

Permalink
Implement message board on team home page
Browse files Browse the repository at this point in the history
  • Loading branch information
xixixao committed Jan 19, 2024
1 parent fea56ec commit 109a76c
Show file tree
Hide file tree
Showing 13 changed files with 179 additions and 24 deletions.
76 changes: 76 additions & 0 deletions app/t/[teamSlug]/MessageBoard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useCurrentTeam } from "@/app/t/[teamSlug]/hooks";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/convex/_generated/api";
import { useMutation, usePaginatedQuery } from "convex/react";
import { useCallback, useRef, useState } from "react";

export function MessageBoard() {
const team = useCurrentTeam();
const {
results: messages,
loadMore,
status,
} = usePaginatedQuery(
api.users.teams.messages.list,
team == null ? "skip" : { teamId: team._id },
{ initialNumItems: 10 }
);
const [message, setMessage] = useState("");
const sendMessage = useMutation(api.users.teams.messages.create);
const listRef = useRef<HTMLElement>(null);
const handleScroll = useCallback(() => {
if (listRef.current === null) {
return;
}
const { scrollTop, scrollHeight, clientHeight } = listRef.current;
if (
scrollHeight - scrollTop <= clientHeight * 1.5 &&
status === "CanLoadMore"
) {
loadMore(10);
}
}, [loadMore, status]);

return (
<div className="max-w-xl flex flex-col gap-2 mt-8">
<form
className="flex gap-2"
onSubmit={(event) => {
event.preventDefault();
void sendMessage({ text: message, teamId: team!._id }).then(() => {
setMessage("");
});
}}
>
<Textarea
name="message"
placeholder="Message text..."
value={message}
onChange={(event) => setMessage(event.target.value)}
/>
<Button type="submit">Send Message</Button>
</form>
<ScrollArea className="h-72 rounded-md border" onScroll={handleScroll}>
<div className="p-4">
{messages.map((message) => (
<div key={message._id} className="text-sm">
<div>
<span className="font-semibold">{message.author}:</span>{" "}
{message.text}
</div>
<Separator className="my-2" />
</div>
))}
{status === "Exhausted" && messages.length === 0 && (
<div className="text-muted-foreground">
There are no messages posted yet
</div>
)}
</div>
</ScrollArea>
</div>
);
}
18 changes: 13 additions & 5 deletions app/t/[teamSlug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
"use client";

import { useState } from "react";
import { MessageBoard } from "@/app/t/[teamSlug]/MessageBoard";
import { useCurrentTeam } from "@/app/t/[teamSlug]/hooks";

export default function Home() {
const [count, setCount] = useState(0);
const team = useCurrentTeam();
if (team == null) {
return null;
}
return (
<main className="container">
<h1 className="text-4xl font-extrabold my-8">Hello there!</h1>
Count: {count}
<button onClick={() => setCount((count) => count + 1)}>Click me</button>
<h1 className="text-4xl font-extrabold my-8">
{team.name}
{"'"}s Projects
</h1>
<p>This is where your product actually lives.</p>
<p>As an example, here{"'"}s a message board for the team:</p>
<MessageBoard />
</main>
);
}
4 changes: 3 additions & 1 deletion convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* Generated by convex@1.9.0-alpha.0.
* Generated by convex@1.8.0.
* To regenerate, run `npx convex dev`.
* @module
*/
Expand All @@ -21,6 +21,7 @@ import type * as permissions from "../permissions.js";
import type * as types from "../types.js";
import type * as users_teams_members_invites from "../users/teams/members/invites.js";
import type * as users_teams_members from "../users/teams/members.js";
import type * as users_teams_messages from "../users/teams/messages.js";
import type * as users_teams_roles from "../users/teams/roles.js";
import type * as users_teams from "../users/teams.js";
import type * as users from "../users.js";
Expand All @@ -42,6 +43,7 @@ declare const fullApi: ApiFromModules<{
types: typeof types;
"users/teams/members/invites": typeof users_teams_members_invites;
"users/teams/members": typeof users_teams_members;
"users/teams/messages": typeof users_teams_messages;
"users/teams/roles": typeof users_teams_roles;
"users/teams": typeof users_teams;
users: typeof users;
Expand Down
2 changes: 1 addition & 1 deletion convex/_generated/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* Generated by convex@1.9.0-alpha.0.
* Generated by convex@1.8.0.
* To regenerate, run `npx convex dev`.
* @module
*/
Expand Down
2 changes: 1 addition & 1 deletion convex/_generated/dataModel.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* Generated by convex@1.9.0-alpha.0.
* Generated by convex@1.8.0.
* To regenerate, run `npx convex dev`.
* @module
*/
Expand Down
2 changes: 1 addition & 1 deletion convex/_generated/server.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* Generated by convex@1.9.0-alpha.0.
* Generated by convex@1.8.0.
* To regenerate, run `npx convex dev`.
* @module
*/
Expand Down
2 changes: 1 addition & 1 deletion convex/_generated/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* Generated by convex@1.9.0-alpha.0.
* Generated by convex@1.8.0.
* To regenerate, run `npx convex dev`.
* @module
*/
Expand Down
7 changes: 6 additions & 1 deletion convex/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const init = internalMutation({
{ name: "Delete Team" },
{ name: "Manage Members" },
{ name: "Read Members" },
{ name: "Contribute" },
]);

await ctx.table("roles").insert({
Expand All @@ -24,12 +25,16 @@ export const init = internalMutation({
await getPermission(ctx, "Delete Team"),
await getPermission(ctx, "Manage Members"),
await getPermission(ctx, "Read Members"),
await getPermission(ctx, "Contribute"),
],
});
await ctx.table("roles").insert({
name: "Member",
isDefault: true,
permissions: [await getPermission(ctx, "Read Members")],
permissions: [
await getPermission(ctx, "Read Members"),
await getPermission(ctx, "Contribute"),
],
});
},
});
17 changes: 10 additions & 7 deletions convex/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export const vPermission = v.union(
v.literal("Manage Team"),
v.literal("Delete Team"),
v.literal("Read Members"),
v.literal("Manage Members")
v.literal("Manage Members"),
v.literal("Contribute")
);
export type Permission = Infer<typeof vPermission>;

Expand All @@ -26,16 +27,18 @@ export async function viewerHasPermissionX(
teamId: Id<"teams">,
name: Permission
) {
const member = await ctx
.table("members", "teamUser", (q) =>
q.eq("teamId", teamId).eq("userId", ctx.viewerX()._id)
)
.uniqueX();
if (
!(await ctx
.table("members", "teamUser", (q) =>
q.eq("teamId", teamId).eq("userId", ctx.viewerX()._id)
)
.uniqueX()
!(await member
.edge("role")
.edge("permissions")
.has(await getPermission(ctx, name)))
) {
throw new Error("Only admins can delete teams");
throw new Error(`Viewer does not have the permission "${name}"`);
}
return member;
}
16 changes: 13 additions & 3 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ const schema = defineEntSchema(
})
.field("slug", v.string(), { unique: true })
.edges("members", { ref: true })
.edges("invites", { ref: true }),
.edges("invites", { ref: true })
.edges("messages", { ref: true }),

members: defineEnt({})
.edge("team")
.edge("user")
.edge("role")
.index("teamUser", ["teamId", "userId"]),
.index("teamUser", ["teamId", "userId"])
.edges("messages", { ref: true }),

invites: defineEnt({
inviterEmail: v.string(),
Expand All @@ -29,7 +31,9 @@ const schema = defineEntSchema(
isDefault: v.boolean(),
})
.field("name", vRole, { unique: true })
.edges("permissions"),
.edges("permissions")
.edges("members", { ref: true })
.edges("invites", { ref: true }),

permissions: defineEnt({})
.field("name", vPermission, { unique: true })
Expand All @@ -44,6 +48,12 @@ const schema = defineEntSchema(
})
.field("tokenIdentifier", v.string(), { unique: true })
.edges("members", { ref: true }),

messages: defineEnt({
text: v.string(),
})
.edge("team")
.edge("member"),
},
{ schemaValidation: false }
);
Expand Down
51 changes: 51 additions & 0 deletions convex/users/teams/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { v } from "convex/values";
import { mutation, query } from "../../functions";
import { viewerHasPermissionX } from "../../permissions";
import { paginationOptsValidator } from "convex/server";

export const list = query({
args: {
teamId: v.id("teams"),
paginationOpts: paginationOptsValidator,
},
handler: async (ctx, { teamId, paginationOpts }) => {
if (ctx.viewer === null) {
return {
page: [],
isDone: true,
continueCursor: "",
};
}
await viewerHasPermissionX(ctx, teamId, "Contribute");
return await ctx
.table("teams")
.getX(teamId)
.edge("messages")
.order("desc")
.paginate(paginationOpts)
.map(async (message) => {
const user = await message.edge("member").edge("user");
return {
_id: message._id,
_creationTime: message._creationTime,
text: message.text,
author: user.firstName ?? user.fullName,
};
});
},
});

export const create = mutation({
args: {
teamId: v.id("teams"),
text: v.string(),
},
handler: async (ctx, { teamId, text }) => {
const member = await viewerHasPermissionX(ctx, teamId, "Contribute");
await ctx.table("messages").insert({
text,
teamId: teamId,
memberId: member._id,
});
},
});
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"convex": "^1.8.0",
"convex-ents": "^0.1.0",
"convex-ents": "^0.2.0",
"convex-helpers": "0.1.9",
"date-fns": "^2.30.0",
"eslint": "8.49.0",
Expand Down

0 comments on commit 109a76c

Please sign in to comment.