webapp/src/components/NotificationsPanel/NotificationsPanel.tsx

235 lines
7.0 KiB
TypeScript
Raw Normal View History

import { clsx } from 'clsx'
2023-12-20 16:54:20 +00:00
import { createEffect, createMemo, createSignal, on, onCleanup, onMount, Show } from 'solid-js'
import { throttle } from 'throttle-debounce'
import { useLocalize } from '../../context/localize'
import { PAGE_SIZE, useNotifications } from '../../context/notifications'
import { useSession } from '../../context/session'
import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler'
import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler'
import { Button } from '../_shared/Button'
import { Icon } from '../_shared/Icon'
import { EmptyMessage } from './EmptyMessage'
2023-12-20 16:54:20 +00:00
import { NotificationGroup } from './NotificationView/NotificationGroup'
import styles from './NotificationsPanel.module.scss'
type Props = {
isOpen: boolean
onClose: () => void
}
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()
}
export const NotificationsPanel = (props: Props) => {
const [isLoading, setIsLoading] = createSignal(false)
const { isAuthenticated } = useSession()
const { t } = useLocalize()
const {
2023-12-19 09:34:24 +00:00
after,
sortedNotifications,
unreadNotificationsCount,
loadedNotificationsCount,
totalNotificationsCount,
actions: { loadNotifications, markAllNotificationsAsRead },
} = useNotifications()
const handleHide = () => {
props.onClose()
}
const panelRef: { current: HTMLDivElement } = {
current: null,
}
useOutsideClickHandler({
containerRef: panelRef,
predicate: () => props.isOpen,
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)
const handleNotificationViewClick = () => {
handleHide()
}
const todayNotifications = createMemo(() => {
2023-11-28 13:18:25 +00:00
return sortedNotifications().filter((notification) => isToday(new Date(notification.created_at * 1000)))
})
const yesterdayNotifications = createMemo(() => {
2023-11-28 13:18:25 +00:00
return sortedNotifications().filter((notification) =>
isYesterday(new Date(notification.created_at * 1000)),
)
})
const earlierNotifications = createMemo(() => {
2023-11-28 13:18:25 +00:00
return sortedNotifications().filter((notification) =>
isEarlier(new Date(notification.created_at * 1000)),
)
})
const scrollContainerRef: { current: HTMLDivElement } = { current: null }
const loadNextPage = async () => {
2023-12-19 09:34:24 +00:00
await loadNotifications({ after: after(), 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)
}
}
const handleScrollThrottled = throttle(50, handleScroll)
onMount(() => {
scrollContainerRef.current.addEventListener('scroll', handleScrollThrottled)
onCleanup(() => {
scrollContainerRef.current.removeEventListener('scroll', handleScrollThrottled)
})
})
createEffect(
on(
() => isAuthenticated(),
async () => {
if (isAuthenticated()) {
setIsLoading(true)
await loadNextPage()
setIsLoading(false)
}
},
),
)
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}>
<Icon class={styles.closeIcon} name="close" />
</div>
<div class={styles.title}>{t('Notifications')}</div>
<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>
2023-12-20 16:54:20 +00:00
<NotificationGroup
notifications={todayNotifications()}
class={styles.notificationView}
onClick={handleNotificationViewClick}
dateTimeFormat={'ago'}
/>
</Show>
<Show when={yesterdayNotifications().length > 0}>
<div class={styles.periodTitle}>{t('yesterday')}</div>
2023-12-20 16:54:20 +00:00
<NotificationGroup
notifications={yesterdayNotifications()}
class={styles.notificationView}
onClick={handleNotificationViewClick}
dateTimeFormat={'time'}
/>
</Show>
<Show when={earlierNotifications().length > 0}>
<div class={styles.periodTitle}>{t('earlier')}</div>
2023-12-20 16:54:20 +00:00
<NotificationGroup
notifications={earlierNotifications()}
class={styles.notificationView}
onClick={handleNotificationViewClick}
dateTimeFormat={'date'}
/>
</Show>
</div>
</div>
</Show>
<Show when={isLoading()}>
<div class={styles.loading}>{t('Loading')}</div>
</Show>
</div>
<Show when={unreadNotificationsCount() > 0}>
<div class={styles.actions}>
<Button
onClick={() => markAllNotificationsAsRead()}
variant="secondary"
value={t('Mark as read')}
/>
</div>
</Show>
</div>
</div>
)
}