From 065e452f9d0386dfd1b426793aca4c7f07c9c0ae Mon Sep 17 00:00:00 2001 From: Snazzah <7025343+Snazzah@users.noreply.github.com> Date: Thu, 5 Aug 2021 14:00:11 -0500 Subject: [PATCH] feat: add replies (+ style changes) (#103) Co-authored-by: Jeroen Claassens --- packages/core/src/components.d.ts | 178 +++++++++++++++++- .../components/author-info/author-info.css | 5 +- .../components/author-info/author-info.tsx | 19 +- .../discord-command/discord-command.css | 15 ++ .../discord-command/discord-command.tsx | 79 ++++++++ .../src/components/discord-command/readme.md | 17 ++ .../discord-mention/discord-mention.css | 1 - .../discord-message/discord-message.css | 40 ++++ .../discord-message/discord-message.tsx | 84 ++++++--- .../src/components/discord-message/readme.md | 24 +-- .../discord-reply/discord-reply.css | 136 +++++++++++++ .../discord-reply/discord-reply.tsx | 141 ++++++++++++++ .../src/components/discord-reply/readme.md | 23 +++ .../src/components/svgs/attachment-reply.tsx | 12 ++ .../core/src/components/svgs/command-icon.tsx | 9 + .../src/components/svgs/command-reply.tsx | 14 ++ .../core/src/components/svgs/reply-icon.tsx | 12 ++ packages/core/src/index.html | 46 ++++- packages/core/src/util.ts | 18 +- packages/react/src/index.ts | 2 + 20 files changed, 816 insertions(+), 59 deletions(-) create mode 100644 packages/core/src/components/discord-command/discord-command.css create mode 100644 packages/core/src/components/discord-command/discord-command.tsx create mode 100644 packages/core/src/components/discord-command/readme.md create mode 100644 packages/core/src/components/discord-reply/discord-reply.css create mode 100644 packages/core/src/components/discord-reply/discord-reply.tsx create mode 100644 packages/core/src/components/discord-reply/readme.md create mode 100644 packages/core/src/components/svgs/attachment-reply.tsx create mode 100644 packages/core/src/components/svgs/command-icon.tsx create mode 100644 packages/core/src/components/svgs/command-reply.tsx create mode 100644 packages/core/src/components/svgs/reply-icon.tsx diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 41b75d01..620f63c7 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -29,6 +29,29 @@ export namespace Components { } interface DiscordAttachments { } + interface DiscordCommand { + /** + * The message author's username. + * @default 'User' + */ + "author": string; + /** + * The message author's avatar. Can be an avatar shortcut, relative path, or external link. + */ + "avatar": string; + /** + * The name of the command invoked. + */ + "command": string; + /** + * The id of the profile data to use. + */ + "profile": string; + /** + * The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). + */ + "roleColor": string; + } interface DiscordEmbed { /** * The author's avatar URL. @@ -168,6 +191,10 @@ export namespace Components { * Whether the message has been edited or not. */ "edited": boolean; + /** + * Whether to highlight this message. + */ + "highlight": boolean; /** * The id of the profile data to use. */ @@ -181,9 +208,13 @@ export namespace Components { */ "server": boolean; /** - * The timestamp to use for the message date. When supplying a string, the format must be `01/31/2000`. + * The timestamp to use for the message date. */ "timestamp": DiscordTimestamp; + /** + * Whether to use 24-hour format for the timestamp. + */ + "twentyFour": boolean; /** * Whether the bot is verified or not. Only works if `bot` is `true` */ @@ -232,6 +263,53 @@ export namespace Components { } interface DiscordReactions { } + interface DiscordReply { + /** + * Whether the referenced message contains attachments. + */ + "attachment": boolean; + /** + * The message author's username. + * @default 'User' + */ + "author": string; + /** + * The message author's avatar. Can be an avatar shortcut, relative path, or external link. + */ + "avatar": string; + /** + * Whether the message author is a bot or not. Only works if `server` is `false` or `undefined`. + */ + "bot": boolean; + /** + * Whether the referenced message is from a response of a slash command. + */ + "command": boolean; + /** + * Whether the message has been edited or not. + */ + "edited": boolean; + /** + * Whether this reply pings the original message sender, prepending an "@" on the author's username. + */ + "mentions": boolean; + /** + * The id of the profile data to use. + */ + "profile": string; + /** + * The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). + */ + "roleColor": string; + /** + * Whether the message author is a server crosspost webhook or not. Only works if `bot` is `false` or `undefined`. + */ + "server": boolean; + /** + * Whether the bot is verified or not. Only works if `bot` is `true` + */ + "verified": boolean; + } } declare global { interface HTMLDiscordAttachmentElement extends Components.DiscordAttachment, HTMLStencilElement { @@ -246,6 +324,12 @@ declare global { prototype: HTMLDiscordAttachmentsElement; new (): HTMLDiscordAttachmentsElement; }; + interface HTMLDiscordCommandElement extends Components.DiscordCommand, HTMLStencilElement { + } + var HTMLDiscordCommandElement: { + prototype: HTMLDiscordCommandElement; + new (): HTMLDiscordCommandElement; + }; interface HTMLDiscordEmbedElement extends Components.DiscordEmbed, HTMLStencilElement { } var HTMLDiscordEmbedElement: { @@ -300,9 +384,16 @@ declare global { prototype: HTMLDiscordReactionsElement; new (): HTMLDiscordReactionsElement; }; + interface HTMLDiscordReplyElement extends Components.DiscordReply, HTMLStencilElement { + } + var HTMLDiscordReplyElement: { + prototype: HTMLDiscordReplyElement; + new (): HTMLDiscordReplyElement; + }; interface HTMLElementTagNameMap { "discord-attachment": HTMLDiscordAttachmentElement; "discord-attachments": HTMLDiscordAttachmentsElement; + "discord-command": HTMLDiscordCommandElement; "discord-embed": HTMLDiscordEmbedElement; "discord-embed-field": HTMLDiscordEmbedFieldElement; "discord-embed-fields": HTMLDiscordEmbedFieldsElement; @@ -312,6 +403,7 @@ declare global { "discord-messages": HTMLDiscordMessagesElement; "discord-reaction": HTMLDiscordReactionElement; "discord-reactions": HTMLDiscordReactionsElement; + "discord-reply": HTMLDiscordReplyElement; } } declare namespace LocalJSX { @@ -337,6 +429,29 @@ declare namespace LocalJSX { } interface DiscordAttachments { } + interface DiscordCommand { + /** + * The message author's username. + * @default 'User' + */ + "author"?: string; + /** + * The message author's avatar. Can be an avatar shortcut, relative path, or external link. + */ + "avatar"?: string; + /** + * The name of the command invoked. + */ + "command"?: string; + /** + * The id of the profile data to use. + */ + "profile"?: string; + /** + * The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). + */ + "roleColor"?: string; + } interface DiscordEmbed { /** * The author's avatar URL. @@ -476,6 +591,10 @@ declare namespace LocalJSX { * Whether the message has been edited or not. */ "edited"?: boolean; + /** + * Whether to highlight this message. + */ + "highlight"?: boolean; /** * The id of the profile data to use. */ @@ -489,9 +608,13 @@ declare namespace LocalJSX { */ "server"?: boolean; /** - * The timestamp to use for the message date. When supplying a string, the format must be `01/31/2000`. + * The timestamp to use for the message date. */ "timestamp"?: DiscordTimestamp; + /** + * Whether to use 24-hour format for the timestamp. + */ + "twentyFour"?: boolean; /** * Whether the bot is verified or not. Only works if `bot` is `true` */ @@ -540,9 +663,57 @@ declare namespace LocalJSX { } interface DiscordReactions { } + interface DiscordReply { + /** + * Whether the referenced message contains attachments. + */ + "attachment"?: boolean; + /** + * The message author's username. + * @default 'User' + */ + "author"?: string; + /** + * The message author's avatar. Can be an avatar shortcut, relative path, or external link. + */ + "avatar"?: string; + /** + * Whether the message author is a bot or not. Only works if `server` is `false` or `undefined`. + */ + "bot"?: boolean; + /** + * Whether the referenced message is from a response of a slash command. + */ + "command"?: boolean; + /** + * Whether the message has been edited or not. + */ + "edited"?: boolean; + /** + * Whether this reply pings the original message sender, prepending an "@" on the author's username. + */ + "mentions"?: boolean; + /** + * The id of the profile data to use. + */ + "profile"?: string; + /** + * The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). + */ + "roleColor"?: string; + /** + * Whether the message author is a server crosspost webhook or not. Only works if `bot` is `false` or `undefined`. + */ + "server"?: boolean; + /** + * Whether the bot is verified or not. Only works if `bot` is `true` + */ + "verified"?: boolean; + } interface IntrinsicElements { "discord-attachment": DiscordAttachment; "discord-attachments": DiscordAttachments; + "discord-command": DiscordCommand; "discord-embed": DiscordEmbed; "discord-embed-field": DiscordEmbedField; "discord-embed-fields": DiscordEmbedFields; @@ -552,6 +723,7 @@ declare namespace LocalJSX { "discord-messages": DiscordMessages; "discord-reaction": DiscordReaction; "discord-reactions": DiscordReactions; + "discord-reply": DiscordReply; } } export { LocalJSX as JSX }; @@ -560,6 +732,7 @@ declare module "@stencil/core" { interface IntrinsicElements { "discord-attachment": LocalJSX.DiscordAttachment & JSXBase.HTMLAttributes; "discord-attachments": LocalJSX.DiscordAttachments & JSXBase.HTMLAttributes; + "discord-command": LocalJSX.DiscordCommand & JSXBase.HTMLAttributes; "discord-embed": LocalJSX.DiscordEmbed & JSXBase.HTMLAttributes; "discord-embed-field": LocalJSX.DiscordEmbedField & JSXBase.HTMLAttributes; "discord-embed-fields": LocalJSX.DiscordEmbedFields & JSXBase.HTMLAttributes; @@ -569,6 +742,7 @@ declare module "@stencil/core" { "discord-messages": LocalJSX.DiscordMessages & JSXBase.HTMLAttributes; "discord-reaction": LocalJSX.DiscordReaction & JSXBase.HTMLAttributes; "discord-reactions": LocalJSX.DiscordReactions & JSXBase.HTMLAttributes; + "discord-reply": LocalJSX.DiscordReply & JSXBase.HTMLAttributes; } } } diff --git a/packages/core/src/components/author-info/author-info.css b/packages/core/src/components/author-info/author-info.css index 9b544ea9..4a18f777 100644 --- a/packages/core/src/components/author-info/author-info.css +++ b/packages/core/src/components/author-info/author-info.css @@ -17,6 +17,7 @@ .discord-message .discord-author-info .discord-application-tag { background-color: hsl(235, 85.6%, 64.7%); + color: #fff; font-size: 0.65em; margin-left: 5px; border-radius: 3px; @@ -44,14 +45,14 @@ } .discord-compact-mode .discord-message .discord-author-info .discord-author-username { - margin-left: 4px; + margin-left: 8px; margin-right: 4px; } .discord-compact-mode .discord-message .discord-author-info .discord-application-tag { margin-left: 0; + margin-left: 5px; margin-right: 5px; padding-left: 3px; padding-right: 3px; - font-size: 0.6em; } diff --git a/packages/core/src/components/author-info/author-info.tsx b/packages/core/src/components/author-info/author-info.tsx index 851f3e71..5350df8b 100644 --- a/packages/core/src/components/author-info/author-info.tsx +++ b/packages/core/src/components/author-info/author-info.tsx @@ -23,13 +23,19 @@ interface AuthorInfoProps { * Whether this bot is verified by Discord. Only works if `bot` is `true` */ verified: boolean; + /** + * Whether to reverse the order of the author info for compact mode. + */ + compact: boolean; } -export const AuthorInfo: FunctionalComponent = ({ author, bot, server, roleColor, verified }) => ( +export const AuthorInfo: FunctionalComponent = ({ author, bot, server, roleColor, verified, compact }) => ( - - {author} - + {!compact && ( + + {author} + + )} { {/* If bot is true then we need to render a Bot tag */} @@ -43,5 +49,10 @@ export const AuthorInfo: FunctionalComponent = ({ author, bot, {server && !bot && Server} } + {compact && ( + + {author} + + )} ); diff --git a/packages/core/src/components/discord-command/discord-command.css b/packages/core/src/components/discord-command/discord-command.css new file mode 100644 index 00000000..036ba2a0 --- /dev/null +++ b/packages/core/src/components/discord-command/discord-command.css @@ -0,0 +1,15 @@ +.discord-replied-message.discord-executed-command .discord-command-name { + color: #00aff4; + font-weight: 500; +} + +.discord-replied-message.discord-executed-command .discord-command-name:hover { + color: #00aff4; + text-decoration: underline; +} + +.discord-replied-message.discord-executed-command .discord-replied-message-username { + margin-right: 0; +} + +@import '../discord-reply/discord-reply.css'; diff --git a/packages/core/src/components/discord-command/discord-command.tsx b/packages/core/src/components/discord-command/discord-command.tsx new file mode 100644 index 00000000..07d342e3 --- /dev/null +++ b/packages/core/src/components/discord-command/discord-command.tsx @@ -0,0 +1,79 @@ +import { Component, ComponentInterface, Element, h, Host, Prop } from '@stencil/core'; +import { avatars, Profile, profiles } from '../../options'; +import CommandIcon from '../svgs/command-icon'; + +@Component({ + tag: 'discord-command', + styleUrl: 'discord-command.css' +}) +export class DiscordCommand implements ComponentInterface { + /** + * The DiscordCommand element. + */ + @Element() + public el: HTMLElement; + + /** + * The id of the profile data to use. + */ + @Prop() + public profile: string; + + /** + * The message author's username. + * @default 'User' + */ + @Prop() + public author = 'User'; + + /** + * The message author's avatar. Can be an avatar shortcut, relative path, or external link. + */ + @Prop() + public avatar: string; + + /** + * The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). + */ + @Prop() + public roleColor: string; + + /** + * The name of the command invoked. + */ + @Prop() + public command: string; + + public render() { + const parent: HTMLDiscordMessageElement = this.el.parentElement as HTMLDiscordMessageElement; + + if (parent.tagName.toLowerCase() !== 'discord-message') { + throw new Error('All components must be direct children of .'); + } + + const resolveAvatar = (avatar: string): string => avatars[avatar] ?? avatar ?? avatars.default; + + const defaultData: Profile = { author: this.author, bot: false, verified: false, server: false, roleColor: this.roleColor }; + const profileData: Profile = Reflect.get(profiles, this.profile) ?? {}; + const profile: Profile = { ...defaultData, ...profileData, ...{ avatar: resolveAvatar(profileData.avatar ?? this.avatar) } }; + + const messageParent: HTMLDiscordMessagesElement = parent.parentElement as HTMLDiscordMessagesElement; + + return ( + + {messageParent.compactMode ? ( +
+ +
+ ) : ( + {profile.author} + )} + + {profile.author} + + {' used '} +
{`/${this.command}`}
+
+ ); + } +} diff --git a/packages/core/src/components/discord-command/readme.md b/packages/core/src/components/discord-command/readme.md new file mode 100644 index 00000000..458b9d99 --- /dev/null +++ b/packages/core/src/components/discord-command/readme.md @@ -0,0 +1,17 @@ +# discord-command + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ----------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | -------- | ----------- | +| `author` | `author` | The message author's username. | `string` | `'User'` | +| `avatar` | `avatar` | The message author's avatar. Can be an avatar shortcut, relative path, or external link. | `string` | `undefined` | +| `command` | `command` | The name of the command invoked. | `string` | `undefined` | +| `profile` | `profile` | The id of the profile data to use. | `string` | `undefined` | +| `roleColor` | `role-color` | The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). | `string` | `undefined` | + +--- + +_Built with [StencilJS](https://stenciljs.com/)_ diff --git a/packages/core/src/components/discord-mention/discord-mention.css b/packages/core/src/components/discord-mention/discord-mention.css index 46418e7d..2e270434 100644 --- a/packages/core/src/components/discord-mention/discord-mention.css +++ b/packages/core/src/components/discord-mention/discord-mention.css @@ -46,7 +46,6 @@ border-radius: 0 3px 3px 0; padding-right: 5px; position: relative; - padding-left: 0.85em; } .discord-light-theme .discord-message.discord-highlight-mention { diff --git a/packages/core/src/components/discord-message/discord-message.css b/packages/core/src/components/discord-message/discord-message.css index c793cce3..0db6f2f0 100644 --- a/packages/core/src/components/discord-message/discord-message.css +++ b/packages/core/src/components/discord-message/discord-message.css @@ -1,6 +1,7 @@ .discord-message { color: #dcddde; display: flex; + flex-direction: column; font-size: 0.9em; font-family: Whitney, Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; padding: 0px 1em; @@ -20,6 +21,14 @@ margin-top: 1.0625rem; } +.discord-message .discord-message-inner { + display: flex; + position: relative; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; +} + .discord-message.discord-message-highlight { background-color: rgba(250, 166, 26, 0.05); } @@ -102,6 +111,8 @@ } .discord-message .discord-message-body { + font-size: 1rem; + font-weight: 400; word-break: break-word; position: relative; } @@ -126,10 +137,31 @@ color: #d1d9de; } +.discord-compact-mode .discord-message .discord-message-timestamp { + display: inline-block; + width: 3.1rem; + text-align: right; + font-size: 0.6875rem; + line-height: 1.375rem; + margin-right: 0.25rem; + margin-left: 0; + text-indent: 0; +} + .discord-compact-mode .discord-message { margin-top: unset; } +.discord-compact-mode .discord-message .discord-message-body { + line-height: 1.375rem; + padding-left: 10px; + text-indent: -6px; +} + +.discord-compact-mode .discord-message .discord-message-compact-indent { + padding-left: 10px; +} + .discord-message:first-of-type { margin-top: 0.5rem; } @@ -139,6 +171,14 @@ border-bottom-width: 0; } +.discord-message .discord-message-markup { + font-size: 1rem; + line-height: 1.375rem; + word-wrap: break-word; + user-select: text; + font-weight: 400; +} + .discord-compact-mode .discord-author-avatar { display: none; } diff --git a/packages/core/src/components/discord-message/discord-message.tsx b/packages/core/src/components/discord-message/discord-message.tsx index bf1ee1d1..926d03ec 100644 --- a/packages/core/src/components/discord-message/discord-message.tsx +++ b/packages/core/src/components/discord-message/discord-message.tsx @@ -69,18 +69,32 @@ export class DiscordMessage implements ComponentInterface { public roleColor: string; /** - * The timestamp to use for the message date. When supplying a string, the format must be `01/31/2000`. + * Whether to highlight this message. + */ + @Prop() + public highlight = false; + + /** + * The timestamp to use for the message date. */ @Prop({ mutable: true, reflect: true }) public timestamp: DiscordTimestamp = new Date(); + /** + * Whether to use 24-hour format for the timestamp. + */ + @Prop() + public twentyFour = false; + @Watch('timestamp') public updateTimestamp(value: DiscordTimestamp): string | null { - return handleTimestamp(value); + const parent: HTMLDiscordMessagesElement = this.el.parentElement as HTMLDiscordMessagesElement; + return handleTimestamp(value, parent.compactMode, this.twentyFour); } public componentWillRender() { - this.timestamp = handleTimestamp(this.timestamp); + const parent: HTMLDiscordMessagesElement = this.el.parentElement as HTMLDiscordMessagesElement; + this.timestamp = handleTimestamp(this.timestamp, parent.compactMode, this.twentyFour); } public render() { @@ -96,48 +110,56 @@ export class DiscordMessage implements ComponentInterface { const profileData: Profile = Reflect.get(profiles, this.profile) ?? {}; const profile: Profile = { ...defaultData, ...profileData, ...{ avatar: resolveAvatar(profileData.avatar ?? this.avatar) } }; - // @ts-expect-error ts doesn't understand this - const highlightMention: boolean = Array.from(this.el.children).some((child: HTMLDiscordMentionElement): boolean => { - return child.tagName.toLowerCase() === 'discord-mention' && child.highlight && child.type !== 'channel'; - }); + const highlightMention: boolean = + // @ts-expect-error ts doesn't understand this + Array.from(this.el.children).some((child: HTMLDiscordMentionElement): boolean => { + return child.tagName.toLowerCase() === 'discord-mention' && child.highlight && ['user', 'role'].includes(child.type); + }) || this.highlight; return ( -
- {profile.author} -
-
- {!parent.compactMode && ( - - - {this.timestamp} - - )} -
- {parent.compactMode && ( + +
+ {parent.compactMode && {this.timestamp}} +
+ {profile.author} +
+
+ {!parent.compactMode && ( - {this.timestamp} + {this.timestamp} )} - - {this.edited ? (edited) : ''} +
+ {parent.compactMode && ( + + )} + + + + {this.edited ? (edited) : ''} +
+
+ + + +
- - -
); diff --git a/packages/core/src/components/discord-message/readme.md b/packages/core/src/components/discord-message/readme.md index c119926a..82e6310e 100644 --- a/packages/core/src/components/discord-message/readme.md +++ b/packages/core/src/components/discord-message/readme.md @@ -4,17 +4,19 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ----------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------ | ------------ | -| `author` | `author` | The message author's username. | `string` | `'User'` | -| `avatar` | `avatar` | The message author's avatar. Can be an avatar shortcut, relative path, or external link. | `string` | `undefined` | -| `bot` | `bot` | Whether the message author is a bot or not. Only works if `server` is `false` or `undefined`. | `boolean` | `false` | -| `edited` | `edited` | Whether the message has been edited or not. | `boolean` | `false` | -| `profile` | `profile` | The id of the profile data to use. | `string` | `undefined` | -| `roleColor` | `role-color` | The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). | `string` | `undefined` | -| `server` | `server` | Whether the message author is a server crosspost webhook or not. Only works if `bot` is `false` or `undefined`. | `boolean` | `false` | -| `timestamp` | `timestamp` | The timestamp to use for the message date. When supplying a string, the format must be `01/31/2000`. | `Date \| null \| string` | `new Date()` | -| `verified` | `verified` | Whether the bot is verified or not. Only works if `bot` is `true` | `boolean` | `false` | +| Property | Attribute | Description | Type | Default | +| ------------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------ | ------------ | +| `author` | `author` | The message author's username. | `string` | `'User'` | +| `avatar` | `avatar` | The message author's avatar. Can be an avatar shortcut, relative path, or external link. | `string` | `undefined` | +| `bot` | `bot` | Whether the message author is a bot or not. Only works if `server` is `false` or `undefined`. | `boolean` | `false` | +| `edited` | `edited` | Whether the message has been edited or not. | `boolean` | `false` | +| `highlight` | `highlight` | Whether to highlight this message. | `boolean` | `false` | +| `profile` | `profile` | The id of the profile data to use. | `string` | `undefined` | +| `roleColor` | `role-color` | The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). | `string` | `undefined` | +| `server` | `server` | Whether the message author is a server crosspost webhook or not. Only works if `bot` is `false` or `undefined`. | `boolean` | `false` | +| `timestamp` | `timestamp` | The timestamp to use for the message date. | `Date \| null \| string` | `new Date()` | +| `twentyFour` | `twenty-four` | Whether to use 24-hour format for the timestamp. | `boolean` | `false` | +| `verified` | `verified` | Whether the bot is verified or not. Only works if `bot` is `true` | `boolean` | `false` | --- diff --git a/packages/core/src/components/discord-reply/discord-reply.css b/packages/core/src/components/discord-reply/discord-reply.css new file mode 100644 index 00000000..a5a229af --- /dev/null +++ b/packages/core/src/components/discord-reply/discord-reply.css @@ -0,0 +1,136 @@ +.discord-replied-message { + color: #b9bbbe; + display: flex; + font-size: 0.875rem; + font-family: Whitney, Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; + + padding-top: 2px; + margin-left: 56px; + margin-bottom: 4px; + align-items: center; + line-height: 1.125rem; + position: relative; + white-space: pre; + user-select: none; +} + +.discord-light-theme .discord-replied-message { + color: #4f5660; +} + +.discord-compact-mode .discord-replied-message { + margin-left: 62px; + margin-bottom: 0; +} + +.discord-replied-message:before { + content: ''; + display: block; + position: absolute; + top: 50%; + right: 100%; + bottom: 0; + left: -36px; + margin-right: 4px; + margin-top: -1px; + margin-left: -1px; + margin-bottom: -2px; + border-left: 2px solid #4f545c; + border-bottom: 0 solid #4f545c; + border-right: 0 solid #4f545c; + border-top: 2px solid #4f545c; + border-top-left-radius: 6px; +} + +.discord-light-theme .discord-replied-message:before { + border-color: #747f8d; +} + +.discord-replied-message .discord-replied-message-avatar, +.discord-replied-message .discord-reply-badge { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 16px; + height: 16px; + border-radius: 50%; + user-select: none; + margin-right: 0.25rem; +} + +.discord-replied-message .discord-reply-badge { + display: flex; + align-items: center; + justify-content: center; + color: #b9bbbe; + background: #202225; +} + +.discord-light-theme .discord-replied-message .discord-reply-badge { + color: #4f5660; + background: #e3e5e8; +} + +.discord-replied-message .discord-application-tag { + background-color: hsl(235, 85.6%, 64.7%); + color: #fff; + font-size: 0.625rem; + margin-right: 0.25rem; + line-height: 100%; + text-transform: uppercase; + /* Use flex layout to ensure both verified icon and "BOT" text are aligned to center */ + display: flex; + align-items: center; + /* Styling taken through Inspect Element on Discord client for Windows */ + height: 0.9375rem; + padding: 0 0.275rem; + margin-top: 0.075em; + border-radius: 0.1875rem; +} + +.discord-replied-message .discord-application-tag .discord-application-tag-verified { + width: 0.9375rem; + height: 0.9375rem; + margin-left: -0.1rem; +} + +.discord-replied-message .discord-replied-message-username { + flex-shrink: 0; + font-size: inherit; + line-height: inherit; + margin-right: 0.25rem; + opacity: 0.64; +} + +.discord-replied-message .discord-replied-message-content { + color: inherit; + font-size: inherit; + line-height: inherit; + white-space: pre; + text-overflow: ellipsis; + user-select: none; + cursor: pointer; +} + +.discord-replied-message .discord-replied-message-content:hover { + color: #fff; +} + +.discord-light-theme .discord-replied-message .discord-replied-message-content:hover { + color: #000; +} + +.discord-replied-message .discord-replied-message-content .discord-message-edited { + margin-left: 0.25rem; +} + +.discord-replied-message .discord-replied-message-content-icon { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 20px; + height: 20px; + margin-left: 4px; +} + +@import '../author-info/author-info.css'; diff --git a/packages/core/src/components/discord-reply/discord-reply.tsx b/packages/core/src/components/discord-reply/discord-reply.tsx new file mode 100644 index 00000000..b2373977 --- /dev/null +++ b/packages/core/src/components/discord-reply/discord-reply.tsx @@ -0,0 +1,141 @@ +import { Component, ComponentInterface, Element, h, Host, Prop } from '@stencil/core'; +import Fragment from '../../Fragment'; +import { avatars, Profile, profiles } from '../../options'; +import AttachmentReply from '../svgs/attachment-reply'; +import CommandReply from '../svgs/command-reply'; +import ReplyIcon from '../svgs/reply-icon'; +import VerifiedTick from '../svgs/verified-tick'; + +@Component({ + tag: 'discord-reply', + styleUrl: 'discord-reply.css' +}) +export class DiscordReply implements ComponentInterface { + /** + * The DiscordReply element. + */ + @Element() + public el: HTMLElement; + + /** + * The id of the profile data to use. + */ + @Prop() + public profile: string; + + /** + * The message author's username. + * @default 'User' + */ + @Prop() + public author = 'User'; + + /** + * The message author's avatar. Can be an avatar shortcut, relative path, or external link. + */ + @Prop() + public avatar: string; + + /** + * Whether the message author is a bot or not. + * Only works if `server` is `false` or `undefined`. + */ + @Prop() + public bot = false; + + /** + * Whether the message author is a server crosspost webhook or not. + * Only works if `bot` is `false` or `undefined`. + */ + @Prop() + public server = false; + + /** + * Whether the bot is verified or not. + * Only works if `bot` is `true` + */ + @Prop() + public verified = false; + + /** + * Whether the message has been edited or not. + */ + @Prop() + public edited = false; + + /** + * The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). + */ + @Prop() + public roleColor: string; + + /** + * Whether the referenced message is from a response of a slash command. + */ + @Prop() + public command = false; + + /** + * Whether the referenced message contains attachments. + */ + @Prop() + public attachment = false; + + /** + * Whether this reply pings the original message sender, prepending an "@" on the author's username. + */ + @Prop() + public mentions = false; + + public render() { + const parent: HTMLDiscordMessageElement = this.el.parentElement as HTMLDiscordMessageElement; + + if (parent.tagName.toLowerCase() !== 'discord-message') { + throw new Error('All components must be direct children of .'); + } + + const resolveAvatar = (avatar: string): string => avatars[avatar] ?? avatar ?? avatars.default; + + const defaultData: Profile = { author: this.author, bot: this.bot, verified: this.verified, server: this.server, roleColor: this.roleColor }; + const profileData: Profile = Reflect.get(profiles, this.profile) ?? {}; + const profile: Profile = { ...defaultData, ...profileData, ...{ avatar: resolveAvatar(profileData.avatar ?? this.avatar) } }; + + const messageParent: HTMLDiscordMessagesElement = parent.parentElement as HTMLDiscordMessagesElement; + + return ( + + {messageParent.compactMode ? ( +
+ +
+ ) : ( + {profile.author} + )} + { + + {profile.bot && !profile.server && ( + + {profile.verified && } + Bot + + )} + {profile.server && !profile.bot && Server} + + } + + {this.mentions && '@'} + {profile.author} + +
+ + {this.edited ? (edited) : ''} +
+ {this.command ? ( + + ) : ( + this.attachment && + )} +
+ ); + } +} diff --git a/packages/core/src/components/discord-reply/readme.md b/packages/core/src/components/discord-reply/readme.md new file mode 100644 index 00000000..d61123a8 --- /dev/null +++ b/packages/core/src/components/discord-reply/readme.md @@ -0,0 +1,23 @@ +# discord-reply + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | +| `attachment` | `attachment` | Whether the referenced message contains attachments. | `boolean` | `false` | +| `author` | `author` | The message author's username. | `string` | `'User'` | +| `avatar` | `avatar` | The message author's avatar. Can be an avatar shortcut, relative path, or external link. | `string` | `undefined` | +| `bot` | `bot` | Whether the message author is a bot or not. Only works if `server` is `false` or `undefined`. | `boolean` | `false` | +| `command` | `command` | Whether the referenced message is from a response of a slash command. | `boolean` | `false` | +| `edited` | `edited` | Whether the message has been edited or not. | `boolean` | `false` | +| `mentions` | `mentions` | Whether this reply pings the original message sender, prepending an "@" on the author's username. | `boolean` | `false` | +| `profile` | `profile` | The id of the profile data to use. | `string` | `undefined` | +| `roleColor` | `role-color` | The message author's primary role color. Can be any [CSS color value](https://www.w3schools.com/cssref/css_colors_legal.asp). | `string` | `undefined` | +| `server` | `server` | Whether the message author is a server crosspost webhook or not. Only works if `bot` is `false` or `undefined`. | `boolean` | `false` | +| `verified` | `verified` | Whether the bot is verified or not. Only works if `bot` is `true` | `boolean` | `false` | + +--- + +_Built with [StencilJS](https://stenciljs.com/)_ diff --git a/packages/core/src/components/svgs/attachment-reply.tsx b/packages/core/src/components/svgs/attachment-reply.tsx new file mode 100644 index 00000000..b73aeec6 --- /dev/null +++ b/packages/core/src/components/svgs/attachment-reply.tsx @@ -0,0 +1,12 @@ +import { h } from '@stencil/core'; + +export default function AttachmentReply(props: T) { + return ( + + + + ); +} diff --git a/packages/core/src/components/svgs/command-icon.tsx b/packages/core/src/components/svgs/command-icon.tsx new file mode 100644 index 00000000..12413e12 --- /dev/null +++ b/packages/core/src/components/svgs/command-icon.tsx @@ -0,0 +1,9 @@ +import { h } from '@stencil/core'; + +export default function CommandIcon(props: T) { + return ( + + + + ); +} diff --git a/packages/core/src/components/svgs/command-reply.tsx b/packages/core/src/components/svgs/command-reply.tsx new file mode 100644 index 00000000..6f12486c --- /dev/null +++ b/packages/core/src/components/svgs/command-reply.tsx @@ -0,0 +1,14 @@ +import { h } from '@stencil/core'; + +export default function CommandReply(props: T) { + return ( + + + + ); +} diff --git a/packages/core/src/components/svgs/reply-icon.tsx b/packages/core/src/components/svgs/reply-icon.tsx new file mode 100644 index 00000000..83e3dfe6 --- /dev/null +++ b/packages/core/src/components/svgs/reply-icon.tsx @@ -0,0 +1,12 @@ +import { h } from '@stencil/core'; + +export default function ReplyIcon(props: T) { + return ( + + + + ); +} diff --git a/packages/core/src/index.html b/packages/core/src/index.html index 5af6b289..e7c9da1c 100644 --- a/packages/core/src/index.html +++ b/packages/core/src/index.html @@ -121,7 +121,7 @@

Mentions

sure to read through the rules. You can ping Support if you need help. Feel free to join - General + General and talk with us.
@@ -181,6 +181,50 @@

Reactions

+

Replies

+ + + What do you think about this image? + Looks nice! + + + Looks nice! + I agree! + + +

Replies in Compact Mode

+ + + What do you think about this image? + Looks nice! + + + Looks nice! + I agree! + + +

Commands

+ + + + Pong! + + + Pong! + Took 100ms. + + +

Commands in Compact Mode

+ + + + Pong! + + + Pong! + Took 100ms. + +

Verified Discord bots

Wow I just got verified! diff --git a/packages/core/src/util.ts b/packages/core/src/util.ts index e0ff016b..88bc89e9 100644 --- a/packages/core/src/util.ts +++ b/packages/core/src/util.ts @@ -1,7 +1,5 @@ export type DiscordTimestamp = Date | string | null; -const dateRegex = /^\d{2}\/\d{2}\/\d{4}$/; - const padZeroes = (value: string): string => { const [month, day, year]: string[] = value.split('/'); return `${month.padStart(2, '0')}/${day.padStart(2, '0')}/${year}`; @@ -12,14 +10,20 @@ const formatDate = (value: DiscordTimestamp): string | null => { return padZeroes(`${value.getMonth() + 1}/${value.getDate()}/${value.getFullYear()}`); }; -export const handleTimestamp = (value: DiscordTimestamp): string | null => { +const formatTime = (value: DiscordTimestamp, hour24 = false): string | null => { + if (!(value instanceof Date)) return value; + if (hour24) return `${value.getHours()}:${value.getMinutes().toString().padStart(2, '0')}`; + const hour = value.getHours() % 12 || 12; + const meridiem = value.getHours() < 12 ? 'AM' : 'PM'; + return `${hour}:${value.getMinutes().toString().padStart(2, '0')} ${meridiem}`; +}; + +export const handleTimestamp = (value: DiscordTimestamp, useTime = false, hour24 = false): string | null => { if (!(value instanceof Date) && typeof value !== 'string') { - throw new TypeError('Timestamp prop must be a Date object or a string in the format of `01/31/2000`.'); - } else if (typeof value === 'string' && !dateRegex.test(value)) { - throw new Error('Date string must be in the format of `01/31/2000`.'); + throw new TypeError('Timestamp prop must be a Date object or a string.'); } - return formatDate(value); + return useTime ? formatTime(value, hour24) : formatDate(value); }; export const findSlotElement = (elements: HTMLCollection, name: string): Element | undefined => { diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index aa7ab142..9dccbda2 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -10,6 +10,7 @@ import { defineCustomElements } from '@skyra/discord-components-core/loader'; defineCustomElements(); export const DiscordAttachment = /*@__PURE__*/ createReactComponent('discord-attachment'); export const DiscordAttachments = /*@__PURE__*/ createReactComponent('discord-attachments'); +export const DiscordCommand = /*@__PURE__*/ createReactComponent('discord-command'); export const DiscordEmbed = /*@__PURE__*/ createReactComponent('discord-embed'); export const DiscordEmbedField = /*@__PURE__*/ createReactComponent('discord-embed-field'); export const DiscordEmbedFields = /*@__PURE__*/ createReactComponent('discord-embed-fields'); @@ -19,3 +20,4 @@ export const DiscordMessage = /*@__PURE__*/ createReactComponent('discord-messages'); export const DiscordReaction = /*@__PURE__*/ createReactComponent('discord-reaction'); export const DiscordReactions = /*@__PURE__*/ createReactComponent('discord-reactions'); +export const DiscordReply = /*@__PURE__*/ createReactComponent('discord-reply');