Skip to content

Commit

Permalink
finish
Browse files Browse the repository at this point in the history
  • Loading branch information
ItzDerock committed Feb 9, 2024
1 parent 6e06241 commit c7a73d3
Show file tree
Hide file tree
Showing 12 changed files with 194 additions and 109 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"dist/**/*.js.map"
],
"devDependencies": {
"@types/node": "^18.19.6",
"@types/node": "^20.11.17",
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^5.62.0",
Expand Down
16 changes: 11 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 32 additions & 31 deletions src/generator/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { ChannelType, type Awaitable, type Channel, type Message, type Role, type User } from 'discord.js';
import ReactDOMServer from 'react-dom/server';
import React from 'react';
import React, { Suspense } from 'react';
import { DiscordHeader, DiscordMessages } from '@derockdev/discord-components-react';
import renderMessage from './renderers/message';
import renderContent, { RenderType } from './renderers/content';
import DiscordMessage from './renderers/message';
import MessageContent, { RenderType } from './renderers/content';
import { buildProfiles } from '../utils/buildProfiles';
import { revealSpoiler, scrollToMessage } from '../static/client';
import { readFileSync } from 'fs';
import path from 'path';
import { renderToString } from '@derockdev/discord-components-core/hydrate';
import { isDefined } from '../utils/utils';
import { streamToString } from '../utils/utils';

// read the package.json file and get the @derockdev/discord-components-core version
let discordComponentsVersion = '^3.6.1';
Expand Down Expand Up @@ -38,22 +38,9 @@ export type RenderMessageContext = {
hydrate: boolean;
};

export default async function renderMessages({ messages, channel, callbacks, ...options }: RenderMessageContext) {
export default async function Messages({ messages, channel, callbacks, ...options }: RenderMessageContext) {
const profiles = buildProfiles(messages);

const chatBody = (
await Promise.all(
messages.map((message) =>
renderMessage(message, {
messages,
channel,
callbacks,
...options,
})
)
)
).filter(isDefined);

const elements = (
<DiscordMessages style={{ minHeight: '100vh' }}>
{/* header */}
Expand All @@ -68,21 +55,30 @@ export default async function renderMessages({ messages, channel, callbacks, ...
}
icon={channel.isDMBased() ? undefined : channel.guild.iconURL({ size: 128 }) ?? undefined}
>
{channel.isThread()
? `Thread channel in ${channel.parent?.name ?? 'Unknown Channel'}`
: channel.isDMBased()
? `Direct Messages`
: channel.isVoiceBased()
? `Voice Text Channel for ${channel.name}`
: channel.type === ChannelType.GuildCategory
? `Category Channel`
: 'topic' in channel && channel.topic
? await renderContent(channel.topic, { messages, channel, callbacks, type: RenderType.REPLY, ...options })
: `This is the start of #${channel.name} channel.`}
{channel.isThread() ? (
`Thread channel in ${channel.parent?.name ?? 'Unknown Channel'}`
) : channel.isDMBased() ? (
`Direct Messages`
) : channel.isVoiceBased() ? (
`Voice Text Channel for ${channel.name}`
) : channel.type === ChannelType.GuildCategory ? (
`Category Channel`
) : 'topic' in channel && channel.topic ? (
<MessageContent
content={channel.topic}
context={{ messages, channel, callbacks, type: RenderType.REPLY, ...options }}
/>
) : (
`This is the start of #${channel.name} channel.`
)}
</DiscordHeader>

{/* body */}
{chatBody}
<Suspense>
{messages.map((message) => (
<DiscordMessage message={message} context={{ messages, channel, callbacks, ...options }} />
))}
</Suspense>

{/* footer */}
<div style={{ textAlign: 'center', width: '100%' }}>
Expand All @@ -104,7 +100,9 @@ export default async function renderMessages({ messages, channel, callbacks, ...
</DiscordMessages>
);

const markup = ReactDOMServer.renderToStaticMarkup(
// NOTE: this renders a STATIC site with no interactivity
// if interactivity is needed, switch to renderToPipeableStream and use hydrateRoot on client.
const stream = ReactDOMServer.renderToStaticNodeStream(
<html>
<head>
<meta charSet="utf-8" />
Expand Down Expand Up @@ -158,11 +156,14 @@ export default async function renderMessages({ messages, channel, callbacks, ...
>
{elements}
</body>

{/* Make sure the script runs after the DOM has loaded */}
{options.hydrate && <script dangerouslySetInnerHTML={{ __html: revealSpoiler }}></script>}
</html>
);

const markup = await streamToString(stream);

if (options.hydrate) {
const result = await renderToString(markup, {
beforeHydrate: async (document) => {
Expand Down
2 changes: 1 addition & 1 deletion src/generator/renderers/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ButtonStyle, ComponentType, type MessageActionRowComponent, type Action
import React from 'react';
import { parseDiscordEmoji } from '../../utils/utils';

export default function ComponentRow(row: ActionRow<MessageActionRowComponent>, id: number) {
export default function ComponentRow({ row, id }: { row: ActionRow<MessageActionRowComponent>; id: number }) {
return (
<DiscordActionRow key={id}>
{row.components.map((component, id) => (
Expand Down
101 changes: 80 additions & 21 deletions src/generator/renderers/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import {
} from '@derockdev/discord-components-react';
import parse, { type RuleTypesExtended } from 'discord-markdown-parser';
import { ChannelType, type APIMessageComponentEmoji } from 'discord.js';
import React, { Fragment, type ReactNode } from 'react';
import { ASTNode } from 'simple-markdown';
import { SingleASTNode } from 'simple-markdown';
import React from 'react';
import type { ASTNode } from 'simple-markdown';
import { ASTNode as MessageASTNodes } from 'simple-markdown';
import type { SingleASTNode } from 'simple-markdown';
import type { RenderMessageContext } from '../';
import { parseDiscordEmoji } from '../../utils/utils';

Expand All @@ -33,7 +34,13 @@ type RenderContentContext = RenderMessageContext & {
};
};

export default async function renderContent(content: string, context: RenderContentContext) {
/**
* Renders discord markdown content
* @param content - The content to render
* @param context - The context to render the content in
* @returns
*/
export default async function MessageContent({ content, context }: { content: string; context: RenderContentContext }) {
if (context.type === RenderType.REPLY && content.length > 180) content = content.slice(0, 180) + '...';

// parse the markdown
Expand All @@ -56,15 +63,31 @@ export default async function renderContent(content: string, context: RenderCont
}
}

return <>{await ASTNode(parsed, context)}</>;
return <MessageASTNodes nodes={parsed} context={context} />;
}

const ASTNode = async (nodes: ASTNode, context: RenderContentContext): Promise<React.ReactNode[]> =>
Array.isArray(nodes)
? await Promise.all(nodes.map((node) => <SingleASTNode node={node} context={context} />))
: [<SingleASTNode node={nodes} context={context} />];
// This function can probably be combined into the MessageSingleASTNode function
async function MessageASTNodes({
nodes,
context,
}: {
nodes: ASTNode;
context: RenderContentContext;
}): Promise<React.JSX.Element> {
if (Array.isArray(nodes)) {
return (
<>
{nodes.map((node) => (
<MessageSingleASTNode node={node} context={context} />
))}
</>
);
} else {
return <MessageSingleASTNode node={nodes} context={context} />;
}
}

export async function SingleASTNode({ node, context }: { node: SingleASTNode; context: RenderContentContext }) {
export async function MessageSingleASTNode({ node, context }: { node: SingleASTNode; context: RenderContentContext }) {
if (!node) return null;

const type = node.type as RuleTypesExtended;
Expand All @@ -74,22 +97,30 @@ export async function SingleASTNode({ node, context }: { node: SingleASTNode; co
return node.content;

case 'link':
return <a href={node.target}>{await ASTNode(node.content, context)}</a>;
return (
<a href={node.target}>
<MessageASTNodes nodes={node.content} context={context} />
</a>
);

case 'url':
case 'autolink':
return (
<a href={node.target} target="_blank" rel="noreferrer">
{...await ASTNode(node.content, context)}
<MessageASTNodes nodes={node.content} context={context} />
</a>
);

case 'blockQuote':
if (context.type === RenderType.REPLY) {
return await ASTNode(node.content, context);
return <MessageASTNodes nodes={node.content} context={context} />;
}

return <DiscordQuote>{...await ASTNode(node.content, context)}</DiscordQuote>;
return (
<DiscordQuote>
<MessageASTNodes nodes={node.content} context={context} />
</DiscordQuote>
);

case 'br':
case 'newline':
Expand Down Expand Up @@ -143,22 +174,46 @@ export async function SingleASTNode({ node, context }: { node: SingleASTNode; co
return <DiscordInlineCode>{node.content}</DiscordInlineCode>;

case 'em':
return <DiscordItalic>{await ASTNode(node.content, context)}</DiscordItalic>;
return (
<DiscordItalic>
<MessageASTNodes nodes={node.content} context={context} />
</DiscordItalic>
);

case 'strong':
return <DiscordBold>{await ASTNode(node.content, context)}</DiscordBold>;
return (
<DiscordBold>
<MessageASTNodes nodes={node.content} context={context} />
</DiscordBold>
);

case 'underline':
return <DiscordUnderlined>{await ASTNode(node.content, context)}</DiscordUnderlined>;
return (
<DiscordUnderlined>
<MessageASTNodes nodes={node.content} context={context} />
</DiscordUnderlined>
);

case 'strikethrough':
return <s>{await ASTNode(node.content, context)}</s>;
return (
<s>
<MessageASTNodes nodes={node.content} context={context} />
</s>
);

case 'emoticon':
return typeof node.content === 'string' ? node.content : await ASTNode(node.content, context);
return typeof node.content === 'string' ? (
node.content
) : (
<MessageASTNodes nodes={node.content} context={context} />
);

case 'spoiler':
return <DiscordSpoiler>{await ASTNode(node.content, context)}</DiscordSpoiler>;
return (
<DiscordSpoiler>
<MessageASTNodes nodes={node.content} context={context} />
</DiscordSpoiler>
);

case 'emoji':
case 'twemoji':
Expand All @@ -176,7 +231,11 @@ export async function SingleASTNode({ node, context }: { node: SingleASTNode; co

default: {
console.log(`Unknown node type: ${type}`, node);
return typeof node.content === 'string' ? node.content : await ASTNode(node.content, context);
return typeof node.content === 'string' ? (
node.content
) : (
<MessageASTNodes nodes={node.content} context={context} />
);
}
}
}
Expand Down
Loading

0 comments on commit c7a73d3

Please sign in to comment.