notification-group-view
This commit is contained in:
parent
db1c00e8af
commit
43b3de572b
|
@ -236,12 +236,6 @@
|
|||
"No notifications yet": "No notifications yet",
|
||||
"Nothing here yet": "There's nothing here yet",
|
||||
"Nothing is here": "There is nothing here",
|
||||
"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}}",
|
||||
"Notifications": "Notifications",
|
||||
"Or paste a link to an image": "Or paste a link to an image",
|
||||
"Ordered list": "Ordered list",
|
||||
|
@ -318,6 +312,8 @@
|
|||
"Slug": "Slug",
|
||||
"Social networks": "Social networks",
|
||||
"Society": "Society",
|
||||
"Some new comments to your publication": "{commentsCount, plural, one {New comment} other {{commentsCount} comments}} to your publication",
|
||||
"Some new replies to your comment": "{commentsCount, plural, one {New reply} other {{commentsCount} replays}} to your publication",
|
||||
"Something went wrong, check email and password": "Something went wrong. Check your email and password",
|
||||
"Something went wrong, please try again": "Something went wrong, please try again",
|
||||
"Song lyrics": "Song lyrics...",
|
||||
|
@ -421,6 +417,7 @@
|
|||
"actions": "actions",
|
||||
"add link": "add link",
|
||||
"all topics": "all topics",
|
||||
"and some more authors": "{restUsersCount, plural, =0 {} one { and one more user} other { and more {restUsersCount} users}}",
|
||||
"article": "article",
|
||||
"author": "author",
|
||||
"authors": "authors",
|
||||
|
@ -442,6 +439,7 @@
|
|||
"feed": "feed",
|
||||
"follower": "follower",
|
||||
"followersWithCount": "{count} {count, plural, one {follower} other {followers}}",
|
||||
"from": "from",
|
||||
"header 1": "header 1",
|
||||
"header 2": "header 2",
|
||||
"header 3": "header 3",
|
||||
|
|
|
@ -248,12 +248,6 @@
|
|||
"No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться",
|
||||
"Nothing here yet": "Здесь пока ничего нет",
|
||||
"Nothing is here": "Здесь ничего нет",
|
||||
"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} пользователей}}",
|
||||
"Notifications": "Уведомления",
|
||||
"Or paste a link to an image": "Или вставьте ссылку на изображение",
|
||||
"Ordered list": "Нумерованный список",
|
||||
|
@ -338,6 +332,8 @@
|
|||
"Slug": "Постоянная ссылка",
|
||||
"Social networks": "Социальные сети",
|
||||
"Society": "Общество",
|
||||
"Some new comments to your publication": "{commentsCount, plural, one {Новый комментарий} few {{commentsCount} новых комментария} other {{commentsCount} новых комментариев}} к вашей публикации",
|
||||
"Some new replies to your comment": "{commentsCount, plural, one {Новый ответ} few {{commentsCount} новых ответа} other {{commentsCount} новых ответов}} на ваш комментарий к публикации",
|
||||
"Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль",
|
||||
"Something went wrong, please try again": "Что-то пошло не так, попробуйте еще раз",
|
||||
"Song lyrics": "Текст песни...",
|
||||
|
@ -444,6 +440,7 @@
|
|||
"actions": "действия",
|
||||
"add link": "добавить ссылку",
|
||||
"all topics": "все темы",
|
||||
"and some more authors": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}",
|
||||
"article": "статья",
|
||||
"author": "автор",
|
||||
"authors": "авторы",
|
||||
|
@ -467,6 +464,7 @@
|
|||
"feed": "лента",
|
||||
"follower": "подписчик",
|
||||
"followersWithCount": "{count} {count, plural, one {подписчик} few {подписчика} other {подписчиков}}",
|
||||
"from": "от",
|
||||
"header 1": "заголовок 1",
|
||||
"header 2": "заголовок 2",
|
||||
"header 3": "заголовок 3",
|
||||
|
|
|
@ -299,7 +299,7 @@ export const FullArticle = (props: Props) => {
|
|||
const ogTitle = props.article.title
|
||||
const keywords = getKeywords(props.article)
|
||||
const getAuthorName = (a: Author) => {
|
||||
return lang() == 'en' && isCyrillic(a.name) ? capitalize(a.slug.replace(/-/, ' ')) : a.name
|
||||
return lang() === 'en' && isCyrillic(a.name) ? capitalize(a.slug.replace(/-/, ' ')) : a.name
|
||||
}
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -4,7 +4,6 @@ import { createMemo, createSignal, Match, Show, Switch } from 'solid-js'
|
|||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { ChatMember } from '../../../graphql/schema/chat.gen'
|
||||
import { Author, FollowingEntity } from '../../../graphql/schema/core.gen'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { follow, unfollow } from '../../../stores/zine/common'
|
||||
|
|
|
@ -59,7 +59,7 @@ const CreateModalContent = (props: Props) => {
|
|||
const handleCreate = async () => {
|
||||
try {
|
||||
const initChat = await actions.createChat(usersId(), chatTitle())
|
||||
console.debug('[components.Inbox] create chat result: ', initChat)
|
||||
console.debug('[components.Inbox] create chat result:', initChat)
|
||||
hideModal()
|
||||
await actions.loadChats()
|
||||
} catch (error) {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { clsx } from 'clsx'
|
||||
import { createEffect, createSignal } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { apiClient } from '../../../graphql/client/core'
|
||||
import { SearchResult } from '../../../graphql/schema/core.gen'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
import styles from './SearchModal.module.scss'
|
||||
import { apiClient } from '../../../graphql/client/core'
|
||||
import { createEffect, createSignal } from 'solid-js'
|
||||
import { SearchResult } from '../../../graphql/schema/core.gen'
|
||||
|
||||
export const SearchModal = () => {
|
||||
const { t } = useLocalize()
|
||||
|
|
|
@ -0,0 +1,194 @@
|
|||
import { getPagePath, openPage } from '@nanostores/router'
|
||||
import { clsx } from 'clsx'
|
||||
import { createEffect, For, Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { useNotifications } from '../../../context/notifications'
|
||||
import { Reaction } from '../../../graphql/schema/core.gen'
|
||||
import { Notification } from '../../../graphql/schema/notifier.gen'
|
||||
import { useRouter, router } from '../../../stores/router'
|
||||
import { GroupAvatar } from '../../_shared/GroupAvatar'
|
||||
import { TimeAgo } from '../../_shared/TimeAgo'
|
||||
import { ArticlePageSearchParams } from '../../Article/FullArticle'
|
||||
|
||||
import styles from './NotificationView.module.scss'
|
||||
|
||||
type NotificationGroupProps = {
|
||||
notifications: Notification[]
|
||||
onClick: () => void
|
||||
dateTimeFormat: 'ago' | 'time' | 'date'
|
||||
class?: string
|
||||
}
|
||||
|
||||
const getTitle = (title: string) => {
|
||||
let shoutTitle = ''
|
||||
let i = 0
|
||||
const shoutTitleWords = title.split(' ')
|
||||
|
||||
while (shoutTitle.length <= 30 && i < shoutTitleWords.length) {
|
||||
shoutTitle += shoutTitleWords[i] + ' '
|
||||
i++
|
||||
}
|
||||
|
||||
if (shoutTitle.length < title.length) {
|
||||
shoutTitle = `${shoutTitle.trim()}...`
|
||||
|
||||
if (shoutTitle[0] === '«') {
|
||||
shoutTitle += '»'
|
||||
}
|
||||
}
|
||||
return shoutTitle
|
||||
}
|
||||
|
||||
const reactionsCaption = (threadId: string) =>
|
||||
threadId.includes('__') ? 'Some new replies to your comment' : 'Some new comments to your publication'
|
||||
|
||||
export const NotificationGroup = (props: NotificationGroupProps) => {
|
||||
const { t, formatTime, formatDate } = useLocalize()
|
||||
const { changeSearchParam } = useRouter<ArticlePageSearchParams>()
|
||||
const {
|
||||
actions: { hideNotificationsPanel, markNotificationAsRead },
|
||||
} = useNotifications()
|
||||
const threads = new Map<string, Reaction[]>()
|
||||
const notificationsByThread = new Map<string, Notification[]>()
|
||||
|
||||
const handleClick = (threadId: string) => {
|
||||
props.onClick()
|
||||
|
||||
notificationsByThread.get(threadId).forEach((n: Notification) => {
|
||||
if (!n.seen) markNotificationAsRead(n)
|
||||
})
|
||||
|
||||
const threadParts = threadId.replace('::seen', '').split('__')
|
||||
openPage(router, 'article', { slug: threadParts[0] })
|
||||
if (threadParts.length > 1) {
|
||||
changeSearchParam({ commentId: threadParts[1] })
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const threadsLatest = {}
|
||||
|
||||
// count occurencies and sort reactions by threads
|
||||
props.notifications.forEach((n: Notification) => {
|
||||
const reaction = JSON.parse(n.payload)
|
||||
const slug = reaction.shout.slug
|
||||
const timestamp = reaction.created_at
|
||||
// threadId never shows up and looks like <slug>-<reaction_id>
|
||||
const threadId = slug + (reaction.reply_to ? `__${reaction.reply_to}` : '') + (n.seen ? `::seen` : '')
|
||||
const rrr = threads.get(threadId) || []
|
||||
const nnn = notificationsByThread.get(threadId) || []
|
||||
switch (n.entity) {
|
||||
case 'reaction': {
|
||||
switch (n.action) {
|
||||
case 'create': {
|
||||
rrr.push(reaction)
|
||||
threads.set(threadId, rrr)
|
||||
nnn.push(n)
|
||||
notificationsByThread.set(threadId, nnn)
|
||||
if (!(threadId in threadsLatest)) threadsLatest[threadId] = timestamp
|
||||
else if (timestamp > threadsLatest) threadsLatest[threadId] = timestamp
|
||||
|
||||
break
|
||||
}
|
||||
case 'delete': {
|
||||
// TODO: remove reaction from thread, update storage
|
||||
|
||||
break
|
||||
}
|
||||
case 'update': {
|
||||
// TODO: ignore for thread, update in storage
|
||||
|
||||
break
|
||||
}
|
||||
// No default
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'shout': {
|
||||
// TODO: handle notification about the
|
||||
// new shout from subscribed author, topic
|
||||
// or community (means all for one community)
|
||||
|
||||
break
|
||||
}
|
||||
case 'follower': {
|
||||
// TODO: handle new follower notification
|
||||
|
||||
break
|
||||
}
|
||||
default:
|
||||
// bypass chat and messages SSE
|
||||
}
|
||||
})
|
||||
|
||||
// sort reactions and threads by created_at
|
||||
Object.entries(threads)
|
||||
.sort(([ak, _av], [bk, _bv]) => threadsLatest[bk] - threadsLatest[ak])
|
||||
.forEach(([threadId, reaction], _idx, _arr) => {
|
||||
const rrr = threads.get(threadId) || []
|
||||
if (!rrr.includes(reaction)) {
|
||||
const updatedReactions: Reaction[] = [...rrr, reaction].sort(
|
||||
(a, b) => b.created_at - a.created_at,
|
||||
)
|
||||
threads.set(threadId, updatedReactions)
|
||||
}
|
||||
})
|
||||
})
|
||||
const handleLinkClick = (event: MouseEvent | TouchEvent) => {
|
||||
event.stopPropagation()
|
||||
hideNotificationsPanel()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<For each={[...threads.entries()]}>
|
||||
{([threadId, reactions], _index) => (
|
||||
<>
|
||||
{t(reactionsCaption(threadId), { commentsCount: reactions.length })}{' '}
|
||||
<div
|
||||
class={clsx(styles.NotificationView, props.class, {
|
||||
[styles.seen]: threadId.endsWith('::seen'),
|
||||
})}
|
||||
onClick={(_) => handleClick(threadId)}
|
||||
>
|
||||
<div class={styles.userpic}>
|
||||
<GroupAvatar authors={reactions.map((r: Reaction) => r.created_by)} />
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href={getPagePath(router, 'article', { slug: reactions[-1].shout.slug })}
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
{getTitle(reactions[-1].shout.title)}
|
||||
</a>{' '}
|
||||
{t('from')}{' '}
|
||||
<a
|
||||
href={getPagePath(router, 'author', { slug: reactions[-1].created_by.slug })}
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
{reactions[-1].created_by.name}
|
||||
</a>{' '}
|
||||
</div>
|
||||
|
||||
<div class={styles.timeContainer}>
|
||||
<Show when={props.dateTimeFormat === 'ago'}>
|
||||
<TimeAgo date={reactions[-1].created_at} />
|
||||
</Show>
|
||||
|
||||
<Show when={props.dateTimeFormat === 'time'}>
|
||||
{formatTime(new Date(reactions[-1].created_at))}
|
||||
</Show>
|
||||
|
||||
<Show when={props.dateTimeFormat === 'date'}>
|
||||
{formatDate(new Date(reactions[-1].created_at), { month: 'numeric', year: '2-digit' })}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
import type { ArticlePageSearchParams } from '../../Article/FullArticle'
|
||||
|
||||
import { getPagePath, openPage } from '@nanostores/router'
|
||||
import { clsx } from 'clsx'
|
||||
import { createMemo, createSignal, onMount, Show } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { useNotifications } from '../../../context/notifications'
|
||||
import { Reaction } from '../../../graphql/schema/core.gen'
|
||||
import { Notification } from '../../../graphql/schema/notifier.gen'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { GroupAvatar } from '../../_shared/GroupAvatar'
|
||||
import { TimeAgo } from '../../_shared/TimeAgo'
|
||||
|
||||
import styles from './NotificationView.module.scss'
|
||||
|
||||
type Props = {
|
||||
notification: Notification
|
||||
onClick: () => void
|
||||
dateTimeFormat: 'ago' | 'time' | 'date'
|
||||
class?: string
|
||||
}
|
||||
|
||||
export const NotificationView = (props: Props) => {
|
||||
const {
|
||||
actions: { markNotificationAsRead, hideNotificationsPanel },
|
||||
} = useNotifications()
|
||||
|
||||
const { changeSearchParam } = useRouter<ArticlePageSearchParams>() // TODO: use search params
|
||||
|
||||
const { t, formatDate, formatTime } = useLocalize()
|
||||
|
||||
const [data, setData] = createSignal<Reaction>(null) // NOTE: supports only SSMessage.entity == "reaction"
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => setData(JSON.parse(props.notification.payload)))
|
||||
})
|
||||
|
||||
const lastUser = createMemo(() => data().created_by)
|
||||
|
||||
const handleLinkClick = (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
hideNotificationsPanel()
|
||||
}
|
||||
|
||||
const content = createMemo(() => {
|
||||
if (!data()) {
|
||||
return null
|
||||
}
|
||||
|
||||
let shoutTitle = ''
|
||||
let i = 0
|
||||
const shoutTitleWords = data().shout.title.split(' ')
|
||||
|
||||
while (shoutTitle.length <= 30 && i < shoutTitleWords.length) {
|
||||
shoutTitle += shoutTitleWords[i] + ' '
|
||||
i++
|
||||
}
|
||||
|
||||
if (shoutTitle.length < data().shout.title.length) {
|
||||
shoutTitle = `${shoutTitle.trim()}...`
|
||||
|
||||
if (shoutTitle[0] === '«') {
|
||||
shoutTitle += '»'
|
||||
}
|
||||
}
|
||||
|
||||
switch (props.notification.action) {
|
||||
case 'create': {
|
||||
if (data()?.reply_to) {
|
||||
return (
|
||||
<>
|
||||
{t('NotificationNewReplyText1', {
|
||||
commentsCount: 0, // FIXME: 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: 0, // FIXME: data().users.length - 1,
|
||||
})}
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{t('NotificationNewCommentText1', {
|
||||
commentsCount: 0, // FIXME: 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: 0, // FIXME: data().users.length - 1,
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleClick = () => {
|
||||
props.onClick()
|
||||
|
||||
if (!props.notification.seen) {
|
||||
markNotificationAsRead(props.notification)
|
||||
}
|
||||
|
||||
openPage(router, 'article', { slug: data().shout.slug })
|
||||
// FIXME:
|
||||
// if (data().reactionIds) {
|
||||
// changeSearchParam({ commentId: data().reactionIds[0].toString() })
|
||||
// }
|
||||
}
|
||||
|
||||
const formattedDateTime = createMemo(() => {
|
||||
switch (props.dateTimeFormat) {
|
||||
case 'ago': {
|
||||
return <TimeAgo date={props.notification.created_at} />
|
||||
}
|
||||
case 'time': {
|
||||
return formatTime(new Date(props.notification.created_at))
|
||||
}
|
||||
case 'date': {
|
||||
return formatDate(new Date(props.notification.created_at), { month: 'numeric', year: '2-digit' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={data()}>
|
||||
<div
|
||||
class={clsx(styles.NotificationView, props.class, {
|
||||
[styles.seen]: props.notification.seen,
|
||||
})}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div class={styles.userpic}>
|
||||
<GroupAvatar authors={[] /*d FIXME: data().users */} />
|
||||
</div>
|
||||
<div>{content()}</div>
|
||||
<div class={styles.timeContainer}>{formattedDateTime()}</div>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
|
@ -1 +1 @@
|
|||
export { NotificationView } from './NotificationView'
|
||||
export { NotificationGroup } from './NotificationGroup'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { clsx } from 'clsx'
|
||||
import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from 'solid-js'
|
||||
import { createEffect, createMemo, createSignal, on, onCleanup, onMount, Show } from 'solid-js'
|
||||
import { throttle } from 'throttle-debounce'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
|
@ -11,7 +11,7 @@ import { Button } from '../_shared/Button'
|
|||
import { Icon } from '../_shared/Icon'
|
||||
|
||||
import { EmptyMessage } from './EmptyMessage'
|
||||
import { NotificationView } from './NotificationView'
|
||||
import { NotificationGroup } from './NotificationView/NotificationGroup'
|
||||
|
||||
import styles from './NotificationsPanel.module.scss'
|
||||
|
||||
|
@ -186,42 +186,30 @@ export const NotificationsPanel = (props: Props) => {
|
|||
<div class="col-xs-24">
|
||||
<Show when={todayNotifications().length > 0}>
|
||||
<div class={styles.periodTitle}>{t('today')}</div>
|
||||
<For each={todayNotifications()}>
|
||||
{(notification) => (
|
||||
<NotificationView
|
||||
notification={notification}
|
||||
<NotificationGroup
|
||||
notifications={todayNotifications()}
|
||||
class={styles.notificationView}
|
||||
onClick={handleNotificationViewClick}
|
||||
dateTimeFormat={'ago'}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
<Show when={yesterdayNotifications().length > 0}>
|
||||
<div class={styles.periodTitle}>{t('yesterday')}</div>
|
||||
<For each={yesterdayNotifications()}>
|
||||
{(notification) => (
|
||||
<NotificationView
|
||||
notification={notification}
|
||||
<NotificationGroup
|
||||
notifications={yesterdayNotifications()}
|
||||
class={styles.notificationView}
|
||||
onClick={handleNotificationViewClick}
|
||||
dateTimeFormat={'time'}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
<Show when={earlierNotifications().length > 0}>
|
||||
<div class={styles.periodTitle}>{t('earlier')}</div>
|
||||
<For each={earlierNotifications()}>
|
||||
{(notification) => (
|
||||
<NotificationView
|
||||
notification={notification}
|
||||
<NotificationGroup
|
||||
notifications={earlierNotifications()}
|
||||
class={styles.notificationView}
|
||||
onClick={handleNotificationViewClick}
|
||||
dateTimeFormat={'date'}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -48,11 +48,9 @@ export const ProfileSettings = () => {
|
|||
const {
|
||||
actions: { showSnackbar },
|
||||
} = useSnackbar()
|
||||
|
||||
const {
|
||||
actions: { setUser, authorizer },
|
||||
actions: { loadAuthor },
|
||||
} = useSession()
|
||||
|
||||
const {
|
||||
actions: { showConfirm },
|
||||
} = useConfirm()
|
||||
|
@ -101,10 +99,8 @@ export const ProfileSettings = () => {
|
|||
}
|
||||
showSnackbar({ type: 'error', body: t('Error') })
|
||||
}
|
||||
const profile = await authorizer().getProfile()
|
||||
if (profile) {
|
||||
setUser(profile)
|
||||
}
|
||||
|
||||
await loadAuthor() // renews author's profile
|
||||
}
|
||||
|
||||
const handleCancel = async () => {
|
||||
|
|
|
@ -52,7 +52,7 @@ type Props = {
|
|||
}
|
||||
|
||||
export const FeedView = (props: Props) => {
|
||||
const { t, lang } = useLocalize()
|
||||
const { t } = useLocalize()
|
||||
const { page, searchParams } = useRouter<FeedSearchParams>()
|
||||
const [isLoading, setIsLoading] = createSignal(false)
|
||||
const [isRightColumnLoaded, setIsRightColumnLoaded] = createSignal(false)
|
||||
|
@ -238,7 +238,7 @@ export const FeedView = (props: Props) => {
|
|||
/>
|
||||
</div>
|
||||
<div class={styles.commentDetails}>
|
||||
<AuthorLink author={comment.createdBy as Author} size={'XS'} />
|
||||
<AuthorLink author={comment.created_by as Author} size={'XS'} />
|
||||
<CommentDate comment={comment} isShort={true} isLastInRow={true} />
|
||||
</div>
|
||||
<div class={clsx('text-truncate', styles.commentArticleTitle)}>
|
||||
|
|
|
@ -17,7 +17,6 @@ import DialogCard from '../Inbox/DialogCard'
|
|||
import DialogHeader from '../Inbox/DialogHeader'
|
||||
import { Message } from '../Inbox/Message'
|
||||
import MessagesFallback from '../Inbox/MessagesFallback'
|
||||
import QuotedMessage from '../Inbox/QuotedMessage'
|
||||
import Search from '../Inbox/Search'
|
||||
import { Modal } from '../Nav/Modal'
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { SearchResult, Shout } from '../../graphql/schema/core.gen'
|
||||
import type { SearchResult } from '../../graphql/schema/core.gen'
|
||||
|
||||
import { Show, For, createSignal } from 'solid-js'
|
||||
|
||||
|
@ -22,7 +22,7 @@ const LOAD_MORE_PAGE_SIZE = 50
|
|||
|
||||
export const SearchView = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
const { articleEntities, sortedArticles } = useArticlesStore()
|
||||
const { sortedArticles } = useArticlesStore()
|
||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||
const [query, setQuery] = createSignal(props.query)
|
||||
const [offset, setOffset] = createSignal(0)
|
||||
|
@ -54,7 +54,7 @@ export const SearchView = (props: Props) => {
|
|||
name="q"
|
||||
ref={searchEl}
|
||||
onInput={handleQueryChange}
|
||||
placeholder={t('Enter text') + '...'}
|
||||
placeholder={query() || t('Enter text') + '...'}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
|
|
|
@ -55,7 +55,7 @@ export const TopicView = (props: Props) => {
|
|||
)
|
||||
const title = () =>
|
||||
`#${capitalize(
|
||||
lang() == 'en' ? topic()?.slug.replace(/-/, ' ') : topic()?.title || topic()?.slug.replace(/-/, ' '),
|
||||
lang() === 'en' ? topic()?.slug.replace(/-/, ' ') : topic()?.title || topic()?.slug.replace(/-/, ' '),
|
||||
true,
|
||||
)}`
|
||||
onMount(() => (document.title = title()))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { Accessor, JSX } from 'solid-js'
|
||||
|
||||
import { createStorageSignal } from '@solid-primitives/storage'
|
||||
import { createContext, createEffect, createMemo, createSignal, onMount, useContext } from 'solid-js'
|
||||
import { createContext, createMemo, createSignal, onMount, useContext } from 'solid-js'
|
||||
import { createStore } from 'solid-js/store'
|
||||
import { Portal } from 'solid-js/web'
|
||||
|
||||
|
@ -41,7 +41,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
|||
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
|
||||
const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0)
|
||||
const [notificationEntities, setNotificationEntities] = createStore<Record<number, Notification>>({})
|
||||
const { isAuthenticated } = useSession()
|
||||
const { isAuthenticated, author } = useSession()
|
||||
const { addHandler } = useConnect()
|
||||
|
||||
const loadNotifications = async (options: { after: number; limit?: number; offset?: number }) => {
|
||||
|
@ -81,10 +81,11 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
|||
|
||||
const markNotificationAsRead = async (notification: Notification) => {
|
||||
if (notifierClient.private) await notifierClient.markNotificationAsRead(notification.id)
|
||||
const nnn = new Set([...notification.seen, notification.id])
|
||||
setNotificationEntities(notification.id, 'seen', [...nnn])
|
||||
notification.seen.push(author().id)
|
||||
setNotificationEntities((nnn: Notification) => ({ ...nnn, [notification.id]: notification }))
|
||||
setUnreadNotificationsCount((oldCount) => oldCount - 1)
|
||||
}
|
||||
|
||||
const markAllNotificationsAsRead = async () => {
|
||||
if (isAuthenticated() && notifierClient.private) {
|
||||
await notifierClient.markAllNotificationsAsRead()
|
||||
|
|
|
@ -30,10 +30,7 @@ const userpicUrl = (userpic: string) => {
|
|||
return userpic
|
||||
}
|
||||
export const ProfileFormProvider = (props: { children: JSX.Element }) => {
|
||||
const {
|
||||
author,
|
||||
actions: { getToken },
|
||||
} = useSession()
|
||||
const { author } = useSession()
|
||||
const [form, setForm] = createStore<ProfileInput>({})
|
||||
|
||||
const currentSlug = createMemo(() => author()?.slug)
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import type { JSX } from 'solid-js'
|
||||
|
||||
import { createContext, createMemo, onCleanup, useContext } from 'solid-js'
|
||||
import { createContext, onCleanup, useContext } from 'solid-js'
|
||||
import { createStore, reconcile } from 'solid-js/store'
|
||||
|
||||
import { apiClient } from '../graphql/client/core'
|
||||
import { Reaction, ReactionBy, ReactionInput, ReactionKind } from '../graphql/schema/core.gen'
|
||||
|
||||
import { useSession } from './session'
|
||||
|
||||
type ReactionsContextType = {
|
||||
reactionEntities: Record<number, Reaction>
|
||||
actions: {
|
||||
|
@ -34,9 +32,6 @@ export function useReactions() {
|
|||
|
||||
export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||
const [reactionEntities, setReactionEntities] = createStore<Record<number, Reaction>>({})
|
||||
const {
|
||||
actions: { getToken },
|
||||
} = useSession()
|
||||
|
||||
const loadReactionsBy = async ({
|
||||
by,
|
||||
|
|
|
@ -48,6 +48,7 @@ export type SessionContextType = {
|
|||
loadSession: () => AuthToken | Promise<AuthToken>
|
||||
setSession: (token: AuthToken | null) => void // setSession
|
||||
loadAuthor: (info?: unknown) => Author | Promise<Author>
|
||||
setAuthor: (a: Author) => void
|
||||
loadSubscriptions: () => Promise<void>
|
||||
requireAuthentication: (
|
||||
callback: (() => Promise<void>) | (() => void),
|
||||
|
@ -92,14 +93,14 @@ export const SessionProvider = (props: {
|
|||
Authorization: tkn,
|
||||
})
|
||||
if (authResult?.access_token) {
|
||||
mutate(authResult)
|
||||
setSession(authResult)
|
||||
// console.debug('[context.session] token after:', authResult.access_token)
|
||||
await loadSubscriptions()
|
||||
return authResult
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[context.session] getSession error:', error)
|
||||
mutate(null)
|
||||
setSession(null)
|
||||
return null
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
|
@ -108,7 +109,7 @@ export const SessionProvider = (props: {
|
|||
}
|
||||
}
|
||||
|
||||
const [session, { refetch: loadSession, mutate }] = createResource<AuthToken>(getSession, {
|
||||
const [session, { refetch: loadSession, mutate: setSession }] = createResource<AuthToken>(getSession, {
|
||||
ssrLoadFrom: 'initial',
|
||||
initialValue: null,
|
||||
})
|
||||
|
@ -142,7 +143,7 @@ export const SessionProvider = (props: {
|
|||
}
|
||||
}
|
||||
|
||||
const [author, { refetch: loadAuthor }] = createResource<Author | null>(
|
||||
const [author, { refetch: loadAuthor, mutate: setAuthor }] = createResource<Author | null>(
|
||||
async () => {
|
||||
const u = session()?.user
|
||||
if (u) {
|
||||
|
@ -162,7 +163,7 @@ export const SessionProvider = (props: {
|
|||
const authResult: AuthToken | void = await authorizer().login(params)
|
||||
|
||||
if (authResult && authResult.access_token) {
|
||||
mutate(authResult)
|
||||
setSession(authResult)
|
||||
await loadSubscriptions()
|
||||
console.debug('[context.session] signed in')
|
||||
} else {
|
||||
|
@ -211,7 +212,7 @@ export const SessionProvider = (props: {
|
|||
setIsAuthWithCallback(() => callback)
|
||||
|
||||
const userdata = await authorizer().getProfile()
|
||||
if (userdata) mutate({ ...session(), user: userdata })
|
||||
if (userdata) setSession({ ...session(), user: userdata })
|
||||
|
||||
if (!isAuthenticated()) {
|
||||
showModal('auth', modalSource)
|
||||
|
@ -220,7 +221,7 @@ export const SessionProvider = (props: {
|
|||
|
||||
const signOut = async () => {
|
||||
await authorizer().logout()
|
||||
mutate(null)
|
||||
setSession(null)
|
||||
setSubscriptions(EMPTY_SUBSCRIPTIONS)
|
||||
showSnackbar({ body: t("You've successfully logged out") })
|
||||
}
|
||||
|
@ -228,7 +229,7 @@ export const SessionProvider = (props: {
|
|||
const confirmEmail = async (input: VerifyEmailInput) => {
|
||||
console.debug(`[context.session] calling authorizer's verify email with`, input)
|
||||
const at: void | AuthToken = await authorizer().verifyEmail(input)
|
||||
if (at) mutate(at)
|
||||
if (at) setSession(at)
|
||||
console.log(`[context.session] confirmEmail got result ${at}`)
|
||||
}
|
||||
|
||||
|
@ -243,7 +244,8 @@ export const SessionProvider = (props: {
|
|||
signOut,
|
||||
confirmEmail,
|
||||
setIsSessionLoaded,
|
||||
setSession: mutate,
|
||||
setSession,
|
||||
setAuthor,
|
||||
authorizer,
|
||||
loadAuthor,
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ export const onBeforeRender = async (pageContext: PageContext) => {
|
|||
const { layout } = pageContext.routeParams
|
||||
|
||||
const expoShouts = await apiClient.getShouts({
|
||||
filters: { layouts: ['audio', 'video', 'literature', 'image'] },
|
||||
filters: { layouts: layout ? [layout] : ['audio', 'video', 'literature', 'image'] },
|
||||
limit: PRERENDERED_ARTICLES_COUNT,
|
||||
})
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import type {
|
||||
Author,
|
||||
Shout,
|
||||
ShoutInput,
|
||||
LoadShoutsOptions,
|
||||
QueryLoad_Shouts_SearchArgs,
|
||||
} from '../../graphql/schema/core.gen'
|
||||
|
|
Loading…
Reference in New Issue
Block a user