feat(react): implement message reactions

This commit is contained in:
Pavel Prichodko 2022-04-14 12:37:59 +02:00
parent e31b7abff5
commit 688fd70d81
No known key found for this signature in database
GPG Key ID: 8E4C82D464215E83
6 changed files with 259 additions and 133 deletions

View File

@ -3,63 +3,64 @@ import React from 'react'
import { styled } from '~/src/styles/config'
import { Flex, Image, Popover, PopoverTrigger } from '~/src/system'
import type { Reaction, Reactions } from '~/src/protocol/use-messages'
interface Props {
children: React.ReactElement
onClick: (emoji: string) => void
reactions: Reactions
onClick: (reaction: Reaction) => void
open?: boolean
onOpenChange?: (open: boolean) => void
}
export const emojis: Record<Reaction, { url: string; symbol: string }> = {
heart: {
symbol: '❤️',
url: 'https://twemoji.maxcdn.com/v/latest/svg/2764.svg',
},
'thumbs-up': {
symbol: '👍️',
url: 'https://twemoji.maxcdn.com/v/latest/svg/1f44d.svg',
},
'thumbs-down': {
symbol: '👎️',
url: 'https://twemoji.maxcdn.com/v/latest/svg/1f44e.svg',
},
smile: {
symbol: '😆',
url: 'https://twemoji.maxcdn.com/v/latest/svg/1f606.svg',
},
sad: {
symbol: '😭',
url: 'https://twemoji.maxcdn.com/v/latest/svg/1f62d.svg',
},
angry: {
symbol: '😡',
url: 'https://twemoji.maxcdn.com/v/latest/svg/1f621.svg',
},
}
export const ReactionPopover = (props: Props) => {
const { children, onClick, ...popoverProps } = props
const { reactions, children, onClick, ...popoverProps } = props
return (
<PopoverTrigger {...popoverProps}>
{children}
<Popover side="top" align="center" sideOffset={6}>
<Flex gap={1} css={{ padding: 8 }}>
<Button onClick={() => onClick('')} active={false}>
<Image
width={30}
src="https://twemoji.maxcdn.com/v/latest/svg/2764.svg"
alt="React with ❤️"
/>
</Button>
<Button onClick={() => onClick('')} active>
<Image
width={30}
src="https://twemoji.maxcdn.com/v/latest/svg/1f44d.svg"
alt="React with 👍️"
/>
</Button>
<Button onClick={() => onClick('')} active>
<Image
width={30}
src="https://twemoji.maxcdn.com/v/latest/svg/1f44e.svg"
alt="React with 👎️"
/>
</Button>
<Button onClick={() => onClick('')} active={false}>
<Image
width={30}
src="https://twemoji.maxcdn.com/v/latest/svg/1f606.svg"
alt="React with 😆"
/>
</Button>
<Button onClick={() => onClick('')} active={false}>
<Image
width={30}
src="https://twemoji.maxcdn.com/v/latest/svg/1f62d.svg"
alt="React with 😭"
/>
</Button>
<Button onClick={() => onClick('')} active={false}>
<Image
width={30}
src="https://twemoji.maxcdn.com/v/latest/svg/1f621.svg"
alt="React with 😡"
/>
</Button>
{Object.entries(reactions).map(([reaction, value]) => {
const emoji = emojis[reaction as Reaction]
return (
<Button
key={reaction}
onClick={() => onClick(reaction as Reaction)}
active={value.me}
aria-label={`React with ${emoji.symbol}`}
>
<Image width={30} src={emoji.url} alt={emoji.symbol} />
</Button>
)
})}
</Flex>
</Popover>
</PopoverTrigger>

View File

@ -1,3 +1,18 @@
export type Reaction =
| 'heart'
| 'thumbs-up'
| 'thumbs-down'
| 'smile'
| 'sad'
| 'angry'
export type Reactions = {
[key in Reaction]: {
count: number
me: boolean
}
}
interface BaseMessage {
id: string
type: 'text' | 'image' | 'image-text'
@ -9,6 +24,7 @@ interface BaseMessage {
pinned: boolean
mention: boolean
reply?: TextReply | ImageReply | ImageTextReply
reactions: Reactions
}
interface TextMessage extends BaseMessage {
@ -68,6 +84,14 @@ export const useMessages = (): Message[] => {
owner: false,
pinned: true,
mention: false,
reactions: {
heart: { count: 0, me: false },
'thumbs-up': { count: 0, me: false },
'thumbs-down': { count: 0, me: false },
smile: { count: 0, me: false },
sad: { count: 0, me: false },
angry: { count: 0, me: false },
},
reply: {
contact: {
name: 'Leila Joyner',
@ -90,6 +114,14 @@ export const useMessages = (): Message[] => {
owner: false,
pinned: false,
mention: false,
reactions: {
heart: { count: 0, me: false },
'thumbs-up': { count: 0, me: false },
'thumbs-down': { count: 0, me: false },
smile: { count: 0, me: false },
sad: { count: 0, me: false },
angry: { count: 0, me: false },
},
reply: {
contact: {
name: 'Leila Joyner',
@ -113,6 +145,14 @@ export const useMessages = (): Message[] => {
owner: false,
pinned: false,
mention: true,
reactions: {
heart: { count: 1, me: false },
'thumbs-up': { count: 1, me: false },
'thumbs-down': { count: 3, me: true },
smile: { count: 0, me: false },
sad: { count: 0, me: false },
angry: { count: 0, me: false },
},
reply: {
contact: {
name: 'Leila Joyner',
@ -137,6 +177,14 @@ export const useMessages = (): Message[] => {
owner: false,
pinned: false,
mention: false,
reactions: {
heart: { count: 0, me: false },
'thumbs-up': { count: 0, me: false },
'thumbs-down': { count: 0, me: false },
smile: { count: 0, me: false },
sad: { count: 0, me: false },
angry: { count: 0, me: false },
},
},
{
id: '5',
@ -150,6 +198,14 @@ export const useMessages = (): Message[] => {
owner: true,
pinned: false,
mention: false,
reactions: {
heart: { count: 0, me: false },
'thumbs-up': { count: 0, me: false },
'thumbs-down': { count: 0, me: false },
smile: { count: 0, me: false },
sad: { count: 1, me: false },
angry: { count: 1, me: true },
},
reply: {
contact: {
name: 'Leila Joyner',
@ -174,6 +230,14 @@ export const useMessages = (): Message[] => {
owner: false,
pinned: false,
mention: false,
reactions: {
heart: { count: 0, me: false },
'thumbs-up': { count: 10, me: true },
'thumbs-down': { count: 3, me: false },
smile: { count: 0, me: false },
sad: { count: 0, me: false },
angry: { count: 0, me: false },
},
},
{
id: '5',
@ -187,6 +251,14 @@ export const useMessages = (): Message[] => {
owner: true,
pinned: false,
mention: false,
reactions: {
heart: { count: 0, me: false },
'thumbs-up': { count: 0, me: false },
'thumbs-down': { count: 0, me: false },
smile: { count: 0, me: false },
sad: { count: 0, me: false },
angry: { count: 0, me: false },
},
},
]
}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React from 'react'
import { ReactionPopover } from '~/src/components/reaction-popover'
import { PencilIcon } from '~/src/icons/pencil-icon'
@ -15,6 +15,8 @@ import {
Tooltip,
} from '~/src/system'
import type { Reactions } from '~/src/protocol/use-messages'
interface Props {
owner: boolean
pinned: boolean
@ -22,6 +24,7 @@ interface Props {
onEditClick: () => void
reacting: boolean
onReactingChange: (reacting: boolean) => void
reactions: Reactions
}
export const Actions = (props: Props) => {
@ -32,11 +35,13 @@ export const Actions = (props: Props) => {
onEditClick,
reacting,
onReactingChange,
reactions,
} = props
return (
<Wrapper open={reacting}>
<ReactionPopover
reactions={reactions}
open={reacting}
onOpenChange={onReactingChange}
onClick={emoji => {

View File

@ -21,7 +21,7 @@ import {
import { ChatInput } from '../chat-input'
import { Actions } from './actions'
import { MessageReply } from './message-reply'
import { Reactions } from './reactions'
import { MessageReactions } from './reactions'
import type { Message } from '~/src/protocol/use-messages'
@ -53,7 +53,7 @@ interface Props {
export const ChatMessage = (props: Props) => {
const { message } = props
const { type, contact, owner, mention, pinned, reply } = message
const { type, contact, owner, mention, pinned, reply, reactions } = message
const [editing, setEditing] = useState(false)
const [reacting, setReacting] = useState(false)
@ -198,7 +198,10 @@ export const ChatMessage = (props: Props) => {
{renderMessage()}
<Reactions onClick={handleReaction} />
<MessageReactions
reactions={reactions}
onClick={handleReaction}
/>
</Box>
</Flex>
@ -209,6 +212,7 @@ export const ChatMessage = (props: Props) => {
onReplyClick={handleReplyClick}
reacting={reacting}
onReactingChange={setReacting}
reactions={reactions}
/>
</Wrapper>
<ContextMenu>

View File

@ -1,115 +1,104 @@
import React from 'react'
import { ReactionPopover } from '~/src/components/reaction-popover'
import { emojis, ReactionPopover } from '~/src/components/reaction-popover'
import { ReactionIcon } from '~/src/icons/reaction-icon'
import { Reaction } from '~/src/protocol/use-messages'
import { styled } from '~/src/styles/config'
import { Flex, Image } from '~/src/system'
import { Flex, Image, Text } from '~/src/system'
import type { Reactions } from '~/src/protocol/use-messages'
interface Props {
onClick: (reaction: string) => void
reactions: Reactions
onClick: (reaction: Reaction) => void
}
export const Reactions = (props: Props) => {
const { onClick } = props
export const MessageReactions = (props: Props) => {
const { reactions, onClick } = props
const hasReaction = Object.values(reactions).some(
reaction => reaction.count !== 0
)
if (hasReaction === false) {
return null
}
return (
<Flex css={{ paddingTop: 6 }} gap={1}>
<Button
onClick={() => onClick('')}
active={false}
aria-label="❤️, 1 reaction, press to react"
>
<Image
width={16}
src="https://twemoji.maxcdn.com/v/latest/svg/2764.svg"
alt="❤️"
<Flex align="center" css={{ paddingTop: 6 }} gap={1}>
{Object.entries(reactions).map(([reaction, value]) => (
<Reaction
key={reaction}
emoji={emojis[reaction as Reaction]}
reaction={value}
onClick={() => onClick(reaction as Reaction)}
/>
1
</Button>
<Button
onClick={() => onClick('')}
active={true}
aria-label="👍️, 1 reaction, press to react"
>
<Image
width={16}
src="https://twemoji.maxcdn.com/v/latest/svg/1f44d.svg"
alt="👍️"
/>
2
</Button>
<Button
onClick={() => onClick('')}
active={true}
aria-label="👎️, 1 reaction, press to react"
>
<Image
width={16}
src="https://twemoji.maxcdn.com/v/latest/svg/1f44e.svg"
alt="👎️"
/>
3
</Button>
<Button
onClick={() => onClick('')}
active={false}
aria-label="😆, 1 reaction, press to react"
>
<Image
width={16}
src="https://twemoji.maxcdn.com/v/latest/svg/1f606.svg"
alt="😆"
/>
1
</Button>
<Button
onClick={() => onClick('')}
active={false}
aria-label="😭, 1 reaction, press to react"
>
<Image
width={16}
src="https://twemoji.maxcdn.com/v/latest/svg/1f62d.svg"
alt="😭"
/>
</Button>
<Button
onClick={() => onClick('')}
active={false}
aria-label="😡, 1 reaction, press to react"
>
<Image
width={16}
src="https://twemoji.maxcdn.com/v/latest/svg/1f621.svg"
alt="😡"
/>
</Button>
<ReactionPopover
onClick={emoji => {
console.log(emoji)
}}
>
<Button>
<ReactionIcon />
</Button>
))}
<ReactionPopover reactions={reactions} onClick={onClick}>
<AddReactionButton aria-label="Add Reaction">
<ReactionIcon width={16} height={16} />
</AddReactionButton>
</ReactionPopover>
</Flex>
)
}
const AddReactionButton = styled('button', {
color: '$gray-1',
width: 16,
height: 16,
"&[aria-expanded='true']": {
color: '$primary-1',
},
})
interface ReactionProps {
emoji: {
url: string
symbol: string
}
reaction: Props['reactions']['smile']
onClick: VoidFunction
}
const Reaction = (props: ReactionProps) => {
const { emoji, reaction, onClick } = props
if (reaction.count === 0) {
return null
}
return (
<Button
onClick={onClick}
active={reaction.me}
aria-label={`${emoji.symbol}, ${reaction.count} reaction, press to react`}
>
<Image width={14} src={emoji.url} alt={emoji.symbol} />
<Text size="12">{reaction.count}</Text>
</Button>
)
}
const Button = styled('button', {
padding: 2,
padding: '0px 8px 0px 3px',
boxShadow: '0px 4px 12px rgba(0, 34, 51, 0.08)',
background: '$accent-8',
borderRadius: '2px 10px 10px 10px',
minWidth: 36,
height: 20,
display: 'inline-flex',
gap: 4,
alignItems: 'center',
variants: {
active: {
true: {},
true: {
border: '1px solid $primary-1',
background: '$primary-3',
},
},
},
})

View File

@ -0,0 +1,55 @@
import React from 'react'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { AppProvider } from '~/src/contexts/app-context'
import { DialogProvider } from '~/src/contexts/dialog-context'
import { styled } from '~/src/styles/config'
import { MainSidebar } from '../components/main-sidebar'
import { Box } from '../system'
import { Chat } from './chat'
import { NewChat } from './new-chat'
import type { Config } from '~/src/types/config'
type Props = Config
export const Community = (props: Props) => {
const {
// theme,
// environment,
// publicKey,
router: Router = BrowserRouter,
} = props
const { options } = props
return (
<Router>
<AppProvider config={props}>
<DialogProvider>
<Box css={{ flex: '1 0 100%' }}>
<Wrapper>
{options.enableMembers && <MainSidebar />}
<Routes>
<Route path="/:id" element={<Chat />} />
<Route path="/new" element={<NewChat />} />
</Routes>
</Wrapper>
</Box>
</DialogProvider>
</AppProvider>
</Router>
)
}
export type { Props as CommunityProps }
const Wrapper = styled('div', {
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'stretch',
})