Skip to content

Commit

Permalink
adding messages notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
Bardala committed Nov 12, 2023
1 parent 58b9ea4 commit e510d7f
Show file tree
Hide file tree
Showing 19 changed files with 217 additions and 27 deletions.
23 changes: 21 additions & 2 deletions backend/src/controllers/space.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
SpaceMember,
SpaceReq,
SpaceRes,
UnReadMsgsNumReq,
UnReadMsgsNumRes,
UpdateSpaceReq,
UpdateSpaceRes,
} from '@nest/shared';
Expand All @@ -48,9 +50,10 @@ export interface spaceController {
addMember: HandlerWithParams<{ spaceId: string }, AddMemberReq, AddMemberRes>;
getSpaceMembers: HandlerWithParams<{ spaceId: string }, MembersReq, MembersRes>;
getChat: HandlerWithParams<{ spaceId: string }, ChatReq, ChatRes>;
getNumOfUnReadMsgs: HandlerWithParams<{ spaceId: string }, UnReadMsgsNumReq, UnReadMsgsNumRes>;
feeds: Handler<FeedsReq, FeedsRes>;
blogs: HandlerWithParams<{ spaceId: string }, SpaceBlogsReq, SpaceBlogsRes>;
// shorts: HandlerWithParams<{ spaceId: string }, SpaceShortsReq, SpaceShortsRes>;

deleteMember: HandlerWithParams<
{ spaceId: string; memberId: string },
DeleteMemReq,
Expand Down Expand Up @@ -133,7 +136,6 @@ export class SpaceController implements spaceController {
const offset = (page - 1) * pageSize;

const feeds = await this.db.infiniteScroll(res.locals.userId, pageSize, offset);
// console.log('blogs.length', feeds.length, 'page', page, 'offset', offset);
return res.send({ feeds, page });
};

Expand All @@ -146,9 +148,26 @@ export class SpaceController implements spaceController {
if (!(await this.db.isMember(spaceId, userId))) return res.status(403);

const messages = await this.db.getSpaceChat(spaceId);
await this.db.updateLastReadMsg({
userId,
spaceId,
msgId: messages[0]?.id || '0',
});
return res.send({ messages });
};

getNumOfUnReadMsgs: HandlerWithParams<{ spaceId: string }, UnReadMsgsNumReq, UnReadMsgsNumRes> =
async (req, res) => {
const userId = res.locals.userId;
const { spaceId } = req.params;

if (!spaceId) return res.status(400).send({ error: ERROR.PARAMS_MISSING });
if (!(await this.db.isMember(spaceId, userId))) return res.status(403);

const numOfUnReadMsgs = await this.db.numOfUnReadMsgs({ userId, spaceId });
return res.send({ numOfUnReadMsgs });
};

createSpace: Handler<CreateSpaceReq, CreateSpaceRes> = async (req, res) => {
const { description, name, status } = req.body;
const ownerId = res.locals.userId;
Expand Down
23 changes: 16 additions & 7 deletions backend/src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import {
GetUsersListRes,
LoginReq,
LoginRes,
PageSize,
SignUpReq,
UnFollowUserReq,
UnFollowUserRes,
UserBlogsReq,
UserBlogsRes,
UserSpacesReq,
UserSpacesRes,
numOfAllUnReadMsgsReq,
numOfAllUnReadMsgsRes,
} from '@nest/shared';
import { getRandomValues, randomUUID } from 'node:crypto';
import validator from 'validator';
Expand All @@ -36,6 +39,7 @@ export interface userController {
getFollowers: HandlerWithParams<{ id: string }, GetFollowersReq, GetFollowersRes>;
getUserBlogs: HandlerWithParams<{ id: string }, UserBlogsReq, UserBlogsRes>;
getUserSpaces: HandlerWithParams<{ id: string }, UserSpacesReq, UserSpacesRes>;
getAllUnReadMsgs: Handler<numOfAllUnReadMsgsReq, numOfAllUnReadMsgsRes>;
}

export class UserController implements userController {
Expand All @@ -45,6 +49,13 @@ export class UserController implements userController {
this.db = db;
}

getAllUnReadMsgs: Handler<numOfAllUnReadMsgsReq, numOfAllUnReadMsgsRes> = async (_, res) => {
return res.send({ error: ERROR.EXPIRE_API });
return res.status(HTTP.OK).send({
numberOfMsgs: await this.db.numOfAllUnReadMsgs(res.locals.userId),
});
};

getUserSpaces: HandlerWithParams<{ id: string }, UserSpacesReq, UserSpacesRes> = async (
req,
res
Expand All @@ -65,7 +76,7 @@ export class UserController implements userController {
}

const page = parseInt(req.params.page);
const pageSize = 3;
const pageSize = PageSize;

const offset = (page - 1) * pageSize;
const blogs = await this.db.getUserBlogs(userId, pageSize, offset);
Expand Down Expand Up @@ -164,12 +175,10 @@ export class UserController implements userController {
if (!validator.isEmail(email))
return res.status(HTTP.BAD_REQUEST).send({ error: ERROR.INVALID_EMAIL });
if (!validator.isStrongPassword(password))
return res
.status(HTTP.BAD_REQUEST)
.send({
error:
ERROR.WEAK_PASSWORD + '. Suggested strong password: ' + this.generateStrongPassword(),
});
return res.status(HTTP.BAD_REQUEST).send({
error:
ERROR.WEAK_PASSWORD + '. Suggested strong password: ' + this.generateStrongPassword(),
});

const user = {
email,
Expand Down
4 changes: 3 additions & 1 deletion backend/src/dataStore/dao/Space.dao.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Blog, ChatMessage, Space, SpaceMember } from '@nest/shared';
import { Blog, ChatMessage, LastReadMsg, Space, SpaceMember } from '@nest/shared';

export interface SpaceDao {
defaultSpcId: string;
Expand All @@ -9,6 +9,8 @@ export interface SpaceDao {

getBlogs(spaceId: string, pageSize: number, offset: number): Promise<Blog[]>;
getSpaceChat(spaceId: string): Promise<ChatMessage[]>;
numOfUnReadMsgs(params: { userId: string; spaceId: string }): Promise<number>;
updateLastReadMsg(lastRead: LastReadMsg): Promise<void>;

addMember(member: SpaceMember): Promise<void>;
spaceMembers(spaceId: string): Promise<SpaceMember[]>;
Expand Down
4 changes: 3 additions & 1 deletion backend/src/dataStore/dao/User.dao.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Blog, Space, User, UserCard, UsersList } from '@nest/shared';
import { Blog, Space, UnReadMsgs, User, UserCard, UsersList } from '@nest/shared';

export interface UserDao {
createUser(user: User): Promise<void>;
Expand All @@ -17,4 +17,6 @@ export interface UserDao {
getUserBlogs(userId: string, pageSize: number, offset: number): Promise<Blog[]>;
getUserSpaces(userId: string): Promise<Space[]>;
isFollow(followingId: string, userId: string): Promise<boolean>;

numOfAllUnReadMsgs(userId: string): Promise<UnReadMsgs[]>;
}
9 changes: 9 additions & 0 deletions backend/src/dataStore/migrations/002_initial.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS last_read (
userId VARCHAR(255) NOT NULL,
spaceId VARCHAR(255) NOT NULL,
lastReadId VARCHAR(255) NOT NULL,
PRIMARY KEY (userId, spaceId),
FOREIGN KEY (userId) REFERENCES users(id),
FOREIGN KEY (spaceId) REFERENCES spaces(id),
FOREIGN KEY (lastReadId) REFERENCES chat(id)
);
39 changes: 39 additions & 0 deletions backend/src/dataStore/sql/SqlDataStore.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ChatMessage,
Comment,
CommentWithUser,
LastReadMsg,
Like,
LikedUser,
Space,
Expand Down Expand Up @@ -45,6 +46,44 @@ export class SqlDataStore implements DataStoreDao {
return this;
}

async numOfUnReadMsgs(params: { userId: string; spaceId: string }): Promise<number> {
const query = `
SELECT COUNT(*) AS unread_count FROM chat c
JOIN
last_read lr
ON c.spaceId = lr.spaceId WHERE c.spaceId = ?
AND lr.userId = ?
AND c.timestamp > (SELECT timestamp FROM chat WHERE id = lr.lastReadId);
`;
const [rows] = await this.pool.query<RowDataPacket[]>(query, [params.spaceId, params.userId]);
return rows[0]['unread_count'] as number;
}

async numOfAllUnReadMsgs(userId: string): Promise<{ spaceId: string; unRead: number }[]> {
const query = `
SELECT spaceId, COUNT(*) AS unread_count FROM chat c
JOIN
last_read lr
ON c.spaceId = lr.spaceId WHERE lr.userId = ?
AND c.timestamp > (SELECT timestamp FROM chat WHERE id = lr.lastReadId)
GROUP BY spaceId;
`;
return this.pool.query<RowDataPacket[]>(query, [userId]).then(([rows]) => rows as any);
}

async updateLastReadMsg(lastRead: LastReadMsg): Promise<void> {
const query = `
INSERT INTO last_read (userId, spaceId, lastReadId) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE lastReadId = ?;
`;
await this.pool.query(query, [
lastRead.userId,
lastRead.spaceId,
lastRead.msgId,
lastRead.msgId,
]);
}

async infiniteScroll(memberId: string, pageSize: number, offset: number): Promise<Blog[]> {
const query = `
SELECT blogs.*, SUBSTRING(blogs.content, 1, 500) AS content FROM blogs
Expand Down
2 changes: 2 additions & 0 deletions backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { errorHandler } from './middleware/errorHandler';
app.get(ENDPOINT.GET_USERS_LIST, requireAuth, asyncHandler(user.getUsersList));
app.get(ENDPOINT.GET_USER_BLOGS, requireAuth, asyncHandler(user.getUserBlogs));
app.get(ENDPOINT.GET_USER_SPACES, requireAuth, asyncHandler(user.getUserSpaces));
app.get(ENDPOINT.GET_ALL_UNREAD_MSGS, requireAuth, asyncHandler(user.getAllUnReadMsgs));

// *Blog
app.post(ENDPOINT.CREATE_BLOG, requireAuth, checkEmptyInput, asyncHandler(blog.createBlog));
Expand Down Expand Up @@ -83,6 +84,7 @@ import { errorHandler } from './middleware/errorHandler';
app.delete(ENDPOINT.DELETE_MEMBER, requireAuth, asyncHandler(space.deleteMember));
app.delete(ENDPOINT.LEAVE_SPACE, requireAuth, asyncHandler(space.leaveSpace));
app.get(ENDPOINT.GET_SPACE_BLOGS, requireAuth, asyncHandler(space.blogs));
app.get(ENDPOINT.GET_UNREAD_MSGS_NUM, requireAuth, asyncHandler(space.getNumOfUnReadMsgs));

//* Message
app.post(ENDPOINT.CREATE_MESSAGE, requireAuth, checkEmptyInput, asyncHandler(chat.createMessage));
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/components/NotificationNumberMsgs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { BiSolidBellRing } from 'react-icons/bi';

import { useSpace } from '../hooks/useSpace';

export const NotificationNumberMsgs: React.FC<{ spaceId: string }> = ({ spaceId }) => {
const { numOfUnReadMsgs } = useSpace(spaceId);
const unRead = numOfUnReadMsgs.data?.numOfUnReadMsgs;

if (unRead! > 0) {
return (
<>
<BiSolidBellRing
className="ring"
style={{
fontSize: '1.2rem',
color: 'red',
margin: '0 0.2rem',
}}
/>
<span
className="unread-count"
style={{
backgroundColor: 'white',
color: 'red',
borderRadius: '50%',
padding: '0 0.2rem',
fontSize: '0.8rem',
}}
>
{unRead}
</span>
</>
);
}

return <></>;
};
18 changes: 13 additions & 5 deletions frontend/src/components/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,30 @@ import { Chat } from './Chat';
import { CreateSpace } from './CreateSpace';
import { EditSpaceForm } from './EditSpace';
import { LeaveSpc } from './LeaveSpc';
import { NotificationNumberMsgs } from './NotificationNumberMsgs';
import { ShortForm } from './ShortForm';
import { SpaceMembers } from './SpaceMembers';

export const Sidebar: React.FC<{ space?: Space; members?: SpaceMember[] }> = ({
space,
members,
}) => {
export const Sidebar: React.FC<{
space?: Space;
members?: SpaceMember[];
numOfUnReadingMsgs?: number;
}> = ({ space, members, numOfUnReadingMsgs }) => {
const { currUser } = useAuthContext();
const { state, dispatch } = useSideBarReducer();
const [list, setList] = useState(false);
const nav = useNavigate();
// const [unRead, setUnRead] = useState(numOfUnReadingMsgs);

const isMember = members?.some(member => member.memberId === currUser?.id);
const isAdmin =
space?.ownerId === currUser?.id ||
members?.some(member => member.memberId === currUser?.id && member.isAdmin);

// useEffect(() => {
// setUnRead(numOfUnReadingMsgs);
// }, [numOfUnReadingMsgs]);

return (
<aside className="side-bar">
<div className="side-bar-nav">
Expand Down Expand Up @@ -101,10 +108,11 @@ export const Sidebar: React.FC<{ space?: Space; members?: SpaceMember[] }> = ({
className="chat-button" // todo: edit this
onClick={() => {
dispatch({ type: 'showChat' });
// setUnRead(0);
setList(false);
}}
>
Chat
Chat {<NotificationNumberMsgs spaceId={space?.id!} />}
</button>
{state.showChat && <Chat space={space!} />}

Expand Down
1 change: 0 additions & 1 deletion frontend/src/hooks/useChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export const useChat = (space: Space) => {
const chatQuery = useQuery<ChatRes, ApiError>(chatKey, chatApi(space.id), {
enabled: !!currUser?.jwt && !!space.id,
refetchOnWindowFocus: false,
refetchOnMount: false,
});
const chatErr = chatQuery.error;

Expand Down
5 changes: 2 additions & 3 deletions frontend/src/hooks/useProfileData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GetUserCardRes, UserBlogsRes, UserSpacesRes } from '@nest/shared';
import { GetUserCardRes, PageSize, UserBlogsRes, UserSpacesRes } from '@nest/shared';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { useState } from 'react';

Expand All @@ -13,7 +13,6 @@ export const useProfileData = (id: string) => {
const cardKey = ['userCard', id];
const spacesKey = ['userSpaces', id];
const blogsKey = ['userBlogs', id];
const pageSize = 3;
const [isEnd, setIsEnd] = useState(false);

const userCardQuery = useQuery<GetUserCardRes, ApiError>(cardKey, userCardApi(id), {
Expand All @@ -33,7 +32,7 @@ export const useProfileData = (id: string) => {
return lastPage.page + 1;
},
onSuccess: data => {
if (data.pages[data.pages.length - 1].blogs.length < pageSize) {
if (data.pages[data.pages.length - 1].blogs.length < PageSize) {
setIsEnd(true);
}
},
Expand Down
21 changes: 20 additions & 1 deletion frontend/src/hooks/useSpace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,21 @@ import {
PageSize,
SpaceBlogsRes,
SpaceRes,
UnReadMsgsNumRes,
} from '@nest/shared';
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';

import { useAuthContext } from '../context/AuthContext';
import { ApiError } from '../fetch/auth';
import { blogsApi, feedsApi, joinSpcApi, membersApi, spcApi } from '../utils/api';
import {
blogsApi,
feedsApi,
getNumOfUnReadMsgsApi,
joinSpcApi,
membersApi,
spcApi,
} from '../utils/api';
import { useScroll } from './useScroll';

export const useSpace = (id: string) => {
Expand All @@ -21,6 +29,7 @@ export const useSpace = (id: string) => {
const spcKey = ['space', id];
const blogsKey = ['blogs', id];
const membersKey = ['members', id];
const msgsNumKey = ['unreadMsgsNum', id];
const pageSize = PageSize;
const [isEnd, setIsEnd] = useState(false);

Expand Down Expand Up @@ -48,10 +57,20 @@ export const useSpace = (id: string) => {
const isMember = membersQuery.data?.members?.some(member => member.memberId === currUser?.id);
useScroll(blogsQuery);

const numOfUnReadMsgs = useQuery<UnReadMsgsNumRes, ApiError>(
msgsNumKey,
getNumOfUnReadMsgsApi(id),
{
enabled: !!currUser?.jwt && !!id && id !== DefaultSpaceId,
// refetchOnWindowFocus: false,
}
);

return {
spaceQuery,
blogsQuery,
membersQuery,
numOfUnReadMsgs,
joinSpaceMutate,
isMember,
isEnd,
Expand Down
Loading

0 comments on commit e510d7f

Please sign in to comment.