Add image message support (#50)
This commit is contained in:
parent
377f4e5409
commit
d4353cad84
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export function uintToImgUrl(img: Uint8Array) {
|
||||||
|
const blob = new Blob([img], { type: "image/png" });
|
||||||
|
return URL.createObjectURL(blob);
|
||||||
|
}
|
|
@ -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]
|
||||||
|
|
|
@ -2,4 +2,5 @@ export type ChatMessage = {
|
||||||
content: string;
|
content: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
sender: string;
|
sender: string;
|
||||||
|
image?: string;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue