fxmpp 1.0.0-alpha.2
fxmpp: ^1.0.0-alpha.2 copied to clipboard
A Flutter plugin for XMPP (Extensible Messaging and Presence Protocol) communication, supporting both iOS and Android platforms.
example/lib/main.dart
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:fxmpp/fxmpp.dart';
import 'dart:async';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'FXMPP Example',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const MyHomePage(title: 'FXMPP Example'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final Fxmpp _fxmpp = Fxmpp();
final TextEditingController _hostController =
TextEditingController(text: 'localhost');
final TextEditingController _portController =
TextEditingController(text: '5222');
final TextEditingController _usernameController =
TextEditingController(text: 'user1');
final TextEditingController _passwordController =
TextEditingController(text: 'user1');
final TextEditingController _domainController =
TextEditingController(text: 'localhost');
final TextEditingController _messageController =
TextEditingController(text: 'Hello, user2!');
final TextEditingController _recipientController =
TextEditingController(text: 'user2@localhost');
// MUC-related controllers
final TextEditingController _roomJidController =
TextEditingController(text: 'testroom@conference.localhost');
final TextEditingController _nicknameController =
TextEditingController(text: 'user1');
final TextEditingController _roomMessageController =
TextEditingController(text: 'Hello everyone!');
final TextEditingController _privateMessageController =
TextEditingController(text: 'Private message');
final TextEditingController _targetNicknameController =
TextEditingController(text: 'user2');
final TextEditingController _inviteUserController =
TextEditingController(text: 'user3@localhost');
XmppConnectionState _connectionState = XmppConnectionState.disconnected;
final List<XmlDocument> _messages = [];
final List<XmlDocument> _presences = [];
final List<XmlDocument> _iqs = [];
final List<MucRoomEvent> _mucEvents = [];
StreamSubscription<XmppConnectionState>? _connectionStateSubscription;
StreamSubscription<XmlDocument>? _messageSubscription;
StreamSubscription<XmlDocument>? _presenceSubscription;
StreamSubscription<XmlDocument>? _iqSubscription;
StreamSubscription<MucRoomEvent>? _mucEventSubscription;
@override
void initState() {
super.initState();
_initializePlugin();
}
Future<void> _initializePlugin() async {
await _fxmpp.initialize();
_connectionStateSubscription = _fxmpp.connectionStateStream.listen((state) {
setState(() {
_connectionState = state;
});
});
_messageSubscription = _fxmpp.messageStream.listen((xmlMessage) {
setState(() {
_messages.insert(0, xmlMessage);
});
});
_presenceSubscription = _fxmpp.presenceStream.listen((xmlPresence) {
setState(() {
_presences.insert(0, xmlPresence);
});
});
_iqSubscription = _fxmpp.iqStream.listen((xmlIq) {
setState(() {
_iqs.insert(0, xmlIq);
});
});
_mucEventSubscription = _fxmpp.mucRoomEventStream.listen((mucEvent) {
setState(() {
_mucEvents.insert(0, mucEvent);
});
});
}
Future<void> _connect() async {
if (_usernameController.text.isEmpty ||
_passwordController.text.isEmpty ||
_domainController.text.isEmpty) {
_showSnackBar('Please fill in all connection fields');
return;
}
final config = XmppConnectionConfig(
host: _hostController.text,
port: int.tryParse(_portController.text) ?? 5222,
username: _usernameController.text,
password: _passwordController.text,
domain: _domainController.text,
useSSL: true,
allowSelfSignedCertificates: true,
resource: 'fxmpp_example',
);
try {
final success = await _fxmpp.connect(config);
if (success) {
_showSnackBar('Connection initiated');
} else {
_showSnackBar('Failed to connect');
}
} catch (e) {
_showSnackBar('Connection error: $e');
}
}
Future<void> _disconnect() async {
await _fxmpp.disconnect();
_showSnackBar('Disconnected');
}
// Helper functions to extract data from XML
String _getMessageFrom(XmlDocument xmlDoc) {
return xmlDoc.rootElement.getAttribute('from') ?? '';
}
String _getMessageTo(XmlDocument xmlDoc) {
return xmlDoc.rootElement.getAttribute('to') ?? '';
}
String _getMessageBody(XmlDocument xmlDoc) {
return xmlDoc.rootElement.findElements('body').first.innerText;
}
String _getPresenceFrom(XmlDocument xmlDoc) {
return xmlDoc.rootElement.getAttribute('from') ?? '';
}
String _getPresenceShow(XmlDocument xmlDoc) {
final showElement = xmlDoc.rootElement.findElements('show');
return showElement.isNotEmpty ? showElement.first.innerText : 'online';
}
String _getPresenceStatus(XmlDocument xmlDoc) {
final statusElement = xmlDoc.rootElement.findElements('status');
return statusElement.isNotEmpty ? statusElement.first.innerText : '';
}
// Helper functions to create XMPP XML using new utility methods
XmlDocument _createMessageXml(String to, String body) {
return Fxmpp.createMessage(
messageId: Fxmpp.generateId('msg'),
type: MessageType.chat,
fromJid: '${_usernameController.text}@${_domainController.text}',
toJid: to,
content: body,
);
}
XmlDocument _createPresenceXml({String? show, String? status}) {
PresenceShow presenceShow = PresenceShow.available;
if (show != null) {
switch (show) {
case 'away':
presenceShow = PresenceShow.away;
break;
case 'dnd':
presenceShow = PresenceShow.dnd;
break;
case 'xa':
presenceShow = PresenceShow.xa;
break;
case 'chat':
presenceShow = PresenceShow.chat;
break;
default:
presenceShow = PresenceShow.available;
}
}
return Fxmpp.createPresence(
presenceId: Fxmpp.generateId('pres'),
type: PresenceType.available,
fromJid: '${_usernameController.text}@${_domainController.text}',
show: presenceShow,
status: status,
priority: 0,
);
}
XmlDocument _createIqBindResourceXml(String type, String to, String resource,
{String? queryNamespace}) {
//<iq type='set' id='purple18f99190'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource>CraftsDev</resource></bind></iq>
final builder = XmlBuilder();
builder.element('iq', nest: () {
builder.attribute('type', 'set');
builder.attribute('id', DateTime.now().millisecondsSinceEpoch.toString());
builder.element('bind', nest: () {
builder.attribute('xmlns', 'urn:ietf:params:xml:ns:xmpp-bind');
builder.element('resource', nest: resource);
});
});
return builder.buildDocument();
}
Future<void> _sendMessage() async {
if (_messageController.text.isEmpty || _recipientController.text.isEmpty) {
_showSnackBar('Please enter both message and recipient');
return;
}
final messageXml =
_createMessageXml(_recipientController.text, _messageController.text);
try {
final success = await _fxmpp.sendMessage(messageXml);
if (success) {
// Add the sent message to the local UI list
setState(() {
_messages.insert(0, messageXml);
});
_messageController.clear();
_showSnackBar('Message sent');
} else {
_showSnackBar('Failed to send message');
}
} catch (e) {
_showSnackBar('Error sending message: $e');
print('FXMPP Debug: Send error: $e');
}
}
Future<void> _sendPresence(String show) async {
final presenceXml = _createPresenceXml(show: show);
try {
final success = await _fxmpp.sendPresence(presenceXml);
if (success) {
_showSnackBar('Presence sent');
} else {
_showSnackBar('Failed to send presence');
}
} catch (e) {
_showSnackBar('Error sending presence: $e');
}
}
void _showSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
@override
void dispose() {
_connectionStateSubscription?.cancel();
_messageSubscription?.cancel();
_presenceSubscription?.cancel();
_iqSubscription?.cancel();
_mucEventSubscription?.cancel();
_fxmpp.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: DefaultTabController(
length: 6,
child: Column(
children: [
Container(
color: _connectionState.isConnected
? Colors.green.shade100
: _connectionState.hasError
? Colors.red.shade100
: Colors.grey.shade100,
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Icon(
_connectionState.isConnected
? Icons.wifi
: _connectionState.isConnecting
? Icons.wifi_find
: Icons.wifi_off,
color: _connectionState.isConnected
? Colors.green
: _connectionState.hasError
? Colors.red
: Colors.grey,
),
const SizedBox(width: 8),
Text(
'Status: ${_connectionState.description}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: _connectionState.isConnected
? Colors.green.shade700
: _connectionState.hasError
? Colors.red.shade700
: Colors.grey.shade700,
),
),
],
),
),
const TabBar(
isScrollable: true,
tabs: [
Tab(icon: Icon(Icons.settings), text: 'Connection'),
Tab(icon: Icon(Icons.message), text: 'Messages'),
Tab(icon: Icon(Icons.people), text: 'Presence'),
Tab(icon: Icon(Icons.help_outline), text: 'IQ'),
Tab(icon: Icon(Icons.group), text: 'MUC'),
Tab(icon: Icon(Icons.code), text: 'Utils'),
],
),
Expanded(
child: TabBarView(
children: [
_buildConnectionTab(),
_buildMessagesTab(),
_buildPresenceTab(),
_buildIqTab(),
_buildMucTab(),
_buildUtilsTab(),
],
),
),
],
),
),
);
}
Widget _buildConnectionTab() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _hostController,
decoration: const InputDecoration(
labelText: 'Host',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
TextField(
controller: _portController,
decoration: const InputDecoration(
labelText: 'Port',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 8),
TextField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Username',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
TextField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
obscureText: true,
),
const SizedBox(height: 8),
TextField(
controller: _domainController,
decoration: const InputDecoration(
labelText: 'Domain',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _connectionState.isConnected ? null : _connect,
child: const Text('Connect'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: _connectionState.isConnected ? _disconnect : null,
child: const Text('Disconnect'),
),
),
],
),
],
),
);
}
Widget _buildMessagesTab() {
return Column(
children: [
if (_connectionState.isConnected) ...[
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _recipientController,
decoration: const InputDecoration(
labelText: 'Recipient (e.g., user@domain.com)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: TextField(
controller: _messageController,
decoration: const InputDecoration(
labelText: 'Message',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _sendMessage(),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _sendMessage,
child: const Text('Send'),
),
],
),
],
),
),
],
Expanded(
child: _messages.isEmpty
? const Center(child: Text('No messages yet'))
: ListView.builder(
itemCount: _messages.length,
itemBuilder: (context, index) {
final message = _messages[index];
final from = _getMessageFrom(message);
final to = _getMessageTo(message);
final body = _getMessageBody(message);
final isOutgoing = from.contains(_usernameController.text);
return Card(
margin: const EdgeInsets.symmetric(
horizontal: 16, vertical: 4),
child: ListTile(
leading: Icon(
isOutgoing ? Icons.send : Icons.inbox,
color: isOutgoing ? Colors.blue : Colors.green,
),
title: Text(
isOutgoing ? 'To: $to' : 'From: $from',
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(body),
const SizedBox(height: 4),
Text(
DateTime.now().toString().substring(11, 16),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
);
},
),
),
],
);
}
Widget _buildPresenceTab() {
return Column(
children: [
if (_connectionState.isConnected) ...[
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const Text(
'Update Your Presence:',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: () => _sendPresence('online'),
child: const Text('Online'),
),
ElevatedButton(
onPressed: () => _sendPresence('away'),
child: const Text('Away'),
),
ElevatedButton(
onPressed: () => _sendPresence('dnd'),
child: const Text('Do Not Disturb'),
),
ElevatedButton(
onPressed: () => _sendPresence('xa'),
child: const Text('Extended Away'),
),
],
),
],
),
),
const Divider(),
],
Expanded(
child: _presences.isEmpty
? const Center(child: Text('No presence updates yet'))
: ListView.builder(
itemCount: _presences.length,
itemBuilder: (context, index) {
final presence = _presences[index];
final from = _getPresenceFrom(presence);
final show = _getPresenceShow(presence);
final status = _getPresenceStatus(presence);
return Card(
margin: const EdgeInsets.symmetric(
horizontal: 16, vertical: 4),
child: ListTile(
leading: Icon(
_getPresenceIcon(show),
color: _getPresenceColor(show),
),
title: Text(
'From: $from',
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Status: $show'),
if (status.isNotEmpty) Text('Message: $status'),
Text(
DateTime.now().toString().substring(11, 16),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
);
},
),
),
],
);
}
IconData _getPresenceIcon(String show) {
switch (show) {
case 'online':
return Icons.circle;
case 'away':
return Icons.schedule;
case 'dnd':
return Icons.do_not_disturb;
case 'xa':
return Icons.schedule_outlined;
case 'chat':
return Icons.chat;
default:
return Icons.circle;
}
}
Color _getPresenceColor(String show) {
switch (show) {
case 'online':
return Colors.green;
case 'away':
return Colors.orange;
case 'dnd':
return Colors.red;
case 'xa':
return Colors.grey;
case 'chat':
return Colors.blue;
default:
return Colors.grey;
}
}
Widget _buildIqTab() {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed:
_connectionState.isConnected ? _sendVersionIq : null,
child: const Text('Send Version IQ'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed:
_connectionState.isConnected ? _sendTimeIq : null,
child: const Text('Send Time IQ'),
),
),
],
),
const SizedBox(height: 8),
ElevatedButton(
onPressed:
_connectionState.isConnected ? _sendDiscoInfoIq : null,
child: const Text('Send Disco Info IQ'),
),
//Send Resource Bind IQ
ElevatedButton(
onPressed:
_connectionState.isConnected ? _sendResourceBindIq : null,
child: const Text('Send Resource Bind IQ'),
),
// Send Ping IQ (simple test)
ElevatedButton(
onPressed: _connectionState.isConnected ? _sendPingIq : null,
child: const Text('Send Ping IQ'),
),
// Send Roster IQ
ElevatedButton(
onPressed: _connectionState.isConnected ? _sendRosterIq : null,
child: const Text('Get Roster List'),
),
],
),
),
const Divider(),
Expanded(
child: _iqs.isEmpty
? const Center(child: Text('No IQ stanzas received'))
: ListView.builder(
itemCount: _iqs.length,
itemBuilder: (context, index) {
final iq = _iqs[index];
final type =
iq.rootElement.getAttribute('type') ?? 'unknown';
final from =
iq.rootElement.getAttribute('from') ?? 'unknown';
final id = iq.rootElement.getAttribute('id') ?? 'no-id';
return Card(
margin: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
type == 'result'
? Icons.check_circle
: type == 'error'
? Icons.error
: type == 'get'
? Icons.download
: Icons.upload,
color: type == 'result'
? Colors.green
: type == 'error'
? Colors.red
: Colors.blue,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'IQ $type from $from',
style:
Theme.of(context).textTheme.titleSmall,
),
),
],
),
const SizedBox(height: 4),
Text(
'ID: $id',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 4),
Text(
iq.toXmlString(),
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
fontFamily: 'monospace',
),
),
],
),
),
);
},
),
),
],
);
}
Future<void> _sendVersionIq() async {
try {
final iq = Fxmpp.createVersionQuery(
iqId: Fxmpp.generateId('version'),
fromJid: '${_usernameController.text}@${_domainController.text}',
toJid: _domainController.text,
);
await _fxmpp.sendIq(iq);
_showSnackBar('Version IQ sent');
} catch (e) {
_showSnackBar('Error sending version IQ: $e');
}
}
Future<void> _sendTimeIq() async {
try {
final iq = Fxmpp.createTimeQuery(
iqId: Fxmpp.generateId('time'),
fromJid: '${_usernameController.text}@${_domainController.text}',
toJid: _domainController.text,
);
await _fxmpp.sendIq(iq);
_showSnackBar('Time IQ sent');
} catch (e) {
_showSnackBar('Error sending time IQ: $e');
}
}
Future<void> _sendDiscoInfoIq() async {
try {
final iq = Fxmpp.createDiscoInfoQuery(
iqId: Fxmpp.generateId('disco'),
fromJid: '${_usernameController.text}@${_domainController.text}',
toJid: _domainController.text,
);
await _fxmpp.sendIq(iq);
_showSnackBar('Disco Info IQ sent');
} catch (e) {
_showSnackBar('Error sending disco info IQ: $e');
}
}
Future<void> _sendResourceBindIq() async {
try {
final iq = _createIqBindResourceXml(
'set', _domainController.text, 'Mobile',
queryNamespace: 'urn:ietf:params:xml:ns:xmpp-bind');
await _fxmpp.sendIq(iq);
_showSnackBar('Resource Bind IQ sent');
} catch (e) {
_showSnackBar('Error sending resource bind IQ: $e');
}
}
Future<void> _sendPingIq() async {
try {
// Create a custom ping IQ using the general createIq method
final pingBuilder = XmlBuilder();
pingBuilder.element('ping', attributes: {
'xmlns': 'urn:xmpp:ping',
});
final pingElement = pingBuilder.buildFragment().firstChild as XmlElement;
final iq = Fxmpp.createIq(
iqId: Fxmpp.generateId('ping'),
type: IqType.get,
fromJid: '${_usernameController.text}@${_domainController.text}',
toJid: _domainController.text,
queryElement: pingElement,
);
log('FXMPP Debug: Ping IQ XML: ${iq.toXmlString()}');
await _fxmpp.sendIq(iq);
_showSnackBar('Ping IQ sent');
} catch (e) {
_showSnackBar('Error sending ping IQ: $e');
}
}
Future<void> _sendRosterIq() async {
try {
// Create a roster query using the general createIq method
final rosterBuilder = XmlBuilder();
rosterBuilder.element('query', attributes: {
'xmlns': 'jabber:iq:roster',
});
final rosterElement =
rosterBuilder.buildFragment().firstChild as XmlElement;
final iq = Fxmpp.createIq(
iqId: Fxmpp.generateId('roster'),
type: IqType.get,
fromJid: '${_usernameController.text}@${_domainController.text}',
toJid: _domainController.text,
queryElement: rosterElement,
);
log('FXMPP Debug: Roster IQ XML: ${iq.toXmlString()}');
await _fxmpp.sendIq(iq);
_showSnackBar('Roster IQ sent');
} catch (e) {
_showSnackBar('Error sending roster IQ: $e');
}
}
Widget _buildMucTab() {
return Column(
children: [
if (_connectionState.isConnected) ...[
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Multi-User Chat (MUC)',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// Room Configuration
Card(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Room Configuration',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
TextField(
controller: _roomJidController,
decoration: const InputDecoration(
labelText: 'Room JID (e.g., room@conference.server.com)',
border: OutlineInputBorder(),
isDense: true,
),
),
const SizedBox(height: 8),
TextField(
controller: _nicknameController,
decoration: const InputDecoration(
labelText: 'Your Nickname',
border: OutlineInputBorder(),
isDense: true,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: _joinMucRoom,
icon: const Icon(Icons.login, size: 16),
label: const Text('Join Room'),
),
ElevatedButton.icon(
onPressed: _createMucRoom,
icon: const Icon(Icons.add, size: 16),
label: const Text('Create Room'),
),
ElevatedButton.icon(
onPressed: _leaveMucRoom,
icon: const Icon(Icons.logout, size: 16),
label: const Text('Leave Room'),
),
],
),
],
),
),
),
const SizedBox(height: 12),
// Messaging
Card(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Room Messaging',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: TextField(
controller: _roomMessageController,
decoration: const InputDecoration(
labelText: 'Group Message',
border: OutlineInputBorder(),
isDense: true,
),
onSubmitted: (_) => _sendMucMessage(),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _sendMucMessage,
child: const Text('Send'),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: TextField(
controller: _targetNicknameController,
decoration: const InputDecoration(
labelText: 'Target Nickname',
border: OutlineInputBorder(),
isDense: true,
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _privateMessageController,
decoration: const InputDecoration(
labelText: 'Private Message',
border: OutlineInputBorder(),
isDense: true,
),
onSubmitted: (_) => _sendMucPrivateMessage(),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _sendMucPrivateMessage,
child: const Text('Private'),
),
],
),
],
),
),
),
const SizedBox(height: 12),
// Participant Management
Card(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Participant Management',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 6,
runSpacing: 6,
children: [
ElevatedButton(
onPressed: _kickParticipant,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
),
child: const Text('Kick'),
),
ElevatedButton(
onPressed: _banUser,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Ban'),
),
ElevatedButton(
onPressed: _grantVoice,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
child: const Text('Grant Voice'),
),
ElevatedButton(
onPressed: _revokeVoice,
child: const Text('Revoke Voice'),
),
ElevatedButton(
onPressed: _grantModerator,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple,
foregroundColor: Colors.white,
),
child: const Text('Moderator'),
),
ElevatedButton(
onPressed: _grantAdmin,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
),
child: const Text('Admin'),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: TextField(
controller: _inviteUserController,
decoration: const InputDecoration(
labelText: 'User JID to Invite',
border: OutlineInputBorder(),
isDense: true,
),
),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: _inviteUser,
icon: const Icon(Icons.person_add, size: 16),
label: const Text('Invite'),
),
],
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: _destroyRoom,
icon: const Icon(Icons.delete_forever, size: 16),
label: const Text('Destroy Room'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white,
),
),
],
),
),
),
],
),
),
const Divider(),
],
Expanded(
child: _mucEvents.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.group_outlined, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'No MUC events yet',
style: TextStyle(color: Colors.grey, fontSize: 16),
),
SizedBox(height: 8),
Text(
'Join a room to see events',
style: TextStyle(color: Colors.grey, fontSize: 12),
),
],
),
)
: ListView.builder(
itemCount: _mucEvents.length,
itemBuilder: (context, index) {
final event = _mucEvents[index];
final eventType = event.type.name;
final roomJid = event.room.jid;
final nickname = event.data?['nickname'] as String? ?? '';
final message = event.data?['message'] as String? ?? '';
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: ListTile(
leading: Icon(
_getMucEventIcon(eventType),
color: _getMucEventColor(eventType),
),
title: Text(
_getMucEventTitle(eventType, nickname, roomJid),
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (message.isNotEmpty) Text(message),
Text(
'Room: ${roomJid.split('@').first}',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
DateTime.now().toString().substring(11, 16),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
);
},
),
),
],
);
}
Widget _buildUtilsTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'FXMPP Utility Methods',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// Message Creation Example
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Create Message Stanza',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'Instead of manually building XML, use:',
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'''final message = Fxmpp.createMessage(
messageId: Fxmpp.generateId('msg'),
type: MessageType.chat,
fromJid: 'user@domain.com',
toJid: 'friend@domain.com',
content: 'Hello, World!',
subject: 'Optional subject',
thread: 'conversation-123',
);''',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: _connectionState.isConnected
? () {
final demoMessage = Fxmpp.createMessage(
messageId: Fxmpp.generateId('demo'),
type: MessageType.chat,
fromJid:
'${_usernameController.text}@${_domainController.text}',
toJid: _recipientController.text.isNotEmpty
? _recipientController.text
: 'demo@${_domainController.text}',
content:
'Demo message created with utility method!',
subject: 'Demo Subject',
);
_fxmpp.sendMessage(demoMessage);
_showSnackBar(
'Demo message sent using utility method');
}
: null,
child: const Text('Send Demo Message'),
),
],
),
),
),
const SizedBox(height: 16),
// Presence Creation Example
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Create Presence Stanza',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'''final presence = Fxmpp.createPresence(
presenceId: Fxmpp.generateId('pres'),
type: PresenceType.available,
fromJid: 'user@domain.com',
show: PresenceShow.dnd,
status: 'Busy working on FXMPP',
priority: 5,
);''',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: _connectionState.isConnected
? () {
final demoPresence = Fxmpp.createPresence(
presenceId: Fxmpp.generateId('demo'),
type: PresenceType.available,
fromJid:
'${_usernameController.text}@${_domainController.text}',
show: PresenceShow.chat,
status:
'Available for chat via utility method!',
priority: 10,
);
_fxmpp.sendPresence(demoPresence);
_showSnackBar('Demo presence sent');
}
: null,
child: const Text('Chat Status'),
),
ElevatedButton(
onPressed: _connectionState.isConnected
? () {
final demoPresence = Fxmpp.createPresence(
presenceId: Fxmpp.generateId('demo'),
type: PresenceType.available,
fromJid:
'${_usernameController.text}@${_domainController.text}',
show: PresenceShow.dnd,
status:
'Do not disturb - using utility methods!',
priority: 0,
);
_fxmpp.sendPresence(demoPresence);
_showSnackBar('DND presence sent');
}
: null,
child: const Text('DND Status'),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// IQ Creation Example
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Create IQ Stanzas',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'''// Built-in IQ queries
final versionIq = Fxmpp.createVersionQuery(
iqId: Fxmpp.generateId('version'),
fromJid: 'user@domain.com',
toJid: 'server.com',
);
final timeIq = Fxmpp.createTimeQuery(
iqId: Fxmpp.generateId('time'),
fromJid: 'user@domain.com',
toJid: 'server.com',
);
final discoIq = Fxmpp.createDiscoInfoQuery(
iqId: Fxmpp.generateId('disco'),
fromJid: 'user@domain.com',
toJid: 'server.com',
);''',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: _connectionState.isConnected
? () {
final versionIq = Fxmpp.createVersionQuery(
iqId: Fxmpp.generateId('demo_version'),
fromJid:
'${_usernameController.text}@${_domainController.text}',
toJid: _domainController.text,
);
_fxmpp.sendIq(versionIq);
_showSnackBar('Version query sent via utility');
}
: null,
child: const Text('Version'),
),
ElevatedButton(
onPressed: _connectionState.isConnected
? () {
final timeIq = Fxmpp.createTimeQuery(
iqId: Fxmpp.generateId('demo_time'),
fromJid:
'${_usernameController.text}@${_domainController.text}',
toJid: _domainController.text,
);
_fxmpp.sendIq(timeIq);
_showSnackBar('Time query sent via utility');
}
: null,
child: const Text('Time'),
),
ElevatedButton(
onPressed: _connectionState.isConnected
? () {
final discoIq = Fxmpp.createDiscoInfoQuery(
iqId: Fxmpp.generateId('demo_disco'),
fromJid:
'${_usernameController.text}@${_domainController.text}',
toJid: _domainController.text,
);
_fxmpp.sendIq(discoIq);
_showSnackBar(
'Disco info query sent via utility');
}
: null,
child: const Text('Disco Info'),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// ID Generation Example
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'ID Generation',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'''// Generate unique IDs for stanzas
final id1 = Fxmpp.generateId(); // Default prefix
final id2 = Fxmpp.generateId('custom'); // Custom prefix''',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () {
final id1 = Fxmpp.generateId();
final id2 = Fxmpp.generateId('demo');
_showSnackBar('Generated IDs: $id1, $id2');
},
child: const Text('Generate Sample IDs'),
),
],
),
),
),
const SizedBox(height: 16),
// Benefits Section
Card(
color: Colors.blue.shade50,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.lightbulb, color: Colors.blue.shade700),
const SizedBox(width: 8),
Text(
'Benefits of Utility Methods',
style:
Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
),
),
],
),
const SizedBox(height: 12),
const Text('✓ No manual XML construction'),
const Text('✓ Type-safe stanza creation'),
const Text('✓ Automatic ID generation'),
const Text('✓ Proper XMPP namespace handling'),
const Text('✓ Reduced boilerplate code'),
const Text('✓ Built-in validation'),
const Text('✓ Consistent stanza formatting'),
],
),
),
),
],
),
);
}
// MUC Helper Methods
Future<void> _joinMucRoom() async {
if (_roomJidController.text.isEmpty || _nicknameController.text.isEmpty) {
_showSnackBar('Please enter room JID and nickname');
return;
}
try {
final success = await _fxmpp.joinMucRoom(
roomJid: _roomJidController.text,
nickname: _nicknameController.text,
);
if (success) {
_showSnackBar('Joined room: ${_roomJidController.text}');
} else {
_showSnackBar('Failed to join room');
}
} catch (e) {
_showSnackBar('Error joining room: $e');
}
}
Future<void> _createMucRoom() async {
if (_roomJidController.text.isEmpty || _nicknameController.text.isEmpty) {
_showSnackBar('Please enter room JID and nickname');
return;
}
try {
final success = await _fxmpp.createMucRoom(
roomJid: _roomJidController.text,
nickname: _nicknameController.text,
);
if (success) {
_showSnackBar('Created room: ${_roomJidController.text}');
} else {
_showSnackBar('Failed to create room');
}
} catch (e) {
_showSnackBar('Error creating room: $e');
}
}
Future<void> _leaveMucRoom() async {
if (_roomJidController.text.isEmpty) {
_showSnackBar('Please enter room JID');
return;
}
try {
final success = await _fxmpp.leaveMucRoom(roomJid: _roomJidController.text);
if (success) {
_showSnackBar('Left room: ${_roomJidController.text}');
} else {
_showSnackBar('Failed to leave room');
}
} catch (e) {
_showSnackBar('Error leaving room: $e');
}
}
Future<void> _sendMucMessage() async {
if (_roomJidController.text.isEmpty || _roomMessageController.text.isEmpty) {
_showSnackBar('Please enter room JID and message');
return;
}
try {
final mucMessage = Fxmpp.createMucMessage(
messageId: Fxmpp.generateId('muc'),
roomJid: _roomJidController.text,
fromJid: '${_usernameController.text}@${_domainController.text}',
message: _roomMessageController.text,
);
final success = await _fxmpp.sendMucMessage(mucMessage);
if (success) {
// Add the sent MUC message to the local UI list
setState(() {
_messages.insert(0, mucMessage);
});
_roomMessageController.clear();
_showSnackBar('Group message sent');
} else {
_showSnackBar('Failed to send group message');
}
} catch (e) {
_showSnackBar('Error sending group message: $e');
}
}
Future<void> _sendMucPrivateMessage() async {
if (_roomJidController.text.isEmpty ||
_targetNicknameController.text.isEmpty ||
_privateMessageController.text.isEmpty) {
_showSnackBar('Please enter room JID, target nickname, and message');
return;
}
try {
final privateMessage = Fxmpp.createMucPrivateMessage(
messageId: Fxmpp.generateId('muc_private'),
roomJid: _roomJidController.text,
nickname: _targetNicknameController.text,
fromJid: '${_usernameController.text}@${_domainController.text}',
message: _privateMessageController.text,
);
final success = await _fxmpp.sendMucPrivateMessage(privateMessage);
if (success) {
// Add the sent private message to the local UI list
setState(() {
_messages.insert(0, privateMessage);
});
_privateMessageController.clear();
_showSnackBar('Private message sent to ${_targetNicknameController.text}');
} else {
_showSnackBar('Failed to send private message');
}
} catch (e) {
_showSnackBar('Error sending private message: $e');
}
}
Future<void> _kickParticipant() async {
if (_roomJidController.text.isEmpty || _targetNicknameController.text.isEmpty) {
_showSnackBar('Please enter room JID and target nickname');
return;
}
try {
final success = await _fxmpp.kickMucParticipant(
roomJid: _roomJidController.text,
nickname: _targetNicknameController.text,
reason: 'Kicked by ${_nicknameController.text}',
);
if (success) {
_showSnackBar('Kicked ${_targetNicknameController.text}');
} else {
_showSnackBar('Failed to kick participant');
}
} catch (e) {
_showSnackBar('Error kicking participant: $e');
}
}
Future<void> _banUser() async {
if (_roomJidController.text.isEmpty || _inviteUserController.text.isEmpty) {
_showSnackBar('Please enter room JID and user JID to ban');
return;
}
try {
final success = await _fxmpp.banMucUser(
roomJid: _roomJidController.text,
userJid: _inviteUserController.text,
reason: 'Banned by ${_nicknameController.text}',
);
if (success) {
_showSnackBar('Banned ${_inviteUserController.text}');
} else {
_showSnackBar('Failed to ban user');
}
} catch (e) {
_showSnackBar('Error banning user: $e');
}
}
Future<void> _grantVoice() async {
if (_roomJidController.text.isEmpty || _targetNicknameController.text.isEmpty) {
_showSnackBar('Please enter room JID and target nickname');
return;
}
try {
final success = await _fxmpp.grantMucVoice(
roomJid: _roomJidController.text,
nickname: _targetNicknameController.text,
);
if (success) {
_showSnackBar('Granted voice to ${_targetNicknameController.text}');
} else {
_showSnackBar('Failed to grant voice');
}
} catch (e) {
_showSnackBar('Error granting voice: $e');
}
}
Future<void> _revokeVoice() async {
if (_roomJidController.text.isEmpty || _targetNicknameController.text.isEmpty) {
_showSnackBar('Please enter room JID and target nickname');
return;
}
try {
final success = await _fxmpp.revokeMucVoice(
roomJid: _roomJidController.text,
nickname: _targetNicknameController.text,
);
if (success) {
_showSnackBar('Revoked voice from ${_targetNicknameController.text}');
} else {
_showSnackBar('Failed to revoke voice');
}
} catch (e) {
_showSnackBar('Error revoking voice: $e');
}
}
Future<void> _grantModerator() async {
if (_roomJidController.text.isEmpty || _targetNicknameController.text.isEmpty) {
_showSnackBar('Please enter room JID and target nickname');
return;
}
try {
final success = await _fxmpp.grantMucModerator(
roomJid: _roomJidController.text,
nickname: _targetNicknameController.text,
);
if (success) {
_showSnackBar('Granted moderator to ${_targetNicknameController.text}');
} else {
_showSnackBar('Failed to grant moderator');
}
} catch (e) {
_showSnackBar('Error granting moderator: $e');
}
}
Future<void> _grantAdmin() async {
if (_roomJidController.text.isEmpty || _inviteUserController.text.isEmpty) {
_showSnackBar('Please enter room JID and user JID');
return;
}
try {
final success = await _fxmpp.grantMucAdmin(
roomJid: _roomJidController.text,
userJid: _inviteUserController.text,
);
if (success) {
_showSnackBar('Granted admin to ${_inviteUserController.text}');
} else {
_showSnackBar('Failed to grant admin');
}
} catch (e) {
_showSnackBar('Error granting admin: $e');
}
}
Future<void> _inviteUser() async {
if (_roomJidController.text.isEmpty || _inviteUserController.text.isEmpty) {
_showSnackBar('Please enter room JID and user JID to invite');
return;
}
try {
final success = await _fxmpp.inviteMucUser(
roomJid: _roomJidController.text,
userJid: _inviteUserController.text,
reason: 'Invited by ${_nicknameController.text}',
);
if (success) {
_showSnackBar('Invited ${_inviteUserController.text}');
} else {
_showSnackBar('Failed to invite user');
}
} catch (e) {
_showSnackBar('Error inviting user: $e');
}
}
Future<void> _destroyRoom() async {
if (_roomJidController.text.isEmpty) {
_showSnackBar('Please enter room JID');
return;
}
try {
final success = await _fxmpp.destroyMucRoom(
roomJid: _roomJidController.text,
reason: 'Room destroyed by ${_nicknameController.text}',
);
if (success) {
_showSnackBar('Destroyed room: ${_roomJidController.text}');
} else {
_showSnackBar('Failed to destroy room');
}
} catch (e) {
_showSnackBar('Error destroying room: $e');
}
}
// MUC Event Helper Methods
IconData _getMucEventIcon(String eventType) {
switch (eventType) {
case 'participantJoined':
return Icons.person_add;
case 'participantLeft':
return Icons.person_remove;
case 'messageReceived':
return Icons.message;
case 'subjectChanged':
return Icons.topic;
case 'roleChanged':
return Icons.admin_panel_settings;
case 'affiliationChanged':
return Icons.security;
case 'invitationReceived':
return Icons.mail;
case 'roomDestroyed':
return Icons.delete_forever;
default:
return Icons.info;
}
}
Color _getMucEventColor(String eventType) {
switch (eventType) {
case 'participantJoined':
return Colors.green;
case 'participantLeft':
return Colors.orange;
case 'messageReceived':
return Colors.blue;
case 'subjectChanged':
return Colors.purple;
case 'roleChanged':
return Colors.indigo;
case 'affiliationChanged':
return Colors.teal;
case 'invitationReceived':
return Colors.pink;
case 'roomDestroyed':
return Colors.red;
default:
return Colors.grey;
}
}
String _getMucEventTitle(String eventType, String nickname, String roomJid) {
final roomName = roomJid.split('@').first;
switch (eventType) {
case 'participantJoined':
return '$nickname joined $roomName';
case 'participantLeft':
return '$nickname left $roomName';
case 'messageReceived':
return 'Message from $nickname';
case 'subjectChanged':
return 'Subject changed by $nickname';
case 'roleChanged':
return 'Role changed for $nickname';
case 'affiliationChanged':
return 'Affiliation changed for $nickname';
case 'invitationReceived':
return 'Invitation from $nickname';
case 'roomDestroyed':
return 'Room $roomName destroyed';
default:
return 'MUC Event: $eventType';
}
}
}