Emoji reactions (#176)

This commit is contained in:
Maria Rushkova 2022-01-10 13:27:48 +01:00 committed by GitHub
parent c170f81054
commit fe3c189f00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 234 additions and 53 deletions

View File

@ -2,16 +2,12 @@ import React from "react";
import styled from "styled-components"; import styled from "styled-components";
type ReactionProps = { type ReactionProps = {
width: number;
height: number;
className?: string; className?: string;
}; };
export function ReactionSvg({ width, height, className }: ReactionProps) { export function ReactionSvg({ className }: ReactionProps) {
return ( return (
<Icon <Icon
width={width}
height={height}
viewBox="0 0 22 22" viewBox="0 0 22 22"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className={className} className={className}
@ -40,9 +36,16 @@ export function ReactionSvg({ width, height, className }: ReactionProps) {
} }
const Icon = styled.svg` const Icon = styled.svg`
width: 22px;
height: 22px;
fill: ${({ theme }) => theme.secondary}; fill: ${({ theme }) => theme.secondary};
&:hover { &:hover {
fill: ${({ theme }) => theme.tertiary}; fill: ${({ theme }) => theme.tertiary};
} }
&.small {
width: 18px;
height: 18px;
}
`; `;

View File

@ -0,0 +1,72 @@
import { BaseEmoji, Emoji } from "emoji-mart";
import React from "react";
import styled from "styled-components";
import { ReactionButton } from "../Reactions/ReactionButton";
interface MessageReactionsProps {
messageReactions: BaseEmoji[];
setMessageReactions: React.Dispatch<React.SetStateAction<BaseEmoji[]>>;
}
export function MessageReactions({
messageReactions,
setMessageReactions,
}: MessageReactionsProps) {
const isMyReactionIncluded = (emoji: BaseEmoji) =>
messageReactions.includes(emoji); // temporary function while message reactions are not added to waku
return (
<Reactions>
{messageReactions.map((reaction) => (
<EmojiReaction
className={`${isMyReactionIncluded(reaction) && "chosen"}`}
>
<Emoji emoji={reaction} set={"twitter"} size={16} />
<p>1</p>
</EmojiReaction>
))}
<ReactionButton
messageReactions={messageReactions}
setMessageReactions={setMessageReactions}
className="small"
/>
</Reactions>
);
}
export const Reactions = styled.div`
display: flex;
align-items: center;
margin-top: 6px;
`;
const EmojiReaction = styled.button`
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.05);
border-radius: 2px 10px 10px 10px;
color: ${({ theme }) => theme.primary};
font-size: 12px;
line-height: 16px;
padding: 2px 8px 2px 2px;
margin-right: 4px;
&:hover {
background: rgba(0, 0, 0, 0.1);
}
& > p {
margin-left: 4px;
}
& > span {
height: 16px;
}
&.chosen {
background: ${({ theme }) => theme.reactionChosen};
border: 1px solid ${({ theme }) => theme.tertiary};
color: ${({ theme }) => theme.tertiary};
}
`;

View File

@ -1,3 +1,4 @@
import { BaseEmoji } from "emoji-mart";
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { utils } from "status-communities/dist/cjs"; import { utils } from "status-communities/dist/cjs";
import styled from "styled-components"; import styled from "styled-components";
@ -11,13 +12,13 @@ import { ChatMessage } from "../../models/ChatMessage";
import { equalDate } from "../../utils"; import { equalDate } from "../../utils";
import { ChatMessageContent } from "../Chat/ChatMessageContent"; import { ChatMessageContent } from "../Chat/ChatMessageContent";
import { ContactMenu } from "../Form/ContactMenu"; import { ContactMenu } from "../Form/ContactMenu";
import { ReactionPicker } from "../Form/ReactionPicker";
import { Reactions } from "../Form/Reactions";
import { Icon } from "../Icons/Icon"; import { Icon } from "../Icons/Icon";
import { UntrustworthIcon } from "../Icons/UntrustworthIcon"; import { UntrustworthIcon } from "../Icons/UntrustworthIcon";
import { UserLogo } from "../Members/UserLogo"; import { UserLogo } from "../Members/UserLogo";
import { Reactions } from "../Reactions/Reactions";
import { MessageQuote } from "./MessageQuote"; import { MessageQuote } from "./MessageQuote";
import { MessageReactions } from "./MessageReactions";
import { import {
ContentWrapper, ContentWrapper,
DateSeparator, DateSeparator,
@ -62,8 +63,9 @@ export function UiMessage({
[message.sender, contacts] [message.sender, contacts]
); );
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
const [showReactions, setShowReactions] = useState(false);
const [mentioned, setMentioned] = useState(false); const [mentioned, setMentioned] = useState(false);
const [messageReactions, setMessageReactions] = useState<BaseEmoji[]>([]);
useEffect(() => { useEffect(() => {
if (mentioned) if (mentioned)
@ -146,14 +148,19 @@ export function UiMessage({
setMentioned={setMentioned} setMentioned={setMentioned}
/> />
</MessageText> </MessageText>
{messageReactions.length > 0 && (
<MessageReactions
messageReactions={messageReactions}
setMessageReactions={setMessageReactions}
/>
)}
</ContentWrapper> </ContentWrapper>
</UserMessageWrapper> </UserMessageWrapper>
{showReactions && <ReactionPicker />}
<Reactions <Reactions
message={message} message={message}
setReply={setReply} setReply={setReply}
showReactions={showReactions} messageReactions={messageReactions}
setShowReactions={setShowReactions} setMessageReactions={setMessageReactions}
/> />
</MessageWrapper> </MessageWrapper>
</MessageOuterWrapper> </MessageOuterWrapper>

View File

@ -0,0 +1,81 @@
import { BaseEmoji } from "emoji-mart";
import React, { useRef, useState } from "react";
import styled from "styled-components";
import { useClickOutside } from "../../hooks/useClickOutside";
import { Tooltip } from "../Form/Tooltip";
import { ReactionSvg } from "../Icons/ReactionIcon";
import { ReactionPicker } from "./ReactionPicker";
interface ReactionButtonProps {
className?: string;
messageReactions: BaseEmoji[];
setMessageReactions: React.Dispatch<React.SetStateAction<BaseEmoji[]>>;
}
export function ReactionButton({
className,
messageReactions,
setMessageReactions,
}: ReactionButtonProps) {
const ref = useRef(null);
useClickOutside(ref, () => setShowReactions(false));
const [showReactions, setShowReactions] = useState(false);
return (
<Wrapper ref={ref}>
{showReactions && (
<ReactionPicker
messageReactions={messageReactions}
setMessageReactions={setMessageReactions}
className={className}
/>
)}
<ReactionBtn
onClick={() => setShowReactions(!showReactions)}
className={className}
>
<ReactionSvg className={className} />
{!className && !showReactions && <Tooltip tip="Add reaction" />}
</ReactionBtn>
</Wrapper>
);
}
const Wrapper = styled.div`
position: relative;
`;
export const ReactionBtn = styled.button`
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
align-self: center;
&:hover {
background: ${({ theme }) => theme.buttonBgHover};
}
&:hover > svg {
fill: ${({ theme }) => theme.tertiary};
}
&:hover > div {
visibility: visible;
}
&.small {
width: 18px;
height: 18px;
padding: 0;
&:hover {
background: inherit;
}
}
`;

View File

@ -1,6 +1,6 @@
import { Emoji, getEmojiDataFromNative } from "emoji-mart"; import { BaseEmoji, Emoji, getEmojiDataFromNative } from "emoji-mart";
import data from "emoji-mart/data/all.json"; import data from "emoji-mart/data/all.json";
import React from "react"; import React, { useCallback } from "react";
import styled from "styled-components"; import styled from "styled-components";
const emojiHeart = getEmojiDataFromNative("❤️", "twitter", data); const emojiHeart = getEmojiDataFromNative("❤️", "twitter", data);
@ -19,11 +19,34 @@ const emojiArr = [
emojiRage, emojiRage,
]; ];
export function ReactionPicker() { interface ReactionPickerProps {
className?: string;
messageReactions: BaseEmoji[];
setMessageReactions: React.Dispatch<React.SetStateAction<BaseEmoji[]>>;
}
export function ReactionPicker({
className,
messageReactions,
setMessageReactions,
}: ReactionPickerProps) {
const handleReaction = useCallback(
(emoji: BaseEmoji) => {
messageReactions.find((e) => e === emoji)
? setMessageReactions((prev) => prev.filter((e) => e != emoji))
: setMessageReactions((prev) => [...prev, emoji]);
},
[messageReactions]
);
return ( return (
<Wrapper> <Wrapper className={className}>
{emojiArr.map((emoji) => ( {emojiArr.map((emoji) => (
<EmojiBtn key={emoji.id}> <EmojiBtn
key={emoji.id}
onClick={() => handleReaction(emoji)}
className={`${messageReactions.includes(emoji) && "chosen"}`}
>
{" "} {" "}
<Emoji <Emoji
emoji={emoji} emoji={emoji}
@ -42,13 +65,19 @@ const Wrapper = styled.div`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
position: absolute; position: absolute;
right: 20px; right: -34px;
top: -78px; top: -60px;
background: ${({ theme }) => theme.bodyBackgroundColor}; background: ${({ theme }) => theme.bodyBackgroundColor};
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.08); box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.08);
border-radius: 16px 16px 4px 16px; border-radius: 16px 16px 4px 16px;
padding: 8px 12px; padding: 8px;
visibility: hidden;
&.small {
right: unset;
left: -100px;
transform: none;
border-radius: 16px 16px 16px 4px;
}
`; `;
const EmojiBtn = styled.button` const EmojiBtn = styled.button`
@ -62,4 +91,9 @@ const EmojiBtn = styled.button`
&:hover { &:hover {
background: ${({ theme }) => theme.inputColor}; background: ${({ theme }) => theme.inputColor};
} }
&.chosen {
background: ${({ theme }) => theme.reactionBg};
border: 1px solid ${({ theme }) => theme.tertiary};
}
`; `;

View File

@ -1,32 +1,33 @@
import { BaseEmoji } from "emoji-mart";
import React from "react"; import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { Reply } from "../../hooks/useReply"; import { Reply } from "../../hooks/useReply";
import { ChatMessage } from "../../models/ChatMessage"; import { ChatMessage } from "../../models/ChatMessage";
import { ReactionSvg } from "../Icons/ReactionIcon"; import { Tooltip } from "../Form/Tooltip";
import { ReplySvg } from "../Icons/ReplyIcon"; import { ReplySvg } from "../Icons/ReplyIcon";
import { Tooltip } from "./Tooltip"; import { ReactionBtn, ReactionButton } from "./ReactionButton";
interface ReactionsProps { interface ReactionsProps {
message: ChatMessage; message: ChatMessage;
showReactions: boolean;
setShowReactions: (val: boolean) => void;
setReply: (val: Reply | undefined) => void; setReply: (val: Reply | undefined) => void;
messageReactions: BaseEmoji[];
setMessageReactions: React.Dispatch<React.SetStateAction<BaseEmoji[]>>;
} }
export function Reactions({ export function Reactions({
message, message,
showReactions,
setShowReactions,
setReply, setReply,
messageReactions,
setMessageReactions,
}: ReactionsProps) { }: ReactionsProps) {
return ( return (
<Wrapper> <Wrapper>
<ReactionBtn onClick={() => setShowReactions(!showReactions)}> <ReactionButton
<ReactionSvg width={22} height={22} /> messageReactions={messageReactions}
<Tooltip tip="Add reaction" /> setMessageReactions={setMessageReactions}
</ReactionBtn> />
<ReactionBtn <ReactionBtn
onClick={() => onClick={() =>
setReply({ setReply({
@ -55,26 +56,3 @@ const Wrapper = styled.div`
padding: 2px; padding: 2px;
visibility: hidden; visibility: hidden;
`; `;
const ReactionBtn = styled.button`
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
align-self: center;
position: relative;
&:hover {
background: ${({ theme }) => theme.buttonBgHover};
}
&:hover > svg {
fill: ${({ theme }) => theme.tertiary};
}
&:hover > div {
visibility: visible;
}
`;

View File

@ -27,6 +27,8 @@ export type Theme = {
mentionBg: string; mentionBg: string;
mentionBgHover: string; mentionBgHover: string;
shadow: string; shadow: string;
reactionBg: string;
reactionChosen: string;
}; };
export const lightTheme: Theme = { export const lightTheme: Theme = {
@ -60,6 +62,8 @@ export const lightTheme: Theme = {
mentionBgHover: "#D4F3FA", mentionBgHover: "#D4F3FA",
shadow: shadow:
"0px 2px 4px rgba(0, 34, 51, 0.16), 0px 4px 12px rgba(0, 34, 51, 0.08)", "0px 2px 4px rgba(0, 34, 51, 0.16), 0px 4px 12px rgba(0, 34, 51, 0.08)",
reactionBg: "#eceffc",
reactionChosen: "rgba(67, 96, 223, 0.1)",
}; };
export const darkTheme: Theme = { export const darkTheme: Theme = {
@ -93,6 +97,8 @@ export const darkTheme: Theme = {
mentionBgHover: "#002D39", mentionBgHover: "#002D39",
shadow: shadow:
"0px 2px 4px rgba(0, 34, 51, 0.16), 0px 4px 12px rgba(0, 34, 51, 0.08)", "0px 2px 4px rgba(0, 34, 51, 0.16), 0px 4px 12px rgba(0, 34, 51, 0.08)",
reactionBg: "#373737",
reactionChosen: "rgba(134, 158, 255, 0.3)",
}; };
export default { lightTheme, darkTheme }; export default { lightTheme, darkTheme };