feat(Message|TextChannel): Inline replies (#4874)

* feat(Message): remove reply functionality

* feat(InlineReplies): add INLINE_REPLY constant/typing

* feat(InlineReplies): add Message#replyReference property

* feat(InlineReplies): add typings for sending inline replies

* feat(InlineReplies): provide support for inline-replying to messages

* feat(Message): add referencedMessage getter

* fix: check that Message#reference is defined in referencedMessage

* refactor(InlineReplies): rename property, rework Message resolution

* docs: update jsdoc for inline replies

* feat(Message): inline reply method

* fix(ApiMessage): finish renaming replyTo

* fix: jsdocs for Message#referencedMessage

Co-authored-by: Tristan Guichaoua <33934311+tguichaoua@users.noreply.github.com>

* fix: restore reply typings

* fix: dont pass channel_id to API when replying

* chore: update jsdocs

* chore: more jsdoc updates

* feat(AllowedMentions): add typings for replied_user

* fix: naming conventions

* fix(Message): referenced_message is null, not undefined

* fix(MessageMentionOptions): repliedUser should be optional

* chore: get this back to the right state

* fix(ApiMessage): pass allowed_mentions when replying without content

* fix(ApiMessage): prevent mutation of client options

Co-authored-by: almostSouji <timoqueezle@gmail.com>
Co-authored-by: Tristan Guichaoua <33934311+tguichaoua@users.noreply.github.com>
This commit is contained in:
monbrey
2020-12-09 07:08:26 +11:00
committed by GitHub
parent 7365f40300
commit 60e5a0e46f
17 changed files with 80 additions and 81 deletions

View File

@@ -79,8 +79,6 @@ class APIMessage {
* @returns {?(string|string[])}
*/
makeContent() {
const GuildMember = require('./GuildMember');
let content;
if (this.options.content === null) {
content = '';
@@ -110,25 +108,14 @@ class APIMessage {
const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false;
const splitOptions = isSplit ? { ...this.options.split } : undefined;
let mentionPart = '';
if (this.options.reply && !this.isUser && this.target.type !== 'dm') {
const id = this.target.client.users.resolveID(this.options.reply);
mentionPart = `<@${this.options.reply instanceof GuildMember && this.options.reply.nickname ? '!' : ''}${id}>, `;
if (isSplit) {
splitOptions.prepend = `${mentionPart}${splitOptions.prepend || ''}`;
}
}
if (content || mentionPart) {
if (content) {
if (isCode) {
const codeName = typeof this.options.code === 'string' ? this.options.code : '';
content = `${mentionPart}\`\`\`${codeName}\n${Util.cleanCodeBlockContent(content)}\n\`\`\``;
content = `\`\`\`${codeName}\n${Util.cleanCodeBlockContent(content)}\n\`\`\``;
if (isSplit) {
splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`;
splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`;
}
} else if (mentionPart) {
content = `${mentionPart}${content}`;
}
if (isSplit) {
@@ -185,19 +172,20 @@ class APIMessage {
typeof this.options.allowedMentions === 'undefined'
? this.target.client.options.allowedMentions
: this.options.allowedMentions;
if (this.options.reply) {
const id = this.target.client.users.resolveID(this.options.reply);
if (allowedMentions) {
// Clone the object as not to alter the ClientOptions object
allowedMentions = Util.cloneObject(allowedMentions);
const parsed = allowedMentions.parse && allowedMentions.parse.includes('users');
// Check if the mention won't be parsed, and isn't supplied in `users`
if (!parsed && !(allowedMentions.users && allowedMentions.users.includes(id))) {
if (!allowedMentions.users) allowedMentions.users = [];
allowedMentions.users.push(id);
}
} else {
allowedMentions = { users: [id] };
if (allowedMentions) {
allowedMentions = Util.cloneObject(allowedMentions);
allowedMentions.replied_user = allowedMentions.repliedUser;
delete allowedMentions.repliedUser;
}
let message_reference;
if (typeof this.options.replyTo !== 'undefined') {
const message_id = this.isMessage
? this.target.channel.messages.resolveID(this.options.replyTo)
: this.target.messages.resolveID(this.options.replyTo);
if (message_id) {
message_reference = { message_id };
}
}
@@ -209,8 +197,10 @@ class APIMessage {
embeds,
username,
avatar_url: avatarURL,
allowed_mentions: typeof content === 'undefined' ? undefined : allowedMentions,
allowed_mentions:
typeof content === 'undefined' && typeof message_reference === 'undefined' ? undefined : allowedMentions,
flags,
message_reference,
};
return this;
}

View File

@@ -82,7 +82,7 @@ class Emoji extends Base {
* @example
* // Send a custom emoji from a guild:
* const emoji = guild.emojis.cache.first();
* msg.reply(`Hello! ${emoji}`);
* msg.channel.send(`Hello! ${emoji}`);
* @example
* // Send the emoji used in a reaction to the channel the reaction is part of
* reaction.message.channel.send(`The emoji used was: ${reaction.emoji}`);

View File

@@ -205,11 +205,11 @@ class Message extends Base {
this.flags = new MessageFlags(data.flags).freeze();
/**
* Reference data sent in a crossposted message.
* Reference data sent in a crossposted message or inline reply.
* @typedef {Object} MessageReference
* @property {string} channelID ID of the channel the message was crossposted from
* @property {?string} guildID ID of the guild the message was crossposted from
* @property {?string} messageID ID of the message that was crossposted
* @property {string} channelID ID of the channel the message was referenced
* @property {?string} guildID ID of the guild the message was referenced
* @property {?string} messageID ID of the message that was referenced
*/
/**
@@ -223,6 +223,10 @@ class Message extends Base {
messageID: data.message_reference.message_id,
}
: null;
if (data.referenced_message) {
this.channel.messages.add(data.referenced_message);
}
}
/**
@@ -425,6 +429,18 @@ class Message extends Base {
);
}
/**
* The Message this crosspost/reply/pin-add references, if cached
* @type {?Message}
* @readonly
*/
get referencedMessage() {
if (!this.reference) return null;
const referenceChannel = this.client.channels.resolve(this.reference.channelID);
if (!referenceChannel) return null;
return referenceChannel.messages.resolve(this.reference.messageID);
}
/**
* Whether the message is crosspostable by the client user
* @type {boolean}
@@ -588,21 +604,19 @@ class Message extends Base {
}
/**
* Replies to the message.
* Send an inline reply to this message.
* @param {StringResolvable|APIMessage} [content=''] The content for the message
* @param {MessageOptions|MessageAdditions} [options={}] The options to provide
* @param {MessageOptions|MessageAdditions} [options] The additional options to provide
* @param {MessageResolvable} [options.replyTo=this] The message to reply to
* @returns {Promise<Message|Message[]>}
* @example
* // Reply to a message
* message.reply('Hey, I\'m a reply!')
* .then(() => console.log(`Sent a reply to ${message.author.username}`))
* .catch(console.error);
*/
reply(content, options) {
return this.channel.send(
content instanceof APIMessage
? content
: APIMessage.transformOptions(content, options, { reply: this.member || this.author }),
: APIMessage.transformOptions(content, options, {
replyTo: this,
}),
);
}

View File

@@ -65,7 +65,7 @@ class TextBasedChannel {
* @property {string|boolean} [code] Language for optional codeblock formatting to apply
* @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if
* it exceeds the character limit. If an object is provided, these are the options for splitting the message
* @property {UserResolvable} [reply] User to reply to (prefixes the message with a mention, except in DMs)
* @property {MessageResolvable} [replyTo] The message to reply to (must be in the same channel)
*/
/**
@@ -74,6 +74,7 @@ class TextBasedChannel {
* @property {MessageMentionTypes[]} [parse] Types of mentions to be parsed
* @property {Snowflake[]} [users] Snowflakes of Users to be parsed as mentions
* @property {Snowflake[]} [roles] Snowflakes of Roles to be parsed as mentions
* @property {boolean} [repliedUser] Whether the author of the Message being replied to should be pinged
*/
/**