mirror of
https://github.com/h3poteto/whalebird-desktop
synced 2025-01-03 12:30:00 +01:00
Make unread notifications stand out
* And implement mark-as-read button
This commit is contained in:
parent
375f13689d
commit
f1d9f42c1d
@ -18,7 +18,8 @@
|
|||||||
"cw": "Media hidden",
|
"cw": "Media hidden",
|
||||||
"report": "Report {user}",
|
"report": "Report {user}",
|
||||||
"open_original": "Open original page"
|
"open_original": "Open original page"
|
||||||
}
|
},
|
||||||
|
"mark_as_read": "Mark as read"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"accounts": {
|
||||||
"new": {
|
"new": {
|
||||||
@ -116,7 +117,8 @@
|
|||||||
"upload_error": "Failed to upload the file",
|
"upload_error": "Failed to upload the file",
|
||||||
"compose": {
|
"compose": {
|
||||||
"post_failed": "Failed to post a status"
|
"post_failed": "Failed to post a status"
|
||||||
}
|
},
|
||||||
|
"failed_mark": "Failed to mark as read"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"follow": "Follow",
|
"follow": "Follow",
|
||||||
|
@ -4,9 +4,12 @@ import { useEffect, useState, useCallback, useRef, Dispatch, SetStateAction } fr
|
|||||||
import { FormattedMessage, useIntl } from 'react-intl'
|
import { FormattedMessage, useIntl } from 'react-intl'
|
||||||
import { Virtuoso } from 'react-virtuoso'
|
import { Virtuoso } from 'react-virtuoso'
|
||||||
import Notification from './notification/Notification'
|
import Notification from './notification/Notification'
|
||||||
import { Input, Spinner } from '@material-tailwind/react'
|
import { Spinner } from '@material-tailwind/react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import Detail from '../detail/Detail'
|
import Detail from '../detail/Detail'
|
||||||
|
import { Marker } from '@/entities/marker'
|
||||||
|
import { FaCheck } from 'react-icons/fa6'
|
||||||
|
import { useToast } from '@/utils/toast'
|
||||||
|
|
||||||
const TIMELINE_STATUSES_COUNT = 30
|
const TIMELINE_STATUSES_COUNT = 30
|
||||||
const TIMELINE_MAX_STATUSES = 2147483647
|
const TIMELINE_MAX_STATUSES = 2147483647
|
||||||
@ -21,11 +24,15 @@ export default function Notifications(props: Props) {
|
|||||||
const [notifications, setNotifications] = useState<Array<Entity.Notification>>([])
|
const [notifications, setNotifications] = useState<Array<Entity.Notification>>([])
|
||||||
const [unreads, setUnreads] = useState<Array<Entity.Notification>>([])
|
const [unreads, setUnreads] = useState<Array<Entity.Notification>>([])
|
||||||
const [firstItemIndex, setFirstItemIndex] = useState(TIMELINE_MAX_STATUSES)
|
const [firstItemIndex, setFirstItemIndex] = useState(TIMELINE_MAX_STATUSES)
|
||||||
|
const [marker, setMarker] = useState<Marker | null>(null)
|
||||||
|
const [pleromaUnreads, setPleromaUnreads] = useState<Array<string>>([])
|
||||||
|
|
||||||
const { formatMessage } = useIntl()
|
|
||||||
const scrollerRef = useRef<HTMLElement | null>(null)
|
const scrollerRef = useRef<HTMLElement | null>(null)
|
||||||
const streaming = useRef<WebSocketInterface | null>(null)
|
const streaming = useRef<WebSocketInterface | null>(null)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const showToast = useToast()
|
||||||
|
|
||||||
|
const { formatMessage } = useIntl()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const f = async () => {
|
const f = async () => {
|
||||||
@ -33,6 +40,7 @@ export default function Notifications(props: Props) {
|
|||||||
setNotifications(res)
|
setNotifications(res)
|
||||||
const instance = await props.client.getInstance()
|
const instance = await props.client.getInstance()
|
||||||
const c = generator(props.account.sns, instance.data.urls.streaming_api, props.account.access_token, 'Whalebird')
|
const c = generator(props.account.sns, instance.data.urls.streaming_api, props.account.access_token, 'Whalebird')
|
||||||
|
updateMarker(props.client)
|
||||||
streaming.current = c.userSocket()
|
streaming.current = c.userSocket()
|
||||||
streaming.current.on('connect', () => {
|
streaming.current.on('connect', () => {
|
||||||
console.log('connected to notifications')
|
console.log('connected to notifications')
|
||||||
@ -43,6 +51,7 @@ export default function Notifications(props: Props) {
|
|||||||
} else {
|
} else {
|
||||||
setNotifications(current => [notification, ...current])
|
setNotifications(current => [notification, ...current])
|
||||||
}
|
}
|
||||||
|
updateMarker(props.client)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
f()
|
f()
|
||||||
@ -60,6 +69,16 @@ export default function Notifications(props: Props) {
|
|||||||
}
|
}
|
||||||
}, [props.client])
|
}, [props.client])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 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 u = allNotifications.slice(0, marker.unread_count).map(n => n.id)
|
||||||
|
setPleromaUnreads(u)
|
||||||
|
}
|
||||||
|
}, [marker, unreads, notifications])
|
||||||
|
|
||||||
const loadNotifications = async (client: MegalodonInterface, maxId?: string): Promise<Array<Entity.Notification>> => {
|
const loadNotifications = async (client: MegalodonInterface, maxId?: string): Promise<Array<Entity.Notification>> => {
|
||||||
let options = { limit: 30 }
|
let options = { limit: 30 }
|
||||||
if (maxId) {
|
if (maxId) {
|
||||||
@ -69,6 +88,18 @@ export default function Notifications(props: Props) {
|
|||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateMarker = async (client: MegalodonInterface) => {
|
||||||
|
try {
|
||||||
|
const res = await client.getMarkers(['notifications'])
|
||||||
|
const marker = res.data as Entity.Marker
|
||||||
|
if (marker.notifications) {
|
||||||
|
setMarker(marker.notifications)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updateStatus = (status: Entity.Status) => {
|
const updateStatus = (status: Entity.Status) => {
|
||||||
const renew = notifications.map(n => {
|
const renew = notifications.map(n => {
|
||||||
if (n.status === undefined || n.status === null) {
|
if (n.status === undefined || n.status === null) {
|
||||||
@ -85,6 +116,22 @@ export default function Notifications(props: Props) {
|
|||||||
setNotifications(renew)
|
setNotifications(renew)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const read = async () => {
|
||||||
|
try {
|
||||||
|
await props.client.saveMarkers({ notifications: { last_read_id: notifications[0].id } })
|
||||||
|
if (props.account.sns === 'pleroma') {
|
||||||
|
await props.client.readNotifications({ max_id: notifications[0].id })
|
||||||
|
}
|
||||||
|
const res = await props.client.getMarkers(['notifications'])
|
||||||
|
const marker = res.data as Entity.Marker
|
||||||
|
if (marker.notifications) {
|
||||||
|
setMarker(marker.notifications)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showToast({ text: formatMessage({ id: 'alert.failed_mark' }), type: 'failure' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadMore = useCallback(async () => {
|
const loadMore = useCallback(async () => {
|
||||||
console.debug('appending')
|
console.debug('appending')
|
||||||
try {
|
try {
|
||||||
@ -119,13 +166,13 @@ export default function Notifications(props: Props) {
|
|||||||
<div className="text-lg font-bold">
|
<div className="text-lg font-bold">
|
||||||
<FormattedMessage id="timeline.notifications" />
|
<FormattedMessage id="timeline.notifications" />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-64 text-xs">
|
<div className="w-64 text-xs text-right">
|
||||||
<form>
|
<button className="text-gray-400 text-base py-1" title={formatMessage({ id: 'timeline.mark_as_read' })} onClick={read}>
|
||||||
<Input type="text" placeholder={formatMessage({ id: 'timeline.search' })} disabled />
|
<FaCheck />
|
||||||
</form>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="timeline overflow-y-auto w-full overflow-x-hidden" style={{ height: 'calc(100% - 50px)' }}>
|
<div className="timeline overflow-y-auto w-full overflow-x-hidden" style={{ height: 'calc(100% - 44px)' }}>
|
||||||
{notifications.length > 0 ? (
|
{notifications.length > 0 ? (
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
style={{ height: '100%' }}
|
style={{ height: '100%' }}
|
||||||
@ -137,16 +184,29 @@ export default function Notifications(props: Props) {
|
|||||||
className="timeline-scrollable"
|
className="timeline-scrollable"
|
||||||
data={notifications}
|
data={notifications}
|
||||||
endReached={loadMore}
|
endReached={loadMore}
|
||||||
itemContent={(_, notification) => (
|
itemContent={(_, notification) => {
|
||||||
<Notification
|
let shadow = {}
|
||||||
client={props.client}
|
if (marker) {
|
||||||
account={props.account}
|
if (marker.unread_count && pleromaUnreads.includes(notification.id)) {
|
||||||
notification={notification}
|
shadow = { boxShadow: '4px 0 2px var(--tw-shadow-color) inset' }
|
||||||
onRefresh={updateStatus}
|
} else if (parseInt(marker.last_read_id) < parseInt(notification.id)) {
|
||||||
key={notification.id}
|
shadow = { boxShadow: '4px 0 2px var(--tw-shadow-color) inset' }
|
||||||
openMedia={media => props.setAttachment(media)}
|
}
|
||||||
/>
|
}
|
||||||
)}
|
|
||||||
|
return (
|
||||||
|
<div className="box-border shadow-teal-500/80" style={shadow}>
|
||||||
|
<Notification
|
||||||
|
client={props.client}
|
||||||
|
account={props.account}
|
||||||
|
notification={notification}
|
||||||
|
onRefresh={updateStatus}
|
||||||
|
key={notification.id}
|
||||||
|
openMedia={media => props.setAttachment(media)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full pt-6" style={{ height: '100%' }}>
|
<div className="w-full pt-6" style={{ height: '100%' }}>
|
||||||
|
6
renderer/entities/marker.ts
Normal file
6
renderer/entities/marker.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export type Marker = {
|
||||||
|
last_read_id: string
|
||||||
|
version: number
|
||||||
|
updated_at: string
|
||||||
|
unread_count?: number
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user