diff --git a/renderer/components/layouts/timelines.tsx b/renderer/components/layouts/timelines.tsx
index db0eae26..328f190e 100644
--- a/renderer/components/layouts/timelines.tsx
+++ b/renderer/components/layouts/timelines.tsx
@@ -1,5 +1,5 @@
import { Account, db } from '@/db'
-import { Card, List, ListItem, ListItemPrefix } from '@material-tailwind/react'
+import { Card, Chip, List, ListItem, ListItemPrefix, ListItemSuffix } from '@material-tailwind/react'
import generator, { Entity } from 'megalodon'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
@@ -7,6 +7,7 @@ import { useHotkeys } from 'react-hotkeys-hook'
import { FaBell, FaGlobe, FaHouse, FaList, FaUsers } from 'react-icons/fa6'
import { useIntl } from 'react-intl'
import Jump from '../Jump'
+import { useUnreads } from '@/utils/unreads'
type LayoutProps = {
children: React.ReactNode
@@ -15,6 +16,7 @@ type LayoutProps = {
export default function Layout({ children }: LayoutProps) {
const router = useRouter()
const { formatMessage } = useIntl()
+ const { unreads } = useUnreads()
const [account, setAccount] = useState
(null)
const [lists, setLists] = useState>([])
@@ -89,6 +91,16 @@ export default function Layout({ children }: LayoutProps) {
>
{page.icon}
{page.title}
+ {page.id === 'notifications' && unreads[account?.id?.toString()] ? (
+
+
+
+ ) : null}
))}
{lists.map(list => (
diff --git a/renderer/components/timelines/Notifications.tsx b/renderer/components/timelines/Notifications.tsx
index d8360a6e..5e1810aa 100644
--- a/renderer/components/timelines/Notifications.tsx
+++ b/renderer/components/timelines/Notifications.tsx
@@ -7,9 +7,10 @@ import Notification from './notification/Notification'
import { Spinner } from '@material-tailwind/react'
import { useRouter } from 'next/router'
import Detail from '../detail/Detail'
-import { Marker } from '@/entities/marker'
+import { Marker, unreadCount } from '@/entities/marker'
import { FaCheck } from 'react-icons/fa6'
import { useToast } from '@/utils/toast'
+import { useUnreads } from '@/utils/unreads'
const TIMELINE_STATUSES_COUNT = 30
const TIMELINE_MAX_STATUSES = 2147483647
@@ -22,7 +23,7 @@ type Props = {
export default function Notifications(props: Props) {
const [notifications, setNotifications] = useState>([])
- const [unreads, setUnreads] = useState>([])
+ const [unreadNotifications, setUnreadNotifications] = useState>([])
const [firstItemIndex, setFirstItemIndex] = useState(TIMELINE_MAX_STATUSES)
const [marker, setMarker] = useState(null)
const [pleromaUnreads, setPleromaUnreads] = useState>([])
@@ -31,7 +32,7 @@ export default function Notifications(props: Props) {
const streaming = useRef(null)
const router = useRouter()
const showToast = useToast()
-
+ const { setUnreads } = useUnreads()
const { formatMessage } = useIntl()
useEffect(() => {
@@ -47,7 +48,7 @@ export default function Notifications(props: Props) {
})
streaming.current.on('notification', (notification: Entity.Notification) => {
if (scrollerRef.current && scrollerRef.current.scrollTop > 10) {
- setUnreads(current => [notification, ...current])
+ setUnreadNotifications(current => [notification, ...current])
} else {
setNotifications(current => [notification, ...current])
}
@@ -57,7 +58,7 @@ export default function Notifications(props: Props) {
f()
return () => {
- setUnreads([])
+ setUnreadNotifications([])
setFirstItemIndex(TIMELINE_MAX_STATUSES)
setNotifications([])
if (streaming.current) {
@@ -73,11 +74,20 @@ export default function Notifications(props: Props) {
// In pleroma, last_read_id is incorrect.
// Items that have not been marked may also be read. So, if marker has unread_count, we should use it for unreads.
if (marker && marker.unread_count) {
- const allNotifications = unreads.concat(notifications)
+ const allNotifications = unreadNotifications.concat(notifications)
const u = allNotifications.slice(0, marker.unread_count).map(n => n.id)
setPleromaUnreads(u)
}
- }, [marker, unreads, notifications])
+
+ if (marker) {
+ const count = unreadCount(marker, unreadNotifications.concat(notifications))
+ setUnreads(current =>
+ Object.assign({}, current, {
+ [props.account.id.toString()]: count
+ })
+ )
+ }
+ }, [marker, unreadNotifications, notifications])
const loadNotifications = async (client: MegalodonInterface, maxId?: string): Promise> => {
let options = { limit: 30 }
@@ -126,6 +136,11 @@ export default function Notifications(props: Props) {
const marker = res.data as Entity.Marker
if (marker.notifications) {
setMarker(marker.notifications)
+ setUnreads(current =>
+ Object.assign({}, current, {
+ [props.account.id?.toString()]: 0
+ })
+ )
}
} catch {
showToast({ text: formatMessage({ id: 'alert.failed_mark' }), type: 'failure' })
@@ -144,13 +159,13 @@ export default function Notifications(props: Props) {
const prependUnreads = useCallback(() => {
console.debug('prepending')
- const u = unreads.slice().reverse().slice(0, TIMELINE_STATUSES_COUNT).reverse()
+ const u = unreadNotifications.slice().reverse().slice(0, TIMELINE_STATUSES_COUNT).reverse()
const remains = u.slice(0, -1 * TIMELINE_STATUSES_COUNT)
- setUnreads(() => remains)
+ setUnreadNotifications(() => remains)
setFirstItemIndex(() => firstItemIndex - u.length)
setNotifications(() => [...u, ...notifications])
return false
- }, [firstItemIndex, notifications, setNotifications, unreads])
+ }, [firstItemIndex, notifications, setNotifications, unreadNotifications])
const timelineClass = () => {
if (router.query.detail) {
diff --git a/renderer/entities/marker.ts b/renderer/entities/marker.ts
index 41a78ea9..09d55ad7 100644
--- a/renderer/entities/marker.ts
+++ b/renderer/entities/marker.ts
@@ -1,6 +1,15 @@
+import { Entity } from 'megalodon'
+
export type Marker = {
last_read_id: string
version: number
updated_at: string
unread_count?: number
}
+
+export function unreadCount(marker: Marker, notifications: Array): number {
+ if (marker.unread_count !== undefined) {
+ return marker.unread_count
+ }
+ return notifications.filter(n => parseInt(n.id) > parseInt(marker.last_read_id)).length
+}
diff --git a/renderer/pages/_app.tsx b/renderer/pages/_app.tsx
index 692568b6..2fc29315 100644
--- a/renderer/pages/_app.tsx
+++ b/renderer/pages/_app.tsx
@@ -5,6 +5,7 @@ import TimelineLayout from '@/components/layouts/timelines'
import { IntlProviderWrapper } from '@/utils/i18n'
import { ThemeProvider } from '@material-tailwind/react'
import { ToastProvider } from '@/utils/toast'
+import { UnreadsProvider } from '@/utils/unreads'
export default function MyApp({ Component, pageProps }: AppProps) {
const customTheme = {
@@ -104,11 +105,13 @@ export default function MyApp({ Component, pageProps }: AppProps) {
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/renderer/utils/unreads.tsx b/renderer/utils/unreads.tsx
new file mode 100644
index 00000000..1990c8b3
--- /dev/null
+++ b/renderer/utils/unreads.tsx
@@ -0,0 +1,29 @@
+import { Dispatch, SetStateAction, createContext, useContext, useState } from 'react'
+
+type UnreadsType = { [key: string]: number }
+
+type Context = {
+ unreads: UnreadsType
+ setUnreads: Dispatch>
+}
+
+const UnreadsContext = createContext({
+ unreads: {},
+ setUnreads: (_: UnreadsType) => {}
+})
+
+UnreadsContext.displayName = 'UnreadsContext'
+
+export const useUnreads = () => {
+ return useContext(UnreadsContext)
+}
+
+type Props = {
+ children: React.ReactNode
+}
+
+export const UnreadsProvider: React.FC = ({ children }) => {
+ const [unreads, setUnreads] = useState({})
+
+ return {children}
+}