Skip to content

[FR-287] Message Editing Support #61

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@widgetbot/message-renderer",
"version": "v2.3.1",
"version": "v2.2.0",
"description": "",
"module": "dist/message-renderer.mjs",
"files": [
Expand Down
20 changes: 19 additions & 1 deletion src/Message/style/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down
41 changes: 41 additions & 0 deletions src/Message/variants/EditMessageInput.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) {
const target = e.target as HTMLInputElement;

if (e.key === "Enter") {
submitMessageCallback(target.value);
}
}

return (
<Styles.MessageEditor
autoCorrect="false"
onKeyDown={onKeyDown}
defaultValue={props.message.content}
/>
);
}

export default EditMessageInput;
46 changes: 28 additions & 18 deletions src/Message/variants/NormalMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
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,
APIUser,
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;
Expand Down Expand Up @@ -163,8 +163,6 @@ const ReplyInfo = memo((props: ReplyInfoProps) => {

ReplyInfo.displayName = "ReplyInfo";

// type Message = Omit<MessageData, "referencedMessage"> & Partial<MessageData>;

interface MessageProps {
isFirstMessage?: boolean;
message: ChatMessage;
Expand All @@ -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;
Expand Down Expand Up @@ -220,6 +225,7 @@ function NormalMessage(props: MessageProps) {
isContextMenuInteraction={props.isContextMenuInteraction}
/>
)}

<Styles.MessageHeaderBase>
<MessageAuthor
guildId={guildId}
Expand All @@ -231,10 +237,14 @@ function NormalMessage(props: MessageProps) {
<LargeTimestamp timestamp={props.message.timestamp} />
)}
</Styles.MessageHeaderBase>
<Content
message={props.message}
noThreadButton={props.noThreadButton}
/>
{editingMessageId === props.message.id && EditMessageComponent ? (
<EditMessageComponent message={props.message} />
) : (
<Content
message={props.message}
noThreadButton={props.noThreadButton}
/>
)}
</Styles.Message>
);

Expand Down
19 changes: 11 additions & 8 deletions src/core/ConfigContext.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { ReactElement } from "react";
import { createContext, useContext } from "react";
import type {
APIChannel,
APIEmbedImage,
Expand All @@ -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<SvgConfig>;

Expand All @@ -29,9 +29,9 @@ export interface ChatBadgeProps {
}

export enum MessageTypeResponse {
InAppError,
ConsoleError,
None,
InAppError = 0,
ConsoleError = 1,
None = 2,
}

export type Config<SvgConfig extends PartialSvgConfig> = {
Expand All @@ -50,6 +50,7 @@ export type Config<SvgConfig extends PartialSvgConfig> = {
avatarUrlOverride?(user: APIUser): UserAvatar | null;
themeOverrideClassName?: string;
unknownMessageTypeResponse?: MessageTypeResponse;
editingMessageId?: string;

// Click handlers
currentUser(): APIUser | null;
Expand All @@ -62,6 +63,8 @@ export type Config<SvgConfig extends PartialSvgConfig> = {
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<Config<PartialSvgConfig>>({
Expand Down
56 changes: 56 additions & 0 deletions src/stories/Normal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,62 @@ VideoAttachment.args = {
],
};

export const Editing: StoryFn<typeof MessageGroup> = 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<typeof MessageGroup> = Template.bind({});
Reply.args = {
messages: [
Expand Down
60 changes: 33 additions & 27 deletions src/stories/Wrapper.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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}
Expand Down