diff --git a/packages/react-chat/src/components/Chat/ChatBody.tsx b/packages/react-chat/src/components/Chat/ChatBody.tsx index 392c0cba..fff19514 100644 --- a/packages/react-chat/src/components/Chat/ChatBody.tsx +++ b/packages/react-chat/src/components/Chat/ChatBody.tsx @@ -25,7 +25,7 @@ interface ChatBodyProps { community: CommunityData; messenger: any; messages: ChatMessage[]; - sendMessage: (text: string) => void; + sendMessage: (text: string, image?: Uint8Array) => void; onClick: () => void; showMembers: boolean; showCommunity: boolean; diff --git a/packages/react-chat/src/components/Chat/ChatInput.tsx b/packages/react-chat/src/components/Chat/ChatInput.tsx index 26f732ca..abe0250c 100644 --- a/packages/react-chat/src/components/Chat/ChatInput.tsx +++ b/packages/react-chat/src/components/Chat/ChatInput.tsx @@ -1,7 +1,8 @@ import { Picker } from "emoji-mart"; -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import styled from "styled-components"; +import { uintToImgUrl } from "../../helpers/uintToImgUrl"; import { lightTheme, Theme } from "../../styles/themes"; import { EmojiIcon } from "../Icons/EmojiIcon"; import { GifIcon } from "../Icons/GifIcon"; @@ -11,13 +12,21 @@ import "emoji-mart/css/emoji-mart.css"; type ChatInputProps = { theme: Theme; - addMessage: (message: string) => void; + addMessage: (message: string, image?: Uint8Array) => void; }; export function ChatInput({ theme, addMessage }: ChatInputProps) { const [content, setContent] = useState(""); const [showEmoji, setShowEmoji] = useState(false); - + const [inputHeight, setInputHeight] = useState(40); + const [imageUint, setImageUint] = useState(undefined); + const image = useMemo(() => { + if (imageUint) { + return uintToImgUrl(imageUint); + } else { + return ""; + } + }, [imageUint]); const addEmoji = (e: any) => { const sym = e.unified.split("-"); const codesArray: any[] = []; @@ -55,27 +64,48 @@ export function ChatInput({ theme, addMessage }: ChatInputProps) { type="file" multiple={true} 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]); + } + }} /> - ) => { - const target = e.target; - target.style.height = "40px"; - target.style.height = `${Math.min(target.scrollHeight, 160)}px`; - setContent(target.value); - }} - onKeyPress={(e: React.KeyboardEvent) => { - if (e.key == "Enter" && !e.getModifierState("Shift")) { - e.preventDefault(); - (e.target as HTMLTextAreaElement).style.height = "40px"; - addMessage(content); - setContent(""); - } - }} - /> + style={{ height: `${inputHeight + (image ? 73 : 0)}px` }} + > + {image && ( + setImageUint(undefined)} /> + )} + ) => { + const target = e.target; + target.style.height = "40px"; + target.style.height = `${Math.min(target.scrollHeight, 160)}px`; + setInputHeight(target.scrollHeight); + setContent(target.value); + }} + onKeyPress={(e: React.KeyboardEvent) => { + if (e.key == "Enter" && !e.getModifierState("Shift")) { + e.preventDefault(); + (e.target as HTMLTextAreaElement).style.height = "40px"; + setInputHeight(40); + addMessage(content, imageUint); + setImageUint(undefined); + setContent(""); + } + }} + /> + setShowEmoji(!showEmoji)}> @@ -100,20 +130,39 @@ const InputWrapper = styled.div` 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` + 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` width: 100%; height: 40px; background: ${({ theme }) => theme.inputColor}; - border-radius: 36px 16px 4px 36px; border: 1px solid ${({ theme }) => theme.inputColor}; color: ${({ theme }) => theme.primary}; - margin-left: 10px; + border-radius: 36px 16px 4px 36px; + outline: none; + resize: none; + padding-top: 9px; padding-bottom: 9px; padding-left: 12px; padding-right: 112px; - outline: none; - resize: none; font-family: Inter; font-style: normal; diff --git a/packages/react-chat/src/components/Chat/ChatMessageContent.tsx b/packages/react-chat/src/components/Chat/ChatMessageContent.tsx index cc04ef89..c248139f 100644 --- a/packages/react-chat/src/components/Chat/ChatMessageContent.tsx +++ b/packages/react-chat/src/components/Chat/ChatMessageContent.tsx @@ -1,7 +1,8 @@ import { decode } from "html-entities"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import styled from "styled-components"; +import { ChatMessage } from "../../models/ChatMessage"; import { Metadata } from "../../models/Metadata"; import { Theme } from "../../styles/themes"; @@ -11,16 +12,17 @@ const regEx = /* eslint-enable no-useless-escape */ type ChatMessageContentProps = { - content: string; + message: ChatMessage; theme: Theme; fetchMetadata?: (url: string) => Promise; }; export function ChatMessageContent({ - content, + message, theme, fetchMetadata, }: ChatMessageContentProps) { + const { content, image } = useMemo(() => message, [message]); const [elements, setElements] = useState<(string | React.ReactElement)[]>([ content, ]); @@ -75,10 +77,11 @@ export function ChatMessageContent({ }; updatePreview(); }, [link]); - if (openGraph) { - return ( - -
{elements.map((el) => el)}
+ return ( + +
{elements.map((el) => el)}
+ {image && } + {openGraph && ( window?.open(link, "_blank", "noopener")?.focus()} > @@ -88,13 +91,18 @@ export function ChatMessageContent({ {openGraph["og:site_name"]} -
- ); - } else { - return <>{elements.map((el) => el)}; - } + )} +
+ ); } +const MessageImage = styled.img` + width: 147px; + height: 196px; + border-radius: 16px; + margin-top: 8px; +`; + const PreviewSiteNameWrapper = styled.div` font-family: Inter; font-style: normal; diff --git a/packages/react-chat/src/components/Chat/ChatMessages.tsx b/packages/react-chat/src/components/Chat/ChatMessages.tsx index 65e95558..d65f44b6 100644 --- a/packages/react-chat/src/components/Chat/ChatMessages.tsx +++ b/packages/react-chat/src/components/Chat/ChatMessages.tsx @@ -78,7 +78,7 @@ export function ChatMessages({ diff --git a/packages/react-chat/src/helpers/uintToImgUrl.ts b/packages/react-chat/src/helpers/uintToImgUrl.ts new file mode 100644 index 00000000..d1774aee --- /dev/null +++ b/packages/react-chat/src/helpers/uintToImgUrl.ts @@ -0,0 +1,4 @@ +export function uintToImgUrl(img: Uint8Array) { + const blob = new Blob([img], { type: "image/png" }); + return URL.createObjectURL(blob); +} diff --git a/packages/react-chat/src/hooks/useMessenger.ts b/packages/react-chat/src/hooks/useMessenger.ts index f1204add..a7716ab2 100644 --- a/packages/react-chat/src/hooks/useMessenger.ts +++ b/packages/react-chat/src/hooks/useMessenger.ts @@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from "react"; import { Identity, Messenger } from "status-communities/dist/cjs"; import { ApplicationMetadataMessage } from "status-communities/dist/cjs/application_metadata_message"; +import { uintToImgUrl } from "../helpers/uintToImgUrl"; import { ChatMessage } from "../models/ChatMessage"; function binarySetInsert( @@ -51,9 +52,15 @@ export function useMessenger(chatId: string, chatIdList: string[]) { }, []); 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 newMessage = { sender, content, date }; + const newMessage = { sender, content, date, image }; setMessages((prev) => { return { ...prev, @@ -77,10 +84,18 @@ export function useMessenger(chatId: string, chatIdList: string[]) { const addNewMessage = useCallback( (msg: ApplicationMetadataMessage, id: string) => { - if (msg.signer && msg.chatMessage?.text && msg.chatMessage.clock) { - const content = msg.chatMessage.text; + if ( + 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); - addNewMessageRaw(msg.signer, content, date, id); + addNewMessageRaw(msg.signer, content, date, id, img); } }, [addNewMessageRaw] @@ -141,7 +156,6 @@ export function useMessenger(chatId: string, chatIdList: string[]) { const loadNextDay = useCallback( (id: string) => { - console.log(id); if (messenger) { const endTime = lastLoadTime[id]; const startTime = new Date(); @@ -163,13 +177,23 @@ export function useMessenger(chatId: string, chatIdList: string[]) { ); const sendMessage = useCallback( - async (messageText: string) => { - await messenger?.sendMessage(messageText, chatId); + async (messageText: string, image?: Uint8Array) => { + let mediaContent = undefined; + if (image) { + mediaContent = { + image, + imageType: 1, + contentType: 1, + }; + } + await messenger?.sendMessage(messageText, chatId, mediaContent); + addNewMessageRaw( messenger?.identity.publicKey ?? new Uint8Array(), messageText, new Date(), - chatId + chatId, + image ? uintToImgUrl(image) : undefined ); }, [chatId, messenger] diff --git a/packages/react-chat/src/models/ChatMessage.ts b/packages/react-chat/src/models/ChatMessage.ts index f598e62f..327ce507 100644 --- a/packages/react-chat/src/models/ChatMessage.ts +++ b/packages/react-chat/src/models/ChatMessage.ts @@ -2,4 +2,5 @@ export type ChatMessage = { content: string; date: Date; sender: string; + image?: string; };