diff --git a/.yarn/cache/data-uri-to-buffer-npm-3.0.1-830646f9ee-c59c300968.zip b/.yarn/cache/data-uri-to-buffer-npm-3.0.1-830646f9ee-c59c300968.zip new file mode 100644 index 00000000..7ce295f5 Binary files /dev/null and b/.yarn/cache/data-uri-to-buffer-npm-3.0.1-830646f9ee-c59c300968.zip differ diff --git a/.yarn/cache/fetch-blob-npm-3.1.2-6076d01b9c-3e3717cf30.zip b/.yarn/cache/fetch-blob-npm-3.1.2-6076d01b9c-3e3717cf30.zip new file mode 100644 index 00000000..d3f1588d Binary files /dev/null and b/.yarn/cache/fetch-blob-npm-3.1.2-6076d01b9c-3e3717cf30.zip differ diff --git a/.yarn/cache/html-entities-npm-2.3.2-366c4c257a-522d8d202d.zip b/.yarn/cache/html-entities-npm-2.3.2-366c4c257a-522d8d202d.zip new file mode 100644 index 00000000..61cf8ebc Binary files /dev/null and b/.yarn/cache/html-entities-npm-2.3.2-366c4c257a-522d8d202d.zip differ diff --git a/.yarn/cache/node-fetch-npm-3.0.0-6aa31e95cd-50224bf682.zip b/.yarn/cache/node-fetch-npm-3.0.0-6aa31e95cd-50224bf682.zip new file mode 100644 index 00000000..750acd3f Binary files /dev/null and b/.yarn/cache/node-fetch-npm-3.0.0-6aa31e95cd-50224bf682.zip differ diff --git a/.yarn/cache/web-streams-polyfill-npm-3.1.1-ba7b0e5b2d-dac85f0a99.zip b/.yarn/cache/web-streams-polyfill-npm-3.1.1-ba7b0e5b2d-dac85f0a99.zip new file mode 100644 index 00000000..895bfcfc Binary files /dev/null and b/.yarn/cache/web-streams-polyfill-npm-3.1.1-ba7b0e5b2d-dac85f0a99.zip differ diff --git a/packages/preview-proxy/package.json b/packages/preview-proxy/package.json new file mode 100644 index 00000000..852a0a8e --- /dev/null +++ b/packages/preview-proxy/package.json @@ -0,0 +1,16 @@ +{ + "name": "@dappconnect/preview-proxy", + "version": "0.1.0", + "main": "index.js", + "license": "MIT", + "type": "module", + "dependencies": { + "node-fetch": "^3.0.0" + }, + "scripts": { + "start": "yarn node src/index.js", + "fix": "", + "build": "", + "test": "" + } +} diff --git a/packages/preview-proxy/src/index.js b/packages/preview-proxy/src/index.js new file mode 100644 index 00000000..94496f59 --- /dev/null +++ b/packages/preview-proxy/src/index.js @@ -0,0 +1,59 @@ +import fetch from 'node-fetch' +import https from 'https' +import fs from 'fs' + +const regEx = new RegExp(/meta +(property|content)="(.+?)" +(property|content)="(.+?)"/g); + +async function listener(req, res){ + const origin = req?.headers?.origin + res.statusCode = 200 + res.setHeader('Content-Type', 'application/json') + if (origin === 'https://0.0.0.0:8080' || origin === 'https://localhost:8080' || origin === 'https://127.0.0.1:8080') { + res.setHeader('Access-Control-Allow-Origin', origin); + } + res.setHeader('Access-Control-Allow-Methods', 'POST'); + res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); + const requestBody = await new Promise((resolve) => { + if (req.method == 'POST') { + let body = ''; + req.on('data', function (data) { + body += data; + if (body.length > 1e6) + req.connection.destroy(); + }); + req.on('end', function () { + try { + resolve(JSON.parse(body)) + } catch { + resolve({}) + } + }); + } else { + resolve({}) + } + }) + const obj = {} + if ('site' in requestBody) { + try { + const response = await fetch(requestBody['site']) + const body = await response.text() + for (const match of body.matchAll(regEx)) { + if (match[1] === 'property') { + obj[match[2]] = match[4] + } else { + obj[match[4]] = match[2] + } + } + } catch { + } + } + res.end(JSON.stringify(obj)); +} + +const options = { + key: fs.readFileSync('../../../cert/CA/localhost/localhost.decrypted.key'), + cert: fs.readFileSync('../../../cert/CA/localhost/localhost.crt') +} + +const server = https.createServer(options, listener); +server.listen(3000, () => console.log('server running at port 3000')); \ No newline at end of file diff --git a/packages/react-chat-example/src/index.tsx b/packages/react-chat-example/src/index.tsx index c8e6f8d4..455f24e3 100644 --- a/packages/react-chat-example/src/index.tsx +++ b/packages/react-chat-example/src/index.tsx @@ -2,9 +2,32 @@ import { community, lightTheme, ReactChat } from "@dappconnect/react-chat"; import React from "react"; import ReactDOM from "react-dom"; +const fetchMetadata = async (link: string) => { + const response = await fetch("https://localhost:3000", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ site: link }), + }); + const body = await response.text(); + const parsedBody = JSON.parse(body); + if ( + "og:image" in parsedBody && + "og:site_name" in parsedBody && + "og:title" in parsedBody + ) { + return JSON.parse(body); + } +}; + ReactDOM.render(
- +
, document.getElementById("root") ); diff --git a/packages/react-chat/package.json b/packages/react-chat/package.json index cf87c959..2ae7f317 100644 --- a/packages/react-chat/package.json +++ b/packages/react-chat/package.json @@ -45,6 +45,7 @@ }, "dependencies": { "emoji-mart": "^3.0.1", + "html-entities": "^2.3.2", "react": "^17.0.2", "react-dom": "^17.0.2", "react-is": "^17.0.2", diff --git a/packages/react-chat/src/components/Chat.tsx b/packages/react-chat/src/components/Chat.tsx index 80fccf3c..a79727d7 100644 --- a/packages/react-chat/src/components/Chat.tsx +++ b/packages/react-chat/src/components/Chat.tsx @@ -5,6 +5,7 @@ import { useNarrow } from "../contexts/narrowProvider"; import { ChannelData, channels } from "../helpers/channelsMock"; import { CommunityData } from "../helpers/communityMock"; import { useMessenger } from "../hooks/useMessenger"; +import { Metadata } from "../models/Metadata"; import { Theme } from "../styles/themes"; import { Channels } from "./Channels"; @@ -14,9 +15,10 @@ import { Members } from "./Members"; interface ChatProps { theme: Theme; community: CommunityData; + fetchMetadata?: (url: string) => Promise; } -export function Chat({ theme, community }: ChatProps) { +export function Chat({ theme, community, fetchMetadata }: ChatProps) { const [activeChannel, setActiveChannel] = useState(channels[0]); const [showMembers, setShowMembers] = useState(true); const [showChannels, setShowChannels] = useState(true); @@ -62,6 +64,7 @@ export function Chat({ theme, community }: ChatProps) { showCommunity={!showChannels} loadNextDay={() => loadNextDay(activeChannel.name)} lastMessage={lastMessage} + fetchMetadata={fetchMetadata} /> ) : ( Connecting to waku diff --git a/packages/react-chat/src/components/Chat/ChatBody.tsx b/packages/react-chat/src/components/Chat/ChatBody.tsx index b612d169..49ec2fbc 100644 --- a/packages/react-chat/src/components/Chat/ChatBody.tsx +++ b/packages/react-chat/src/components/Chat/ChatBody.tsx @@ -5,6 +5,7 @@ import { useNarrow } from "../../contexts/narrowProvider"; import { ChannelData } from "../../helpers/channelsMock"; import { CommunityData } from "../../helpers/communityMock"; import { ChatMessage } from "../../models/ChatMessage"; +import { Metadata } from "../../models/Metadata"; import { Theme } from "../../styles/themes"; import { Channel } from "../Channels"; import { Community } from "../Community"; @@ -30,6 +31,7 @@ interface ChatBodyProps { activeChannelId: number; loadNextDay: () => void; lastMessage: Date; + fetchMetadata?: (url: string) => Promise; } export function ChatBody({ @@ -46,6 +48,7 @@ export function ChatBody({ activeChannelId, loadNextDay, lastMessage, + fetchMetadata, }: ChatBodyProps) { const narrow = useNarrow(); const [showChannelsList, setShowChannelsList] = useState(false); @@ -103,7 +106,11 @@ export function ChatBody({ Last message date {lastMessage.toDateString()} {" "} {messages.length > 0 ? ( - + ) : ( )} diff --git a/packages/react-chat/src/components/Chat/ChatMessageContent.tsx b/packages/react-chat/src/components/Chat/ChatMessageContent.tsx index d4034a05..cc04ef89 100644 --- a/packages/react-chat/src/components/Chat/ChatMessageContent.tsx +++ b/packages/react-chat/src/components/Chat/ChatMessageContent.tsx @@ -1,6 +1,8 @@ +import { decode } from "html-entities"; import React, { useEffect, useState } from "react"; import styled from "styled-components"; +import { Metadata } from "../../models/Metadata"; import { Theme } from "../../styles/themes"; /* eslint-disable no-useless-escape */ @@ -11,15 +13,19 @@ const regEx = type ChatMessageContentProps = { content: string; theme: Theme; + fetchMetadata?: (url: string) => Promise; }; export function ChatMessageContent({ content, theme, + fetchMetadata, }: ChatMessageContentProps) { const [elements, setElements] = useState<(string | React.ReactElement)[]>([ content, ]); + const [link, setLink] = useState(undefined); + const [openGraph, setOpenGraph] = useState(undefined); useEffect(() => { const split = content.split(regEx); @@ -44,13 +50,101 @@ export function ChatMessageContent({ split[idx + 1] ); }); + const match = matches[0]; + const link = + match.startsWith("http://") || match.startsWith("https://") + ? match + : "https://" + match; + setLink(link); setElements(newSplit); } }, [content]); - return <>{elements.map((el) => el)}; + useEffect(() => { + const updatePreview = async () => { + if (link && fetchMetadata) { + try { + const metadata = await fetchMetadata(link); + if (metadata) { + setOpenGraph(metadata); + } + } catch { + return; + } + } + }; + updatePreview(); + }, [link]); + if (openGraph) { + return ( + +
{elements.map((el) => el)}
+ window?.open(link, "_blank", "noopener")?.focus()} + > + + {openGraph["og:title"]} + + {openGraph["og:site_name"]} + + +
+ ); + } else { + return <>{elements.map((el) => el)}; + } } +const PreviewSiteNameWrapper = styled.div` + font-family: Inter; + font-style: normal; + font-weight: normal; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.1px; + margin-top: 2px; + color: #939ba1; + margin-left: 12px; +`; + +const PreviewTitleWrapper = styled.div` + margin-top: 7px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + font-family: Inter; + font-style: normal; + font-weight: 500; + font-size: 13px; + width: 290px; + line-height: 18px; + margin-left: 12px; +`; + +const PreviewImage = styled.img` + border-radius: 15px 15px 15px 4px; + width: 305px; + height: 170px; +`; + +const PreviewWrapper = styled.div` + margin-top: 9px; + background: #ffffff; + width: 305px; + height: 224px; + border: 1px solid #eef2f5; + box-sizing: border-box; + border-radius: 16px 16px 16px 4px; + display: flex; + flex-direction: column; + padding: 0px; +`; + +const ContentWrapper = styled.div` + display: flex; + flex-direction: column; +`; + const Link = styled.a` text-decoration: underline; color: ${({ theme }) => theme.memberNameColor}; diff --git a/packages/react-chat/src/components/Chat/ChatMessages.tsx b/packages/react-chat/src/components/Chat/ChatMessages.tsx index f4303fec..65e95558 100644 --- a/packages/react-chat/src/components/Chat/ChatMessages.tsx +++ b/packages/react-chat/src/components/Chat/ChatMessages.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import styled from "styled-components"; import { ChatMessage } from "../../models/ChatMessage"; +import { Metadata } from "../../models/Metadata"; import { Theme } from "../../styles/themes"; import { UserIcon } from "../Icons/UserIcon"; import { textSmallStyles } from "../Text"; @@ -11,9 +12,14 @@ import { ChatMessageContent } from "./ChatMessageContent"; type ChatMessagesProps = { messages: ChatMessage[]; theme: Theme; + fetchMetadata?: (url: string) => Promise; }; -export function ChatMessages({ messages, theme }: ChatMessagesProps) { +export function ChatMessages({ + messages, + theme, + fetchMetadata, +}: ChatMessagesProps) { const [scrollOnBot, setScrollOnBot] = useState(false); const ref = useRef(null); const today = useMemo(() => new Date().getDay(), []); @@ -71,7 +77,11 @@ export function ChatMessages({ messages, theme }: ChatMessagesProps) { - + diff --git a/packages/react-chat/src/components/ReactChat.tsx b/packages/react-chat/src/components/ReactChat.tsx index ff911b8a..14aa83f0 100644 --- a/packages/react-chat/src/components/ReactChat.tsx +++ b/packages/react-chat/src/components/ReactChat.tsx @@ -2,6 +2,7 @@ import React, { useRef } from "react"; import { NarrowProvider } from "../contexts/narrowProvider"; import { CommunityData } from "../helpers/communityMock"; +import { Metadata } from "../models/Metadata"; import { GlobalStyle } from "../styles/GlobalStyle"; import { Theme } from "../styles/themes"; @@ -10,15 +11,20 @@ import { Chat } from "./Chat"; interface ReactChatProps { theme: Theme; community: CommunityData; + fetchMetadata?: (url: string) => Promise; } -export function ReactChat({ theme, community }: ReactChatProps) { +export function ReactChat({ theme, community, fetchMetadata }: ReactChatProps) { const ref = useRef(null); return (
- +
); diff --git a/packages/react-chat/src/models/Metadata.ts b/packages/react-chat/src/models/Metadata.ts new file mode 100644 index 00000000..a24aa18b --- /dev/null +++ b/packages/react-chat/src/models/Metadata.ts @@ -0,0 +1,5 @@ +export interface Metadata { + "og:site_name": string; + "og:title": string; + "og:image": string; +} diff --git a/yarn.lock b/yarn.lock index fe96c77e..aeb9342b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -209,6 +209,14 @@ __metadata: languageName: node linkType: hard +"@dappconnect/preview-proxy@workspace:packages/preview-proxy": + version: 0.0.0-use.local + resolution: "@dappconnect/preview-proxy@workspace:packages/preview-proxy" + dependencies: + node-fetch: ^3.0.0 + languageName: unknown + linkType: soft + "@dappconnect/react-chat-example@workspace:packages/react-chat-example": version: 0.0.0-use.local resolution: "@dappconnect/react-chat-example@workspace:packages/react-chat-example" @@ -275,6 +283,7 @@ __metadata: copyfiles: ^2.4.1 emoji-mart: ^3.0.1 eslint: ^7.32.0 + html-entities: ^2.3.2 jsdom: ^16.7.0 jsdom-global: ^3.0.2 mocha: ^9.0.3 @@ -3170,6 +3179,13 @@ __metadata: languageName: node linkType: hard +"data-uri-to-buffer@npm:^3.0.1": + version: 3.0.1 + resolution: "data-uri-to-buffer@npm:3.0.1" + checksum: c59c3009686a78c071806b72f4810856ec28222f0f4e252aa495ec027ed9732298ceea99c50328cf59b151dd34cbc3ad6150bbb43e41fc56fa19f48c99e9fc30 + languageName: node + linkType: hard + "data-urls@npm:^2.0.0": version: 2.0.0 resolution: "data-urls@npm:2.0.0" @@ -4506,6 +4522,15 @@ __metadata: languageName: node linkType: hard +"fetch-blob@npm:^3.1.2": + version: 3.1.2 + resolution: "fetch-blob@npm:3.1.2" + dependencies: + web-streams-polyfill: ^3.0.3 + checksum: 3e3717cf30da9f204aee83dded63f1a9f9c8bda7a0dc59648f89eeb1e88ee592231f4d922e1f119e1390383520768594dd3a1fe5e844f2f2f014d17ce04213a5 + languageName: node + linkType: hard + "file-entry-cache@npm:^6.0.1": version: 6.0.1 resolution: "file-entry-cache@npm:6.0.1" @@ -5295,6 +5320,13 @@ fsevents@~2.3.2: languageName: node linkType: hard +"html-entities@npm:^2.3.2": + version: 2.3.2 + resolution: "html-entities@npm:2.3.2" + checksum: 522d8d202df301ff51b517a379e642023ed5c81ea9fb5674ffad88cff386165733d00b6089d5c2fcc644e44777d6072017b6216d8fa40f271d3610420d00a886 + languageName: node + linkType: hard + "html-minifier-terser@npm:^5.0.1": version: 5.1.1 resolution: "html-minifier-terser@npm:5.1.1" @@ -7872,6 +7904,16 @@ fsevents@~2.3.2: languageName: node linkType: hard +"node-fetch@npm:^3.0.0": + version: 3.0.0 + resolution: "node-fetch@npm:3.0.0" + dependencies: + data-uri-to-buffer: ^3.0.1 + fetch-blob: ^3.1.2 + checksum: 50224bf682a0bc3d44faee0f38df6269d8ae646de343595ef37f9d94b4322d3763a49819fb7b2df9330fcae16e0a20e5fb129dfed8725cf0e8f720277db7611c + languageName: node + linkType: hard + "node-forge@npm:^0.10.0": version: 0.10.0 resolution: "node-forge@npm:0.10.0" @@ -11635,6 +11677,13 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard +"web-streams-polyfill@npm:^3.0.3": + version: 3.1.1 + resolution: "web-streams-polyfill@npm:3.1.1" + checksum: dac85f0a990fb1ddcd15e2eda8ce4696bc9bc567e34cfdaeb9e740e26417d8649a6f466468907f50fd6e09967c25e0cf1f296c30aef9650ab7b118d5f69fb176 + languageName: node + linkType: hard + "webidl-conversions@npm:^5.0.0": version: 5.0.0 resolution: "webidl-conversions@npm:5.0.0"