From 9262367f688d31b92f1ea9f5b6573bf18e45b570 Mon Sep 17 00:00:00 2001 From: Ilya Y <75578537+ilya-bkv@users.noreply.github.com> Date: Mon, 16 Oct 2023 20:24:33 +0300 Subject: [PATCH] notification system update (#265) * notification system update Co-authored-by: Igor Lobanov --- public/locales/en/translation.json | 15 +++- public/locales/ru/translation.json | 14 +++- src/components/Article/Comment.tsx | 4 +- src/components/Article/CommentsTree.tsx | 2 +- src/components/Article/FullArticle.tsx | 31 +++++-- src/components/Nav/Header/Header.tsx | 2 +- .../EmptyMessage/EmptyMessage.module.scss | 12 +++ .../EmptyMessage/EmptyMessage.tsx | 14 ++++ .../NotificationsPanel/EmptyMessage/index.ts | 1 + .../NotificationView.module.scss | 7 ++ .../NotificationView/NotificationView.tsx | 83 +++++++++++++------ .../NotificationsPanel/NotificationsPanel.tsx | 20 ++++- src/context/notifications.tsx | 7 +- src/pages/article.page.tsx | 13 +-- 14 files changed, 162 insertions(+), 63 deletions(-) create mode 100644 src/components/NotificationsPanel/EmptyMessage/EmptyMessage.module.scss create mode 100644 src/components/NotificationsPanel/EmptyMessage/EmptyMessage.tsx create mode 100644 src/components/NotificationsPanel/EmptyMessage/index.ts diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 1f5e43b4..577ddfcf 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -210,12 +210,19 @@ "New only": "New only", "New password": "New password", "New stories every day and even more!": "New stories and more are waiting for you every day!", - "NewCommentNotificationText": "{commentsCount, plural, one {New comment} other {{commentsCount} comments}} to your publication {shoutTitle} from {lastCommenterName}{restUsersCount, plural, =0 {} one { one more user} other { and more {restUsersCount} users}}", - "NewReplyNotificationText": "{commentsCount, plural, one {New reply} other {{commentsCount} replays} other {{commentsCount} новых ответов}} to your publication {shoutTitle} от {lastCommenterName}{restUsersCount, plural, =0 {} one { and one more user} other { and more {restUsersCount} users}}", + + "NotificationNewCommentText1": "{commentsCount, plural, one {New comment} other {{commentsCount} comments}} to your publication", + "NotificationNewCommentText2": "from", + "NotificationNewCommentText3": "{restUsersCount, plural, =0 {} one { one more user} other { and more {restUsersCount} users}}", + + "NotificationNewReplyText1": "{commentsCount, plural, one {New reply} other {{commentsCount} replays}} to your publication", + "NotificationNewReplyText2": "from", + "NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { and one more user} other { and more {restUsersCount} users}}", + "Newsletter": "Newsletter", "Night mode": "Night mode", - "No notifications, yet": "No notifications, yet", - "No such account, please try to register": "No such account found, please try to register", + "No notifications yet": "No notifications yet", + "Write good articles, comment\nand it won't be so empty here": "Write good articles, comment\nand it won't be so empty here", "Nothing here yet": "There's nothing here yet", "Nothing is here": "There is nothing here", "Notifications": "Notifications", diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index cb2559b3..a263456e 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -220,11 +220,19 @@ "New only": "Только новые", "New password": "Новый пароль", "New stories every day and even more!": "Каждый день вас ждут новые истории и ещё много всего интересного!", - "NewCommentNotificationText": "{commentsCount, plural, one {Новый комментарий} few {{commentsCount} новых комментария} other {{commentsCount} новых комментариев}} к вашей публикации {shoutTitle} от {lastCommenterName}{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}", - "NewReplyNotificationText": "{commentsCount, plural, one {Новый ответ} few {{commentsCount} новых ответа} other {{commentsCount} новых ответов}} к вашему комментарию к публикации {shoutTitle} от {lastCommenterName}{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}", + + "NotificationNewCommentText1": "{commentsCount, plural, one {Новый комментарий} few {{commentsCount} новых комментария} other {{commentsCount} новых комментариев}} к вашей публикации", + "NotificationNewCommentText2": "от", + "NotificationNewCommentText3": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}", + + "NotificationNewReplyText1": "{commentsCount, plural, one {Новый ответ} few {{commentsCount} новых ответа} other {{commentsCount} новых ответов}} к вашему комментарию к публикации", + "NotificationNewReplyText2": "от", + "NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}", + "Newsletter": "Рассылка", "Night mode": "Ночная тема", - "No notifications, yet": "Тут пока пусто", + "No notifications yet": "Уведомлений пока нет", + "Write good articles, comment\nand it won't be so empty here": "Пишите хорошие статьи, комментируйте,\nи здесь станет не так пусто", "No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться", "Nothing here yet": "Здесь пока ничего нет", "Nothing is here": "Здесь ничего нет", diff --git a/src/components/Article/Comment.tsx b/src/components/Article/Comment.tsx index 823a59f7..3405b49b 100644 --- a/src/components/Article/Comment.tsx +++ b/src/components/Article/Comment.tsx @@ -141,7 +141,7 @@ export const Comment = (props: Props) => { })} /> - {comment()?.shout.title || ''} + {comment()?.shout.title || ''} } @@ -179,7 +179,7 @@ export const Comment = (props: Props) => { -
+
}> {t('Loading')}

}> { handleSubmitComment(value)} diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 7d68604b..705fb083 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -28,11 +28,24 @@ import styles from './Article.module.scss' import { CardTopic } from '../Feed/CardTopic' import { createPopper } from '@popperjs/core' -interface Props { +type Props = { article: Shout scrollToComments?: boolean } +export type ArticlePageSearchParams = { + scrollTo: 'comments' + commentId: string +} + +const scrollTo = (el: HTMLElement) => { + window.scrollTo({ + top: el.offsetTop - 96, + left: 0, + behavior: 'smooth' + }) +} + export const FullArticle = (props: Props) => { const { t } = useLocalize() const { @@ -78,15 +91,12 @@ export const FullArticle = (props: Props) => { }) const commentsRef: { current: HTMLDivElement } = { current: null } + const scrollToComments = () => { - window.scrollTo({ - top: commentsRef.current.offsetTop - 96, - left: 0, - behavior: 'smooth' - }) + scrollTo(commentsRef.current) } - const { searchParams, changeSearchParam } = useRouter() + const { searchParams, changeSearchParam } = useRouter() createEffect(() => { if (props.scrollToComments) { @@ -105,9 +115,12 @@ export const FullArticle = (props: Props) => { createEffect(() => { if (searchParams().commentId && isReactionsLoaded()) { - const commentElement = document.querySelector(`[id='comment_${searchParams().commentId}']`) + const commentElement = document.querySelector( + `[id='comment_${searchParams().commentId}']` + ) + changeSearchParam({ commentId: null }) if (commentElement) { - commentElement.scrollIntoView({ behavior: 'smooth' }) + scrollTo(commentElement) } } }) diff --git a/src/components/Nav/Header/Header.tsx b/src/components/Nav/Header/Header.tsx index 4ce186bd..855086ac 100644 --- a/src/components/Nav/Header/Header.tsx +++ b/src/components/Nav/Header/Header.tsx @@ -67,7 +67,7 @@ export const Header = (props: Props) => { let windowScrollTop = 0 createEffect(() => { - const mainContent = document.querySelector('.main-content') as HTMLDivElement + const mainContent = document.querySelector('.main-content') if (fixed() || modal() !== null) { windowScrollTop = window.scrollY diff --git a/src/components/NotificationsPanel/EmptyMessage/EmptyMessage.module.scss b/src/components/NotificationsPanel/EmptyMessage/EmptyMessage.module.scss new file mode 100644 index 00000000..b1484a0d --- /dev/null +++ b/src/components/NotificationsPanel/EmptyMessage/EmptyMessage.module.scss @@ -0,0 +1,12 @@ +.EmptyMessage { + // TODO: check markup + color: var(--black-500); + text-align: center; + font-size: 15px; + line-height: 24px; + white-space: pre-line; +} + +.title { + font-weight: 500; +} diff --git a/src/components/NotificationsPanel/EmptyMessage/EmptyMessage.tsx b/src/components/NotificationsPanel/EmptyMessage/EmptyMessage.tsx new file mode 100644 index 00000000..855009ff --- /dev/null +++ b/src/components/NotificationsPanel/EmptyMessage/EmptyMessage.tsx @@ -0,0 +1,14 @@ +import { clsx } from 'clsx' +import styles from './EmptyMessage.module.scss' +import { useLocalize } from '../../../context/localize' + +export const EmptyMessage = () => { + const { t } = useLocalize() + + return ( +
+
{t('No notifications yet')}
+
{t("Write good articles, comment\nand it won't be so empty here")}
+
+ ) +} diff --git a/src/components/NotificationsPanel/EmptyMessage/index.ts b/src/components/NotificationsPanel/EmptyMessage/index.ts new file mode 100644 index 00000000..f26b9a9c --- /dev/null +++ b/src/components/NotificationsPanel/EmptyMessage/index.ts @@ -0,0 +1 @@ +export { EmptyMessage } from './EmptyMessage' diff --git a/src/components/NotificationsPanel/NotificationView/NotificationView.module.scss b/src/components/NotificationsPanel/NotificationView/NotificationView.module.scss index 284af187..26401acd 100644 --- a/src/components/NotificationsPanel/NotificationView/NotificationView.module.scss +++ b/src/components/NotificationsPanel/NotificationView/NotificationView.module.scss @@ -20,6 +20,13 @@ &:hover { background-color: var(--gray-100); } + + a, + a:visited { + padding-bottom: 0 !important; + border-bottom: none !important; + font-weight: 700; + } } .userpic { diff --git a/src/components/NotificationsPanel/NotificationView/NotificationView.tsx b/src/components/NotificationsPanel/NotificationView/NotificationView.tsx index 079d3a73..00cfa019 100644 --- a/src/components/NotificationsPanel/NotificationView/NotificationView.tsx +++ b/src/components/NotificationsPanel/NotificationView/NotificationView.tsx @@ -3,11 +3,12 @@ import styles from './NotificationView.module.scss' import type { Notification } from '../../../graphql/types.gen' import { createMemo, createSignal, onMount, Show } from 'solid-js' import { NotificationType } from '../../../graphql/types.gen' -import { openPage } from '@nanostores/router' -import { router } from '../../../stores/router' +import { getPagePath, openPage } from '@nanostores/router' +import { router, useRouter } from '../../../stores/router' import { useNotifications } from '../../../context/notifications' import { Userpic } from '../../Author/Userpic' import { useLocalize } from '../../../context/localize' +import type { ArticlePageSearchParams } from '../../Article/FullArticle' type Props = { notification: Notification @@ -26,13 +27,16 @@ type NotificationData = { slug: string userpic: string }[] + reactionIds: number[] } export const NotificationView = (props: Props) => { const { - actions: { markNotificationAsRead } + actions: { markNotificationAsRead, hideNotificationsPanel } } = useNotifications() + const { changeSearchParam } = useRouter() + const { t } = useLocalize() const [data, setData] = createSignal(null) @@ -49,6 +53,11 @@ export const NotificationView = (props: Props) => { return data().users[data().users.length - 1] }) + const handleLinkClick = (event: MouseEvent) => { + event.stopPropagation() + hideNotificationsPanel() + } + const content = createMemo(() => { if (!data()) { return null @@ -64,47 +73,67 @@ export const NotificationView = (props: Props) => { } if (shoutTitle.length < data().shout.title.length) { - shoutTitle += '...' + shoutTitle = `${shoutTitle.trim()}...` + + if (shoutTitle[0] === '«') { + shoutTitle += '»' + } } switch (props.notification.type) { case NotificationType.NewComment: { - return t('NewCommentNotificationText', { - commentsCount: props.notification.occurrences, - shoutTitle, - lastCommenterName: lastUser().name, - restUsersCount: data().users.length - 1 - }) + return ( + <> + {t('NotificationNewCommentText1', { + commentsCount: props.notification.occurrences + })}{' '} + + {shoutTitle} + {' '} + {t('NotificationNewCommentText2')}{' '} + + {lastUser().name} + {' '} + {t('NotificationNewCommentText3', { + restUsersCount: data().users.length - 1 + })} + + ) } case NotificationType.NewReply: { - return t('NewReplyNotificationText', { - commentsCount: props.notification.occurrences, - shoutTitle, - lastCommenterName: lastUser().name, - restUsersCount: data().users.length - 1 - }) + return ( + <> + {t('NotificationNewReplyText1', { + commentsCount: props.notification.occurrences + })}{' '} + + {shoutTitle} + {' '} + {t('NotificationNewReplyText2')}{' '} + + {lastUser().name} + {' '} + {t('NotificationNewReplyText3', { + restUsersCount: data().users.length - 1 + })} + + ) } } }) const handleClick = () => { + props.onClick() + if (!props.notification.seen) { markNotificationAsRead(props.notification) } openPage(router, 'article', { slug: data().shout.slug }) - props.onClick() - // switch (props.notification.type) { - // case NotificationType.NewComment: { - // openPage(router, 'article', { slug: data().shout.slug }) - // break - // } - // case NotificationType.NewReply: { - // openPage(router, 'article', { slug: data().shout.slug }) - // break - // } - // } + if (data().reactionIds) { + changeSearchParam({ commentId: data().reactionIds[0].toString() }) + } } return ( diff --git a/src/components/NotificationsPanel/NotificationsPanel.tsx b/src/components/NotificationsPanel/NotificationsPanel.tsx index 4041dbed..c47e770f 100644 --- a/src/components/NotificationsPanel/NotificationsPanel.tsx +++ b/src/components/NotificationsPanel/NotificationsPanel.tsx @@ -7,6 +7,7 @@ import { Icon } from '../_shared/Icon' import { createEffect, For } from 'solid-js' import { useNotifications } from '../../context/notifications' import { NotificationView } from './NotificationView' +import { EmptyMessage } from './EmptyMessage' type Props = { isOpen: boolean @@ -30,8 +31,22 @@ export const NotificationsPanel = (props: Props) => { handler: () => handleHide() }) + let windowScrollTop = 0 + createEffect(() => { + const mainContent = document.querySelector('.main-content') + + if (props.isOpen) { + windowScrollTop = window.scrollY + mainContent.style.marginTop = `-${windowScrollTop}px` + } + document.body.classList.toggle('fixed', props.isOpen) + + if (!props.isOpen) { + mainContent.style.marginTop = '' + window.scrollTo(0, windowScrollTop) + } }) useEscKeyDownHandler(handleHide) @@ -52,10 +67,7 @@ export const NotificationsPanel = (props: Props) => {
{t('Notifications')}
- {t('No notifications, yet')}
} - > + }> {(notification) => ( actions: { showNotificationsPanel: () => void + hideNotificationsPanel: () => void markNotificationAsRead: (notification: Notification) => Promise } } @@ -80,7 +81,11 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => { setIsNotificationsPanelOpen(true) } - const actions = { showNotificationsPanel, markNotificationAsRead } + const hideNotificationsPanel = () => { + setIsNotificationsPanelOpen(false) + } + + const actions = { showNotificationsPanel, hideNotificationsPanel, markNotificationAsRead } const value: NotificationsContextType = { notificationEntities, diff --git a/src/pages/article.page.tsx b/src/pages/article.page.tsx index c522b76d..c5ed9aac 100644 --- a/src/pages/article.page.tsx +++ b/src/pages/article.page.tsx @@ -11,18 +11,9 @@ import { setPageLoadManagerPromise } from '../utils/pageLoadManager' export const ArticlePage = (props: PageProps) => { const shouts = props.article ? [props.article] : [] + const { page } = useRouter() - const slug = createMemo(() => { - const { page: getPage } = useRouter() - - const page = getPage() - - if (page.route !== 'article') { - throw new Error('ts guard') - } - - return page.params.slug - }) + const slug = createMemo(() => page().params['slug'] as string) const { articleEntities } = useArticlesStore({ shouts