diff --git a/package.json b/package.json index cf36254..3084e46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@widgetbot/message-renderer", - "version": "v2.3.1", + "version": "v2.2.0", "description": "", "module": "dist/message-renderer.mjs", "files": [ diff --git a/src/Message/style/message.ts b/src/Message/style/message.ts index 53ca466..56fbc3b 100644 --- a/src/Message/style/message.ts +++ b/src/Message/style/message.ts @@ -3,8 +3,8 @@ import { styled, theme, } from "../../Stitches/stitches.config"; -import { Link } from "../../markdown/render/elements"; import SvgFromUrl from "../../SvgFromUrl"; +import { Link } from "../../markdown/render/elements"; export const SmallTimestamp = styled.withConfig({ displayName: "small-timestamp", @@ -96,6 +96,24 @@ export const MessageHeaderBase = styled.withConfig({ flexWrap: "wrap", }); +export const MessageEditor = styled.withConfig({ + displayName: "message-editor", + componentId: commonComponentId, +})("input", { + paddingTop: theme.space.xl, + paddingBottom: theme.space.xl, + paddingLeft: theme.space.xxl, + paddingRight: theme.space.large, + backgroundColor: theme.colors.primaryOpacity10, + outline: "none", + borderRadius: 8, + border: "none", + color: theme.colors.primaryOpacity80, + fontWeight: 400, + lineHeight: "1.375rem", + width: "100%", +}); + export const AutomodHeaderText = styled.withConfig({ displayName: "automod-header-text", componentId: commonComponentId, diff --git a/src/Message/variants/EditMessageInput.tsx b/src/Message/variants/EditMessageInput.tsx new file mode 100644 index 0000000..91beb94 --- /dev/null +++ b/src/Message/variants/EditMessageInput.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { useConfig } from "../../core/ConfigContext"; +import type { ChatMessage } from "../../types"; + +import * as Styles from "../style/message"; + +interface EditMessageInputProps { + message: ChatMessage; +} + +function EditMessageInput(props: EditMessageInputProps) { + const { messageEditOnSubmit } = useConfig(); + + function submitMessageCallback(content: string) { + if (!messageEditOnSubmit || !content) return; + + messageEditOnSubmit({ + ...props.message, + content: content, + edited_timestamp: new Date().getMilliseconds().toString(), + }); + } + + function onKeyDown(e: React.KeyboardEvent) { + const target = e.target as HTMLInputElement; + + if (e.key === "Enter") { + submitMessageCallback(target.value); + } + } + + return ( + + ); +} + +export default EditMessageInput; diff --git a/src/Message/variants/NormalMessage.tsx b/src/Message/variants/NormalMessage.tsx index d169d2a..a4b5ac2 100644 --- a/src/Message/variants/NormalMessage.tsx +++ b/src/Message/variants/NormalMessage.tsx @@ -1,13 +1,3 @@ -import React, { memo, useMemo } from "react"; -import MessageAuthor from "../MessageAuthor"; -import Content from "../../Content"; -import Moment from "moment"; -import Tooltip from "../../Tooltip"; -import type { GetAvatarOptions } from "../../utils/getAvatar"; -import getAvatar from "../../utils/getAvatar"; -import LargeTimestamp from "../LargeTimestamp"; -import ChatTag from "../../ChatTag"; -import * as Styles from "../style/message"; import type { APIMessageInteraction, APIRole, @@ -15,9 +5,19 @@ import type { Snowflake, } from "discord-api-types/v10"; import { MessageType } from "discord-api-types/v10"; +import Moment from "moment"; +import React, { memo, useMemo } from "react"; +import ChatTag from "../../ChatTag"; +import Content from "../../Content"; +import Tooltip from "../../Tooltip"; import { useConfig } from "../../core/ConfigContext"; -import getDisplayName from "../../utils/getDisplayName"; import type { ChatMessage } from "../../types"; +import type { GetAvatarOptions } from "../../utils/getAvatar"; +import getAvatar from "../../utils/getAvatar"; +import getDisplayName from "../../utils/getDisplayName"; +import LargeTimestamp from "../LargeTimestamp"; +import MessageAuthor from "../MessageAuthor"; +import * as Styles from "../style/message"; interface ReplyInfoProps { channelId: Snowflake; @@ -163,8 +163,6 @@ const ReplyInfo = memo((props: ReplyInfoProps) => { ReplyInfo.displayName = "ReplyInfo"; -// type Message = Omit & Partial; - interface MessageProps { isFirstMessage?: boolean; message: ChatMessage; @@ -181,7 +179,14 @@ function NormalMessage(props: MessageProps) { const shouldShowReply = props.message.type === MessageType.Reply || Boolean(props.message.interaction); - const { currentUser, resolveChannel } = useConfig(); + + const { + currentUser, + resolveChannel, + editingMessageId, + EditMessageComponent, + } = useConfig(); + const channel = resolveChannel(props.message.channel_id); const guildId = channel !== null && "guild_id" in channel ? channel.guild_id : null; @@ -220,6 +225,7 @@ function NormalMessage(props: MessageProps) { isContextMenuInteraction={props.isContextMenuInteraction} /> )} + )} - + {editingMessageId === props.message.id && EditMessageComponent ? ( + + ) : ( + + )} ); diff --git a/src/core/ConfigContext.ts b/src/core/ConfigContext.ts index 076f1de..2436444 100644 --- a/src/core/ConfigContext.ts +++ b/src/core/ConfigContext.ts @@ -1,5 +1,3 @@ -import type { ReactElement } from "react"; -import { createContext, useContext } from "react"; import type { APIChannel, APIEmbedImage, @@ -9,11 +7,13 @@ import type { APIUser, Snowflake, } from "discord-api-types/v10"; -import type { SvgConfig } from "./svgs"; -import type { Tag } from "../ChatTag/style"; import type { APIAttachment } from "discord-api-types/v10"; -import type { UserAvatar } from "../utils/getAvatar"; +import type { ReactElement } from "react"; +import { createContext, useContext } from "react"; +import type { Tag } from "../ChatTag/style"; import type { ChatMessage } from "../types"; +import type { UserAvatar } from "../utils/getAvatar"; +import type { SvgConfig } from "./svgs"; export type PartialSvgConfig = Partial; @@ -29,9 +29,9 @@ export interface ChatBadgeProps { } export enum MessageTypeResponse { - InAppError, - ConsoleError, - None, + InAppError = 0, + ConsoleError = 1, + None = 2, } export type Config = { @@ -50,6 +50,7 @@ export type Config = { avatarUrlOverride?(user: APIUser): UserAvatar | null; themeOverrideClassName?: string; unknownMessageTypeResponse?: MessageTypeResponse; + editingMessageId?: string; // Click handlers currentUser(): APIUser | null; @@ -62,6 +63,8 @@ export type Config = { attachmentImageOnClick?(image: APIAttachment): void; embedImageOnClick?(image: APIEmbedImage): void; externalLinkOpenRequested?(url: string): void; + messageEditOnSubmit?(message: ChatMessage): void; + EditMessageComponent?: (props: { message: ChatMessage }) => JSX.Element; }; export const ConfigContext = createContext>({ diff --git a/src/stories/Normal.stories.tsx b/src/stories/Normal.stories.tsx index a39a6c5..42b9fe6 100644 --- a/src/stories/Normal.stories.tsx +++ b/src/stories/Normal.stories.tsx @@ -1006,6 +1006,62 @@ VideoAttachment.args = { ], }; +export const Editing: StoryFn = Template.bind({}); +Editing.args = { + showButtons: true, + messages: [ + { + id: "1101275906716213331", + type: 0, + content: + "Small update: We needed to roll this back ~~for 24 hours~~ to patch some security issues. It'll be back real soon. Update: we don't want to re-roll it out on a friday afternoon, so thisll be back next week.", + channel_id: "697138785317814292", + author: { + id: "132819036282159104", + username: "mrquine", + global_name: "Mr. Quine", + avatar: "3a30ffeeeb354950804d77ded94162d3", + discriminator: "0001", + public_flags: 4457220, + }, + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-04-27T22:37:16.878000+00:00", + edited_timestamp: "2023-04-28T21:00:43.827000+00:00", + flags: 0, + components: [], + message_reference: { + channel_id: "697138785317814292", + guild_id: "613425648685547541", + message_id: "1101188115344920607", + }, + reactions: [ + { + emoji: { + id: null, + name: "👍", + }, + count: 234, + me: false, + }, + { + emoji: { + id: "1085363933579329656", + name: "App_Broom", + }, + count: 185, + me: false, + }, + ], + }, + ], +}; + export const Reply: StoryFn = Template.bind({}); Reply.args = { messages: [ diff --git a/src/stories/Wrapper.tsx b/src/stories/Wrapper.tsx index f7fce22..4bd23df 100644 --- a/src/stories/Wrapper.tsx +++ b/src/stories/Wrapper.tsx @@ -1,42 +1,42 @@ -import { MessageRendererProvider } from "../index"; import React from "react"; +import { MessageRendererProvider } from "../index"; -// SVGs -import SvgFileAudio from "../assets/storybookOnlyAssets/file-audio.svg"; -import SvgSketch from "../assets/storybookOnlyAssets/file-sketch.svg"; -import SvgFileArchive from "../assets/storybookOnlyAssets/file-archive.svg"; -import SvgFileUnknown from "../assets/storybookOnlyAssets/file-unknown.svg"; +import SvgAcrobat from "../assets/storybookOnlyAssets/file-acrobat.svg"; import SvgAe from "../assets/storybookOnlyAssets/file-ae.svg"; import SvgAi from "../assets/storybookOnlyAssets/file-ai.svg"; -import SvgAcrobat from "../assets/storybookOnlyAssets/file-acrobat.svg"; +import SvgFileArchive from "../assets/storybookOnlyAssets/file-archive.svg"; +// SVGs +import SvgFileAudio from "../assets/storybookOnlyAssets/file-audio.svg"; import SvgCode from "../assets/storybookOnlyAssets/file-code.svg"; import SvgDocument from "../assets/storybookOnlyAssets/file-document.svg"; +import SvgSketch from "../assets/storybookOnlyAssets/file-sketch.svg"; import SvgSpreadsheet from "../assets/storybookOnlyAssets/file-spreadsheet.svg"; +import SvgFileUnknown from "../assets/storybookOnlyAssets/file-unknown.svg"; import SvgWebCode from "../assets/storybookOnlyAssets/file-webcode.svg"; import SvgIconAdd from "../assets/storybookOnlyAssets/icon-add.svg"; -import SvgIconRemove from "../assets/storybookOnlyAssets/icon-remove.svg"; -import SvgWarning from "../assets/storybookOnlyAssets/icon-warning.svg"; -import SvgIconDownload from "../assets/storybookOnlyAssets/icon-download.svg"; -import SvgIconCheckmark from "../assets/storybookOnlyAssets/icon-checkmark.svg"; -import SvgIconCross from "../assets/storybookOnlyAssets/icon-cross.svg"; -import SvgIconPin from "../assets/storybookOnlyAssets/icon-pin.svg"; -import SvgIconPencil from "../assets/storybookOnlyAssets/icon-pencil.svg"; +import SvgIconAttachment from "../assets/storybookOnlyAssets/icon-attachment.svg"; import SvgIconBoost from "../assets/storybookOnlyAssets/icon-boost.svg"; -import SvgIconThreadCreated from "../assets/storybookOnlyAssets/icon-thread-created.svg"; -import SvgIconId from "../assets/storybookOnlyAssets/icon-id.svg"; -import SvgIconSticker from "../assets/storybookOnlyAssets/icon-sticker.svg"; +import SvgIconCheckmark from "../assets/storybookOnlyAssets/icon-checkmark.svg"; import SvgIconCommand from "../assets/storybookOnlyAssets/icon-command.svg"; -import SvgIconAttachment from "../assets/storybookOnlyAssets/icon-attachment.svg"; +import SvgIconCross from "../assets/storybookOnlyAssets/icon-cross.svg"; import SvgIconDanger from "../assets/storybookOnlyAssets/icon-danger.svg"; -import SvgIconPause from "../assets/storybookOnlyAssets/icon-pause.svg"; +import SvgIconDownload from "../assets/storybookOnlyAssets/icon-download.svg"; import SvgIconFullscreen from "../assets/storybookOnlyAssets/icon-fullscreen.svg"; +import SvgIconId from "../assets/storybookOnlyAssets/icon-id.svg"; +import SvgIconLinkExternal from "../assets/storybookOnlyAssets/icon-link-external.svg"; +import SvgIconPause from "../assets/storybookOnlyAssets/icon-pause.svg"; +import SvgIconPencil from "../assets/storybookOnlyAssets/icon-pencil.svg"; +import SvgIconPin from "../assets/storybookOnlyAssets/icon-pin.svg"; import SvgIconPlay from "../assets/storybookOnlyAssets/icon-play.svg"; -import SvgIconTextChannel from "../assets/storybookOnlyAssets/icon-text-channel.svg"; -import SvgIconVoiceChannel from "../assets/storybookOnlyAssets/icon-voice-channel.svg"; +import SvgIconRemove from "../assets/storybookOnlyAssets/icon-remove.svg"; import SvgIconStageChannel from "../assets/storybookOnlyAssets/icon-stage-channel.svg"; -import SvgIconLinkExternal from "../assets/storybookOnlyAssets/icon-link-external.svg"; +import SvgIconSticker from "../assets/storybookOnlyAssets/icon-sticker.svg"; +import SvgIconTextChannel from "../assets/storybookOnlyAssets/icon-text-channel.svg"; +import SvgIconThreadCreated from "../assets/storybookOnlyAssets/icon-thread-created.svg"; import SvgIconUnknownReply from "../assets/storybookOnlyAssets/icon-unknown-reply.svg"; +import SvgIconVoiceChannel from "../assets/storybookOnlyAssets/icon-voice-channel.svg"; +import SvgWarning from "../assets/storybookOnlyAssets/icon-warning.svg"; import ggSansNormal400 from "../assets/storybookOnlyAssets/gg-sans-normal-400.woff2"; import ggSansNormal500 from "../assets/storybookOnlyAssets/gg-sans-normal-500.woff2"; @@ -50,10 +50,10 @@ import ggSansItalic600 from "../assets/storybookOnlyAssets/gg-sans-italic-600.wo import ggSansItalic700 from "../assets/storybookOnlyAssets/gg-sans-italic-700.woff2"; import ggSansItalic800 from "../assets/storybookOnlyAssets/gg-sans-italic-800.woff2"; -import automodAvatarStill from "../assets/storybookOnlyAssets/automod-avatar.png"; import automodAvatarAnimated from "../assets/storybookOnlyAssets/automod-avatar.gif"; +import automodAvatarStill from "../assets/storybookOnlyAssets/automod-avatar.png"; -import SvgMiscDiscordImageFailure from "../assets/storybookOnlyAssets/misc-discord-image-failure.svg"; +import type { Decorator } from "@storybook/react"; import type { APIChannel, APIGuild, @@ -63,16 +63,17 @@ import type { Snowflake, } from "discord-api-types/v10"; import { ChannelType, GuildNSFWLevel } from "discord-api-types/v10"; +import EditMessageInput from "../Message/variants/EditMessageInput"; import { globalCss, prefix, styled, theme } from "../Stitches/stitches.config"; -import getDisplayName from "../utils/getDisplayName"; +import SvgMiscDiscordImageFailure from "../assets/storybookOnlyAssets/misc-discord-image-failure.svg"; import type { ChatBadgeProps, MessageButtonListOption, } from "../core/ConfigContext"; import { MessageTypeResponse } from "../core/ConfigContext"; -import { testTextChannel, testVoiceChannel } from "./commonTestData"; -import type { Decorator } from "@storybook/react"; import type { ChatMessage } from "../types"; +import getDisplayName from "../utils/getDisplayName"; +import { testTextChannel, testVoiceChannel } from "./commonTestData"; const svgUrls = { FileAudio: SvgFileAudio, @@ -382,6 +383,11 @@ const Wrapper: Decorator = (Story) => { still: automodAvatarStill, animated: automodAvatarAnimated, }} + EditMessageComponent={EditMessageInput} + messageEditOnSubmit={(message) => { + alert(`Edited message: ${message.id}`); + }} + editingMessageId="1101275906716213331" messageButtons={getButtons} resolveRole={resolveRole} resolveChannel={resolveChannel}