feat(platform): Phase 134 thread pin/mute and discovery diff banner (v0.8.99)

- toggleThreadPinned and setThreadNotificationMuted APIs
- _wireDiscoveryEvents at init; discoveryDiffHint on listing ingest
- Pass pinned/muted maps into buildThreadsPanel

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-05-22 11:53:33 -04:00
co-authored by Cursor
parent f0884ebdbb
commit db54620007
+94 -14
View File
@@ -327,6 +327,9 @@ class PearcordPlatform extends EventEmitter {
this._threadsPanelParentFilter = null
this._threadsPanelSort = 'activity'
this._discoveryMeshRefreshAt = 0
this._discoveryDiffHint = null
this._discoveryDiffUntil = 0
this._discoveryListingIds = new Set()
this._readOnly = opts.readOnly === true
this._companionMode = opts.companionMode === true || opts.readOnly === true
this._discoveryFilter = { query: '', sort: 'newest', minMembers: 0, tag: '' }
@@ -355,6 +358,7 @@ class PearcordPlatform extends EventEmitter {
await this.moderation.ready()
this.discovery = new PearcordDiscovery({ storagePath: this.storagePath })
await this.discovery.ready()
this._wireDiscoveryEvents()
this.embeds = new PearcordEmbed({ storagePath: this.storagePath })
await this.embeds.ready()
this.botLimits = new BotRateLimits({ storagePath: this.storagePath })
@@ -453,6 +457,7 @@ class PearcordPlatform extends EventEmitter {
await this.moderation.ready()
this.discovery = new PearcordDiscovery({ storagePath: this.storagePath })
await this.discovery.ready()
this._wireDiscoveryEvents()
this.embeds = new PearcordEmbed({ storagePath: this.storagePath })
await this.embeds.ready()
this.botLimits = new BotRateLimits({ storagePath: this.storagePath })
@@ -506,6 +511,9 @@ class PearcordPlatform extends EventEmitter {
this._threadsPanelParentFilter = null
this._threadsPanelSort = 'activity'
this._discoveryMeshRefreshAt = 0
this._discoveryDiffHint = null
this._discoveryDiffUntil = 0
this._discoveryListingIds = new Set()
this.guilds = await this.listGuilds()
void this._finishSessionStartup()
this.emit('user', user)
@@ -582,25 +590,38 @@ class PearcordPlatform extends EventEmitter {
}
}
_wireDiscoveryEvents () {
if (!this.discovery || this._discoveryWired) return
this._discoveryWired = true
this.discovery.on('listing', (row) => {
this._noteDiscoveryListingDelta(row)
this.emit('discovery')
})
this.discovery.on('listing-prune', ({ removed }) => {
if (removed > 0) {
this._discoveryDiffHint = `${removed} stale listing${removed === 1 ? '' : 's'} pruned`
this._discoveryDiffUntil = Date.now() + 8000
}
this.emit('discovery')
})
this.discovery.on('peer', ({ type }) => {
this.emit('discovery')
if (type === 'join') {
setTimeout(() => this._refreshDiscoveryAnnouncements().catch(() => {}), 600)
}
})
this.discovery.on('hub-post', (p) => {
this._ingestAnnouncementHubPost(p).catch(() => {})
})
}
async _joinDiscoveryMesh () {
if (!this.discovery || !this.onboarded || !this.identity?.keyPair) return
const Hyperswarm = require('hyperswarm')
if (!this._discoverySwarm) {
this._discoverySwarm = new Hyperswarm({ keyPair: this.identity.keyPair })
}
if (!this._discoveryWired) {
this._discoveryWired = true
this.discovery.on('listing', () => this.emit('discovery'))
this.discovery.on('peer', ({ type }) => {
this.emit('discovery')
if (type === 'join') {
setTimeout(() => this._refreshDiscoveryAnnouncements().catch(() => {}), 600)
}
})
this.discovery.on('hub-post', (p) => {
this._ingestAnnouncementHubPost(p).catch(() => {})
})
}
this._wireDiscoveryEvents()
await this.discovery.joinMesh(this._discoverySwarm)
await this._refreshDiscoveryAnnouncements().catch(() => {})
}
@@ -1150,6 +1171,21 @@ class PearcordPlatform extends EventEmitter {
return { ...this._discoveryFilter }
}
_noteDiscoveryListingDelta (row) {
if (!row?.id) {
this._discoveryDiffHint = 'Discovery listings updated'
this._discoveryDiffUntil = Date.now() + 8000
return
}
const isNew = !this._discoveryListingIds.has(row.id)
this._discoveryListingIds.add(row.id)
const name = row.name || 'Server'
this._discoveryDiffHint = isNew
? `New public listing: ${name}`
: `Listing updated: ${name}`
this._discoveryDiffUntil = Date.now() + 8000
}
/** Prune stale cache and re-broadcast listings on discovery mesh (Explore refresh). */
async refreshDiscoveryListingsFromMesh () {
if (!this.discovery) return { synced: 0, pruned: 0 }
@@ -8138,6 +8174,28 @@ class PearcordPlatform extends EventEmitter {
return { panelSort: this._threadsPanelSort }
}
async toggleThreadPinned (threadId) {
if (!threadId || !this.userSettings) throw new Error('threadId required')
const prefs = await this.userSettings.getPrefs()
const ids = new Set(prefs.pinnedThreadIds || [])
if (ids.has(threadId)) ids.delete(threadId)
else ids.add(threadId)
await this.updateUserPrefs({ pinnedThreadIds: [...ids] })
this.emit('channel')
return { threadId, pinned: ids.has(threadId) }
}
async setThreadNotificationMuted (threadId, muted = true) {
if (!threadId || !this.notifications) throw new Error('threadId required')
const prefs = await this.notifications.getPrefs()
const ids = new Set(prefs.mutedChannelIds || [])
if (muted) ids.add(threadId)
else ids.delete(threadId)
await this.notifications.setPrefs({ mutedChannelIds: [...ids] })
this.emit('channel')
return { threadId, muted: ids.has(threadId) }
}
async archiveThread (threadId, archived = true) {
if (!this.guild?.guild) throw new Error('no guild')
const channels = await this.guild.listChannels()
@@ -8570,6 +8628,23 @@ class PearcordPlatform extends EventEmitter {
if (root) threadRootMessages[ch.rootMessageId] = root
}
}
const userPrefsForThreads = this.userSettings
? await this.userSettings.getPrefs()
: { pinnedThreadIds: [] }
const notifPrefsForThreads = this.notifications
? await this.notifications.getPrefs()
: { mutedChannelIds: [] }
const pinnedThreadMap = Object.fromEntries(
(userPrefsForThreads.pinnedThreadIds || []).map((id) => [id, true])
)
const mutedThreadMap = Object.fromEntries(
(notifPrefsForThreads.mutedChannelIds || []).map((id) => [id, true])
)
if (publicListings?.length) {
for (const row of publicListings) {
if (row?.id) this._discoveryListingIds.add(row.id)
}
}
const threadsPanel = this.mode === 'guild' && guild
? buildThreadsPanel({
channels,
@@ -8582,7 +8657,9 @@ class PearcordPlatform extends EventEmitter {
includeArchived: this._threadsPanelIncludeArchived,
threadFilter: this._threadsPanelFilter,
parentFilterChannelId: this._threadsPanelParentFilter,
panelSort: this._threadsPanelSort
panelSort: this._threadsPanelSort,
pinnedThreadIds: pinnedThreadMap,
mutedChannelIds: mutedThreadMap
})
: null
const activeChannel = channels.find((c) => c.id === this.activeChannelId) || null
@@ -9056,6 +9133,9 @@ class PearcordPlatform extends EventEmitter {
threadsPanelFilter: this._threadsPanelFilter,
threadsPanelSort: this._threadsPanelSort,
discoveryMeshRefreshAt: this._discoveryMeshRefreshAt,
discoveryDiffHint:
this._discoveryDiffUntil > Date.now() ? this._discoveryDiffHint : null,
discoveryDiffUntil: this._discoveryDiffUntil,
threadsPanelParentFilter: this._threadsPanelParentFilter,
threadTypingByParent,
forumPosts,