2023-10-14 11:39:24 +00:00
|
|
|
import { clsx } from 'clsx'
|
|
|
|
import styles from './NotificationsPanel.module.scss'
|
|
|
|
import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler'
|
|
|
|
import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler'
|
|
|
|
import { useLocalize } from '../../context/localize'
|
|
|
|
import { Icon } from '../_shared/Icon'
|
2023-11-01 15:13:54 +00:00
|
|
|
import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from 'solid-js'
|
|
|
|
import { PAGE_SIZE, useNotifications } from '../../context/notifications'
|
2023-10-14 11:39:24 +00:00
|
|
|
import { NotificationView } from './NotificationView'
|
2023-10-16 17:24:33 +00:00
|
|
|
import { EmptyMessage } from './EmptyMessage'
|
2023-11-01 15:13:54 +00:00
|
|
|
import { Button } from '../_shared/Button'
|
2023-11-13 15:55:36 +00:00
|
|
|
import { throttle } from 'throttle-debounce'
|
2023-11-01 15:13:54 +00:00
|
|
|
import { useSession } from '../../context/session'
|
2023-10-14 11:39:24 +00:00
|
|
|
|
|
|
|
type Props = {
|
|
|
|
isOpen: boolean
|
|
|
|
onClose: () => void
|
|
|
|
}
|
|
|
|
|
2023-10-18 10:56:41 +00:00
|
|
|
const getYesterdayStart = () => {
|
|
|
|
const now = new Date()
|
|
|
|
return new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 0, 0, 0, 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
const isSameDate = (date1: Date, date2: Date) =>
|
|
|
|
date1.getDate() === date2.getDate() &&
|
|
|
|
date1.getMonth() === date2.getMonth() &&
|
|
|
|
date1.getFullYear() === date2.getFullYear()
|
|
|
|
|
|
|
|
const isToday = (date: Date) => {
|
|
|
|
return isSameDate(date, new Date())
|
|
|
|
}
|
|
|
|
|
|
|
|
const isYesterday = (date: Date) => {
|
|
|
|
const yesterday = getYesterdayStart()
|
|
|
|
return isSameDate(date, yesterday)
|
|
|
|
}
|
|
|
|
|
|
|
|
const isEarlier = (date: Date) => {
|
|
|
|
const yesterday = getYesterdayStart()
|
|
|
|
return date.getTime() < yesterday.getTime()
|
|
|
|
}
|
|
|
|
|
2023-10-14 11:39:24 +00:00
|
|
|
export const NotificationsPanel = (props: Props) => {
|
2023-11-01 15:13:54 +00:00
|
|
|
const [isLoading, setIsLoading] = createSignal(false)
|
|
|
|
|
|
|
|
const { isAuthenticated } = useSession()
|
2023-10-14 11:39:24 +00:00
|
|
|
const { t } = useLocalize()
|
2023-11-01 15:13:54 +00:00
|
|
|
const {
|
|
|
|
sortedNotifications,
|
|
|
|
unreadNotificationsCount,
|
|
|
|
loadedNotificationsCount,
|
|
|
|
totalNotificationsCount,
|
|
|
|
actions: { loadNotifications, markAllNotificationsAsRead }
|
|
|
|
} = useNotifications()
|
2023-10-14 11:39:24 +00:00
|
|
|
const handleHide = () => {
|
|
|
|
props.onClose()
|
|
|
|
}
|
|
|
|
|
|
|
|
const panelRef: { current: HTMLDivElement } = {
|
|
|
|
current: null
|
|
|
|
}
|
|
|
|
|
|
|
|
useOutsideClickHandler({
|
|
|
|
containerRef: panelRef,
|
|
|
|
predicate: () => props.isOpen,
|
|
|
|
handler: () => handleHide()
|
|
|
|
})
|
|
|
|
|
2023-10-16 17:24:33 +00:00
|
|
|
let windowScrollTop = 0
|
|
|
|
|
2023-10-14 11:39:24 +00:00
|
|
|
createEffect(() => {
|
2023-10-16 17:24:33 +00:00
|
|
|
const mainContent = document.querySelector<HTMLDivElement>('.main-content')
|
|
|
|
|
|
|
|
if (props.isOpen) {
|
|
|
|
windowScrollTop = window.scrollY
|
|
|
|
mainContent.style.marginTop = `-${windowScrollTop}px`
|
|
|
|
}
|
|
|
|
|
2023-10-14 11:39:24 +00:00
|
|
|
document.body.classList.toggle('fixed', props.isOpen)
|
2023-10-16 17:24:33 +00:00
|
|
|
|
|
|
|
if (!props.isOpen) {
|
|
|
|
mainContent.style.marginTop = ''
|
|
|
|
window.scrollTo(0, windowScrollTop)
|
|
|
|
}
|
2023-10-14 11:39:24 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
useEscKeyDownHandler(handleHide)
|
|
|
|
|
|
|
|
const handleNotificationViewClick = () => {
|
|
|
|
handleHide()
|
|
|
|
}
|
|
|
|
|
2023-10-18 10:56:41 +00:00
|
|
|
const todayNotifications = createMemo(() => {
|
|
|
|
return sortedNotifications().filter((notification) => isToday(new Date(notification.createdAt)))
|
|
|
|
})
|
|
|
|
|
|
|
|
const yesterdayNotifications = createMemo(() => {
|
|
|
|
return sortedNotifications().filter((notification) => isYesterday(new Date(notification.createdAt)))
|
|
|
|
})
|
|
|
|
|
|
|
|
const earlierNotifications = createMemo(() => {
|
|
|
|
return sortedNotifications().filter((notification) => isEarlier(new Date(notification.createdAt)))
|
|
|
|
})
|
|
|
|
|
2023-11-01 15:13:54 +00:00
|
|
|
const scrollContainerRef: { current: HTMLDivElement } = { current: null }
|
|
|
|
const loadNextPage = async () => {
|
|
|
|
await loadNotifications({ limit: PAGE_SIZE, offset: loadedNotificationsCount() })
|
|
|
|
if (loadedNotificationsCount() < totalNotificationsCount()) {
|
|
|
|
const hasMore = scrollContainerRef.current.scrollHeight <= scrollContainerRef.current.offsetHeight
|
|
|
|
|
|
|
|
if (hasMore) {
|
|
|
|
await loadNextPage()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const handleScroll = async () => {
|
|
|
|
if (!scrollContainerRef.current || isLoading()) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (totalNotificationsCount() === loadedNotificationsCount()) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const isNearBottom =
|
|
|
|
scrollContainerRef.current.scrollHeight - scrollContainerRef.current.scrollTop <=
|
|
|
|
scrollContainerRef.current.clientHeight * 1.5
|
|
|
|
|
|
|
|
if (isNearBottom) {
|
|
|
|
setIsLoading(true)
|
|
|
|
await loadNextPage()
|
|
|
|
setIsLoading(false)
|
|
|
|
}
|
|
|
|
}
|
2023-11-13 15:55:36 +00:00
|
|
|
const handleScrollThrottled = throttle(50, handleScroll)
|
2023-11-01 15:13:54 +00:00
|
|
|
|
|
|
|
onMount(() => {
|
|
|
|
scrollContainerRef.current.addEventListener('scroll', handleScrollThrottled)
|
|
|
|
onCleanup(() => {
|
|
|
|
scrollContainerRef.current.removeEventListener('scroll', handleScrollThrottled)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
createEffect(
|
|
|
|
on(
|
|
|
|
() => isAuthenticated(),
|
|
|
|
async () => {
|
|
|
|
if (isAuthenticated()) {
|
|
|
|
setIsLoading(true)
|
|
|
|
await loadNextPage()
|
|
|
|
setIsLoading(false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2023-10-14 11:39:24 +00:00
|
|
|
return (
|
|
|
|
<div
|
|
|
|
class={clsx(styles.container, {
|
|
|
|
[styles.isOpened]: props.isOpen
|
|
|
|
})}
|
|
|
|
>
|
|
|
|
<div ref={(el) => (panelRef.current = el)} class={styles.panel}>
|
|
|
|
<div class={styles.closeButton} onClick={handleHide}>
|
2023-11-02 03:31:00 +00:00
|
|
|
<Icon class={styles.closeIcon} name="close" />
|
2023-10-14 11:39:24 +00:00
|
|
|
</div>
|
|
|
|
<div class={styles.title}>{t('Notifications')}</div>
|
2023-11-01 15:13:54 +00:00
|
|
|
<div class={clsx('wide-container', styles.content)} ref={(el) => (scrollContainerRef.current = el)}>
|
|
|
|
<Show
|
|
|
|
when={sortedNotifications().length > 0}
|
|
|
|
fallback={
|
|
|
|
<Show when={!isLoading()}>
|
|
|
|
<EmptyMessage />
|
|
|
|
</Show>
|
|
|
|
}
|
|
|
|
>
|
|
|
|
<div class="row position-relative">
|
|
|
|
<div class="col-xs-24">
|
|
|
|
<Show when={todayNotifications().length > 0}>
|
|
|
|
<div class={styles.periodTitle}>{t('today')}</div>
|
|
|
|
<For each={todayNotifications()}>
|
|
|
|
{(notification) => (
|
|
|
|
<NotificationView
|
|
|
|
notification={notification}
|
|
|
|
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}
|
|
|
|
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}
|
|
|
|
class={styles.notificationView}
|
|
|
|
onClick={handleNotificationViewClick}
|
|
|
|
dateTimeFormat={'date'}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</For>
|
|
|
|
</Show>
|
|
|
|
</div>
|
|
|
|
</div>
|
2023-10-18 10:56:41 +00:00
|
|
|
</Show>
|
2023-11-01 15:13:54 +00:00
|
|
|
<Show when={isLoading()}>
|
|
|
|
<div class={styles.loading}>{t('Loading')}</div>
|
2023-10-18 10:56:41 +00:00
|
|
|
</Show>
|
2023-11-01 15:13:54 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
<Show when={unreadNotificationsCount() > 0}>
|
|
|
|
<div class={styles.actions}>
|
|
|
|
<Button
|
|
|
|
onClick={() => markAllNotificationsAsRead()}
|
|
|
|
variant="secondary"
|
|
|
|
value={t('Mark as read')}
|
|
|
|
/>
|
|
|
|
</div>
|
2023-10-18 10:56:41 +00:00
|
|
|
</Show>
|
2023-10-14 11:39:24 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|