feat(react): implement message reactions
This commit is contained in:
parent
1080dd7ee3
commit
151b80bc9b
|
@ -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 😡"
|
||||
/>
|
||||
{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>
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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',
|
||||
})
|
Loading…
Reference in New Issue