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"