feat(react): implement message reactions

This commit is contained in:
Pavel Prichodko 2022-04-14 12:37:59 +02:00
parent 1080dd7ee3
commit 151b80bc9b
No known key found for this signature in database
GPG Key ID: 0EB8D75C775AB6F1
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 { styled } from '~/src/styles/config'
import { Flex, Image, Popover, PopoverTrigger } from '~/src/system' import { Flex, Image, Popover, PopoverTrigger } from '~/src/system'
import type { Reaction, Reactions } from '~/src/protocol/use-messages'
interface Props { interface Props {
children: React.ReactElement children: React.ReactElement
onClick: (emoji: string) => void reactions: Reactions
onClick: (reaction: Reaction) => void
open?: boolean open?: boolean
onOpenChange?: (open: boolean) => void 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) => { export const ReactionPopover = (props: Props) => {
const { children, onClick, ...popoverProps } = props const { reactions, children, onClick, ...popoverProps } = props
return ( return (
<PopoverTrigger {...popoverProps}> <PopoverTrigger {...popoverProps}>
{children} {children}
<Popover side="top" align="center" sideOffset={6}> <Popover side="top" align="center" sideOffset={6}>
<Flex gap={1} css={{ padding: 8 }}> <Flex gap={1} css={{ padding: 8 }}>
<Button onClick={() => onClick('')} active={false}> {Object.entries(reactions).map(([reaction, value]) => {
<Image const emoji = emojis[reaction as Reaction]
width={30} return (
src="https://twemoji.maxcdn.com/v/latest/svg/2764.svg" <Button
alt="React with ❤️" key={reaction}
/> onClick={() => onClick(reaction as Reaction)}
</Button> active={value.me}
<Button onClick={() => onClick('')} active> aria-label={`React with ${emoji.symbol}`}
<Image >
width={30} <Image width={30} src={emoji.url} alt={emoji.symbol} />
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> </Button>
)
})}
</Flex> </Flex>
</Popover> </Popover>
</PopoverTrigger> </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 { interface BaseMessage {
id: string id: string
type: 'text' | 'image' | 'image-text' type: 'text' | 'image' | 'image-text'
@ -9,6 +24,7 @@ interface BaseMessage {
pinned: boolean pinned: boolean
mention: boolean mention: boolean
reply?: TextReply | ImageReply | ImageTextReply reply?: TextReply | ImageReply | ImageTextReply
reactions: Reactions
} }
interface TextMessage extends BaseMessage { interface TextMessage extends BaseMessage {
@ -68,6 +84,14 @@ export const useMessages = (): Message[] => {
owner: false, owner: false,
pinned: true, pinned: true,
mention: 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: { reply: {
contact: { contact: {
name: 'Leila Joyner', name: 'Leila Joyner',
@ -90,6 +114,14 @@ export const useMessages = (): Message[] => {
owner: false, owner: false,
pinned: false, pinned: false,
mention: 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: { reply: {
contact: { contact: {
name: 'Leila Joyner', name: 'Leila Joyner',
@ -113,6 +145,14 @@ export const useMessages = (): Message[] => {
owner: false, owner: false,
pinned: false, pinned: false,
mention: true, 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: { reply: {
contact: { contact: {
name: 'Leila Joyner', name: 'Leila Joyner',
@ -137,6 +177,14 @@ export const useMessages = (): Message[] => {
owner: false, owner: false,
pinned: false, pinned: false,
mention: 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', id: '5',
@ -150,6 +198,14 @@ export const useMessages = (): Message[] => {
owner: true, owner: true,
pinned: false, pinned: false,
mention: 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: { reply: {
contact: { contact: {
name: 'Leila Joyner', name: 'Leila Joyner',
@ -174,6 +230,14 @@ export const useMessages = (): Message[] => {
owner: false, owner: false,
pinned: false, pinned: false,
mention: 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', id: '5',
@ -187,6 +251,14 @@ export const useMessages = (): Message[] => {
owner: true, owner: true,
pinned: false, pinned: false,
mention: 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 { ReactionPopover } from '~/src/components/reaction-popover'
import { PencilIcon } from '~/src/icons/pencil-icon' import { PencilIcon } from '~/src/icons/pencil-icon'
@ -15,6 +15,8 @@ import {
Tooltip, Tooltip,
} from '~/src/system' } from '~/src/system'
import type { Reactions } from '~/src/protocol/use-messages'
interface Props { interface Props {
owner: boolean owner: boolean
pinned: boolean pinned: boolean
@ -22,6 +24,7 @@ interface Props {
onEditClick: () => void onEditClick: () => void
reacting: boolean reacting: boolean
onReactingChange: (reacting: boolean) => void onReactingChange: (reacting: boolean) => void
reactions: Reactions
} }
export const Actions = (props: Props) => { export const Actions = (props: Props) => {
@ -32,11 +35,13 @@ export const Actions = (props: Props) => {
onEditClick, onEditClick,
reacting, reacting,
onReactingChange, onReactingChange,
reactions,
} = props } = props
return ( return (
<Wrapper open={reacting}> <Wrapper open={reacting}>
<ReactionPopover <ReactionPopover
reactions={reactions}
open={reacting} open={reacting}
onOpenChange={onReactingChange} onOpenChange={onReactingChange}
onClick={emoji => { onClick={emoji => {

View File

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

View File

@ -1,115 +1,104 @@
import React from 'react' 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 { ReactionIcon } from '~/src/icons/reaction-icon'
import { Reaction } from '~/src/protocol/use-messages'
import { styled } from '~/src/styles/config' 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 { interface Props {
onClick: (reaction: string) => void reactions: Reactions
onClick: (reaction: Reaction) => void
} }
export const Reactions = (props: Props) => { export const MessageReactions = (props: Props) => {
const { onClick } = props const { reactions, onClick } = props
const hasReaction = Object.values(reactions).some(
reaction => reaction.count !== 0
)
if (hasReaction === false) {
return null
}
return ( return (
<Flex css={{ paddingTop: 6 }} gap={1}> <Flex align="center" css={{ paddingTop: 6 }} gap={1}>
<Button {Object.entries(reactions).map(([reaction, value]) => (
onClick={() => onClick('')} <Reaction
active={false} key={reaction}
aria-label="❤️, 1 reaction, press to react" emoji={emojis[reaction as Reaction]}
> reaction={value}
<Image onClick={() => onClick(reaction as Reaction)}
width={16}
src="https://twemoji.maxcdn.com/v/latest/svg/2764.svg"
alt="❤️"
/> />
1 ))}
</Button>
<Button <ReactionPopover reactions={reactions} onClick={onClick}>
onClick={() => onClick('')} <AddReactionButton aria-label="Add Reaction">
active={true} <ReactionIcon width={16} height={16} />
aria-label="👍️, 1 reaction, press to react" </AddReactionButton>
>
<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> </ReactionPopover>
</Flex> </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', { const Button = styled('button', {
padding: 2, padding: '0px 8px 0px 3px',
boxShadow: '0px 4px 12px rgba(0, 34, 51, 0.08)', boxShadow: '0px 4px 12px rgba(0, 34, 51, 0.08)',
background: '$accent-8', background: '$accent-8',
borderRadius: '2px 10px 10px 10px', borderRadius: '2px 10px 10px 10px',
minWidth: 36, minWidth: 36,
height: 20, height: 20,
display: 'inline-flex', display: 'inline-flex',
gap: 4,
alignItems: 'center', alignItems: 'center',
variants: { variants: {
active: { 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',
})