Skip to content

Commit

Permalink
Major refactor on realtime feature - frontend
Browse files Browse the repository at this point in the history
Fixed a bug where error is displayed on selecting view comments from singlePost, deleteActivity controller will only be used for cleanup, DB rows will cascade on post or comment deletion
  • Loading branch information
nidhish-nayak committed Mar 14, 2024
1 parent 67990bc commit f983917
Show file tree
Hide file tree
Showing 17 changed files with 329 additions and 206 deletions.
3 changes: 3 additions & 0 deletions client/src/components/addComment/addComment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ const AddComment = ({ postId }: { postId: number }) => {
queryClient.invalidateQueries({
queryKey: ["comments"],
});
queryClient.invalidateQueries({
queryKey: ["commentsCount"],
});
return setUploading(false);
},
onError(error) {
Expand Down
8 changes: 7 additions & 1 deletion client/src/components/comment/comment.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useContext, useState } from "react";
import { useContext, useEffect, useState } from "react";

import Delete from "@mui/icons-material/Delete";
import MoreVertIcon from "@mui/icons-material/MoreVert";

import { useParams } from "react-router-dom";
import { AuthContext } from "../../context/authContext";
import { axiosRequest } from "../../utils/axios.utils";
import formatTime from "../../utils/date.utils";
import "./comment.scss";
import { CommentTypes } from "./comment.types";

const Comment = ({ comment }: CommentTypes) => {
const postId = useParams();
const { id, profilePic, name, createdAt, desc, userId } = comment;
const [isOpen, setIsOpen] = useState(false);

Expand All @@ -19,6 +21,10 @@ const Comment = ({ comment }: CommentTypes) => {

const time = formatTime(createdAt);

useEffect(() => {
setIsOpen(false);
}, [postId]);

const mutation = useMutation({
mutationFn: () => axiosRequest.delete(`comments/${id}`),
onSuccess: () => {
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/post/post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const Post = ({ post }: PostTypes) => {
data: count,
error: commentsError,
} = useQuery({
queryKey: ["comments", post],
queryKey: ["commentsCount", id],
queryFn: getCommentsCount,
});

Expand Down
6 changes: 1 addition & 5 deletions client/src/components/posts/posts.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useIntersection } from "@mantine/hooks";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useContext, useEffect, useRef } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useParams } from "react-router-dom";
import { axiosRequest } from "../../utils/axios.utils";

import Spinner from "../spinner/spinner";
Expand All @@ -15,7 +15,6 @@ import { PostPageTypes, PostsTypes } from "./posts.types";

const Posts = () => {
const param = useParams();
const navigate = useNavigate();
const { search } = useContext(SearchContext);
const profileUserId = param.id ? parseInt(param.id) : undefined;
const lastPostRef = useRef<HTMLElement>(null);
Expand Down Expand Up @@ -52,9 +51,6 @@ const Posts = () => {

if (error) {
console.error(error.message);
localStorage.removeItem("user");
navigate("/login");
alert("Please login again!");
return <PostsError />;
}

Expand Down
204 changes: 55 additions & 149 deletions client/src/components/rightbar/activity/activity.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
import { RealtimeChannel } from "@supabase/supabase-js";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useContext, useEffect, useState } from "react";
import { Fragment, useContext, useEffect } from "react";
import { ActivityContext } from "../../../context/activityContext";
import { AuthContext } from "../../../context/authContext";
import { supabase } from "../../../supabase/db";
import usePostRealtime from "../../../hooks/usePostRealtime";
import { axiosRequest } from "../../../utils/axios.utils";
import formatTime from "../../../utils/date.utils";
import Spinner from "../../spinner/spinner";
import "../rightbar.scss";
import ActivityError from "./activity.error";
import {
ACTIVITY_GET_TYPES,
ACTIVITY_POST_TYPES,
REALTIME_TYPE,
} from "./activity.types";
import { ACTIVITY_GET_TYPES, ACTIVITY_POST_TYPES } from "./activity.types";
import ActivityItem from "./activityItem";

const Activity = () => {
const queryClient = useQueryClient();
const { currentUser } = useContext(AuthContext);
const [activity, setActivity] = useState<REALTIME_TYPE | null>(null);
const [prevActivity, setPrevActivity] = useState<REALTIME_TYPE | null>(
null
);
const [isRealtime, setIsRealtime] = useState(false);
const { isRealtime, setIsRealtime, activity, setActivity, prevActivity } =
useContext(ActivityContext);

if (!currentUser) throw Error("User not found!");

const setIsRealtimeFunction = () => {
queryClient.invalidateQueries({ queryKey: ["activities"] });
setIsRealtime(!isRealtime);
};

const activityMutation = useMutation({
const activityPostAddMutation = useMutation({
mutationFn: (body: ACTIVITY_POST_TYPES) =>
axiosRequest.post("/activities", body),
onSuccess: () => {
Expand All @@ -38,113 +35,50 @@ const Activity = () => {
},
});

const activityDeleteMutation = useMutation({
mutationFn: (table_id: number) =>
axiosRequest.delete(`/activities/${table_id}`),
onSuccess: () => {
setActivity(null);
return queryClient.invalidateQueries({ queryKey: ["activities"] });
},
onError(error) {
console.error("Activity deletion failed: ", error);
},
});

// Subscribe to INSERT events for posts table
useEffect(() => {
let postAddChannel: RealtimeChannel;

if (isRealtime === true) {
postAddChannel = supabase
.channel("inserted-post")
.on(
"postgres_changes",
{ event: "INSERT", schema: "public", table: "posts" },
(payload) => {
setPrevActivity(activity);
setActivity(payload);
queryClient.invalidateQueries({
queryKey: ["activities"],
});
queryClient.invalidateQueries({
queryKey: ["posts"],
});
}
)
.on(
"postgres_changes",
{
event: "DELETE",
schema: "public",
table: "posts",
},
(payload) => {
setPrevActivity(activity);
setActivity(payload);
queryClient.invalidateQueries({
queryKey: ["activities"],
});
queryClient.invalidateQueries({
queryKey: ["posts"],
});
}
)
.subscribe();
}

return () => {
if (postAddChannel) {
postAddChannel.unsubscribe();
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isRealtime]);
// Listening to post channel for changes
usePostRealtime();

useEffect(() => {
if (
activity &&
activityMutation.isPending === false &&
prevActivity !== activity &&
activity.eventType === "INSERT"
activity.eventType === "INSERT" &&
activityPostAddMutation.isPending === false
) {
const table_name: string = activity.table;
const message = "Added a new post";
const activity_created_at: string = activity.commit_timestamp;
const table_id: number = activity.new.id;
const user_id: number = activity.new.userId;

if (user_id !== currentUser?.id) {
const addPostBody: ACTIVITY_POST_TYPES = {
table_name: activity.table,
message: "Added a new post",
activity_created_at: activity.commit_timestamp,
user_id: activity.new.userId,
post_id: activity.new.id,
};

if (addPostBody.user_id !== currentUser.id) {
// Essential for other user's activity refresh
queryClient.invalidateQueries({
queryKey: ["activities"],
});
} else {
return activityMutation.mutate({
table_name: table_name,
table_id: table_id,
message: message,
activity_created_at: activity_created_at,
user_id: user_id,
});
return activityPostAddMutation.mutate(addPostBody);
}
}

if (
activity &&
activityMutation.isPending === false &&
prevActivity !== activity &&
activity.eventType === "DELETE"
) {
const table_id: number = activity.old.id;

return activityDeleteMutation.mutate(table_id);
// Deleting does not require another request - CASCADED rows
setActivity(null);
queryClient.invalidateQueries({ queryKey: ["activities"] });
}
}, [
activity,
activityDeleteMutation,
activityMutation,
currentUser?.id,
activityPostAddMutation,
currentUser.id,
prevActivity,
queryClient,
setActivity,
]);

const getActivities = async (): Promise<ACTIVITY_GET_TYPES[]> => {
Expand All @@ -161,67 +95,39 @@ const Activity = () => {
return <ActivityError setFunction={setIsRealtimeFunction} />;
}

if (!isRealtime)
return (
<div className="item">
<div className="item-container">
<div className="item-title-realtime">Latest Activities</div>
<div
className="item-realtime"
title="Realtime updates cause heavy load on servers. Default state is set to disabled."
onClick={() => setIsRealtime(true)}
>
<p className="realtime-title">Realtime</p>
<div className="realtime-circle offline" />
</div>
</div>
<div className="user offline">Realtime activities off</div>
</div>
);

return (
<div className="item">
<div className="item-container">
<div className="item-title-realtime">Latest Activities</div>
<div
className="item-realtime"
title="Realtime updates cause heavy load on servers. Default state is set to disabled."
onClick={() => setIsRealtime(false)}
onClick={() => setIsRealtime(!isRealtime)}
>
<p className="realtime-title">Realtime</p>
<div className="realtime-circle" />
<div
className={`realtime-circle ${
!isRealtime && "offline"
}`}
/>
</div>
</div>
{isLoading || !data ? (
<Spinner />
) : (
data.map((activity) => (
<div className="user" key={activity.id}>
<div className="userInfo">
<img src={activity.profilePic} alt="user-image" />
<div className="activity-container">
<div>
<span
className="user-name"
title="username"
>
{activity.name}
</span>
<p className="user-time">
{formatTime(
new Date(
activity.activity_created_at
)
)}
</p>
</div>
<p className="user-activity">
{activity.message}
</p>
</div>
</div>
</div>
))
{!isRealtime && (
<div className="user offline">Realtime activities off</div>
)}
{isRealtime && (
<Fragment>
{data && !isLoading ? (
data.map((activity) => (
<ActivityItem
activity={activity}
key={activity.id}
/>
))
) : (
<Spinner />
)}
</Fragment>
)}
</div>
);
Expand Down
6 changes: 4 additions & 2 deletions client/src/components/rightbar/activity/activity.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,21 @@ export type REALTIME_TYPE =

export type ACTIVITY_POST_TYPES = {
table_name: string;
table_id: number;
message: string;
activity_created_at: string;
user_id: number;
post_id: number;
comment_id?: number;
};

export type ACTIVITY_GET_TYPES = {
id: number;
table_name: string;
table_id: number;
message: string;
activity_created_at: string;
user_id: number;
post_id: number;
comment_id: number | null;
name: string;
profilePic: string;
};
Loading

0 comments on commit f983917

Please sign in to comment.