diff --git a/packages/chat/src/chat-bubble/chat-bubble-ui.stories.tsx b/packages/chat/src/chat-bubble/chat-bubble-ui.stories.tsx index 7711b25d87..4306c0e815 100644 --- a/packages/chat/src/chat-bubble/chat-bubble-ui.stories.tsx +++ b/packages/chat/src/chat-bubble/chat-bubble-ui.stories.tsx @@ -40,6 +40,7 @@ export const Text = { unreadCount: 1, createdAt: new Date(2022, 10, 1).toISOString(), profileName: '테스트계정', + thanks: { count: 1, haveMine: false }, }, } diff --git a/packages/chat/src/chat-bubble/chat-bubble-ui.tsx b/packages/chat/src/chat-bubble/chat-bubble-ui.tsx index da0c8f577c..801ee1a922 100644 --- a/packages/chat/src/chat-bubble/chat-bubble-ui.tsx +++ b/packages/chat/src/chat-bubble/chat-bubble-ui.tsx @@ -20,6 +20,7 @@ import { } from './elements' import BubblePayload from './bubble-payload' import BlindedBubble from './blinded' +import Thanks from './thanks' const CHAT_CONTAINER_STYLES = { marginTop: 20, @@ -28,53 +29,76 @@ const CHAT_CONTAINER_STYLES = { width: '100%', } as const +interface ChatContainerProps { + createdAt?: string + unreadCount: number | null + thanks?: { count: number; haveMine: boolean } + onThanksClick?: () => void + showBubbleInfo: boolean +} + function SentChatContainer({ createdAt, unreadCount, onRetry, onCancel, + thanks, + onThanksClick, children, showBubbleInfo, -}: PropsWithChildren<{ - createdAt?: string - unreadCount: number | null - onRetry?: () => Promise | undefined - onCancel?: () => void - showBubbleInfo: boolean -}>) { +}: PropsWithChildren< + { + onRetry?: () => Promise | undefined + onCancel?: () => void + } & ChatContainerProps +>) { const [show, setShow] = useState(true) return show ? ( - - {!createdAt ? ( - - { - if (await onRetry?.()) { + +
+ {!createdAt ? ( + + { + if (await onRetry?.()) { + setShow(false) + } + }} + /> + { + onCancel?.() setShow(false) - } - }} - /> - { - onCancel?.() - setShow(false) - }} - /> - - ) : ( - <> - {showBubbleInfo && ( - - )} - - )} - - {children} + + ) : ( + <> + {showBubbleInfo && ( + + )} + + )} + {children} +
+ {thanks && onThanksClick ? ( + + ) : null}
) : null } @@ -84,16 +108,15 @@ function ReceivedChatContainer({ profileName, unreadCount, createdAt, + thanks, + onThanksClick, showBubbleInfo, children, }: { profileImageUrl?: string profileName?: string - unreadCount: number | null - createdAt?: string - showBubbleInfo: boolean children: React.ReactNode -}) { +} & ChatContainerProps) { return ( @@ -111,6 +134,15 @@ function ReceivedChatContainer({ css={{ marginLeft: 8, textAlign: 'left' }} /> ) : null} + + {thanks && onThanksClick ? ( + + ) : null}
) @@ -139,6 +171,8 @@ export interface ChatBubbleUIProps { * 'sent' 타입일 때, 메시지 전송 실패할 경우 재시도를 취소하는 함수 */ onCancel?: () => void + onThanksClick?: () => void + thanks?: { count: number; haveMine: boolean } bubbleStyle?: ChatBubbleStyle } @@ -152,8 +186,12 @@ export function ChatBubbleUI({ blindedAt, blindedText, onRetry, + thanks, + onThanksClick, bubbleStyle, }: ChatBubbleUIProps) { + const showThanks = !blindedAt + switch (type) { case 'sent': { const sentBubbleStyle = bubbleStyle?.sent @@ -163,6 +201,7 @@ export function ChatBubbleUI({ showBubbleInfo={payload.type !== MessageType.PRODUCT} unreadCount={unreadCount} onRetry={onRetry} + {...(showThanks && { thanks, onThanksClick })} > {blindedAt ? ( {blindedAt ? ( void onRetryCancelButtonClick?: (message: MessageInterface) => void + onThanksClick?: (id: number, haveMyThanks: boolean) => void blindedText?: string bubbleStyle?: ChatBubbleStyle } @@ -36,6 +37,7 @@ const ChatBubble = ({ postMessageAction, onRetryButtonClick, onRetryCancelButtonClick, + onThanksClick, disableUnreadCount = false, blindedText, bubbleStyle, @@ -96,6 +98,16 @@ const ChatBubble = ({ onCancel={onCancel} blindedAt={message.blindedAt} blindedText={blindedText} + thanks={message.reactions?.thanks} + onThanksClick={ + onThanksClick + ? () => { + if (message.reactions?.thanks) { + onThanksClick?.(message.id, message.reactions.thanks.haveMine) + } + } + : undefined + } bubbleStyle={bubbleStyle} /> ) diff --git a/packages/chat/src/chat-bubble/thanks.tsx b/packages/chat/src/chat-bubble/thanks.tsx new file mode 100644 index 0000000000..bcbe4553ad --- /dev/null +++ b/packages/chat/src/chat-bubble/thanks.tsx @@ -0,0 +1,44 @@ +import { Button } from '@titicaca/core-elements' +import styled from 'styled-components' + +const ThanksButton = styled(Button)<{ haveMine: boolean }>` + display: flex; + gap: 3px; + height: 20px; + align-items: center; + background-color: ${({ haveMine }) => + haveMine ? 'var(--color-white)' : 'var(--color-gray50)'}; + color: ${({ haveMine }) => (haveMine ? '#1DBEB2' : 'var(--color-gray700)')}; + ${({ haveMine }) => (haveMine ? 'border: 1px solid #1DBEB2;' : '')} + padding: 3.5px 6px 4.5px 7px; + font-weight: ${({ haveMine }) => (haveMine ? '700' : '500')}; + font-size: 10px; +` + +const ThanksCount = styled.span` + font-size: 10px; + line-height: 11px; +` + +export default function Thanks({ + count, + haveMine, + onClick, + ...props +}: { + count: number + haveMine: boolean + onClick?: () => void +}) { + return ( + onClick?.()} {...props}> + 좋아요 아이콘 + {count === 0 ? null : {count}} + + ) +} diff --git a/packages/chat/src/chat/chat.tsx b/packages/chat/src/chat/chat.tsx index de07412242..2227089f64 100644 --- a/packages/chat/src/chat/chat.tsx +++ b/packages/chat/src/chat/chat.tsx @@ -11,7 +11,9 @@ import { ImagePayload, MessageInterface, PostMessageType, + ReactionType, RoomInterface, + RoomType, TextPayload, UpdateChatData, UserInterface, @@ -55,7 +57,14 @@ export interface ChatProps { showFailToast?: (message: string) => void onRetryButtonClick?: () => void onRetryCancelButtonClick?: () => void - + addReactions?: ( + messageId: number, + reaction: ReactionType, + ) => Promise<{ success: boolean }> + removeReactions?: ( + messageId: number, + reaction: ReactionType, + ) => Promise<{ success: boolean }> updateChatData?: UpdateChatData disableUnreadCount?: boolean blindedText?: string @@ -73,7 +82,6 @@ export const Chat = ({ room, messages: initMessages, beforeSentMessages = [], - postMessage, getMessages, getUnreadRoom, @@ -81,6 +89,8 @@ export const Chat = ({ showFailToast, onRetryButtonClick, onRetryCancelButtonClick, + addReactions, + removeReactions, updateChatData, disableUnreadCount = false, blindedText, @@ -282,6 +292,46 @@ export const Chat = ({ onRetryButtonClick?.() } + async function onThanksClick(messageId: number, haveMyThanks: boolean) { + if (!haveMyThanks && addReactions) { + const { success } = await addReactions(messageId, 'thanks') + const message = messages.find((message) => message.id === messageId) + if (success && message) { + dispatch({ + action: ChatActions.UPDATE_MESSAGE, + message: { + ...message, + reactions: { + thanks: { + count: (message.reactions?.thanks?.count || 0) + 1, + haveMine: true, + }, + }, + }, + }) + } + } + if (haveMyThanks && removeReactions) { + const { success } = await removeReactions(messageId, 'thanks') + const message = messages.find((message) => message.id === messageId) + if (success && message) { + const thanksCount = message.reactions?.thanks?.count + dispatch({ + action: ChatActions.UPDATE_MESSAGE, + message: { + ...message, + reactions: { + thanks: { + count: thanksCount ? thanksCount - 1 : 0, + haveMine: false, + }, + }, + }, + }) + } + } + } + return ( <> @@ -304,6 +354,9 @@ export const Chat = ({ onRetryCancelButtonClick={onRetryCancel} disableUnreadCount={disableUnreadCount} blindedText={blindedText} + onThanksClick={ + room.type === RoomType.EVENT ? onThanksClick : undefined + } bubbleStyle={bubbleStyle} /> @@ -323,6 +376,9 @@ export const Chat = ({ onRetryCancelButtonClick={onRetryCancel} disableUnreadCount={disableUnreadCount} blindedText={blindedText} + onThanksClick={ + room.type === RoomType.EVENT ? onThanksClick : undefined + } bubbleStyle={bubbleStyle} /> diff --git a/packages/chat/src/chat/reducer.ts b/packages/chat/src/chat/reducer.ts index f5bad16e30..5018ef1d37 100644 --- a/packages/chat/src/chat/reducer.ts +++ b/packages/chat/src/chat/reducer.ts @@ -7,6 +7,7 @@ export enum ChatActions { POST, // 메시지 전송 FAILED_TO_POST, // 메시지 전송 실패 UPDATE, // 읽음 표시 업데이트 + UPDATE_MESSAGE, // 메시지 하나 업데이트 REMOVE_FROM_FAILED, // 전송 실패 메세지 재전송 또는 삭제 } @@ -46,6 +47,7 @@ export type ChatAction = action: ChatActions.UPDATE otherUnreadInfo: OtherUnreadInterface[] } + | { action: ChatActions.UPDATE_MESSAGE; message: MessageInterface } | { action: ChatActions.REMOVE_FROM_FAILED; message: MessageInterface } export const ChatReducer = ( @@ -101,7 +103,13 @@ export const ChatReducer = ( ...state, otherUnreadInfo: action.otherUnreadInfo, } - + case ChatActions.UPDATE_MESSAGE: + return { + ...state, + messages: state.messages.map((message) => + message.id === action.message.id ? action.message : message, + ), + } case ChatActions.REMOVE_FROM_FAILED: return { ...state, diff --git a/packages/chat/src/types/index.ts b/packages/chat/src/types/index.ts index 6e5dab6327..bc22371385 100644 --- a/packages/chat/src/types/index.ts +++ b/packages/chat/src/types/index.ts @@ -62,6 +62,7 @@ export interface RoomInterface { export type DisplayTargetAll = 'all' +export type ReactionType = 'thanks' export interface MessageInterface { id: number roomId: string @@ -72,6 +73,7 @@ export interface MessageInterface { alternative?: TextPayload | ImagePayload | RichPayload blindedAt?: string sender: UserInterface + reactions?: { [type in ReactionType]?: { count: number; haveMine: boolean } } } export interface UserInterface { diff --git a/packages/chat/src/utils/constants.ts b/packages/chat/src/utils/constants.ts index 0cc0878bc2..fda3f9b05a 100644 --- a/packages/chat/src/utils/constants.ts +++ b/packages/chat/src/utils/constants.ts @@ -92,6 +92,9 @@ export const CHAT_ARGS: ChatProps = { message: '', }, }, + reactions: { + thanks: { count: 0, haveMine: false }, + }, }, { id: 5749, @@ -120,7 +123,7 @@ export const CHAT_ARGS: ChatProps = { }, room: { id: '6344c73a53749900140bca43', - type: RoomType.DEFAULT, + type: RoomType.DEFAULT, // RoomType.EVENT로 수정 시 좋아요 버튼을 확인할 수 있습니다. createdAt: '2022-10-11T01:30:34.519Z', name: '', isDirect: true, @@ -207,4 +210,10 @@ export const CHAT_ARGS: ChatProps = { ], } }, + addReactions: async () => { + return { success: true } + }, + removeReactions: async () => { + return { success: true } + }, }