실시간 채팅 테스트1차

This commit is contained in:
eld_master 2025-02-17 14:51:36 +09:00
parent dcd246f589
commit d07c6f93dd
5 changed files with 464 additions and 2 deletions

View 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,
);
}
}

View File

@ -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(),
),
);

View File

@ -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;

View File

@ -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';

View File

@ -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())