From 576eee8de26bf9e62f5487f6e25e9d5f5eaaa882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Fri, 16 Jul 2021 13:20:05 +0200 Subject: [PATCH] refactor: remove typing caching (#6114) Co-authored-by: DTrombett <73136330+DTrombett@users.noreply.github.com> --- src/client/actions/ChannelUpdate.js | 2 - src/client/actions/GuildDelete.js | 6 +- src/client/actions/TypingStart.js | 41 ++------ src/errors/Messages.js | 2 - src/structures/ClientUser.js | 5 - src/structures/DMChannel.js | 6 +- src/structures/TextChannel.js | 6 +- src/structures/ThreadChannel.js | 7 +- src/structures/Typing.js | 82 ++++++++++++++++ src/structures/User.js | 28 ------ src/structures/interfaces/TextBasedChannel.js | 93 ++----------------- typings/index.d.ts | 37 ++++---- typings/index.ts | 25 +++-- 13 files changed, 140 insertions(+), 200 deletions(-) create mode 100644 src/structures/Typing.js diff --git a/src/client/actions/ChannelUpdate.js b/src/client/actions/ChannelUpdate.js index d44239973..1228eb838 100644 --- a/src/client/actions/ChannelUpdate.js +++ b/src/client/actions/ChannelUpdate.js @@ -15,8 +15,6 @@ class ChannelUpdateAction extends Action { if (ChannelTypes[channel.type] !== data.type) { const newChannel = Channel.create(this.client, data, channel.guild); for (const [id, message] of channel.messages.cache) newChannel.messages.cache.set(id, message); - newChannel._typing = new Map(channel._typing); - channel = newChannel; this.client.channels.cache.set(channel.id, channel); } diff --git a/src/client/actions/GuildDelete.js b/src/client/actions/GuildDelete.js index 3edd6fbe2..21d8ebadc 100644 --- a/src/client/actions/GuildDelete.js +++ b/src/client/actions/GuildDelete.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const { Events, TextBasedChannelTypes } = require('../../util/Constants'); +const { Events } = require('../../util/Constants'); class GuildDeleteAction extends Action { constructor(client) { @@ -14,10 +14,6 @@ class GuildDeleteAction extends Action { let guild = client.guilds.cache.get(data.id); if (guild) { - for (const channel of guild.channels.cache.values()) { - if (TextBasedChannelTypes.includes(channel.type)) channel.stopTyping(true); - } - if (data.unavailable) { // Guild is unavailable guild.available = false; diff --git a/src/client/actions/TypingStart.js b/src/client/actions/TypingStart.js index ba1c58704..af6bfadae 100644 --- a/src/client/actions/TypingStart.js +++ b/src/client/actions/TypingStart.js @@ -1,6 +1,7 @@ 'use strict'; const Action = require('./Action'); +const Typing = require('../../structures/Typing'); const { Events, TextBasedChannelTypes } = require('../../util/Constants'); class TypingStart extends Action { @@ -15,43 +16,15 @@ class TypingStart extends Action { } const user = this.getUserFromMember(data); - const timestamp = new Date(data.timestamp * 1000); - if (channel && user) { - if (channel._typing.has(user.id)) { - const typing = channel._typing.get(user.id); - - typing.lastTimestamp = timestamp; - typing.elapsedTime = Date.now() - typing.since; - this.client.clearTimeout(typing.timeout); - typing.timeout = this.tooLate(channel, user); - } else { - const since = new Date(); - const lastTimestamp = new Date(); - channel._typing.set(user.id, { - user, - since, - lastTimestamp, - elapsedTime: Date.now() - since, - timeout: this.tooLate(channel, user), - }); - - /** - * Emitted whenever a user starts typing in a channel. - * @event Client#typingStart - * @param {DMChannel|TextChannel|NewsChannel} channel The channel the user started typing in - * @param {User} user The user that started typing - */ - this.client.emit(Events.TYPING_START, channel, user); - } + /** + * Emitted whenever a user starts typing in a channel. + * @event Client#typingStart + * @param {Typing} typing The typing state + */ + this.client.emit(Events.TYPING_START, new Typing(channel, user, data)); } } - - tooLate(channel, user) { - return channel.client.setTimeout(() => { - channel._typing.delete(user.id); - }, 10000); - } } module.exports = TypingStart; diff --git a/src/errors/Messages.js b/src/errors/Messages.js index f082ceaed..82f1c6c2e 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -78,8 +78,6 @@ const Messages = { MESSAGE_NONCE_TYPE: 'Message nonce must be an integer or a string.', MESSAGE_CONTENT_TYPE: 'Message content must be a non-empty string.', - TYPING_COUNT: 'Count must be at least 1', - SPLIT_MAX_LEN: 'Chunk exceeds the max length and contains no split characters.', BAN_RESOLVE_ID: (ban = false) => `Couldn't resolve the user id to ${ban ? 'ban' : 'unban'}.`, diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 9d18ae5e8..cbf9be392 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -8,11 +8,6 @@ const DataResolver = require('../util/DataResolver'); * @extends {User} */ class ClientUser extends User { - constructor(client, data) { - super(client, data); - this._typing = new Map(); - } - _patch(data) { super._patch(data); diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 4d549cddd..5989cbfbe 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -25,7 +25,6 @@ class DMChannel extends Channel { * @type {MessageManager} */ this.messages = new MessageManager(this); - this._typing = new Map(); } _patch(data) { @@ -87,10 +86,7 @@ class DMChannel extends Channel { get lastMessage() {} get lastPinAt() {} send() {} - startTyping() {} - stopTyping() {} - get typing() {} - get typingCount() {} + sendTyping() {} createMessageCollector() {} awaitMessages() {} createMessageComponentCollector() {} diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index f6b4a3fd5..63ad79657 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -38,7 +38,6 @@ class TextChannel extends GuildChannel { * @type {boolean} */ this.nsfw = Boolean(data.nsfw); - this._typing = new Map(); } _patch(data) { @@ -193,10 +192,7 @@ class TextChannel extends GuildChannel { get lastMessage() {} get lastPinAt() {} send() {} - startTyping() {} - stopTyping() {} - get typing() {} - get typingCount() {} + sendTyping() {} createMessageCollector() {} awaitMessages() {} createMessageComponentCollector() {} diff --git a/src/structures/ThreadChannel.js b/src/structures/ThreadChannel.js index 2c47232c0..aad85652c 100644 --- a/src/structures/ThreadChannel.js +++ b/src/structures/ThreadChannel.js @@ -43,8 +43,6 @@ class ThreadChannel extends Channel { * @type {ThreadMemberManager} */ this.members = new ThreadMemberManager(this); - - this._typing = new Map(); if (data) this._patch(data); } @@ -413,10 +411,7 @@ class ThreadChannel extends Channel { get lastMessage() {} get lastPinAt() {} send() {} - startTyping() {} - stopTyping() {} - get typing() {} - get typingCount() {} + sendTyping() {} createMessageCollector() {} awaitMessages() {} createMessageComponentCollector() {} diff --git a/src/structures/Typing.js b/src/structures/Typing.js new file mode 100644 index 000000000..e8cd406b1 --- /dev/null +++ b/src/structures/Typing.js @@ -0,0 +1,82 @@ +'use strict'; + +const Base = require('./Base'); + +/** + * Represents a typing state for a user in a channel. + * @extends {Base} + */ +class Typing extends Base { + /** + * @param {TextChannel|DMChannel|NewsChannel|ThreadChannel} channel The channel this typing came from + * @param {User} user The user that started typing + * @param {APITypingStart} data The raw data received + */ + constructor(channel, user, data) { + super(channel.client); + + /** + * The channel the status is from + * @type {TextChannel|DMChannel|NewsChannel|ThreadChannel} + */ + this.channel = channel; + + /** + * The user who is typing + * @type {User} + */ + this.user = user; + + this._patch(data); + } + + _patch(data) { + /** + * The UNIX timestamp in milliseconds the user started typing at + * @type {number} + */ + this.startedTimestamp = data.timestamp * 1000; + } + + /** + * Indicates whether the status is received from a guild. + * @returns {boolean} + */ + inGuild() { + return this.guild !== null; + } + + /** + * The time the user started typing at + * @type {Date} + * @readonly + */ + get startedAt() { + return new Date(this.startedTimestamp); + } + + /** + * The guild the status is from + * @type {?Guild} + * @readonly + */ + get guild() { + return this.channel.guild ?? null; + } + + /** + * The member who is typing + * @type {?GuildMember} + * @readonly + */ + get member() { + return this.guild?.members.resolve(this.user) ?? null; + } +} + +module.exports = Typing; + +/** + * @external APITypingStart + * @see {@link https://discord.com/developers/docs/topics/gateway#typing-start-typing-start-event-fields} + */ diff --git a/src/structures/User.js b/src/structures/User.js index 9fc048fa2..bbe7f5530 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -159,34 +159,6 @@ class User extends Base { return typeof this.username === 'string' ? `${this.username}#${this.discriminator}` : null; } - /** - * Checks whether the user is typing in a channel. - * @param {ChannelResolvable} channel The channel to check in - * @returns {boolean} - */ - typingIn(channel) { - return this.client.channels.resolve(channel)._typing.has(this.id); - } - - /** - * Gets the time that the user started typing. - * @param {ChannelResolvable} channel The channel to get the time in - * @returns {?Date} - */ - typingSinceIn(channel) { - channel = this.client.channels.resolve(channel); - return channel._typing.has(this.id) ? new Date(channel._typing.get(this.id).since) : null; - } - - /** - * Gets the amount of time the user has been typing in a channel for (in milliseconds), or -1 if they're not typing. - * @param {ChannelResolvable} channel The channel to get the time in - * @returns {number} - */ - typingDurationIn(channel) { - return this.client.channels.resolve(channel)._typing.get(this.id)?.elapsedTime ?? -1; - } - /** * The DM between the client's user and this user * @type {?DMChannel} diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index e0745db4b..d50ed88b7 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -6,7 +6,7 @@ const MessagePayload = require('../MessagePayload'); const SnowflakeUtil = require('../../util/SnowflakeUtil'); const Collection = require('../../util/Collection'); const { InteractionTypes } = require('../../util/Constants'); -const { RangeError, TypeError, Error } = require('../../errors'); +const { TypeError, Error } = require('../../errors'); const InteractionCollector = require('../InteractionCollector'); /** @@ -172,88 +172,14 @@ class TextBasedChannel { } /** - * Starts a typing indicator in the channel. - * @param {number} [count=1] The number of times startTyping should be considered to have been called - * @returns {Promise} Resolves once the bot stops typing gracefully, or rejects when an error occurs + * Sends a typing indicator in the channel. + * @returns {Promise} Resolves upon the typing status being sent * @example - * // Start typing in a channel, or increase the typing count by one - * channel.startTyping(); - * @example - * // Start typing in a channel with a typing count of five, or set it to five - * channel.startTyping(5); + * // Start typing in a channel + * channel.sendTyping(); */ - startTyping(count) { - if (typeof count !== 'undefined' && count < 1) throw new RangeError('TYPING_COUNT'); - if (this.client.user._typing.has(this.id)) { - const entry = this.client.user._typing.get(this.id); - entry.count = count ?? entry.count + 1; - return entry.promise; - } - - const entry = {}; - entry.promise = new Promise((resolve, reject) => { - const endpoint = this.client.api.channels[this.id].typing; - Object.assign(entry, { - count: count ?? 1, - interval: this.client.setInterval(() => { - endpoint.post().catch(error => { - this.client.clearInterval(entry.interval); - this.client.user._typing.delete(this.id); - reject(error); - }); - }, 9000), - resolve, - }); - endpoint.post().catch(error => { - this.client.clearInterval(entry.interval); - this.client.user._typing.delete(this.id); - reject(error); - }); - this.client.user._typing.set(this.id, entry); - }); - return entry.promise; - } - - /** - * Stops the typing indicator in the channel. - * The indicator will only stop if this is called as many times as startTyping(). - * It can take a few seconds for the client user to stop typing. - * @param {boolean} [force=false] Whether or not to reset the call count and force the indicator to stop - * @example - * // Reduce the typing count by one and stop typing if it reached 0 - * channel.stopTyping(); - * @example - * // Force typing to fully stop regardless of typing count - * channel.stopTyping(true); - */ - stopTyping(force = false) { - if (this.client.user._typing.has(this.id)) { - const entry = this.client.user._typing.get(this.id); - entry.count--; - if (entry.count <= 0 || force) { - this.client.clearInterval(entry.interval); - this.client.user._typing.delete(this.id); - entry.resolve(); - } - } - } - - /** - * Whether or not the typing indicator is being shown in the channel - * @type {boolean} - * @readonly - */ - get typing() { - return this.client.user._typing.has(this.id); - } - - /** - * Number of times `startTyping` has been called - * @type {number} - * @readonly - */ - get typingCount() { - return this.client.user._typing.get(this.id)?.count ?? 0; + async sendTyping() { + await this.client.api.channels(this.id).typing.post(); } /** @@ -404,10 +330,7 @@ class TextBasedChannel { 'lastMessage', 'lastPinAt', 'bulkDelete', - 'startTyping', - 'stopTyping', - 'typing', - 'typingCount', + 'sendTyping', 'createMessageCollector', 'awaitMessages', 'createMessageComponentCollector', diff --git a/typings/index.d.ts b/typings/index.d.ts index c8caf4dee..c3c8eed08 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1649,6 +1649,24 @@ export class ThreadMemberFlags extends BitField { public static resolve(bit?: BitFieldResolvable): number; } +export class Typing extends Base { + public constructor( + channel: TextChannel | PartialDMChannel | NewsChannel | ThreadChannel, + user: PartialUser, + data?: object, + ); + public channel: TextChannel | PartialDMChannel | NewsChannel | ThreadChannel; + public user: PartialUser; + public startedTimestamp: number; + public readonly startedAt: Date; + public readonly guild: Guild | null; + public readonly member: GuildMember | null; + public inGuild(): this is this & { + channel: TextChannel | NewsChannel | ThreadChannel; + readonly guild: Guild; + }; +} + export class User extends PartialTextBasedChannel(Base) { public constructor(client: Client, data: unknown); public avatar: string | null; @@ -1672,9 +1690,6 @@ export class User extends PartialTextBasedChannel(Base) { public fetch(force?: boolean): Promise; public fetchFlags(force?: boolean): Promise; public toString(): UserMention; - public typingDurationIn(channel: ChannelResolvable): number; - public typingIn(channel: ChannelResolvable): boolean; - public typingSinceIn(channel: ChannelResolvable): Date; } export class UserFlags extends BitField { @@ -2420,13 +2435,10 @@ export interface PartialTextBasedChannelFields { } export interface TextBasedChannelFields extends PartialTextBasedChannelFields { - _typing: Map; lastMessageId: Snowflake | null; readonly lastMessage: Message | null; lastPinTimestamp: number | null; readonly lastPinAt: Date | null; - typing: boolean; - typingCount: number; awaitMessageComponent( options?: AwaitMessageComponentOptions, ): Promise; @@ -2439,8 +2451,7 @@ export interface TextBasedChannelFields extends PartialTextBasedChannelFields { options?: InteractionCollectorOptions, ): InteractionCollector; createMessageCollector(options?: MessageCollectorOptions): MessageCollector; - startTyping(count?: number): Promise; - stopTyping(force?: boolean): void; + sendTyping(): Promise; } export function PartialWebhookMixin(Base?: Constructable): Constructable; @@ -2835,7 +2846,7 @@ export interface ClientEvents { mewMembers: Collection, ]; threadUpdate: [oldThread: ThreadChannel, newThread: ThreadChannel]; - typingStart: [channel: Channel | PartialDMChannel, user: User | PartialUser]; + typingStart: [typing: Typing]; userUpdate: [oldUser: User | PartialUser, newUser: User]; voiceStateUpdate: [oldState: VoiceState, newState: VoiceState]; webhookUpdate: [channel: TextChannel]; @@ -3962,14 +3973,6 @@ export type SystemChannelFlagsResolvable = BitFieldResolvable; -export interface TypingData { - user: User | PartialUser; - since: Date; - lastTimestamp: Date; - elapsedTime: number; - timeout: NodeJS.Timeout; -} - export interface StageInstanceEditOptions { topic?: string; privacyLevel?: PrivacyLevel | number; diff --git a/typings/index.ts b/typings/index.ts index 17a2af10f..ed9377ff6 100644 --- a/typings/index.ts +++ b/typings/index.ts @@ -10,6 +10,7 @@ import { Collection, Constants, DMChannel, + Guild, GuildApplicationCommandManager, GuildChannelManager, GuildEmoji, @@ -27,7 +28,9 @@ import { MessageReaction, NewsChannel, Options, + PartialDMChannel, PartialTextBasedChannelFields, + PartialUser, Permissions, ReactionCollector, Role, @@ -41,6 +44,7 @@ import { TextBasedChannelFields, TextChannel, ThreadChannel, + Typing, User, VoiceChannel, } from '..'; @@ -607,13 +611,22 @@ assertType>>(guildEmojiManager.fetch() assertType>>(guildEmojiManager.fetch(undefined, {})); assertType>(guildEmojiManager.fetch('0')); -// Test partials structures -client.on('typingStart', (channel, user) => { - if (channel.partial) assertType(channel.lastMessageId); - if (user.partial) return assertType(user.username); - assertType(user.username); -}); +declare const typing: Typing; +assertType(typing.user); +if (typing.user.partial) assertType(typing.user.username); +assertType(typing.channel); +if (typing.channel.partial) assertType(typing.channel.lastMessageId); + +assertType(typing.member); +assertType(typing.guild); + +if (typing.inGuild()) { + assertType(typing.channel.guild); + assertType(typing.guild); +} + +// Test partials structures client.on('guildMemberRemove', member => { if (member.partial) return assertType(member.joinedAt); assertType(member.joinedAt);