diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a52afb389..4aacdee6b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -96,6 +96,7 @@ body: - Message - Reaction - GuildScheduledEvent + - ThreadMember multiple: true validations: required: true diff --git a/packages/discord.js/src/client/actions/Action.js b/packages/discord.js/src/client/actions/Action.js index f70d3ca70..7f0553012 100644 --- a/packages/discord.js/src/client/actions/Action.js +++ b/packages/discord.js/src/client/actions/Action.js @@ -110,6 +110,10 @@ class GenericAction { Partials.GuildScheduledEvent, ); } + + getThreadMember(id, manager) { + return this.getPayload({ user_id: id }, manager, id, Partials.ThreadMember, false); + } } module.exports = GenericAction; diff --git a/packages/discord.js/src/client/actions/ThreadMembersUpdate.js b/packages/discord.js/src/client/actions/ThreadMembersUpdate.js index 26ab70efb..6f7f0a4ff 100644 --- a/packages/discord.js/src/client/actions/ThreadMembersUpdate.js +++ b/packages/discord.js/src/client/actions/ThreadMembersUpdate.js @@ -1,5 +1,6 @@ 'use strict'; +const { Collection } = require('@discordjs/collection'); const Action = require('./Action'); const Events = require('../../util/Events'); @@ -8,24 +9,35 @@ class ThreadMembersUpdateAction extends Action { const client = this.client; const thread = client.channels.cache.get(data.id); if (thread) { - const old = thread.members.cache.clone(); thread.memberCount = data.member_count; + const addedMembers = new Collection(); + const removedMembers = new Collection(); - data.added_members?.forEach(rawMember => { - thread.members._add(rawMember); - }); + data.added_members?.reduce( + (_addedMembers, addedMember) => _addedMembers.set(addedMember.user_id, thread.members._add(addedMember)), + addedMembers, + ); - data.removed_member_ids?.forEach(memberId => { - thread.members.cache.delete(memberId); - }); + data.removed_member_ids?.reduce((removedMembersIds, removedMembersId) => { + const threadMember = this.getThreadMember(removedMembersId, thread.members); + if (threadMember) removedMembersIds.set(threadMember.id, threadMember); + thread.members.cache.delete(removedMembersId); + return removedMembersIds; + }, removedMembers); + + if (addedMembers.size === 0 && removedMembers.size === 0) { + // Uncached thread member(s) left. + return {}; + } /** * Emitted whenever members are added or removed from a thread. Requires `GUILD_MEMBERS` privileged intent * @event Client#threadMembersUpdate - * @param {Collection} oldMembers The members before the update - * @param {Collection} newMembers The members after the update + * @param {ThreadChannel} thread The thread where members got updated + * @param {Collection} addedMembers The members that were added + * @param {Collection} removedMembers The members that were removed */ - client.emit(Events.ThreadMembersUpdate, old, thread.members.cache); + client.emit(Events.ThreadMembersUpdate, thread, addedMembers, removedMembers); } return {}; } diff --git a/packages/discord.js/src/structures/ThreadMember.js b/packages/discord.js/src/structures/ThreadMember.js index 9da8677d0..2b6743a93 100644 --- a/packages/discord.js/src/structures/ThreadMember.js +++ b/packages/discord.js/src/structures/ThreadMember.js @@ -23,6 +23,12 @@ class ThreadMember extends Base { */ this.joinedTimestamp = null; + /** + * The flags for this thread member. This will be `null` if partial. + * @type {?ThreadMemberFlagsBitField} + */ + this.flags = null; + /** * The id of the thread member * @type {Snowflake} @@ -34,14 +40,16 @@ class ThreadMember extends Base { _patch(data) { if ('join_timestamp' in data) this.joinedTimestamp = Date.parse(data.join_timestamp); + if ('flags' in data) this.flags = new ThreadMemberFlagsBitField(data.flags).freeze(); + } - if ('flags' in data) { - /** - * The flags for this thread member - * @type {ThreadMemberFlagsBitField} - */ - this.flags = new ThreadMemberFlagsBitField(data.flags).freeze(); - } + /** + * Whether this thread member is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return this.flags === null; } /** diff --git a/packages/discord.js/src/util/Partials.js b/packages/discord.js/src/util/Partials.js index 7bbe517d3..0429b2327 100644 --- a/packages/discord.js/src/util/Partials.js +++ b/packages/discord.js/src/util/Partials.js @@ -2,4 +2,12 @@ const { createEnum } = require('./Enums'); -module.exports = createEnum(['User', 'Channel', 'GuildMember', 'Message', 'Reaction', 'GuildScheduledEvent']); +module.exports = createEnum([ + 'User', + 'Channel', + 'GuildMember', + 'Message', + 'Reaction', + 'GuildScheduledEvent', + 'ThreadMember', +]); diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 82a92a5a4..30ab2a843 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -2338,6 +2338,7 @@ export class ThreadMember extends Base { public get manageable(): boolean; public thread: ThreadChannel; public get user(): User | null; + public get partial(): false; public remove(reason?: string): Promise; } @@ -3289,7 +3290,7 @@ export interface AddGuildMemberOptions { fetchWhenExisting?: boolean; } -export type AllowedPartial = User | Channel | GuildMember | Message | MessageReaction; +export type AllowedPartial = User | Channel | GuildMember | Message | MessageReaction | ThreadMember; export type AllowedThreadTypeForNewsChannel = ChannelType.GuildNewsThread; @@ -3671,8 +3672,9 @@ export interface ClientEvents { threadListSync: [threads: Collection]; threadMemberUpdate: [oldMember: ThreadMember, newMember: ThreadMember]; threadMembersUpdate: [ - oldMembers: Collection, - newMembers: Collection, + thread: ThreadChannel, + addedMembers: Collection, + removedMembers: Collection, ]; threadUpdate: [oldThread: ThreadChannel, newThread: ThreadChannel]; typingStart: [typing: Typing]; @@ -4877,6 +4879,8 @@ export interface PartialMessage export interface PartialMessageReaction extends Partialize {} +export interface PartialThreadMember extends Partialize {} + export interface PartialOverwriteData { id: Snowflake | number; type?: OverwriteType; @@ -4895,6 +4899,7 @@ export enum Partials { Message, Reaction, GuildScheduledEvent, + ThreadMember, } export interface PartialUser extends Partialize {} diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index d1fe99a1a..892acf532 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -107,6 +107,8 @@ import { CategoryChannelChildManager, ActionRowData, MessageActionRowComponentData, + PartialThreadMember, + ThreadMemberFlagsBitField, Embed, } from '.'; import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotType, expectType } from 'tsd'; @@ -719,6 +721,22 @@ client.on('messageCreate', async message => { }); }); +client.on('threadMembersUpdate', (thread, addedMembers, removedMembers) => { + expectType(thread); + expectType>(addedMembers); + expectType>(removedMembers); + const left = removedMembers.first(); + if (!left) return; + + if (left.partial) { + expectType(left); + expectType(left.flags); + } else { + expectType(left); + expectType(left.flags); + } +}); + client.on('interactionCreate', async interaction => { expectType(interaction.guildId); expectType(interaction.channelId);