notification system update (#265)

* notification system update
Co-authored-by: Igor Lobanov <igor.lobanov@onetwotrip.com>
This commit is contained in:
Ilya Y 2023-10-16 20:24:33 +03:00 committed by GitHub
parent 4da78d2e68
commit 9262367f68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 162 additions and 63 deletions

View File

@ -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",

View File

@ -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": "Здесь ничего нет",

View File

@ -141,7 +141,7 @@ export const Comment = (props: Props) => {
})}
/>
<small>
<a href={`#comment-${comment()?.id}`}>{comment()?.shout.title || ''}</a>
<a href={`#comment_${comment()?.id}`}>{comment()?.shout.title || ''}</a>
</small>
</div>
}
@ -179,7 +179,7 @@ export const Comment = (props: Props) => {
<CommentRatingControl comment={comment()} />
</div>
</Show>
<div class={styles.commentBody} id={'comment-' + (comment().id || '')}>
<div class={styles.commentBody}>
<Show when={editMode()} fallback={<MD body={body()} />}>
<Suspense fallback={<p>{t('Loading')}</p>}>
<SimplifiedEditor

View File

@ -181,7 +181,7 @@ export const CommentsTree = (props: Props) => {
<SimplifiedEditor
quoteEnabled={true}
imageEnabled={true}
autoFocus={true}
autoFocus={false}
submitByCtrlEnter={true}
placeholder={t('Write a comment...')}
onSubmit={(value) => handleSubmitComment(value)}

View File

@ -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<ArticlePageSearchParams>()
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<HTMLElement>(
`[id='comment_${searchParams().commentId}']`
)
changeSearchParam({ commentId: null })
if (commentElement) {
commentElement.scrollIntoView({ behavior: 'smooth' })
scrollTo(commentElement)
}
}
})

View File

@ -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<HTMLDivElement>('.main-content')
if (fixed() || modal() !== null) {
windowScrollTop = window.scrollY

View File

@ -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;
}

View File

@ -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 (
<div class={clsx(styles.EmptyMessage)}>
<div class={styles.title}>{t('No notifications yet')}</div>
<div>{t("Write good articles, comment\nand it won't be so empty here")}</div>
</div>
)
}

View File

@ -0,0 +1 @@
export { EmptyMessage } from './EmptyMessage'

View File

@ -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 {

View File

@ -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<ArticlePageSearchParams>()
const { t } = useLocalize()
const [data, setData] = createSignal<NotificationData>(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
})}{' '}
<a href={getPagePath(router, 'article', { slug: data().shout.slug })} onClick={handleLinkClick}>
{shoutTitle}
</a>{' '}
{t('NotificationNewCommentText2')}{' '}
<a href={getPagePath(router, 'author', { slug: lastUser().slug })} onClick={handleLinkClick}>
{lastUser().name}
</a>{' '}
{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
})}{' '}
<a href={getPagePath(router, 'article', { slug: data().shout.slug })} onClick={handleLinkClick}>
{shoutTitle}
</a>{' '}
{t('NotificationNewReplyText2')}{' '}
<a href={getPagePath(router, 'author', { slug: lastUser().slug })} onClick={handleLinkClick}>
{lastUser().name}
</a>{' '}
{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 (

View File

@ -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<HTMLDivElement>('.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) => {
<Icon name="close" />
</div>
<div class={styles.title}>{t('Notifications')}</div>
<For
each={sortedNotifications()}
fallback={<div class={styles.emptyMessageContainer}>{t('No notifications, yet')}</div>}
>
<For each={sortedNotifications()} fallback={<EmptyMessage />}>
{(notification) => (
<NotificationView
notification={notification}

View File

@ -16,6 +16,7 @@ type NotificationsContextType = {
sortedNotifications: Accessor<Notification[]>
actions: {
showNotificationsPanel: () => void
hideNotificationsPanel: () => void
markNotificationAsRead: (notification: Notification) => Promise<void>
}
}
@ -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,

View File

@ -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