Add preview links (#29)

This commit is contained in:
Szymon Szlachtowicz 2021-10-07 09:46:51 +02:00 committed by GitHub
parent 6ff3f99f79
commit 3f5e31f794
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 281 additions and 8 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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": ""
}
}

View File

@ -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'));

View File

@ -2,9 +2,32 @@ import { community, lightTheme, ReactChat } from "@dappconnect/react-chat";
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; 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( ReactDOM.render(
<div style={{ height: "100%" }}> <div style={{ height: "100%" }}>
<ReactChat theme={lightTheme} community={community} /> <ReactChat
theme={lightTheme}
community={community}
fetchMetadata={fetchMetadata}
/>
</div>, </div>,
document.getElementById("root") document.getElementById("root")
); );

View File

@ -45,6 +45,7 @@
}, },
"dependencies": { "dependencies": {
"emoji-mart": "^3.0.1", "emoji-mart": "^3.0.1",
"html-entities": "^2.3.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-is": "^17.0.2", "react-is": "^17.0.2",

View File

@ -5,6 +5,7 @@ import { useNarrow } from "../contexts/narrowProvider";
import { ChannelData, channels } from "../helpers/channelsMock"; import { ChannelData, channels } from "../helpers/channelsMock";
import { CommunityData } from "../helpers/communityMock"; import { CommunityData } from "../helpers/communityMock";
import { useMessenger } from "../hooks/useMessenger"; import { useMessenger } from "../hooks/useMessenger";
import { Metadata } from "../models/Metadata";
import { Theme } from "../styles/themes"; import { Theme } from "../styles/themes";
import { Channels } from "./Channels"; import { Channels } from "./Channels";
@ -14,9 +15,10 @@ import { Members } from "./Members";
interface ChatProps { interface ChatProps {
theme: Theme; theme: Theme;
community: CommunityData; community: CommunityData;
fetchMetadata?: (url: string) => Promise<Metadata | undefined>;
} }
export function Chat({ theme, community }: ChatProps) { export function Chat({ theme, community, fetchMetadata }: ChatProps) {
const [activeChannel, setActiveChannel] = useState<ChannelData>(channels[0]); const [activeChannel, setActiveChannel] = useState<ChannelData>(channels[0]);
const [showMembers, setShowMembers] = useState(true); const [showMembers, setShowMembers] = useState(true);
const [showChannels, setShowChannels] = useState(true); const [showChannels, setShowChannels] = useState(true);
@ -62,6 +64,7 @@ export function Chat({ theme, community }: ChatProps) {
showCommunity={!showChannels} showCommunity={!showChannels}
loadNextDay={() => loadNextDay(activeChannel.name)} loadNextDay={() => loadNextDay(activeChannel.name)}
lastMessage={lastMessage} lastMessage={lastMessage}
fetchMetadata={fetchMetadata}
/> />
) : ( ) : (
<Loading>Connecting to waku</Loading> <Loading>Connecting to waku</Loading>

View File

@ -5,6 +5,7 @@ import { useNarrow } from "../../contexts/narrowProvider";
import { ChannelData } from "../../helpers/channelsMock"; import { ChannelData } from "../../helpers/channelsMock";
import { CommunityData } from "../../helpers/communityMock"; import { CommunityData } from "../../helpers/communityMock";
import { ChatMessage } from "../../models/ChatMessage"; import { ChatMessage } from "../../models/ChatMessage";
import { Metadata } from "../../models/Metadata";
import { Theme } from "../../styles/themes"; import { Theme } from "../../styles/themes";
import { Channel } from "../Channels"; import { Channel } from "../Channels";
import { Community } from "../Community"; import { Community } from "../Community";
@ -30,6 +31,7 @@ interface ChatBodyProps {
activeChannelId: number; activeChannelId: number;
loadNextDay: () => void; loadNextDay: () => void;
lastMessage: Date; lastMessage: Date;
fetchMetadata?: (url: string) => Promise<Metadata | undefined>;
} }
export function ChatBody({ export function ChatBody({
@ -46,6 +48,7 @@ export function ChatBody({
activeChannelId, activeChannelId,
loadNextDay, loadNextDay,
lastMessage, lastMessage,
fetchMetadata,
}: ChatBodyProps) { }: ChatBodyProps) {
const narrow = useNarrow(); const narrow = useNarrow();
const [showChannelsList, setShowChannelsList] = useState(false); const [showChannelsList, setShowChannelsList] = useState(false);
@ -103,7 +106,11 @@ export function ChatBody({
Last message date {lastMessage.toDateString()} Last message date {lastMessage.toDateString()}
</button>{" "} </button>{" "}
{messages.length > 0 ? ( {messages.length > 0 ? (
<ChatMessages messages={messages} theme={theme} /> <ChatMessages
messages={messages}
theme={theme}
fetchMetadata={fetchMetadata}
/>
) : ( ) : (
<EmptyChannel theme={theme} channel={channel} /> <EmptyChannel theme={theme} channel={channel} />
)} )}

View File

@ -1,6 +1,8 @@
import { decode } from "html-entities";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { Metadata } from "../../models/Metadata";
import { Theme } from "../../styles/themes"; import { Theme } from "../../styles/themes";
/* eslint-disable no-useless-escape */ /* eslint-disable no-useless-escape */
@ -11,15 +13,19 @@ const regEx =
type ChatMessageContentProps = { type ChatMessageContentProps = {
content: string; content: string;
theme: Theme; theme: Theme;
fetchMetadata?: (url: string) => Promise<Metadata | undefined>;
}; };
export function ChatMessageContent({ export function ChatMessageContent({
content, content,
theme, theme,
fetchMetadata,
}: ChatMessageContentProps) { }: ChatMessageContentProps) {
const [elements, setElements] = useState<(string | React.ReactElement)[]>([ const [elements, setElements] = useState<(string | React.ReactElement)[]>([
content, content,
]); ]);
const [link, setLink] = useState<string | undefined>(undefined);
const [openGraph, setOpenGraph] = useState<Metadata | undefined>(undefined);
useEffect(() => { useEffect(() => {
const split = content.split(regEx); const split = content.split(regEx);
@ -44,13 +50,101 @@ export function ChatMessageContent({
split[idx + 1] split[idx + 1]
); );
}); });
const match = matches[0];
const link =
match.startsWith("http://") || match.startsWith("https://")
? match
: "https://" + match;
setLink(link);
setElements(newSplit); setElements(newSplit);
} }
}, [content]); }, [content]);
useEffect(() => {
const updatePreview = async () => {
if (link && fetchMetadata) {
try {
const metadata = await fetchMetadata(link);
if (metadata) {
setOpenGraph(metadata);
}
} catch {
return;
}
}
};
updatePreview();
}, [link]);
if (openGraph) {
return (
<ContentWrapper>
<div>{elements.map((el) => el)}</div>
<PreviewWrapper
onClick={() => window?.open(link, "_blank", "noopener")?.focus()}
>
<PreviewImage src={decodeURI(decode(openGraph["og:image"]))} />
<PreviewTitleWrapper>{openGraph["og:title"]}</PreviewTitleWrapper>
<PreviewSiteNameWrapper>
{openGraph["og:site_name"]}
</PreviewSiteNameWrapper>
</PreviewWrapper>
</ContentWrapper>
);
} else {
return <>{elements.map((el) => el)}</>; 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` const Link = styled.a`
text-decoration: underline; text-decoration: underline;
color: ${({ theme }) => theme.memberNameColor}; color: ${({ theme }) => theme.memberNameColor};

View File

@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { ChatMessage } from "../../models/ChatMessage"; import { ChatMessage } from "../../models/ChatMessage";
import { Metadata } from "../../models/Metadata";
import { Theme } from "../../styles/themes"; import { Theme } from "../../styles/themes";
import { UserIcon } from "../Icons/UserIcon"; import { UserIcon } from "../Icons/UserIcon";
import { textSmallStyles } from "../Text"; import { textSmallStyles } from "../Text";
@ -11,9 +12,14 @@ import { ChatMessageContent } from "./ChatMessageContent";
type ChatMessagesProps = { type ChatMessagesProps = {
messages: ChatMessage[]; messages: ChatMessage[];
theme: Theme; theme: Theme;
fetchMetadata?: (url: string) => Promise<Metadata | undefined>;
}; };
export function ChatMessages({ messages, theme }: ChatMessagesProps) { export function ChatMessages({
messages,
theme,
fetchMetadata,
}: ChatMessagesProps) {
const [scrollOnBot, setScrollOnBot] = useState(false); const [scrollOnBot, setScrollOnBot] = useState(false);
const ref = useRef<HTMLHeadingElement>(null); const ref = useRef<HTMLHeadingElement>(null);
const today = useMemo(() => new Date().getDay(), []); const today = useMemo(() => new Date().getDay(), []);
@ -71,7 +77,11 @@ export function ChatMessages({ messages, theme }: ChatMessagesProps) {
</TimeWrapper> </TimeWrapper>
</MessageHeaderWrapper> </MessageHeaderWrapper>
<MessageText theme={theme}> <MessageText theme={theme}>
<ChatMessageContent content={message.content} theme={theme} /> <ChatMessageContent
content={message.content}
theme={theme}
fetchMetadata={fetchMetadata}
/>
</MessageText> </MessageText>
</ContentWrapper> </ContentWrapper>
</MessageWrapper> </MessageWrapper>

View File

@ -2,6 +2,7 @@ import React, { useRef } from "react";
import { NarrowProvider } from "../contexts/narrowProvider"; import { NarrowProvider } from "../contexts/narrowProvider";
import { CommunityData } from "../helpers/communityMock"; import { CommunityData } from "../helpers/communityMock";
import { Metadata } from "../models/Metadata";
import { GlobalStyle } from "../styles/GlobalStyle"; import { GlobalStyle } from "../styles/GlobalStyle";
import { Theme } from "../styles/themes"; import { Theme } from "../styles/themes";
@ -10,15 +11,20 @@ import { Chat } from "./Chat";
interface ReactChatProps { interface ReactChatProps {
theme: Theme; theme: Theme;
community: CommunityData; community: CommunityData;
fetchMetadata?: (url: string) => Promise<Metadata | undefined>;
} }
export function ReactChat({ theme, community }: ReactChatProps) { export function ReactChat({ theme, community, fetchMetadata }: ReactChatProps) {
const ref = useRef<HTMLHeadingElement>(null); const ref = useRef<HTMLHeadingElement>(null);
return ( return (
<NarrowProvider myRef={ref}> <NarrowProvider myRef={ref}>
<div ref={ref}> <div ref={ref}>
<GlobalStyle /> <GlobalStyle />
<Chat theme={theme} community={community} /> <Chat
theme={theme}
community={community}
fetchMetadata={fetchMetadata}
/>
</div> </div>
</NarrowProvider> </NarrowProvider>
); );

View File

@ -0,0 +1,5 @@
export interface Metadata {
"og:site_name": string;
"og:title": string;
"og:image": string;
}

View File

@ -209,6 +209,14 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@dappconnect/react-chat-example@workspace:packages/react-chat-example":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@dappconnect/react-chat-example@workspace:packages/react-chat-example" resolution: "@dappconnect/react-chat-example@workspace:packages/react-chat-example"
@ -275,6 +283,7 @@ __metadata:
copyfiles: ^2.4.1 copyfiles: ^2.4.1
emoji-mart: ^3.0.1 emoji-mart: ^3.0.1
eslint: ^7.32.0 eslint: ^7.32.0
html-entities: ^2.3.2
jsdom: ^16.7.0 jsdom: ^16.7.0
jsdom-global: ^3.0.2 jsdom-global: ^3.0.2
mocha: ^9.0.3 mocha: ^9.0.3
@ -3170,6 +3179,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "data-urls@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "data-urls@npm:2.0.0" resolution: "data-urls@npm:2.0.0"
@ -4506,6 +4522,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "file-entry-cache@npm:^6.0.1":
version: 6.0.1 version: 6.0.1
resolution: "file-entry-cache@npm:6.0.1" resolution: "file-entry-cache@npm:6.0.1"
@ -5295,6 +5320,13 @@ fsevents@~2.3.2:
languageName: node languageName: node
linkType: hard 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": "html-minifier-terser@npm:^5.0.1":
version: 5.1.1 version: 5.1.1
resolution: "html-minifier-terser@npm:5.1.1" resolution: "html-minifier-terser@npm:5.1.1"
@ -7872,6 +7904,16 @@ fsevents@~2.3.2:
languageName: node languageName: node
linkType: hard 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": "node-forge@npm:^0.10.0":
version: 0.10.0 version: 0.10.0
resolution: "node-forge@npm:0.10.0" resolution: "node-forge@npm:0.10.0"
@ -11635,6 +11677,13 @@ resolve@^2.0.0-next.3:
languageName: node languageName: node
linkType: hard 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": "webidl-conversions@npm:^5.0.0":
version: 5.0.0 version: 5.0.0
resolution: "webidl-conversions@npm:5.0.0" resolution: "webidl-conversions@npm:5.0.0"