392 lines
13 KiB
Dart
392 lines
13 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:firebase_database/firebase_database.dart';
|
|
import 'dart:async';
|
|
|
|
enum ChatType {
|
|
all, // 전체 채팅
|
|
team // 팀 채팅
|
|
}
|
|
|
|
class ChatMessage {
|
|
final String message;
|
|
final String senderNickname;
|
|
final int timestamp;
|
|
|
|
ChatMessage({
|
|
required this.message,
|
|
required this.senderNickname,
|
|
required this.timestamp,
|
|
});
|
|
|
|
factory ChatMessage.fromSnapshot(DataSnapshot snapshot) {
|
|
final data = snapshot.value as Map<dynamic, dynamic>;
|
|
return ChatMessage(
|
|
message: data['message'] ?? '',
|
|
senderNickname: data['sender_nickname'] ?? data['sender_name'] ?? '',
|
|
timestamp: data['timestamp'] ?? 0,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ChatDialog extends StatefulWidget {
|
|
const ChatDialog({
|
|
Key? key,
|
|
required this.roomType,
|
|
required this.roomStatus,
|
|
required this.roomSeq,
|
|
required this.teamName,
|
|
required this.myNickname,
|
|
}) : super(key: key);
|
|
|
|
final String roomType;
|
|
final String roomStatus;
|
|
final String roomSeq;
|
|
final String teamName; // 사용자의 팀 이름
|
|
final String myNickname; // 사용자 닉네임
|
|
|
|
@override
|
|
State<ChatDialog> createState() => _ChatDialogState();
|
|
}
|
|
|
|
class _ChatDialogState extends State<ChatDialog> {
|
|
ChatType _selectedChatType = ChatType.all;
|
|
final TextEditingController _messageController = TextEditingController();
|
|
final ScrollController _scrollController = ScrollController();
|
|
List<ChatMessage> _messages = [];
|
|
|
|
late DatabaseReference _chatRef;
|
|
StreamSubscription<DatabaseEvent>? _chatSubscription;
|
|
|
|
bool get _isTeamChatEnabled => widget.roomType.toUpperCase() == 'TEAM';
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initChatRef();
|
|
}
|
|
|
|
void _initChatRef() {
|
|
_subscribeToChatMessages();
|
|
}
|
|
|
|
void _subscribeToChatMessages() {
|
|
_chatSubscription?.cancel();
|
|
|
|
final roomKey = 'korea-${widget.roomSeq}';
|
|
final path = _selectedChatType == ChatType.all
|
|
? 'rooms/$roomKey/chats/all'
|
|
: 'rooms/$roomKey/chats/team/${widget.teamName}';
|
|
|
|
_chatRef = FirebaseDatabase.instance.ref(path);
|
|
|
|
// orderByKey()를 사용하여 메시지 ID(timestamp) 기준으로 정렬
|
|
_chatSubscription = _chatRef
|
|
.orderByKey()
|
|
.onValue
|
|
.listen((event) {
|
|
if (!event.snapshot.exists) {
|
|
setState(() => _messages = []);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final messagesMap = event.snapshot.value as Map<dynamic, dynamic>;
|
|
final List<ChatMessage> messages = [];
|
|
|
|
messagesMap.forEach((key, value) {
|
|
if (value is Map) {
|
|
messages.add(ChatMessage(
|
|
message: value['message'] ?? '',
|
|
senderNickname: value['sender_nickname'] ?? '',
|
|
timestamp: value['timestamp'] ?? 0,
|
|
));
|
|
}
|
|
});
|
|
|
|
setState(() => _messages = messages);
|
|
_scrollToBottom();
|
|
} catch (e) {
|
|
setState(() => _messages = []);
|
|
}
|
|
});
|
|
}
|
|
|
|
void _scrollToBottom() {
|
|
if (_scrollController.hasClients) {
|
|
_scrollController.animateTo(
|
|
_scrollController.position.maxScrollExtent,
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.easeOut,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _sendMessage() async {
|
|
if (_messageController.text.trim().isEmpty) return;
|
|
|
|
try {
|
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
|
final messageId = 'id$timestamp'; // timestamp를 이용한 메시지 ID 생성
|
|
final message = _messageController.text.trim();
|
|
|
|
final messageData = {
|
|
'message': message,
|
|
'sender_nickname': widget.myNickname,
|
|
'timestamp': timestamp,
|
|
};
|
|
|
|
// messageId를 사용하여 직접 참조 생성
|
|
final newMessageRef = _chatRef.child(messageId);
|
|
|
|
await newMessageRef.set(messageData);
|
|
|
|
_messageController.clear();
|
|
_scrollToBottom();
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('message error: ${e.toString()}')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_chatSubscription?.cancel();
|
|
_messageController.dispose();
|
|
_scrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Dialog(
|
|
child: Container(
|
|
width: MediaQuery.of(context).size.width * 0.8,
|
|
height: MediaQuery.of(context).size.height * 0.7,
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
children: [
|
|
// 모달 헤더
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'Chat',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
],
|
|
),
|
|
// 채팅 타입 선택 탭 - TEAM 타입일 때만 표시
|
|
if (_isTeamChatEnabled) ...[
|
|
Container(
|
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[200],
|
|
borderRadius: BorderRadius.circular(25),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
setState(() => _selectedChatType = ChatType.all);
|
|
_subscribeToChatMessages();
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
decoration: BoxDecoration(
|
|
color: _selectedChatType == ChatType.all
|
|
? Colors.white
|
|
: Colors.transparent,
|
|
borderRadius: BorderRadius.circular(25),
|
|
boxShadow: _selectedChatType == ChatType.all
|
|
? [
|
|
BoxShadow(
|
|
color: Colors.grey.withOpacity(0.3),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
)
|
|
]
|
|
: null,
|
|
),
|
|
child: const Text(
|
|
'Global Chat',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
setState(() => _selectedChatType = ChatType.team);
|
|
_subscribeToChatMessages();
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
decoration: BoxDecoration(
|
|
color: _selectedChatType == ChatType.team
|
|
? Colors.white
|
|
: Colors.transparent,
|
|
borderRadius: BorderRadius.circular(25),
|
|
boxShadow: _selectedChatType == ChatType.team
|
|
? [
|
|
BoxShadow(
|
|
color: Colors.grey.withOpacity(0.3),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
)
|
|
]
|
|
: null,
|
|
),
|
|
child: const Text(
|
|
'Team Chat',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Divider(),
|
|
],
|
|
// 채팅 내용이 표시될 영역
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(8.0),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: ListView.builder(
|
|
controller: _scrollController,
|
|
itemCount: _messages.length,
|
|
itemBuilder: (context, index) {
|
|
final message = _messages[index];
|
|
final isMyMessage = message.senderNickname == widget.myNickname;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
|
child: Row(
|
|
mainAxisAlignment: isMyMessage
|
|
? MainAxisAlignment.end
|
|
: MainAxisAlignment.start,
|
|
children: [
|
|
if (!isMyMessage) ...[
|
|
Text(
|
|
message.senderNickname,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
],
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 8,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: isMyMessage ? Colors.blue[100] : Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: Colors.grey[300]!,
|
|
),
|
|
),
|
|
child: Text(message.message),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
// 메시지 입력 영역
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _messageController,
|
|
decoration: InputDecoration(
|
|
hintText: 'Enter a message',
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 8,
|
|
),
|
|
),
|
|
onSubmitted: (_) => _sendMessage(),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
IconButton(
|
|
icon: const Icon(Icons.send),
|
|
onPressed: _sendMessage,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ChatButton extends StatelessWidget {
|
|
const ChatButton({
|
|
Key? key,
|
|
required this.roomType,
|
|
required this.roomStatus,
|
|
required this.roomSeq,
|
|
required this.teamName,
|
|
required this.myNickname,
|
|
}) : super(key: key);
|
|
|
|
final String roomType;
|
|
final String roomStatus;
|
|
final String roomSeq;
|
|
final String teamName;
|
|
final String myNickname;
|
|
|
|
void _openChatDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => ChatDialog(
|
|
roomType: roomType,
|
|
roomStatus: roomStatus,
|
|
roomSeq: roomSeq,
|
|
teamName: teamName,
|
|
myNickname: myNickname,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FloatingActionButton(
|
|
onPressed: () => _openChatDialog(context),
|
|
child: const Icon(Icons.chat),
|
|
mini: true,
|
|
);
|
|
}
|
|
} |