Skip to content

Commit

Permalink
remove message endpoint, hook, integration and css
Browse files Browse the repository at this point in the history
  • Loading branch information
Diivvuu committed Oct 15, 2024
1 parent c660fd7 commit bba83ce
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 75 deletions.
21 changes: 21 additions & 0 deletions convex/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,27 @@ const getMember = async (
.unique();
};

export const remove = mutation({
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) throw new Error("Message not found");

const member = await getMember(ctx, message.workspaceId, userId);
if (!member || member._id !== message.memberId)
throw new Error("Unauthorized");

await ctx.db.delete(args.id);

return args.id;
},
});

export const update = mutation({
args: {
id: v.id("messages"),
Expand Down
189 changes: 114 additions & 75 deletions src/components/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { Toolbar } from "./toolbar";

import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
import { cn } from "@/lib/utils";
import { useRemoveMessage } from "@/features/messages/api/use-remove-message";
import { useConfirm } from "@/hooks/use-confirm";

const Renderer = dynamic(() => import("@/components/renderer"), { ssr: false });
const Editor = dynamic(() => import("@/components/editor"), { ssr: false });
Expand Down Expand Up @@ -57,11 +59,36 @@ export const Message = ({
threadImage,
threadTimestamp,
}: MessageProps) => {
const [ConfirmDialog, confirm] = useConfirm(
"Delete Message",
"Are you sure you want to delete this message? This action is irreversible."
);

const { mutate: updateMessage, isPending: isUpdatingMessage } =
useUpdateMessage();

const { mutate: removeMessage, isPending: isRemovingMessage } =
useRemoveMessage();
const isPending = isUpdatingMessage;

const handleRemove = async () => {
const ok = await confirm();
if (!ok) return;

removeMessage(
{ id },
{
onSuccess: () => {
toast.success("Message deleted");

//close thread if opened
},
onError: () => {
toast.error("Failed to delete message");
},
}
);
};

const handleUpdate = ({ body }: { body: string }) => {
updateMessage(
{ id, body },
Expand All @@ -82,20 +109,82 @@ export const Message = ({
};
if (isCompact) {
return (
<>
<ConfirmDialog />
<div
className={cn(
"flex flex-col gap-2 p-1.5 px-5 hover:bg-gray-100 group relative",
isEditing && "bg-[#f2c74433] hover:bg-[#f2c74433]",
isRemovingMessage &&
"bg-rose-500/50 transform transition-all scale-y-0 origin-bottom duration-200"
)}
>
<div className="flex items-start gap-2">
<Hint label={formatFulltime(new Date(createdAt))}>
<button className="text-xs text-muted-foreground opacity-0 group-hover:opacity-100 w-[40px] leading-[22px] text-center hover:underline">
{format(new Date(createdAt), "hh:mm")}
</button>
</Hint>
{isEditing ? (
<div className="w-full h-full">
<Editor
onSubmit={handleUpdate}
disabled={isPending}
defaultValue={JSON.parse(body)}
onCancel={() => setEditingId(null)}
variant="update"
/>
</div>
) : (
<div className="flex flex-col w-full">
<Renderer value={body} />
<Thumbnail url={image} />
{updatedAt ? (
<span className="text-xs text-muted-foreground">
(edited)
</span>
) : null}
</div>
)}
</div>
{!isEditing && (
<Toolbar
isAuthor={isAuthor}
isPending={isPending}
handleEdit={() => setEditingId(id)}
handleThread={() => {}}
handleDelete={handleRemove}
handleReaction={() => {}}
hideThreadButton={hideThreadButton}
/>
)}
</div>
</>
);
}

const avatarFallback = authorName.charAt(0).toUpperCase();

return (
<>
<ConfirmDialog />
<div
className={cn(
"flex flex-col gap-2 p-1.5 px-5 hover:bg-gray-100 group relative",
isEditing && "bg-[#f2c74433] hover:bg-[#f2c74433]"
isEditing && "bg-[#f2c74433] hover:bg-[#f2c74433]",
isRemovingMessage &&
"bg-rose-500/50 transform transition-all scale-y-0 origin-bottom duration-200"
)}
>
<div className="flex items-start gap-2">
<Hint label={formatFulltime(new Date(createdAt))}>
<button className="text-xs text-muted-foreground opacity-0 group-hover:opacity-100 w-[40px] leading-[22px] text-center hover:underline">
{format(new Date(createdAt), "hh:mm")}
</button>
</Hint>
<button>
<Avatar>
<AvatarImage src={authorImage} />
<AvatarFallback>{avatarFallback}</AvatarFallback>
</Avatar>
</button>
{isEditing ? (
<div className="w-full h-full">
<div className="w-full h-ful">
<Editor
onSubmit={handleUpdate}
disabled={isPending}
Expand All @@ -105,7 +194,21 @@ export const Message = ({
/>
</div>
) : (
<div className="flex flex-col w-full">
<div className="flex flex-col w-full overflow-hidden">
<div className="text-sm">
<button
onClick={() => {}}
className="font-bold text-primary hover:underline"
>
{authorName}
</button>
<span>&nbsp;&nbsp;</span>
<Hint label={formatFulltime(new Date(createdAt))}>
<button className="text-xs text-muted-foreground hover:underline">
{format(new Date(createdAt), "h:mm a")}
</button>
</Hint>
</div>
<Renderer value={body} />
<Thumbnail url={image} />
{updatedAt ? (
Expand All @@ -120,76 +223,12 @@ export const Message = ({
isPending={isPending}
handleEdit={() => setEditingId(id)}
handleThread={() => {}}
handleDelete={() => {}}
handleDelete={handleRemove}
handleReaction={() => {}}
hideThreadButton={hideThreadButton}
/>
)}
</div>
);
}

const avatarFallback = authorName.charAt(0).toUpperCase();

return (
<div
className={cn(
"flex flex-col gap-2 p-1.5 px-5 hover:bg-gray-100 group relative",
isEditing && "bg-[#f2c74433] hover:bg-[#f2c74433]"
)}
>
<div className="flex items-start gap-2">
<button>
<Avatar>
<AvatarImage src={authorImage} />
<AvatarFallback>{avatarFallback}</AvatarFallback>
</Avatar>
</button>
{isEditing ? (
<div className="w-full h-ful">
<Editor
onSubmit={handleUpdate}
disabled={isPending}
defaultValue={JSON.parse(body)}
onCancel={() => setEditingId(null)}
variant="update"
/>
</div>
) : (
<div className="flex flex-col w-full overflow-hidden">
<div className="text-sm">
<button
onClick={() => {}}
className="font-bold text-primary hover:underline"
>
{authorName}
</button>
<span>&nbsp;&nbsp;</span>
<Hint label={formatFulltime(new Date(createdAt))}>
<button className="text-xs text-muted-foreground hover:underline">
{format(new Date(createdAt), "h:mm a")}
</button>
</Hint>
</div>
<Renderer value={body} />
<Thumbnail url={image} />
{updatedAt ? (
<span className="text-xs text-muted-foreground">(edited)</span>
) : null}
</div>
)}
</div>
{!isEditing && (
<Toolbar
isAuthor={isAuthor}
isPending={isPending}
handleEdit={() => setEditingId(id)}
handleThread={() => {}}
handleDelete={() => {}}
handleReaction={() => {}}
hideThreadButton={hideThreadButton}
/>
)}
</div>
</>
);
};
64 changes: 64 additions & 0 deletions src/features/messages/api/use-remove-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useCallback, useMemo, useState } from "react";
import { Id } from "../../../../convex/_generated/dataModel";
import { useMutation } from "convex/react";
import { api } from "../../../../convex/_generated/api";

type RequestType = {
id: Id<"messages">;
};
type ResponseType = Id<"messages"> | null;
type Options = {
onSuccess?: (data: ResponseType) => void;
onError?: (error: Error) => void;
onSettled?: () => void;
throwError?: boolean;
};

export const useRemoveMessage = () => {
const [data, setData] = useState<ResponseType>(null);
const [error, setError] = useState<Error | null>(null);
const [status, setStatus] = useState<
"pending" | "success" | "error" | "settled" | null
>(null);

const isPending = useMemo(() => status === "pending", [status]);
const isSuccess = useMemo(() => status === "success", [status]);
const isError = useMemo(() => status === "error", [status]);
const isSettled = useMemo(() => status === "settled", [status]);

const mutation = useMutation(api.messages.remove);

const mutate = useCallback(
async (values: RequestType, options?: Options) => {
try {
setData(null);
setError(null);
setStatus("pending");

const response = await mutation(values);
options?.onSuccess?.(response);
setStatus("success");
return response;
} catch (error) {
setStatus("error");
options?.onError?.(error as Error);
if (options?.throwError) {
throw error;
}
} finally {
setStatus("settled");
options?.onSettled?.();
}
},
[mutation]
);
return {
mutate,
data,
error,
isPending,
isSuccess,
isError,
isSettled,
};
};

0 comments on commit bba83ce

Please sign in to comment.