Add preview links (#29)
This commit is contained in:
parent
6ff3f99f79
commit
3f5e31f794
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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": ""
|
||||
}
|
||||
}
|
|
@ -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'));
|
|
@ -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(
|
||||
<div style={{ height: "100%" }}>
|
||||
<ReactChat theme={lightTheme} community={community} />
|
||||
<ReactChat
|
||||
theme={lightTheme}
|
||||
community={community}
|
||||
fetchMetadata={fetchMetadata}
|
||||
/>
|
||||
</div>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<Metadata | undefined>;
|
||||
}
|
||||
|
||||
export function Chat({ theme, community }: ChatProps) {
|
||||
export function Chat({ theme, community, fetchMetadata }: ChatProps) {
|
||||
const [activeChannel, setActiveChannel] = useState<ChannelData>(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}
|
||||
/>
|
||||
) : (
|
||||
<Loading>Connecting to waku</Loading>
|
||||
|
|
|
@ -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<Metadata | undefined>;
|
||||
}
|
||||
|
||||
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()}
|
||||
</button>{" "}
|
||||
{messages.length > 0 ? (
|
||||
<ChatMessages messages={messages} theme={theme} />
|
||||
<ChatMessages
|
||||
messages={messages}
|
||||
theme={theme}
|
||||
fetchMetadata={fetchMetadata}
|
||||
/>
|
||||
) : (
|
||||
<EmptyChannel theme={theme} channel={channel} />
|
||||
)}
|
||||
|
|
|
@ -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<Metadata | undefined>;
|
||||
};
|
||||
|
||||
export function ChatMessageContent({
|
||||
content,
|
||||
theme,
|
||||
fetchMetadata,
|
||||
}: ChatMessageContentProps) {
|
||||
const [elements, setElements] = useState<(string | React.ReactElement)[]>([
|
||||
content,
|
||||
]);
|
||||
const [link, setLink] = useState<string | undefined>(undefined);
|
||||
const [openGraph, setOpenGraph] = useState<Metadata | undefined>(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 (
|
||||
<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)}</>;
|
||||
}
|
||||
}
|
||||
|
||||
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};
|
||||
|
|
|
@ -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<Metadata | undefined>;
|
||||
};
|
||||
|
||||
export function ChatMessages({ messages, theme }: ChatMessagesProps) {
|
||||
export function ChatMessages({
|
||||
messages,
|
||||
theme,
|
||||
fetchMetadata,
|
||||
}: ChatMessagesProps) {
|
||||
const [scrollOnBot, setScrollOnBot] = useState(false);
|
||||
const ref = useRef<HTMLHeadingElement>(null);
|
||||
const today = useMemo(() => new Date().getDay(), []);
|
||||
|
@ -71,7 +77,11 @@ export function ChatMessages({ messages, theme }: ChatMessagesProps) {
|
|||
</TimeWrapper>
|
||||
</MessageHeaderWrapper>
|
||||
<MessageText theme={theme}>
|
||||
<ChatMessageContent content={message.content} theme={theme} />
|
||||
<ChatMessageContent
|
||||
content={message.content}
|
||||
theme={theme}
|
||||
fetchMetadata={fetchMetadata}
|
||||
/>
|
||||
</MessageText>
|
||||
</ContentWrapper>
|
||||
</MessageWrapper>
|
||||
|
|
|
@ -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<Metadata | undefined>;
|
||||
}
|
||||
|
||||
export function ReactChat({ theme, community }: ReactChatProps) {
|
||||
export function ReactChat({ theme, community, fetchMetadata }: ReactChatProps) {
|
||||
const ref = useRef<HTMLHeadingElement>(null);
|
||||
return (
|
||||
<NarrowProvider myRef={ref}>
|
||||
<div ref={ref}>
|
||||
<GlobalStyle />
|
||||
<Chat theme={theme} community={community} />
|
||||
<Chat
|
||||
theme={theme}
|
||||
community={community}
|
||||
fetchMetadata={fetchMetadata}
|
||||
/>
|
||||
</div>
|
||||
</NarrowProvider>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export interface Metadata {
|
||||
"og:site_name": string;
|
||||
"og:title": string;
|
||||
"og:image": string;
|
||||
}
|
49
yarn.lock
49
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"
|
||||
|
|
Loading…
Reference in New Issue