Add image message support (#50)

This commit is contained in:
Szymon Szlachtowicz 2021-10-07 13:41:40 +02:00 committed by GitHub
parent 377f4e5409
commit d4353cad84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 134 additions and 48 deletions

View File

@ -25,7 +25,7 @@ interface ChatBodyProps {
community: CommunityData; community: CommunityData;
messenger: any; messenger: any;
messages: ChatMessage[]; messages: ChatMessage[];
sendMessage: (text: string) => void; sendMessage: (text: string, image?: Uint8Array) => void;
onClick: () => void; onClick: () => void;
showMembers: boolean; showMembers: boolean;
showCommunity: boolean; showCommunity: boolean;

View File

@ -1,7 +1,8 @@
import { Picker } from "emoji-mart"; import { Picker } from "emoji-mart";
import React, { useState } from "react"; import React, { useMemo, useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { uintToImgUrl } from "../../helpers/uintToImgUrl";
import { lightTheme, Theme } from "../../styles/themes"; import { lightTheme, Theme } from "../../styles/themes";
import { EmojiIcon } from "../Icons/EmojiIcon"; import { EmojiIcon } from "../Icons/EmojiIcon";
import { GifIcon } from "../Icons/GifIcon"; import { GifIcon } from "../Icons/GifIcon";
@ -11,13 +12,21 @@ import "emoji-mart/css/emoji-mart.css";
type ChatInputProps = { type ChatInputProps = {
theme: Theme; theme: Theme;
addMessage: (message: string) => void; addMessage: (message: string, image?: Uint8Array) => void;
}; };
export function ChatInput({ theme, addMessage }: ChatInputProps) { export function ChatInput({ theme, addMessage }: ChatInputProps) {
const [content, setContent] = useState(""); const [content, setContent] = useState("");
const [showEmoji, setShowEmoji] = useState(false); const [showEmoji, setShowEmoji] = useState(false);
const [inputHeight, setInputHeight] = useState(40);
const [imageUint, setImageUint] = useState<undefined | Uint8Array>(undefined);
const image = useMemo(() => {
if (imageUint) {
return uintToImgUrl(imageUint);
} else {
return "";
}
}, [imageUint]);
const addEmoji = (e: any) => { const addEmoji = (e: any) => {
const sym = e.unified.split("-"); const sym = e.unified.split("-");
const codesArray: any[] = []; const codesArray: any[] = [];
@ -55,27 +64,48 @@ export function ChatInput({ theme, addMessage }: ChatInputProps) {
type="file" type="file"
multiple={true} multiple={true}
accept="image/png, image/jpeg" accept="image/png, image/jpeg"
onChange={(e) => {
const fileReader = new FileReader();
fileReader.onloadend = (s) => {
const arr = new Uint8Array(s.target?.result as ArrayBuffer);
setImageUint(arr);
};
if (e?.target?.files?.[0]) {
fileReader.readAsArrayBuffer(e.target.files[0]);
}
}}
/> />
</AddPictureBtn> </AddPictureBtn>
<Input <InputImageWrapper
theme={theme} theme={theme}
placeholder={"Message"} style={{ height: `${inputHeight + (image ? 73 : 0)}px` }}
value={content} >
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => { {image && (
const target = e.target; <ImagePreview src={image} onClick={() => setImageUint(undefined)} />
target.style.height = "40px"; )}
target.style.height = `${Math.min(target.scrollHeight, 160)}px`; <Input
setContent(target.value); theme={theme}
}} placeholder={"Message"}
onKeyPress={(e: React.KeyboardEvent<HTMLTextAreaElement>) => { value={content}
if (e.key == "Enter" && !e.getModifierState("Shift")) { onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
e.preventDefault(); const target = e.target;
(e.target as HTMLTextAreaElement).style.height = "40px"; target.style.height = "40px";
addMessage(content); target.style.height = `${Math.min(target.scrollHeight, 160)}px`;
setContent(""); setInputHeight(target.scrollHeight);
} setContent(target.value);
}} }}
/> onKeyPress={(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key == "Enter" && !e.getModifierState("Shift")) {
e.preventDefault();
(e.target as HTMLTextAreaElement).style.height = "40px";
setInputHeight(40);
addMessage(content, imageUint);
setImageUint(undefined);
setContent("");
}
}}
/>
</InputImageWrapper>
<AddEmojiBtn onClick={() => setShowEmoji(!showEmoji)}> <AddEmojiBtn onClick={() => setShowEmoji(!showEmoji)}>
<EmojiIcon theme={theme} isActive={showEmoji} /> <EmojiIcon theme={theme} isActive={showEmoji} />
</AddEmojiBtn> </AddEmojiBtn>
@ -100,20 +130,39 @@ const InputWrapper = styled.div`
position: relative; position: relative;
`; `;
const ImagePreview = styled.img`
width: 64px;
height: 64px;
border-radius: 16px 16px 4px 16px;
margin-left: 8px;
margin-top: 9px;
`;
const InputImageWrapper = styled.div<ThemeProps>`
width: 100%;
display: flex;
flex-direction: column;
background: ${({ theme }) => theme.inputColor};
border-radius: 36px 16px 4px 36px;
height: 40px;
margin-right: 8px;
margin-left: 10px;
`;
const Input = styled.textarea<ThemeProps>` const Input = styled.textarea<ThemeProps>`
width: 100%; width: 100%;
height: 40px; height: 40px;
background: ${({ theme }) => theme.inputColor}; background: ${({ theme }) => theme.inputColor};
border-radius: 36px 16px 4px 36px;
border: 1px solid ${({ theme }) => theme.inputColor}; border: 1px solid ${({ theme }) => theme.inputColor};
color: ${({ theme }) => theme.primary}; color: ${({ theme }) => theme.primary};
margin-left: 10px; border-radius: 36px 16px 4px 36px;
outline: none;
resize: none;
padding-top: 9px; padding-top: 9px;
padding-bottom: 9px; padding-bottom: 9px;
padding-left: 12px; padding-left: 12px;
padding-right: 112px; padding-right: 112px;
outline: none;
resize: none;
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;

View File

@ -1,7 +1,8 @@
import { decode } from "html-entities"; import { decode } from "html-entities";
import React, { useEffect, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { ChatMessage } from "../../models/ChatMessage";
import { Metadata } from "../../models/Metadata"; import { Metadata } from "../../models/Metadata";
import { Theme } from "../../styles/themes"; import { Theme } from "../../styles/themes";
@ -11,16 +12,17 @@ const regEx =
/* eslint-enable no-useless-escape */ /* eslint-enable no-useless-escape */
type ChatMessageContentProps = { type ChatMessageContentProps = {
content: string; message: ChatMessage;
theme: Theme; theme: Theme;
fetchMetadata?: (url: string) => Promise<Metadata | undefined>; fetchMetadata?: (url: string) => Promise<Metadata | undefined>;
}; };
export function ChatMessageContent({ export function ChatMessageContent({
content, message,
theme, theme,
fetchMetadata, fetchMetadata,
}: ChatMessageContentProps) { }: ChatMessageContentProps) {
const { content, image } = useMemo(() => message, [message]);
const [elements, setElements] = useState<(string | React.ReactElement)[]>([ const [elements, setElements] = useState<(string | React.ReactElement)[]>([
content, content,
]); ]);
@ -75,10 +77,11 @@ export function ChatMessageContent({
}; };
updatePreview(); updatePreview();
}, [link]); }, [link]);
if (openGraph) { return (
return ( <ContentWrapper>
<ContentWrapper> <div>{elements.map((el) => el)}</div>
<div>{elements.map((el) => el)}</div> {image && <MessageImage src={image} />}
{openGraph && (
<PreviewWrapper <PreviewWrapper
onClick={() => window?.open(link, "_blank", "noopener")?.focus()} onClick={() => window?.open(link, "_blank", "noopener")?.focus()}
> >
@ -88,13 +91,18 @@ export function ChatMessageContent({
{openGraph["og:site_name"]} {openGraph["og:site_name"]}
</PreviewSiteNameWrapper> </PreviewSiteNameWrapper>
</PreviewWrapper> </PreviewWrapper>
</ContentWrapper> )}
); </ContentWrapper>
} else { );
return <>{elements.map((el) => el)}</>;
}
} }
const MessageImage = styled.img`
width: 147px;
height: 196px;
border-radius: 16px;
margin-top: 8px;
`;
const PreviewSiteNameWrapper = styled.div` const PreviewSiteNameWrapper = styled.div`
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;

View File

@ -78,7 +78,7 @@ export function ChatMessages({
</MessageHeaderWrapper> </MessageHeaderWrapper>
<MessageText theme={theme}> <MessageText theme={theme}>
<ChatMessageContent <ChatMessageContent
content={message.content} message={message}
theme={theme} theme={theme}
fetchMetadata={fetchMetadata} fetchMetadata={fetchMetadata}
/> />

View File

@ -0,0 +1,4 @@
export function uintToImgUrl(img: Uint8Array) {
const blob = new Blob([img], { type: "image/png" });
return URL.createObjectURL(blob);
}

View File

@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from "react";
import { Identity, Messenger } from "status-communities/dist/cjs"; import { Identity, Messenger } from "status-communities/dist/cjs";
import { ApplicationMetadataMessage } from "status-communities/dist/cjs/application_metadata_message"; import { ApplicationMetadataMessage } from "status-communities/dist/cjs/application_metadata_message";
import { uintToImgUrl } from "../helpers/uintToImgUrl";
import { ChatMessage } from "../models/ChatMessage"; import { ChatMessage } from "../models/ChatMessage";
function binarySetInsert<T>( function binarySetInsert<T>(
@ -51,9 +52,15 @@ export function useMessenger(chatId: string, chatIdList: string[]) {
}, []); }, []);
const addNewMessageRaw = useCallback( const addNewMessageRaw = useCallback(
(signer: Uint8Array, content: string, date: Date, id: string) => { (
signer: Uint8Array,
content: string,
date: Date,
id: string,
image?: string
) => {
const sender = signer.reduce((p, c) => p + c.toString(16), "0x"); const sender = signer.reduce((p, c) => p + c.toString(16), "0x");
const newMessage = { sender, content, date }; const newMessage = { sender, content, date, image };
setMessages((prev) => { setMessages((prev) => {
return { return {
...prev, ...prev,
@ -77,10 +84,18 @@ export function useMessenger(chatId: string, chatIdList: string[]) {
const addNewMessage = useCallback( const addNewMessage = useCallback(
(msg: ApplicationMetadataMessage, id: string) => { (msg: ApplicationMetadataMessage, id: string) => {
if (msg.signer && msg.chatMessage?.text && msg.chatMessage.clock) { if (
const content = msg.chatMessage.text; msg.signer &&
(msg.chatMessage?.text || msg.chatMessage?.image) &&
msg.chatMessage.clock
) {
const content = msg.chatMessage.text ?? "";
let img: string | undefined = undefined;
if (msg.chatMessage?.image) {
img = uintToImgUrl(msg.chatMessage?.image.payload);
}
const date = new Date(msg.chatMessage.clock); const date = new Date(msg.chatMessage.clock);
addNewMessageRaw(msg.signer, content, date, id); addNewMessageRaw(msg.signer, content, date, id, img);
} }
}, },
[addNewMessageRaw] [addNewMessageRaw]
@ -141,7 +156,6 @@ export function useMessenger(chatId: string, chatIdList: string[]) {
const loadNextDay = useCallback( const loadNextDay = useCallback(
(id: string) => { (id: string) => {
console.log(id);
if (messenger) { if (messenger) {
const endTime = lastLoadTime[id]; const endTime = lastLoadTime[id];
const startTime = new Date(); const startTime = new Date();
@ -163,13 +177,23 @@ export function useMessenger(chatId: string, chatIdList: string[]) {
); );
const sendMessage = useCallback( const sendMessage = useCallback(
async (messageText: string) => { async (messageText: string, image?: Uint8Array) => {
await messenger?.sendMessage(messageText, chatId); let mediaContent = undefined;
if (image) {
mediaContent = {
image,
imageType: 1,
contentType: 1,
};
}
await messenger?.sendMessage(messageText, chatId, mediaContent);
addNewMessageRaw( addNewMessageRaw(
messenger?.identity.publicKey ?? new Uint8Array(), messenger?.identity.publicKey ?? new Uint8Array(),
messageText, messageText,
new Date(), new Date(),
chatId chatId,
image ? uintToImgUrl(image) : undefined
); );
}, },
[chatId, messenger] [chatId, messenger]

View File

@ -2,4 +2,5 @@ export type ChatMessage = {
content: string; content: string;
date: Date; date: Date;
sender: string; sender: string;
image?: string;
}; };