feat(threads-forum): Phase 679 mixin, heal auto-archive, deep-link create
Add threads-forum.js partition heal for forum tag palettes, extend thread heal with watermark and auto-archive reconciliation, guild-scoped threads panel, create-thread deep link navigation payload, and thread.create autoArchiveMinutes (Phase 679 / v0.8.654). Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
Application facade: one `PearcordPlatform` class that wires identity, database, guild mesh, DMs, invites, voice, discovery, bots, and dozens of feature modules for the Pearcord desktop sidecar and companion.
|
||||
|
||||
**Phase 679 (v0.8.654):** Thread panel & forum parity — `threads-forum.js` mixin, `createThread` auto-archive, `_healThreadMetadataOnPartition` watermark + auto-archive heal, guild-scoped `buildThreadsPanel`, deep link `create-thread` query. Bundle: `npm run test:ci-phase679`.
|
||||
|
||||
**Phase 678 (v0.8.653):** Invites & discovery join — `invites-discovery.js` mixin (`listGuildInvites`, `_healInvitesOnPartition`, `_healDiscoveryListingsOnPartition`, `_shouldGossipInvite`); view `guildInvites`, `inviteCount`, `discoveryListingVersion`; `createInvite` ttl/maxUses validation. Bundle: `npm run test:ci-phase678`.
|
||||
|
||||
**Phase 677 (v0.8.652):** Moderation & audit log — `moderation.js` mixin (`unbanMember`, audit/ban gossip dedupe, `_healAuditLogOnPartition`, `_healBanListOnPartition`, `_assertCanModerateMember`, `moderation.action` spans). Bundle: `npm run test:ci-phase677`.
|
||||
|
||||
@@ -1564,6 +1564,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
const moderationHeal = await this._healBanListOnPartition(gid).catch(() => ({ relisted: 0 }))
|
||||
const invitesHeal = await this._healInvitesOnPartition(gid).catch(() => ({ relisted: 0 }))
|
||||
const discoveryHeal = await this._healDiscoveryListingsOnPartition(gid).catch(() => ({ relisted: 0 }))
|
||||
const forumHeal = await this._healForumTagPaletteOnPartition(gid).catch(() => ({ relisted: 0 }))
|
||||
return {
|
||||
voiceApplied,
|
||||
emojiSlots,
|
||||
@@ -1587,7 +1588,8 @@ class PearcordPlatform extends EventEmitter {
|
||||
auditHeal,
|
||||
moderationHeal,
|
||||
invitesHeal,
|
||||
discoveryHeal
|
||||
discoveryHeal,
|
||||
forumHeal
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5395,7 +5397,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
this._voiceRosterDeafenedOnly = !!prefs.voiceRosterDeafenedOnly
|
||||
}
|
||||
if (prefs.threadsPanelFilter) {
|
||||
const allowed = new Set(['active', 'archived', 'all', 'pinned', 'muted', 'pinned-muted'])
|
||||
const allowed = new Set(['active', 'archived', 'all', 'pinned', 'muted', 'pinned-muted', 'locked'])
|
||||
if (allowed.has(prefs.threadsPanelFilter)) {
|
||||
this._threadsPanelFilter = prefs.threadsPanelFilter
|
||||
if (this._threadsPanelFilter === 'archived' || this._threadsPanelFilter === 'all') {
|
||||
@@ -13510,8 +13512,15 @@ class PearcordPlatform extends EventEmitter {
|
||||
try {
|
||||
const channels = await this.guild.listChannels().catch(() => [])
|
||||
let relisted = 0
|
||||
const watermark = Date.now()
|
||||
this._threadHealWatermark = watermark
|
||||
for (const ch of channels) {
|
||||
if (!isThreadChannel(ch)) continue
|
||||
if (ch.guildId && ch.guildId !== gid) continue
|
||||
const meta = await this.guild?._getThreadMeta?.(gid, ch.id).catch(() => null)
|
||||
if (meta?.autoArchiveAt && !ch.archived && Date.now() >= Number(meta.autoArchiveAt)) {
|
||||
await this.archiveThread(ch.id, true).catch(() => {})
|
||||
}
|
||||
const payload = {
|
||||
guildId: gid,
|
||||
channelId: ch.id,
|
||||
@@ -13524,7 +13533,11 @@ class PearcordPlatform extends EventEmitter {
|
||||
relisted++
|
||||
}
|
||||
}
|
||||
span.end({ relisted, rowCount: channels.filter((c) => isThreadChannel(c)).length })
|
||||
span.end({
|
||||
relisted,
|
||||
watermark,
|
||||
rowCount: channels.filter((c) => isThreadChannel(c) && (!c.guildId || c.guildId === gid)).length
|
||||
})
|
||||
return { relisted, rowCount: channels.filter((c) => isThreadChannel(c)).length }
|
||||
} catch (err) {
|
||||
this.log.error('thread.heal error', { guildId: gid, error: err?.message || String(err) })
|
||||
@@ -16951,7 +16964,14 @@ class PearcordPlatform extends EventEmitter {
|
||||
if (parsed.kind === 'guild-channel' && parsed.guildId && parsed.channelId) {
|
||||
await this.loadGuild(parsed.guildId)
|
||||
await this.selectChannel(parsed.channelId)
|
||||
return { kind: 'guild-channel', found: true, guildId: parsed.guildId, channelId: parsed.channelId }
|
||||
return {
|
||||
kind: 'guild-channel',
|
||||
found: true,
|
||||
guildId: parsed.guildId,
|
||||
channelId: parsed.channelId,
|
||||
openCreateThread: !!parsed.createThreadMessageId,
|
||||
messageId: parsed.createThreadMessageId || null
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.kind === 'guild' && parsed.guildId) {
|
||||
@@ -22811,7 +22831,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
return t
|
||||
}
|
||||
|
||||
async createThread ({ messageId, name }) {
|
||||
async createThread ({ messageId, name, autoArchiveMinutes = 0 }) {
|
||||
if (this.mode === 'dm') return this.createDmThread({ messageId, name })
|
||||
const guildId = this.guild?.guild?.id || null
|
||||
const parentId = this.activeChannelId || null
|
||||
@@ -22843,7 +22863,8 @@ class PearcordPlatform extends EventEmitter {
|
||||
const thread = await this.guild.createThread({
|
||||
parentChannelId: parent.id,
|
||||
rootMessageId: messageId,
|
||||
name: threadName
|
||||
name: threadName,
|
||||
autoArchiveMinutes: Math.max(0, Math.min(10080, Number(autoArchiveMinutes) || 0))
|
||||
})
|
||||
this.guild.gossipChannel(thread)
|
||||
await this._audit('thread.create', {
|
||||
@@ -22861,6 +22882,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
threadName: thread.name || threadName,
|
||||
fromMessage: !!messageId,
|
||||
hasRootMessage: !!messageId,
|
||||
autoArchiveMinutes: Math.max(0, Number(autoArchiveMinutes) || 0),
|
||||
activeChannelId: this.activeChannelId,
|
||||
guildCount: (this.guilds || []).length
|
||||
})
|
||||
@@ -22886,7 +22908,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
}
|
||||
|
||||
async setThreadsPanelFilter (filter) {
|
||||
const allowed = new Set(['active', 'archived', 'all', 'pinned', 'muted', 'pinned-muted'])
|
||||
const allowed = new Set(['active', 'archived', 'all', 'pinned', 'muted', 'pinned-muted', 'locked'])
|
||||
this._threadsPanelFilter = allowed.has(filter) ? filter : 'active'
|
||||
if (this._threadsPanelFilter === 'archived' || this._threadsPanelFilter === 'all') {
|
||||
this._threadsPanelIncludeArchived = true
|
||||
@@ -24805,6 +24827,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
this.mode === 'guild' && guild
|
||||
? buildThreadsPanel({
|
||||
channels,
|
||||
guildId: guild.id,
|
||||
threadsByParent,
|
||||
unread,
|
||||
threadMessageCounts,
|
||||
@@ -25457,6 +25480,15 @@ class PearcordPlatform extends EventEmitter {
|
||||
discoveryListingVersion:
|
||||
(publicListings || []).length +
|
||||
Number(this._discoveryHealWatermark ? 1 : 0),
|
||||
threadCount:
|
||||
(threadsPanel?.threads || []).length + (threadsPanel?.archivedThreads || []).length,
|
||||
threadPanelVersion:
|
||||
(threadsPanel?.threads || []).length +
|
||||
(threadsPanel?.archivedThreads || []).length +
|
||||
Number(this._threadHealWatermark ? 1 : 0),
|
||||
forumPostCount: (forumPosts || []).length,
|
||||
forumPostVersion:
|
||||
(forumPosts || []).length + Number(this._forumHealWatermark ? 1 : 0),
|
||||
communicationDisabled,
|
||||
stagedAttachments,
|
||||
stagedAttachmentUi:
|
||||
@@ -26065,8 +26097,10 @@ const { notificationsActivityMixin } = require('./notifications-activity')
|
||||
const { userSettingsMixin } = require('./user-settings')
|
||||
const { moderationMixin } = require('./moderation')
|
||||
const { invitesDiscoveryMixin } = require('./invites-discovery')
|
||||
const { threadsForumMixin } = require('./threads-forum')
|
||||
Object.assign(PearcordPlatform.prototype, pollSchedulingMixin)
|
||||
Object.assign(PearcordPlatform.prototype, notificationsActivityMixin)
|
||||
Object.assign(PearcordPlatform.prototype, userSettingsMixin)
|
||||
Object.assign(PearcordPlatform.prototype, moderationMixin)
|
||||
Object.assign(PearcordPlatform.prototype, invitesDiscoveryMixin)
|
||||
Object.assign(PearcordPlatform.prototype, threadsForumMixin)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
'use strict'
|
||||
|
||||
const { COLLECTIONS, CHANNEL_TYPES } = require('pearcord-shared')
|
||||
|
||||
const threadsForumMixin = {
|
||||
_threadHealWatermark: null,
|
||||
_forumHealWatermark: null,
|
||||
|
||||
async _healForumTagPaletteOnPartition (guildId) {
|
||||
const gid = guildId || this.guild?.guild?.id || null
|
||||
const span = this.log.time('forum.heal', { spanKind: 'forum.heal', guildId: gid })
|
||||
try {
|
||||
if (!gid || !this.guild?.listChannels) {
|
||||
span.end({ relisted: 0, skipped: true })
|
||||
return { relisted: 0, skipped: true }
|
||||
}
|
||||
const channels = await this.guild.listChannels().catch(() => [])
|
||||
let relisted = 0
|
||||
for (const ch of channels) {
|
||||
if (ch.type !== CHANNEL_TYPES.FORUM) continue
|
||||
if (this.forum?.parsePaletteFromTopic) {
|
||||
const tags = this.forum.parsePaletteFromTopic(ch.topic)
|
||||
if (tags?.length) relisted += 1
|
||||
}
|
||||
}
|
||||
const watermark = Date.now()
|
||||
this._forumHealWatermark = watermark
|
||||
span.end({ relisted, watermark, forumPostCount: relisted })
|
||||
return { relisted, watermark }
|
||||
} catch (err) {
|
||||
this.log.error('forum.heal error', { guildId: gid, error: err?.message || String(err) })
|
||||
span.fail(err)
|
||||
return { relisted: 0, error: err?.message || String(err) }
|
||||
}
|
||||
},
|
||||
|
||||
listGuildThreadChannels (guildId) {
|
||||
const gid = guildId || this.guild?.guild?.id || null
|
||||
if (!gid) return []
|
||||
const channels = this._lastViewChannelsCache || []
|
||||
return channels.filter((c) => c.type === CHANNEL_TYPES.THREAD && c.guildId === gid)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { threadsForumMixin }
|
||||
Reference in New Issue
Block a user