Skip to content

Commit

Permalink
Merge pull request #407 from cristianrdu/main
Browse files Browse the repository at this point in the history
feature: add twitter extension
  • Loading branch information
andrewdoro authored Jun 17, 2024
2 parents 1c4df17 + a5b7891 commit 746cf43
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 2 deletions.
9 changes: 9 additions & 0 deletions apps/web/components/tailwind/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
TaskList,
TiptapImage,
TiptapLink,
Twitter,
UpdatedImage,
Youtube,
} from "novel/extensions";
Expand Down Expand Up @@ -122,6 +123,13 @@ const youtube = Youtube.configure({
inline: false,
});

const twitter = Twitter.configure({
HTMLAttributes: {
class: cx("not-prose"),
},
inline: false,
});

const characterCount = CharacterCount.configure();

export const defaultExtensions = [
Expand All @@ -136,6 +144,7 @@ export const defaultExtensions = [
aiHighlight,
codeBlockLowlight,
youtube,
twitter,
characterCount,
GlobalDragHandle,
];
26 changes: 26 additions & 0 deletions apps/web/components/tailwind/slash-command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
MessageSquarePlus,
Text,
TextQuote,
Twitter,
Youtube,
} from "lucide-react";
import { createSuggestionItems } from "novel/extensions";
Expand Down Expand Up @@ -153,6 +154,31 @@ export const suggestionItems = createSuggestionItems([
}
},
},
{
title: "Twitter",
description: "Embed a Tweet.",
searchTerms: ["twitter", "embed"],
icon: <Twitter size={18} />,
command: ({ editor, range }) => {
const tweetLink = prompt("Please enter Twitter Link");
const tweetRegex = new RegExp(/^https?:\/\/(www\.)?x\.com\/([a-zA-Z0-9_]{1,15})(\/status\/(\d+))?(\/\S*)?$/);

if (tweetRegex.test(tweetLink)) {
editor
.chain()
.focus()
.deleteRange(range)
.setTweet({
src: tweetLink,
})
.run();
} else {
if (tweetLink !== null) {
alert("Please enter a correct Twitter Link");
}
}
},
},
]);

export const slashCommand = Command.configure({
Expand Down
20 changes: 20 additions & 0 deletions apps/web/lib/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,26 @@ export const defaultEditorContent = {
},
],
},
{
type: "listItem",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: "Add tweets from the command slash menu:",
},
],
},
{
type: "twitter",
attrs: {
src: "https://x.com/elonmusk/status/1800759252224729577",
},
},
],
},
],
},
{
Expand Down
1 change: 1 addition & 0 deletions packages/headless/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"jotai": "^2.6.4",
"react-markdown": "^8.0.7",
"react-moveable": "^0.56.0",
"react-tweet": "^3.2.1",
"tippy.js": "^6.3.7",
"tiptap-extension-global-drag-handle": "^0.1.7",
"tiptap-markdown": "^0.8.9",
Expand Down
4 changes: 2 additions & 2 deletions packages/headless/src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import StarterKit from "@tiptap/starter-kit";
import { Markdown } from "tiptap-markdown";
import CustomKeymap from "./custom-keymap";
import { ImageResizer } from "./image-resizer";
import { Twitter } from "./twitter";
import UpdatedImage from "./updated-image";

import CharacterCount from "@tiptap/extension-character-count";
Expand Down Expand Up @@ -80,8 +81,7 @@ export {
UpdatedImage,
simpleExtensions,
Youtube,
Twitter,
CharacterCount,
GlobalDragHandle,
};


153 changes: 153 additions & 0 deletions packages/headless/src/extensions/twitter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { Node, mergeAttributes, nodePasteRule } from "@tiptap/core";
import { NodeViewWrapper, ReactNodeViewRenderer, type ReactNodeViewRendererOptions } from "@tiptap/react";
import { Tweet } from "react-tweet";
export const TWITTER_REGEX_GLOBAL = /(https?:\/\/)?(www\.)?x\.com\/([a-zA-Z0-9_]{1,15})(\/status\/(\d+))?(\/\S*)?/g;
export const TWITTER_REGEX = /^https?:\/\/(www\.)?x\.com\/([a-zA-Z0-9_]{1,15})(\/status\/(\d+))?(\/\S*)?$/;

export const isValidTwitterUrl = (url: string) => {
return url.match(TWITTER_REGEX);
};

const TweetComponent = ({ node }: { node: Partial<ReactNodeViewRendererOptions> }) => {
const url = node?.attrs?.src;
const tweetId = url?.split("/").pop();

if (!tweetId) {
return null;
}

return (
<NodeViewWrapper>
<div data-twitter="">
<Tweet id={tweetId} />
</div>
</NodeViewWrapper>
);
};

export interface TwitterOptions {
/**
* Controls if the paste handler for tweets should be added.
* @default true
* @example false
*/
addPasteHandler: boolean;

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
HTMLAttributes: Record<string, any>;

/**
* Controls if the twitter node should be inline or not.
* @default false
* @example true
*/
inline: boolean;

/**
* The origin of the tweet.
* @default ''
* @example 'https://tiptap.dev'
*/
origin: string;
}

/**
* The options for setting a tweet.
*/
type SetTweetOptions = { src: string };

declare module "@tiptap/core" {
interface Commands<ReturnType> {
twitter: {
/**
* Insert a tweet
* @param options The tweet attributes
* @example editor.commands.setTweet({ src: 'https://x.com/seanpk/status/1800145949580517852' })
*/
setTweet: (options: SetTweetOptions) => ReturnType;
};
}
}

/**
* This extension adds support for tweets.
*/
export const Twitter = Node.create<TwitterOptions>({
name: "twitter",

addOptions() {
return {
addPasteHandler: true,
HTMLAttributes: {},
inline: false,
origin: "",
};
},

addNodeView() {
return ReactNodeViewRenderer(TweetComponent, { attrs: this.options.HTMLAttributes });
},

inline() {
return this.options.inline;
},

group() {
return this.options.inline ? "inline" : "block";
},

draggable: true,

addAttributes() {
return {
src: {
default: null,
},
};
},

parseHTML() {
return [
{
tag: "div[data-twitter]",
},
];
},

addCommands() {
return {
setTweet:
(options: SetTweetOptions) =>
({ commands }) => {
if (!isValidTwitterUrl(options.src)) {
return false;
}

return commands.insertContent({
type: this.name,
attrs: options,
});
},
};
},

addPasteRules() {
if (!this.options.addPasteHandler) {
return [];
}

return [
nodePasteRule({
find: TWITTER_REGEX_GLOBAL,
type: this.type,
getAttributes: (match) => {
return { src: match.input };
},
}),
];
},

renderHTML({ HTMLAttributes }) {
return ["div", mergeAttributes({ "data-twitter": "" }, HTMLAttributes)];
},
});
35 changes: 35 additions & 0 deletions pnpm-lock.yaml

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

0 comments on commit 746cf43

Please sign in to comment.