license added, and toast notifications.

This commit is contained in:
Moon.eth
2026-07-29 14:22:08 +09:00
parent a42145c678
commit cc132ad6b4
8 changed files with 1109 additions and 0 deletions
+46
View File
@@ -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')
})
})
+90
View File
@@ -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,
}
}