Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wr-419: Display notification about New articles written by followed authors on all pages #451

Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { type Knex } from 'knex';

const NOT_STARTED_ARTICLE_ACHIEVEMENT_DESCRIPTION =
'Get started on your writing journey! Your first article is just a few keystrokes away. Dive in and share your thoughts with the world.';
const NOT_STARTED_COMMENT_ACHIEVEMENT_DESCRIPTION =
'Ready to join the conversation? Leave your first comment and become a part of the discussion!';
const UPDATED_ACHIEVEMENTS = [
{
key: 'write_first_article',
description: {
notStarted: NOT_STARTED_ARTICLE_ACHIEVEMENT_DESCRIPTION,
earlyStage: NOT_STARTED_ARTICLE_ACHIEVEMENT_DESCRIPTION,
inProgress: NOT_STARTED_ARTICLE_ACHIEVEMENT_DESCRIPTION,
done: 'Congratulations on completing your first article! 🎉 Your journey as a writer has begun. Share your work with pride!',
},
},
{
key: 'write_5_articles',
description: {
notStarted: NOT_STARTED_ARTICLE_ACHIEVEMENT_DESCRIPTION,
earlyStage:
'You are off to a great start with your first article! Keep those ideas flowing and your pen moving.',
inProgress:
'You are making progress on your path to writing five articles! Keep those creative juices flowing, and you will reach your next milestone soon.',
done: 'Congratulations on writing your fifth article! 🎉 You are building a substantial body of work. Keep the momentum going!',
},
},
{
key: 'write_10_articles',
description: {
notStarted: NOT_STARTED_ARTICLE_ACHIEVEMENT_DESCRIPTION,
earlyStage:
'You are making progress toward writing ten articles! Stay committed, and you will soon reach this significant milestone.',
inProgress:
'You are well on your way to completing your tenth article! Keep up the excellent work, and you will achieve this milestone in no time.',
done: 'Congratulations on writing your tenth article! 🎉 You have proven your dedication to writing. Keep the creativity flowing!',
},
},
{
key: 'write_15_articles',
description: {
notStarted: NOT_STARTED_ARTICLE_ACHIEVEMENT_DESCRIPTION,
earlyStage: 'You are making progress toward writing fifteen articles!',
inProgress:
'You are well on your way to completing your fifteenth article! Keep up the excellent work!',
done: 'Congratulations on writing your fifteenth article! 🎉 Your dedication to writing is truly impressive. Keep those creative ideas flowing!',
},
},
{
key: 'write_25_articles',
description: {
notStarted: NOT_STARTED_ARTICLE_ACHIEVEMENT_DESCRIPTION,
earlyStage:
'You are making headway toward writing twenty-five articles! Keep those creative juices flowing, and you will soon achieve this significant milestone.',
inProgress:
'You are well on your way to completing your twenty-fifth article! Your dedication to writing is paying off. Keep up the great work!',
done: 'Congratulations on writing your twenty-fifth article! 🎉 Your body of work is substantial, and your commitment to writing is commendable. Keep inspiring others!',
},
},
{
key: 'write_50_articles',
description: {
notStarted: NOT_STARTED_ARTICLE_ACHIEVEMENT_DESCRIPTION,
earlyStage:
'You are on your way to writing fifty articles! Keep the momentum going!',
inProgress:
'You are getting closer to writing your fiftieth article! Your dedication to writing is evident. Keep pushing forward!',
done: 'Congratulations on writing your fiftieth article! 🎉 You are a prolific writer, and your dedication is an inspiration to others. Keep the words flowing!',
},
},
{
key: 'write_first_comment',
description: {
notStarted: NOT_STARTED_COMMENT_ACHIEVEMENT_DESCRIPTION,
earlyStage: NOT_STARTED_COMMENT_ACHIEVEMENT_DESCRIPTION,
inProgress: NOT_STARTED_COMMENT_ACHIEVEMENT_DESCRIPTION,
done: 'Congratulations on leaving your first comment! 🎉 Your voice matters, and you are making an impact in the community.',
},
},
{
key: 'write_5_comments',
description: {
notStarted: NOT_STARTED_COMMENT_ACHIEVEMENT_DESCRIPTION,
earlyStage:
'You are off to a great start with your first comment! Keep up the great work!',
inProgress:
'You are actively engaging with discussions! Your participation is valuable, and you are well on your way to achieving this milestone.',
done: 'Congratulations on leaving your fifth comment! 🎉 You are becoming a respected voice in the community. Keep those insights flowing!',
},
},
{
key: 'write_10_comments',
description: {
notStarted: NOT_STARTED_COMMENT_ACHIEVEMENT_DESCRIPTION,
earlyStage:
'You are making great strides in the world of commenting! Keep engaging with the community!',
inProgress:
'You are actively engaging with discussions and making an impact! Keep it up, and you will achieve this milestone in no time.',
done: 'Congratulations on leaving your tenth comment! 🎉 Your contributions are valued, and you are a key part of the community.',
},
},
{
key: 'write_15_comments',
description: {
notStarted: NOT_STARTED_COMMENT_ACHIEVEMENT_DESCRIPTION,
earlyStage:
'You are on your way to writing fifteen comments! Keep engaging with the community!',
inProgress:
'You are actively participating in discussions and making your voice heard!',
done: 'Congratulations on leaving your fifteenth comment! 🎉 Your contributions are invaluable, and you are a respected member of the community.',
},
},
{
key: 'write_25_comments',
description: {
notStarted: NOT_STARTED_COMMENT_ACHIEVEMENT_DESCRIPTION,
earlyStage:
'You are actively engaging with discussions and contributing to the community! Your dedication is taking you closer to this achievement.',
inProgress: 'You are well on your way to leaving twenty-five comments!',
done: 'Congratulations on leaving your twenty-fifth comment! 🎉 You are a valuable member of the community, and your insights are making a difference.',
},
},
{
key: 'write_50_comments',
description: {
notStarted: NOT_STARTED_COMMENT_ACHIEVEMENT_DESCRIPTION,
earlyStage:
'You are making substantial contributions to discussions! Keep sharing your insights.',
inProgress:
'You are actively participating in discussions and leaving your mark! Your dedication to commenting is paying off as you work toward this achievement.',
done: 'Congratulations on leaving your fiftieth comment! 🎉 You are a prolific commenter and a cornerstone of the community. Keep the conversations alive!',
},
},
];

const TABLE_NAME = 'achievements';

const ColumnName = {
KEY: 'key',
DESCRIPTION: 'description',
} as const;

async function up(knex: Knex): Promise<void> {
for (const achievement of UPDATED_ACHIEVEMENTS) {
await knex(TABLE_NAME)
.where(ColumnName.KEY, achievement.key)
.update({ [ColumnName.DESCRIPTION]: achievement.description });
}
}

async function down(): Promise<void> {}

export { down, up };
1 change: 1 addition & 0 deletions backend/src/packages/articles/article-socket.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class ArticleSocketService {
this.namespace.to(SocketRoom.ARTICLES_FEED).emit(NEW_ARTICLE, {
article,
isByFollowingAuthor: authorFollowersIds.has(socketUserId),
socketUserId,
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/libs/components/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DataStatus } from '~/libs/enums/enums.js';
import {
useAppDispatch,
useAppSelector,
useArticlesFeedRoom,
useEffect,
useNavigate,
} from '~/libs/hooks/hooks.js';
Expand All @@ -27,6 +28,7 @@ const App: React.FC = () => {
}));
const dispatch = useAppDispatch();
const navigate = useNavigate();
useArticlesFeedRoom();

const hasUser = Boolean(user);

Expand Down
Copy link
Collaborator

@artemmatiushenko1 artemmatiushenko1 Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you should cooperate with @canterbery, your tasks seem to be related #458

Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { AppRoute } from '~/libs/enums/app-route.enum.js';
import { useEffect, useLocation } from '~/libs/hooks/hooks.js';
import { getFullName } from '~/libs/helpers/helpers.js';
import {
useAppDispatch,
useAppSelector,
useEffect,
useLocation,
} from '~/libs/hooks/hooks.js';
import { SocketNamespace, SocketRoom } from '~/libs/packages/socket/socket.js';
import {
type ArticleReactionsSocketEventPayload,
Expand All @@ -9,15 +15,16 @@ import {
ArticleReactionsSocketEvent,
ArticleSocketEvent,
} from '~/packages/articles/articles.js';
import { actions as appActions } from '~/slices/app/app.js';
import { actions as articleActions } from '~/slices/articles/articles.js';

import { useAppDispatch } from '../use-app-dispatch/use-app-dispatch.hook.js';
import { useSocketNamespace } from '../use-socket-namespace/use-socket-namespace.hook.js';

const { NEW_ARTICLE } = ArticleSocketEvent;
const { NEW_REACTION } = ArticleReactionsSocketEvent;

const useArticlesFeedRoom = (): void => {
const { user } = useAppSelector(({ auth }) => auth);
const dispatch = useAppDispatch();
const location = useLocation();

Expand All @@ -40,8 +47,24 @@ const useArticlesFeedRoom = (): void => {
articlesSocket?.on(
NEW_ARTICLE,
(newArticle: ArticleSocketEventPayload[typeof NEW_ARTICLE]) => {
if (location.pathname === AppRoute.ARTICLES) {
void dispatch(articleActions.addArticle(newArticle));
if (user?.id === newArticle.socketUserId) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why we need to send socketUserId in the event's payload, can't it be
if (user?.id === newArticle.article.id) return ? Maybe I misunderstood something 😅

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case we only check the author of the article, but on the server we loop through all the sockets in the namespace and send to each user info about each socket on every loop cycle (for example if we have 5 sockets each user will get 5 payloads), so if only one user follows the author, every user will be notified of this publication due this one payload from that user.

if (location.pathname === AppRoute.ARTICLES) {
void dispatch(articleActions.addArticle(newArticle));
}

if (newArticle.isByFollowingAuthor) {
const { author } = newArticle.article;

void dispatch(
appActions.notify({
type: 'info',
message: `New article from ${getFullName(
author.firstName,
author.lastName,
)}`,
}),
);
}
}
},
);
Expand All @@ -52,7 +75,13 @@ const useArticlesFeedRoom = (): void => {
void dispatch(articleActions.addReactionToArticlesFeed(reaction));
},
);
}, [dispatch, articlesSocketReference, reactionsSocketReference, location]);
}, [
dispatch,
articlesSocketReference,
reactionsSocketReference,
location,
user,
]);
};

export { useArticlesFeedRoom };
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useAppSelector, useEffect, useReference } from '../hooks.js';
const useSocketNamespace = (
namespace: ValueOf<typeof SocketNamespace>,
roomId: string,
dependecies?: DependencyList,
dependencies?: DependencyList,
): MutableRefObject<Socket | null> => {
const userId = useAppSelector((state) => state.auth.user?.id);
const socketInstanceReference = useReference<Socket | null>(null);
Expand All @@ -35,7 +35,7 @@ const useSocketNamespace = (
namespace,
socketInstanceReference,
// eslint-disable-next-line react-hooks/exhaustive-deps
...(dependecies ?? []),
...(dependencies ?? []),
]);

return socketInstanceReference;
Expand Down
49 changes: 22 additions & 27 deletions frontend/src/pages/articles/articles-page.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,30 @@
import { Layout, Link, RouterOutlet } from '~/libs/components/components.js';
import { AppRoute } from '~/libs/enums/enums.js';
import { useArticlesFeedRoom } from '~/libs/hooks/hooks.js';

import styles from './styles.module.scss';

const ArticlesPage: React.FC = () => {
useArticlesFeedRoom();

return (
<Layout>
<div className={styles.wrapper}>
<div className={styles.tabsWrapper}>
<Link
to={AppRoute.ARTICLES}
className={styles.tab}
activeClassName={styles.activeTab}
>
Feed
</Link>
<Link
to={AppRoute.ARTICLES_MY_ARTICLES}
className={styles.tab}
activeClassName={styles.activeTab}
>
My articles
</Link>
</div>
<RouterOutlet />
const ArticlesPage: React.FC = () => (
<Layout>
<div className={styles.wrapper}>
<div className={styles.tabsWrapper}>
<Link
to={AppRoute.ARTICLES}
className={styles.tab}
activeClassName={styles.activeTab}
>
Feed
</Link>
<Link
to={AppRoute.ARTICLES_MY_ARTICLES}
className={styles.tab}
activeClassName={styles.activeTab}
>
My articles
</Link>
</div>
</Layout>
);
};
<RouterOutlet />
</div>
</Layout>
);

export { ArticlesPage };
18 changes: 2 additions & 16 deletions frontend/src/slices/articles/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,31 +66,17 @@ const addArticle = createAsyncThunk<
| null,
ArticleSocketEventPayload[typeof ArticleSocketEvent.NEW_ARTICLE],
AsyncThunkConfig
>(`${sliceName}/add-article`, (socketPayload, { getState, dispatch }) => {
>(`${sliceName}/add-article`, (socketPayload, { getState }) => {
const {
auth: { user },
} = getState();

const { article, isByFollowingAuthor } = socketPayload;
const { article } = socketPayload;

if (user?.id === article.userId) {
return null;
}

if (isByFollowingAuthor) {
const { author } = article;

void dispatch(
appActions.notify({
type: 'info',
message: `New article from ${getFullName(
author.firstName,
author.lastName,
)}`,
}),
);
}

return article;
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ type ArticleSocketEventPayload = {
[ArticleSocketEvent.NEW_ARTICLE]: {
isByFollowingAuthor: boolean;
article: ArticleWithCountsResponseDto;
socketUserId: number;
};
};

Expand Down
Loading