+ {defaultChannels.length > 0 && (
+
+
Channels
+ {defaultChannels.map(renderChannel)}
+
+ )}
+
+ {customChannels.length > 0 && (
+
+
Custom Channels
+ {customChannels.map(renderChannel)}
+
+ )}
+
+ );
+}
diff --git a/src/frontend/components/GameForgeChat/ChannelView.css b/src/frontend/components/GameForgeChat/ChannelView.css
new file mode 100644
index 0000000..94c5a3a
--- /dev/null
+++ b/src/frontend/components/GameForgeChat/ChannelView.css
@@ -0,0 +1,46 @@
+.channel-view {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background: var(--bg-primary, #ffffff);
+}
+
+.channel-view.loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--text-secondary, #666);
+}
+
+.channel-header {
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--border-color, #e0e0e0);
+ background: var(--bg-secondary, #fafafa);
+}
+
+.channel-header h3 {
+ margin: 0 0 4px 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary, #333);
+}
+
+.channel-description {
+ margin: 0;
+ font-size: 13px;
+ color: var(--text-secondary, #666);
+}
+
+.restricted-badge {
+ display: inline-block;
+ margin-top: 8px;
+ padding: 4px 8px;
+ background: var(--warning-bg, #fff3cd);
+ color: var(--warning-text, #856404);
+ border: 1px solid var(--warning-border, #ffeeba);
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+/* Reuse MessageList and MessageInput from Chat component */
diff --git a/src/frontend/components/GameForgeChat/ChannelView.jsx b/src/frontend/components/GameForgeChat/ChannelView.jsx
new file mode 100644
index 0000000..7031598
--- /dev/null
+++ b/src/frontend/components/GameForgeChat/ChannelView.jsx
@@ -0,0 +1,175 @@
+import React, { useState, useEffect } from 'react';
+import { useSocket } from '../../contexts/SocketContext';
+import MessageList from '../Chat/MessageList';
+import MessageInput from '../Chat/MessageInput';
+import { encryptMessage, decryptMessage } from '../../utils/crypto';
+import { useAuth } from '../../contexts/AuthContext';
+import './ChannelView.css';
+
+export default function ChannelView({ channel, projectId }) {
+ const { socket } = useSocket();
+ const { user } = useAuth();
+ const [messages, setMessages] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ if (channel) {
+ loadMessages();
+ }
+ }, [channel]);
+
+ // Socket listeners
+ useEffect(() => {
+ if (!socket || !channel) return;
+
+ const handleNewMessage = async (data) => {
+ if (data.conversationId === channel.id) {
+ const message = data.message;
+
+ // Decrypt if not system message
+ if (message.contentType !== 'system') {
+ try {
+ const decrypted = await decryptMessage(
+ JSON.parse(message.content),
+ user.password,
+ user.publicKey
+ );
+ message.content = decrypted;
+ } catch (error) {
+ console.error('Failed to decrypt message:', error);
+ message.content = '[Failed to decrypt]';
+ }
+ }
+
+ setMessages(prev => [message, ...prev]);
+ }
+ };
+
+ socket.on('message:new', handleNewMessage);
+
+ return () => {
+ socket.off('message:new', handleNewMessage);
+ };
+ }, [socket, channel]);
+
+ const loadMessages = async () => {
+ try {
+ setLoading(true);
+
+ const response = await fetch(`/api/conversations/${channel.id}/messages`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ // Decrypt messages
+ const decryptedMessages = await Promise.all(
+ data.messages.map(async (msg) => {
+ // System messages are not encrypted
+ if (msg.contentType === 'system') {
+ return msg;
+ }
+
+ try {
+ const decrypted = await decryptMessage(
+ JSON.parse(msg.content),
+ user.password,
+ user.publicKey
+ );
+
+ return {
+ ...msg,
+ content: decrypted
+ };
+ } catch (error) {
+ console.error('Failed to decrypt message:', error);
+ return {
+ ...msg,
+ content: '[Failed to decrypt]'
+ };
+ }
+ })
+ );
+
+ setMessages(decryptedMessages);
+ }
+
+ } catch (error) {
+ console.error('Failed to load messages:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const sendMessage = async (content) => {
+ if (!content.trim()) return;
+
+ try {
+ // Get recipient public keys (all channel participants)
+ const participantsResponse = await fetch(`/api/conversations/${channel.id}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ const participantsData = await participantsResponse.json();
+
+ if (!participantsData.success) {
+ throw new Error('Failed to get participants');
+ }
+
+ const recipientKeys = participantsData.conversation.participants
+ .map(p => p.publicKey)
+ .filter(Boolean);
+
+ // Encrypt message
+ const encrypted = await encryptMessage(content, recipientKeys);
+
+ // Send via WebSocket
+ socket.emit('message:send', {
+ conversationId: channel.id,
+ content: JSON.stringify(encrypted),
+ contentType: 'text',
+ clientId: `temp-${Date.now()}`
+ });
+
+ } catch (error) {
+ console.error('Failed to send message:', error);
+ }
+ };
+
+ if (loading) {
+ return Loading messages...
;
+ }
+
+ return (
+