실시간 채팅 테스트1차
This commit is contained in:
parent
dcd246f589
commit
d07c6f93dd
392
lib/dialogs/chat_dialog.dart
Normal file
392
lib/dialogs/chat_dialog.dart
Normal file
@ -0,0 +1,392 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ import '../../dialogs/score_edit_dialog.dart'; // 점수 수정 모달
|
||||
import '../../dialogs/user_info_basic_dialog.dart'; // 일반 유저 정보 모달
|
||||
import '../../plugins/admob.dart';
|
||||
import '../../config/config.dart';
|
||||
import '../../dialogs/chat_dialog.dart';
|
||||
|
||||
class PlayingPrivatePage extends StatefulWidget {
|
||||
final int roomSeq;
|
||||
@ -62,6 +63,9 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
|
||||
// userListMap: { userSeq: true/false }
|
||||
Map<String, bool> _userListMap = {};
|
||||
|
||||
String _teamName = '';
|
||||
String _myNickname = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -157,6 +161,11 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
|
||||
// Mark admin (사회자 표시)
|
||||
uData['nickname'] = '☆' + (uData['nickname'] ?? 'User');
|
||||
}
|
||||
// 채팅 데이터 세팅
|
||||
if (uSeq.toString() == mySeq) {
|
||||
_teamName = uData['team_name'] ?? '';
|
||||
_myNickname = uData['nickname'] ?? '';
|
||||
}
|
||||
rawList.add({
|
||||
'user_seq': uSeq,
|
||||
'participant_type': (uData['participant_type'] ?? '').toString().toUpperCase(),
|
||||
@ -551,6 +560,15 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
bottom: 30, // AdBannerWidget 위에 위치하도록 조정
|
||||
right: 0,
|
||||
child: ChatButton(roomType: 'PRIVATE', roomStatus: 'RUNNING', roomSeq: widget.roomSeq.toString(), teamName: _teamName, myNickname: _myNickname),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: AdBannerWidget(),
|
||||
),
|
||||
);
|
||||
|
@ -11,6 +11,7 @@ import '../../dialogs/score_edit_dialog.dart';
|
||||
import '../../dialogs/user_info_basic_dialog.dart';
|
||||
import '../../plugins/admob.dart';
|
||||
import '../../config/config.dart';
|
||||
import '../../dialogs/chat_dialog.dart';
|
||||
|
||||
class PlayingTeamPage extends StatefulWidget {
|
||||
final int roomSeq;
|
||||
@ -65,6 +66,9 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
|
||||
// 점수 공개 범위
|
||||
String scoreOpenRange = 'ALL';
|
||||
|
||||
String _teamName = '';
|
||||
String _myNickname = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -159,6 +163,11 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
|
||||
// 관리자(사회자) 표시
|
||||
uData['nickname'] = '☆' + (uData['nickname'] ?? 'User');
|
||||
}
|
||||
// 채팅 데이터 세팅
|
||||
if (uSeq.toString() == mySeq) {
|
||||
_teamName = uData['team_name'] ?? '';
|
||||
_myNickname = uData['nickname'] ?? '';
|
||||
}
|
||||
rawList.add({
|
||||
'user_seq': uSeq,
|
||||
'participant_type': (uData['participant_type'] ?? '').toString().toUpperCase(),
|
||||
@ -517,6 +526,15 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
bottom: 30, // AdBannerWidget 위에 위치하도록 조정
|
||||
right: 0,
|
||||
child: ChatButton(roomType: 'TEAM', roomStatus: 'RUNNING', roomSeq: widget.roomSeq.toString(), teamName: _teamName, myNickname: _myNickname),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: AdBannerWidget(),
|
||||
),
|
||||
);
|
||||
@ -569,7 +587,6 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
|
||||
final score = (scoreOpenRange == 'ALL') ? tempScore : '-';
|
||||
final nickname = userData['nickname'] ?? 'User'; // '유저'
|
||||
final profileImg = userData['profile_img'] ?? '';
|
||||
print('profileImg: $profileImg');
|
||||
|
||||
final bool isActive = _userListMap[userSeq] ?? true;
|
||||
final hasExited = !isActive;
|
||||
|
@ -9,6 +9,7 @@ import '../../dialogs/response_dialog.dart';
|
||||
import '../../dialogs/yes_no_dialog.dart';
|
||||
import '../../dialogs/room_setting_dialog.dart';
|
||||
import '../../dialogs/user_info_private_dialog.dart';
|
||||
import '../../dialogs/chat_dialog.dart';
|
||||
import 'playing_private_page.dart';
|
||||
|
||||
// Ads
|
||||
@ -75,6 +76,10 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
|
||||
double scaleFactor = 1.0;
|
||||
double buttonScaleFactor = 1.0;
|
||||
|
||||
// 채팅을 위한 데이터
|
||||
String _myNickname = '';
|
||||
String _teamName = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -150,6 +155,10 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
|
||||
if (val is Map) {
|
||||
final tempUserSeq = val['user_seq'].toString() ?? '0';
|
||||
if (tempUserSeq != '0') {
|
||||
if (val['user_seq'].toString() == mySeq) {
|
||||
_teamName = val['team_name'] ?? '';
|
||||
_myNickname = val['nickname'] ?? '';
|
||||
}
|
||||
tempList.add({
|
||||
'user_seq': tempUserSeq,
|
||||
'participant_type': val['participant_type'] ?? '',
|
||||
@ -655,6 +664,15 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
|
||||
onPressed: () => _onLeaveRoom(),
|
||||
),
|
||||
),
|
||||
floatingActionButton: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
bottom: 30, // AdBannerWidget 위에 위치하도록 조정
|
||||
right: 0,
|
||||
child: ChatButton(roomType: widget.roomType, roomStatus: 'WAIT', roomSeq: widget.roomSeq.toString(), teamName: _teamName, myNickname: _myNickname),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: AdBannerWidget(),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
@ -730,7 +748,6 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
|
||||
} else if (isAdmin) {
|
||||
roleIcon = '☆ '; // 사회자
|
||||
}
|
||||
print('profileImg: ${Config.baseUrl}/images$profileImg');
|
||||
|
||||
final displayName = '$roleIcon$userName';
|
||||
|
||||
|
@ -15,6 +15,7 @@ import '../../dialogs/yes_no_dialog.dart';
|
||||
import '../../dialogs/room_setting_dialog.dart';
|
||||
import '../../dialogs/user_info_team_dialog.dart';
|
||||
import '../../dialogs/team_name_edit_dialog.dart';
|
||||
import '../../dialogs/chat_dialog.dart';
|
||||
import 'playing_team_page.dart';
|
||||
|
||||
// Ads
|
||||
@ -85,6 +86,10 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
|
||||
double scaleFactor = 1.0;
|
||||
double buttonScaleFactor = 1.0;
|
||||
|
||||
// 채팅을 위한 데이터
|
||||
String _teamName = '';
|
||||
String _myNickname = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -161,6 +166,10 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
|
||||
final tempUserSeq = val['user_seq'].toString() ?? '0';
|
||||
if (tempUserSeq != '0') {
|
||||
if (val is Map) {
|
||||
if (val['user_seq'].toString() == mySeq) {
|
||||
_teamName = val['team_name'] ?? '';
|
||||
_myNickname = val['nickname'] ?? '';
|
||||
}
|
||||
tempList.add({
|
||||
'user_seq': val['user_seq'].toString() ?? '0',
|
||||
'participant_type': val['participant_type'] ?? '',
|
||||
@ -697,6 +706,15 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
|
||||
onPressed: () => _onLeaveRoom(),
|
||||
),
|
||||
),
|
||||
floatingActionButton: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
bottom: 30, // AdBannerWidget 위에 위치하도록 조정
|
||||
right: 0,
|
||||
child: ChatButton(roomType: widget.roomType, roomStatus: 'WAIT', roomSeq: widget.roomSeq.toString(), teamName: _teamName, myNickname: _myNickname),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: AdBannerWidget(),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
|
Loading…
Reference in New Issue
Block a user