Skip to content

Commit

Permalink
Merge pull request #286 from boostcampwm-2024/Feature/fe/Chat-cleanBot
Browse files Browse the repository at this point in the history
[Feature] 채팅 클린봇 모드 구현
  • Loading branch information
chologmaesil authored Nov 28, 2024
2 parents 77c4e1f + 4f64846 commit d00e094
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 27 deletions.
6 changes: 3 additions & 3 deletions Frontend/src/components/chat/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ export default function ChatHeader({ onClose, onSettingsClick }: ChatHeaderProps
aria-label="채팅창 닫기"
type="button"
onClick={onClose}
className="text-lico-gray-2 hover:text-lico-gray-1"
className="rounded-md p-2 text-lico-gray-2 hover:bg-lico-gray-3"
>
{videoPlayerState && isVerticalMode ? <LuArrowDownToLine size={20} /> : <LuArrowRightToLine size={20} />}
</button>
<h3 className="font-bold text-base text-lico-orange-2">채팅</h3>
<h3 className="font-bold text-xl text-lico-orange-2">채팅</h3>
<button
aria-label="채팅창 설정"
type="button"
onClick={onSettingsClick}
className="rounded-md p-2 text-lico-gray-2 hover:text-lico-gray-1"
className="rounded-md p-2 text-lico-gray-2 hover:bg-lico-gray-3"
>
<HiDotsVertical size={20} />
</button>
Expand Down
14 changes: 10 additions & 4 deletions Frontend/src/components/chat/ChatMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
interface ChatMessageProps {
id: number;
userId: number;
content: string;
nickname: string;
timestamp?: string;
color?: string;
onUserClick: (userId: number, element: HTMLElement) => void;
filteringResult: boolean;
}

export default function ChatMessage({
id,
userId,
content,
nickname,
timestamp = '',
color = 'text-lico-orange-2',
onUserClick,
filteringResult,
}: ChatMessageProps) {
return (
<button
type="button"
className="z-20 rounded-3xl p-1.5 hover:bg-lico-gray-3"
onClick={e => onUserClick(id, e.currentTarget)}
onClick={e => onUserClick(userId, e.currentTarget)}
>
<div className="flex gap-1.5 px-1">
{timestamp && <span className="font-medium text-xs text-lico-gray-2">{timestamp}</span>}
<span className={`max-w-40 truncate whitespace-nowrap font-bold text-base ${color}`}>{nickname}</span>
<p className="flex items-center break-all text-start font-medium text-sm text-lico-gray-1">{content}</p>
<p
className={`flex items-center break-all text-start font-medium text-sm ${filteringResult ? 'text-lico-gray-1' : 'text-lico-gray-2'}`}
>
{content}
</p>
</div>
</button>
);
Expand Down
54 changes: 54 additions & 0 deletions Frontend/src/components/chat/ChatSettingsMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { RiRobot2Line } from 'react-icons/ri';
import { BsBoxArrowUpRight } from 'react-icons/bs';
import Toggle from '@components/common/Toggle';

interface ChatSettingsMenuProps {
onClose?: () => void;
position?: {
top: string;
right: string;
};
cleanBotEnabled: boolean;
onCleanBotChange: (enabled: boolean) => void;
}

function ChatSettingsMenu({
onClose,
position = { top: '50px', right: '8px' },
cleanBotEnabled,
onCleanBotChange,
}: ChatSettingsMenuProps) {
return (
<>
<button aria-label="채팅세팅메뉴 닫기" type="button" className="z fixed inset-0" onClick={onClose} />
<div
className="absolute z-50 min-w-[252px] rounded-lg border border-lico-gray-3 bg-lico-gray-5 p-1 shadow-lg"
style={{
top: position.top,
right: position.right,
}}
>
<div className="flex w-full items-center justify-between rounded-md px-3 py-2 hover:bg-lico-gray-4">
<div className="flex items-center gap-3 font-bold text-lg text-lico-gray-2">
<RiRobot2Line size={18} />
<span>클린봇</span>
</div>
<Toggle checked={cleanBotEnabled} onChange={onCleanBotChange} />
</div>

<button
type="button"
onClick={() => window.open('/chat-popup', '_blank', 'width=400,height=600')}
className="flex w-full items-center justify-between rounded-md px-3 py-2 hover:bg-lico-gray-4"
>
<div className="flex items-center gap-3 font-bold text-lg text-lico-gray-2">
<BsBoxArrowUpRight size={16} />
<span>채팅창 팝업</span>
</div>
</button>
</div>
</>
);
}

export default ChatSettingsMenu;
80 changes: 60 additions & 20 deletions Frontend/src/components/chat/ChatWindow.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { io, Socket } from 'socket.io-client';
import { FaAngleDown } from 'react-icons/fa';
import ChatHeader from '@components/chat/ChatHeader';
import ChatInput from '@components/chat/ChatInput';
import useLayoutStore from '@store/useLayoutStore';
import { getConsistentTextColor } from '@utils/chatUtils';
import { io, Socket } from 'socket.io-client';
import { chatApi } from '@apis/chat';
import { useAuthStore } from '@store/useAuthStore';
import { config } from '@config/env';
import useLayoutStore from '@store/useLayoutStore';
import ChatHeader from '@components/chat/ChatHeader';
import ChatInput from '@components/chat/ChatInput';
import ChatProfileModal from '@components/chat/ChatProfileModal';
import PendingMessageNotification from '@components/chat/PendingMessageNotification';
import { chatApi } from '@apis/chat.ts';
import type { Message } from '@/types/live';
import ChatSettingsMenu from '@components/chat/ChatSettingsMenu';
import ChatMessage from './ChatMessage';
import type { Message } from '@/types/live';

interface ChatWindowProps {
onAir: boolean;
Expand All @@ -24,6 +25,7 @@ interface SelectedMessage {
}

const MESSAGE_LIMIT = 200;
const CLEAN_BOT_MESSAGE = '클린봇이 삭제한 메세지입니다.';

const updateMessagesWithLimit = (prevMessages: Message[], newMessages: Message[]) => {
const updatedMessages = [...prevMessages, ...newMessages];
Expand All @@ -35,6 +37,8 @@ export default function ChatWindow({ onAir, id }: ChatWindowProps) {
const [showScrollButton, setShowScrollButton] = useState(false);
const [selectedMessage, setSelectedMessage] = useState<SelectedMessage | null>(null);
const [isScrollPaused, setIsScrollPaused] = useState(false);
const [showChatSettingsMenu, setShowChatSettingsMenu] = useState(false);
const [cleanBotEnabled, setCleanBotEnabled] = useState(false);
const [showPendingMessages, setShowPendingMessages] = useState(false);
const [pendingMessages, setPendingMessages] = useState<Message[]>([]);
const chatRef = useRef<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -79,9 +83,18 @@ export default function ChatWindow({ onAir, id }: ChatWindowProps) {
}, 100);
};

const handleCleanBotChange = (enabled: boolean) => {
setCleanBotEnabled(enabled);
};

const chatHandler = useCallback(
(data: string[]) => {
const newMessages = data.map(v => JSON.parse(v));
const newMessages = data
.map(v => JSON.parse(v))
.map(msg => ({
...msg,
content: msg.filteringResult ? msg.content : CLEAN_BOT_MESSAGE,
}));

if (isScrollPaused) {
setShowPendingMessages(true);
Expand Down Expand Up @@ -131,38 +144,63 @@ export default function ChatWindow({ onAir, id }: ChatWindowProps) {

const socket = io(`${config.chatUrl}`, {
transports: ['websocket'],
extraHeaders: {
Authorization: `Bearer ${accessToken}`, // 헤더에 토큰 추가
},
});

socket.emit('join', { channelId: id });

socketRef.current = socket;

return () => {
if (!onAir) {
socket.disconnect();
socket.removeAllListeners();
socketRef.current = null;
}
socket.disconnect();
socket.removeAllListeners();
socketRef.current = null;
};
}, [onAir, accessToken, id]);
}, [onAir, id]);

useEffect(() => {
const socket = socketRef.current;
if (!socket) return undefined;

socket.off('chat').on('chat', chatHandler);

const handleFilter = (data: { chatId: string; filteringResult: boolean }[]) => {
const filteredMessage = data[0];
if (!cleanBotEnabled || filteredMessage.filteringResult) return;

setMessages(prevMessages => {
const messageIndex = prevMessages.findIndex(msg => msg.chatId === filteredMessage.chatId);

if (messageIndex === -1 || prevMessages[messageIndex].content === CLEAN_BOT_MESSAGE) {
return prevMessages;
}

const updatedMessages = [...prevMessages];
updatedMessages[messageIndex] = {
...updatedMessages[messageIndex],
content: CLEAN_BOT_MESSAGE,
filteringResult: false,
};
return updatedMessages;
});
};

socket.off('filter').on('filter', handleFilter);

return () => {
socket.off('chat');
socket.off('filter');
};
}, [chatHandler]);
}, [chatHandler, cleanBotEnabled]);

return (
<div className="relative flex h-full flex-col border-l border-lico-gray-3 bg-lico-gray-4">
<ChatHeader onClose={toggleChat} onSettingsClick={() => {}} />
<ChatHeader onClose={toggleChat} onSettingsClick={() => setShowChatSettingsMenu(!showChatSettingsMenu)} />
{showChatSettingsMenu && (
<ChatSettingsMenu
onClose={() => setShowChatSettingsMenu(false)}
cleanBotEnabled={cleanBotEnabled}
onCleanBotChange={handleCleanBotChange}
/>
)}
<div
role="log"
aria-label="채팅 메시지"
Expand All @@ -174,10 +212,12 @@ export default function ChatWindow({ onAir, id }: ChatWindowProps) {
<div className="flex break-after-all flex-col">
{messages?.map(message => (
<ChatMessage
id={message.userId}
key={message.chatId}
userId={message.userId}
nickname={message.nickname}
content={message.content}
color={getConsistentTextColor(message.nickname)}
filteringResult={message.filteringResult}
onUserClick={(userId, element) => handleUserClick(userId, element)}
/>
))}
Expand Down
35 changes: 35 additions & 0 deletions Frontend/src/components/common/Toggle/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
interface ToggleProps {
checked: boolean;
onChange: (checked: boolean) => void;
className?: string;
}

function Toggle({ checked, onChange, className = '' }: ToggleProps) {
return (
<button
type="button"
onClick={() => onChange(!checked)}
className={`relative inline-flex h-6 w-14 items-center rounded-full transition-colors duration-200 ease-in-out ${checked ? 'bg-lico-orange-1' : 'bg-lico-gray-3'} ${className}`}
>
<div className="absolute inset-0 flex items-center justify-between px-1.5 font-medium text-xs">
<span
className={`font-bold text-lico-gray-3 transition-opacity duration-200 ${checked ? 'opacity-100' : 'opacity-0'}`}
>
ON
</span>
<span
className={`font-bold text-lico-gray-1 transition-opacity duration-200 ${checked ? 'opacity-0' : 'opacity-100'}`}
>
OFF
</span>
</div>
<div
className={`${
checked ? 'translate-x-8' : 'translate-x-1'
} z-10 inline-block h-4 w-4 transform rounded-full bg-lico-gray-1 transition duration-300 ease-in-out`}
/>
</button>
);
}

export default Toggle;
2 changes: 2 additions & 0 deletions Frontend/src/types/live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,6 @@ export interface Message {
nickname: string;
content: string;
timestamp: string;
chatId: string;
filteringResult: boolean;
}

0 comments on commit d00e094

Please sign in to comment.