diff --git a/apps/x/apps/renderer/package.json b/apps/x/apps/renderer/package.json index 359f1709..a193b3f1 100644 --- a/apps/x/apps/renderer/package.json +++ b/apps/x/apps/renderer/package.json @@ -49,6 +49,7 @@ "radix-ui": "^1.4.3", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-tweet": "^3.2.2", "recharts": "^3.8.0", "remark-breaks": "^4.0.0", "sonner": "^2.0.7", diff --git a/apps/x/apps/renderer/src/extensions/embed-block.tsx b/apps/x/apps/renderer/src/extensions/embed-block.tsx index b3bc6969..27039ecb 100644 --- a/apps/x/apps/renderer/src/extensions/embed-block.tsx +++ b/apps/x/apps/renderer/src/extensions/embed-block.tsx @@ -1,6 +1,7 @@ import { mergeAttributes, Node } from '@tiptap/react' import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react' import { X, ExternalLink } from 'lucide-react' +import { Tweet } from 'react-tweet' import { blocks } from '@x/shared' function getEmbedUrl(provider: string, url: string): string | null { @@ -24,6 +25,28 @@ function getEmbedUrl(provider: string, url: string): string | null { return null } +function extractTweetId(url: string): string | null { + try { + const parsed = new URL(url) + const hostname = parsed.hostname + .toLowerCase() + .replace(/^www\./, '') + .replace(/^mobile\./, '') + if (hostname !== 'twitter.com' && hostname !== 'x.com') return null + + const segments = parsed.pathname.split('/').filter(Boolean) + for (let i = 0; i < segments.length - 1; i += 1) { + if ((segments[i] === 'status' || segments[i] === 'statuses') && /^\d+$/.test(segments[i + 1])) { + return segments[i + 1] + } + } + } catch { + return null + } + + return null +} + function EmbedBlockView({ node, deleteNode }: { node: { attrs: Record }; deleteNode: () => void }) { const raw = node.attrs.data as string let config: blocks.EmbedBlock | null = null @@ -45,6 +68,7 @@ function EmbedBlockView({ node, deleteNode }: { node: { attrs: Record - {embedUrl ? ( + {config.provider === 'tweet' && tweetId ? ( +
event.stopPropagation()} + > + +
+ ) : embedUrl ? (