mirror of
https://git.shipoclu.com/moon/plspace.git
synced 2026-08-13 02:42:30 +00:00
license added, and toast notifications.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { account, status } from '$test/fixtures'
|
||||
import type { Notification } from './api/types'
|
||||
import { NotificationTracker, presentNotification } from './notifications'
|
||||
|
||||
function notification(overrides: Partial<Notification> = {}): Notification {
|
||||
return {
|
||||
id: 'notification-1',
|
||||
type: 'mention',
|
||||
created_at: '2026-01-01T00:00:00.000Z',
|
||||
account: account({ display_name: 'Bob', acct: 'bob' }),
|
||||
status: status({ id: 'mentioned-entry', content: '<p>Hello there</p>' }),
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('NotificationTracker', () => {
|
||||
it('uses the first page as a quiet baseline and emits only unseen items later', () => {
|
||||
const tracker = new NotificationTracker()
|
||||
const baseline = notification({ id: 'old' })
|
||||
const fresh = notification({ id: 'new' })
|
||||
|
||||
expect(tracker.ingest([baseline])).toEqual([])
|
||||
expect(tracker.cursor).toBe('old')
|
||||
expect(tracker.ingest([fresh, baseline])).toEqual([fresh])
|
||||
expect(tracker.cursor).toBe('new')
|
||||
expect(tracker.ingest([fresh, baseline])).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('presentNotification', () => {
|
||||
it('links status notifications to the relevant entry', () => {
|
||||
expect(presentNotification(notification())).toEqual({
|
||||
actor: 'Bob',
|
||||
message: 'mentioned you in an entry',
|
||||
excerpt: 'Hello there',
|
||||
href: '#/blog/mentioned-entry',
|
||||
})
|
||||
})
|
||||
|
||||
it('links friend requests to their manager', () => {
|
||||
expect(
|
||||
presentNotification(notification({ type: 'follow_request', status: null })).href,
|
||||
).toBe('#/mail/requests')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { Notification } from './api/types'
|
||||
import { toPlainText } from './util/html'
|
||||
import { displayNameOf, profilePath } from './util/profile'
|
||||
|
||||
export const NOTIFICATION_POLL_INTERVAL_MS = 45_000
|
||||
export const NOTIFICATION_DISMISS_AFTER_MS = 7_000
|
||||
|
||||
/** Keeps polling incremental and prevents overlapping server pages from
|
||||
* producing the same toast twice. The first page is baseline only. */
|
||||
export class NotificationTracker {
|
||||
private initialized = false
|
||||
private known = new Set<string>()
|
||||
private knownOrder: string[] = []
|
||||
|
||||
cursor: string | undefined
|
||||
|
||||
reset(): void {
|
||||
this.initialized = false
|
||||
this.known = new Set()
|
||||
this.knownOrder = []
|
||||
this.cursor = undefined
|
||||
}
|
||||
|
||||
ingest(items: Notification[]): Notification[] {
|
||||
const fresh = this.initialized
|
||||
? items.filter((notification) => !this.known.has(notification.id))
|
||||
: []
|
||||
|
||||
for (const notification of items) this.remember(notification.id)
|
||||
// Both Mastodon and Pleroma return notification pages newest-first.
|
||||
if (items[0]) this.cursor = items[0].id
|
||||
this.initialized = true
|
||||
return fresh
|
||||
}
|
||||
|
||||
private remember(id: string): void {
|
||||
if (this.known.has(id)) return
|
||||
this.known.add(id)
|
||||
this.knownOrder.push(id)
|
||||
|
||||
// A long-running tab should not retain an unbounded notification history.
|
||||
while (this.knownOrder.length > 200) {
|
||||
const oldest = this.knownOrder.shift()
|
||||
if (oldest) this.known.delete(oldest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface NotificationPresentation {
|
||||
actor: string
|
||||
message: string
|
||||
excerpt: string
|
||||
href: string
|
||||
}
|
||||
|
||||
const MESSAGE: Record<string, string> = {
|
||||
mention: 'mentioned you in an entry',
|
||||
status: 'posted a new entry',
|
||||
reblog: 'reposted your entry',
|
||||
follow: 'added you as a friend',
|
||||
follow_request: 'sent you a friend request',
|
||||
favourite: 'gave your entry kudos',
|
||||
poll: 'has a poll that just ended',
|
||||
update: 'edited an entry',
|
||||
'pleroma:emoji_reaction': 'reacted to your entry',
|
||||
'admin.sign_up': 'joined the server',
|
||||
'admin.report': 'was included in a report',
|
||||
'pleroma:report': 'was included in a report',
|
||||
}
|
||||
|
||||
export function presentNotification(notification: Notification): NotificationPresentation {
|
||||
const status = notification.status ?? null
|
||||
const excerpt = status
|
||||
? toPlainText(status.spoiler_text || status.content).slice(0, 100)
|
||||
: ''
|
||||
|
||||
const href =
|
||||
notification.type === 'follow_request'
|
||||
? '#/mail/requests'
|
||||
: status
|
||||
? `#/blog/${status.id}`
|
||||
: profilePath(notification.account)
|
||||
|
||||
return {
|
||||
actor: displayNameOf(notification.account),
|
||||
message: MESSAGE[notification.type] ?? 'sent you a notification',
|
||||
excerpt,
|
||||
href,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user