notification system update (#265)
* notification system update Co-authored-by: Igor Lobanov <igor.lobanov@onetwotrip.com>
This commit is contained in:
parent
4da78d2e68
commit
9262367f68
|
@ -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",
|
||||
|
|
|
@ -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": "Здесь ничего нет",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
1
src/components/NotificationsPanel/EmptyMessage/index.ts
Normal file
1
src/components/NotificationsPanel/EmptyMessage/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { EmptyMessage } from './EmptyMessage'
|
|
@ -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 {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user