mirror of https://github.com/CympleTech/ESSE.git
19 changed files with 1494 additions and 81 deletions
@ -0,0 +1,588 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:provider/provider.dart'; |
||||||
|
|
||||||
|
import 'package:esse/utils/adaptive.dart'; |
||||||
|
import 'package:esse/utils/toast.dart'; |
||||||
|
import 'package:esse/utils/pick_image.dart'; |
||||||
|
import 'package:esse/utils/pick_file.dart'; |
||||||
|
import 'package:esse/l10n/localizations.dart'; |
||||||
|
import 'package:esse/widgets/emoji.dart'; |
||||||
|
import 'package:esse/widgets/shadow_dialog.dart'; |
||||||
|
import 'package:esse/widgets/audio_recorder.dart'; |
||||||
|
import 'package:esse/widgets/user_info.dart'; |
||||||
|
import 'package:esse/widgets/chat_message.dart'; |
||||||
|
import 'package:esse/global.dart'; |
||||||
|
import 'package:esse/provider.dart'; |
||||||
|
|
||||||
|
import 'package:esse/apps/chat/provider.dart'; |
||||||
|
import 'package:esse/apps/group_chat/models.dart'; |
||||||
|
import 'package:esse/apps/group_chat/provider.dart'; |
||||||
|
|
||||||
|
class GroupChatPage extends StatelessWidget { |
||||||
|
const GroupChatPage({Key key}) : super(key: key); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return Scaffold( |
||||||
|
body: SafeArea( |
||||||
|
child: GroupChatDetail(), |
||||||
|
)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class GroupChatDetail extends StatefulWidget { |
||||||
|
const GroupChatDetail({Key key}) : super(key: key); |
||||||
|
|
||||||
|
@override |
||||||
|
_GroupChatDetailState createState() => _GroupChatDetailState(); |
||||||
|
} |
||||||
|
|
||||||
|
class _GroupChatDetailState extends State<GroupChatDetail> { |
||||||
|
TextEditingController textController = TextEditingController(); |
||||||
|
FocusNode textFocus = FocusNode(); |
||||||
|
bool emojiShow = false; |
||||||
|
bool sendShow = false; |
||||||
|
bool menuShow = false; |
||||||
|
bool recordShow = false; |
||||||
|
String _recordName; |
||||||
|
|
||||||
|
GroupChat group; |
||||||
|
|
||||||
|
@override |
||||||
|
initState() { |
||||||
|
super.initState(); |
||||||
|
textFocus.addListener(() { |
||||||
|
if (textFocus.hasFocus) { |
||||||
|
setState(() { |
||||||
|
emojiShow = false; |
||||||
|
menuShow = false; |
||||||
|
recordShow = false; |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
_generateRecordPath() { |
||||||
|
this._recordName = DateTime.now().millisecondsSinceEpoch.toString() + |
||||||
|
'_' + |
||||||
|
this.group.id.toString() + |
||||||
|
'.m4a'; |
||||||
|
} |
||||||
|
|
||||||
|
void _sendMessage() async { |
||||||
|
if (textController.text.length < 1) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
context.read<GroupChatProvider>().messageCreate(Message(group.id, MessageType.String, textController.text)); |
||||||
|
setState(() { |
||||||
|
textController.text = ''; |
||||||
|
textFocus.requestFocus(); |
||||||
|
|
||||||
|
emojiShow = false; |
||||||
|
sendShow = false; |
||||||
|
menuShow = false; |
||||||
|
recordShow = false; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
void _selectEmoji(value) { |
||||||
|
textController.text += value; |
||||||
|
} |
||||||
|
|
||||||
|
void _sendImage() async { |
||||||
|
final image = await pickImage(); |
||||||
|
if (image != null) { |
||||||
|
context.read<GroupChatProvider>().messageCreate(Message(group.id, MessageType.Image, image)); |
||||||
|
} |
||||||
|
setState(() { |
||||||
|
textFocus.requestFocus(); |
||||||
|
emojiShow = false; |
||||||
|
sendShow = false; |
||||||
|
menuShow = false; |
||||||
|
recordShow = false; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
void _sendFile() async { |
||||||
|
final file = await pickFile(); |
||||||
|
if (file != null) { |
||||||
|
context.read<GroupChatProvider>().messageCreate(Message(group.id, MessageType.File, file)); |
||||||
|
} |
||||||
|
setState(() { |
||||||
|
textFocus.requestFocus(); |
||||||
|
emojiShow = false; |
||||||
|
sendShow = false; |
||||||
|
menuShow = false; |
||||||
|
recordShow = false; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
void _sendRecord(int time) async { |
||||||
|
final raw = Message.rawRecordName(time, _recordName); |
||||||
|
context.read<GroupChatProvider>().messageCreate(Message(group.id, MessageType.Record, raw)); |
||||||
|
|
||||||
|
setState(() { |
||||||
|
textFocus.requestFocus(); |
||||||
|
emojiShow = false; |
||||||
|
sendShow = false; |
||||||
|
menuShow = false; |
||||||
|
recordShow = false; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
void _sendContact(ColorScheme color, AppLocalizations lang, friends) { |
||||||
|
showShadowDialog( |
||||||
|
context, |
||||||
|
Icons.person_rounded, |
||||||
|
'Contact', |
||||||
|
Column(children: [ |
||||||
|
Container( |
||||||
|
height: 40.0, |
||||||
|
decoration: BoxDecoration( |
||||||
|
color: color.surface, |
||||||
|
borderRadius: BorderRadius.circular(15.0)), |
||||||
|
child: TextField( |
||||||
|
autofocus: false, |
||||||
|
textInputAction: TextInputAction.search, |
||||||
|
textAlignVertical: TextAlignVertical.center, |
||||||
|
style: TextStyle(fontSize: 14.0), |
||||||
|
onSubmitted: (value) { |
||||||
|
toast(context, 'WIP...'); |
||||||
|
}, |
||||||
|
decoration: InputDecoration( |
||||||
|
hintText: lang.search, |
||||||
|
hintStyle: TextStyle(color: color.onPrimary.withOpacity(0.5)), |
||||||
|
border: InputBorder.none, |
||||||
|
contentPadding: |
||||||
|
EdgeInsets.only(left: 15.0, right: 15.0, bottom: 15.0), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
SizedBox(height: 15.0), |
||||||
|
Column( |
||||||
|
children: friends.map<Widget>((contact) { |
||||||
|
return GestureDetector( |
||||||
|
behavior: HitTestBehavior.opaque, |
||||||
|
onTap: () async { |
||||||
|
context.read<GroupChatProvider>().messageCreate(Message(friend.id, MessageType.Contact, "${contact.id}")); |
||||||
|
Navigator.of(context).pop(); |
||||||
|
setState(() { |
||||||
|
textFocus.requestFocus(); |
||||||
|
emojiShow = false; |
||||||
|
sendShow = false; |
||||||
|
menuShow = false; |
||||||
|
recordShow = false; |
||||||
|
}); |
||||||
|
}, |
||||||
|
child: Padding( |
||||||
|
padding: const EdgeInsets.symmetric( |
||||||
|
horizontal: 20.0, vertical: 14.0), |
||||||
|
child: Row( |
||||||
|
children: [ |
||||||
|
contact.showAvatar(), |
||||||
|
SizedBox(width: 15.0), |
||||||
|
Text(contact.name, style: TextStyle(fontSize: 16.0)), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
}).toList()) |
||||||
|
])); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
final color = Theme.of(context).colorScheme; |
||||||
|
final lang = AppLocalizations.of(context); |
||||||
|
final isDesktop = isDisplayDesktop(context); |
||||||
|
|
||||||
|
final provider = context.watch<GroupChatProvider>(); |
||||||
|
final recentMessages = provider.activedMessages; |
||||||
|
final recentMessageKeys = recentMessages.keys.toList().reversed.toList(); |
||||||
|
|
||||||
|
final meName = context.read<AccountProvider>().activedAccount.name; |
||||||
|
this.group = provider.activedGroup; |
||||||
|
|
||||||
|
if (this.group == null) { |
||||||
|
return Container( |
||||||
|
padding: EdgeInsets.only(left: 20.0, right: 20.0, top: 10.0, bottom: 10.0), |
||||||
|
child: Text('Waiting...') |
||||||
|
); |
||||||
|
} |
||||||
|
final isOnline = this.group.online; |
||||||
|
|
||||||
|
return Column( |
||||||
|
children: [ |
||||||
|
Container( |
||||||
|
padding: EdgeInsets.only(left: 20.0, right: 20.0, top: 10.0, bottom: 10.0), |
||||||
|
child: Row( |
||||||
|
children: [ |
||||||
|
if (!isDesktop) |
||||||
|
GestureDetector( |
||||||
|
onTap: () { |
||||||
|
context.read<GroupChatProvider>().clearActivedGroup(); |
||||||
|
Navigator.pop(context); |
||||||
|
}, |
||||||
|
child: Container( |
||||||
|
width: 20.0, |
||||||
|
child: |
||||||
|
Icon(Icons.arrow_back, color: color.primary)), |
||||||
|
), |
||||||
|
SizedBox(width: 15.0), |
||||||
|
Expanded( |
||||||
|
child: Column( |
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||||
|
children: [ |
||||||
|
Text( |
||||||
|
this.group.name, |
||||||
|
style: TextStyle(fontWeight: FontWeight.bold), |
||||||
|
), |
||||||
|
SizedBox(height: 6.0), |
||||||
|
Text(this.group.isClosed |
||||||
|
? lang.unfriended |
||||||
|
: (isOnline ? lang.online : lang.offline), |
||||||
|
style: TextStyle( |
||||||
|
color: color.onPrimary.withOpacity(0.5), |
||||||
|
fontSize: 14.0)) |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
SizedBox(width: 20.0), |
||||||
|
GestureDetector( |
||||||
|
onTap: () {}, |
||||||
|
child: Container( |
||||||
|
width: 20.0, |
||||||
|
child: Icon(Icons.phone_rounded, |
||||||
|
color: Color(0x26ADB0BB))), |
||||||
|
), |
||||||
|
SizedBox(width: 20.0), |
||||||
|
GestureDetector( |
||||||
|
onTap: () {}, |
||||||
|
child: Container( |
||||||
|
width: 20.0, |
||||||
|
child: Icon(Icons.videocam_rounded, |
||||||
|
color: Color(0x26ADB0BB))), |
||||||
|
), |
||||||
|
SizedBox(width: 20.0), |
||||||
|
PopupMenuButton<int>( |
||||||
|
shape: RoundedRectangleBorder( |
||||||
|
borderRadius: BorderRadius.circular(15) |
||||||
|
), |
||||||
|
color: const Color(0xFFEDEDED), |
||||||
|
child: Icon(Icons.more_vert_rounded, color: color.primary), |
||||||
|
onSelected: (int value) { |
||||||
|
if (value == 1) { |
||||||
|
Provider.of<GroupChatProvider>(context, listen: false).groupUpdate( |
||||||
|
this.group.id, isTop: !this.group.isTop); |
||||||
|
} else if (value == 2) { |
||||||
|
showShadowDialog( |
||||||
|
context, |
||||||
|
Icons.info, |
||||||
|
lang.groupInfo, |
||||||
|
UserInfo( |
||||||
|
id: 'EH' + this.group.gid.toUpperCase(), |
||||||
|
name: this.group.name, |
||||||
|
addr: '0x' + this.group.addr) |
||||||
|
); |
||||||
|
} else if (value == 3) { |
||||||
|
print('TODO remark'); |
||||||
|
} else if (value == 4) { |
||||||
|
showDialog( |
||||||
|
context: context, |
||||||
|
builder: (BuildContext context) { |
||||||
|
return AlertDialog( |
||||||
|
title: Text(lang.unfriend), |
||||||
|
content: Text(this.group.name, |
||||||
|
style: TextStyle(color: color.primary)), |
||||||
|
actions: [ |
||||||
|
TextButton( |
||||||
|
child: Text(lang.cancel), |
||||||
|
onPressed: () => Navigator.pop(context), |
||||||
|
), |
||||||
|
TextButton( |
||||||
|
child: Text(lang.ok), |
||||||
|
onPressed: () { |
||||||
|
Navigator.pop(context); |
||||||
|
Provider.of<GroupChatProvider>( |
||||||
|
context, listen: false).groupClose(this.group.id); |
||||||
|
if (!isDesktop) { |
||||||
|
Navigator.pop(context); |
||||||
|
} |
||||||
|
}, |
||||||
|
), |
||||||
|
] |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
} else if (value == 5) { |
||||||
|
Provider.of<GroupChatProvider>(context, listen: false).requestCreate( |
||||||
|
Request(this.group.gid, this.group.addr, this.group.name, lang.fromContactCard(meName))); |
||||||
|
} else if (value == 6) { |
||||||
|
showDialog( |
||||||
|
context: context, |
||||||
|
builder: (BuildContext context) { |
||||||
|
return AlertDialog( |
||||||
|
title: Text(lang.delete + " " + lang.group), |
||||||
|
content: Text(this.group.name, |
||||||
|
style: TextStyle(color: Colors.red)), |
||||||
|
actions: [ |
||||||
|
TextButton( |
||||||
|
child: Text(lang.cancel), |
||||||
|
onPressed: () => Navigator.pop(context), |
||||||
|
), |
||||||
|
TextButton( |
||||||
|
child: Text(lang.ok), |
||||||
|
onPressed: () { |
||||||
|
Navigator.pop(context); |
||||||
|
Provider.of<GroupChatProvider>( |
||||||
|
context, listen: false).groupDelete(this.group.id); |
||||||
|
if (!isDesktop) { |
||||||
|
Navigator.pop(context); |
||||||
|
} |
||||||
|
}, |
||||||
|
), |
||||||
|
] |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
}, |
||||||
|
itemBuilder: (context) { |
||||||
|
return <PopupMenuEntry<int>>[ |
||||||
|
_menuItem(Color(0xFF6174FF), 1, Icons.vertical_align_top_rounded, this.group.isTop ? lang.cancelTop : lang.setTop), |
||||||
|
_menuItem(Color(0xFF6174FF), 2, Icons.qr_code_rounded, lang.groupInfo), |
||||||
|
//_menuItem(color.primary, 3, Icons.turned_in_rounded, lang.remark), |
||||||
|
this.group.isClosed |
||||||
|
? _menuItem(Color(0xFF6174FF), 5, Icons.send_rounded, lang.addGroup) |
||||||
|
: _menuItem(Color(0xFF6174FF), 4, Icons.block_rounded, lang.ungroup), |
||||||
|
_menuItem(Colors.red, 6, Icons.delete_rounded, lang.delete), |
||||||
|
]; |
||||||
|
}, |
||||||
|
) |
||||||
|
] |
||||||
|
), |
||||||
|
), |
||||||
|
const Divider(height: 1.0, color: Color(0x40ADB0BB)), |
||||||
|
Expanded( |
||||||
|
child: ListView.builder( |
||||||
|
padding: EdgeInsets.symmetric(horizontal: 20.0), |
||||||
|
itemCount: recentMessageKeys.length, |
||||||
|
reverse: true, |
||||||
|
itemBuilder: (BuildContext context, index) => ChatMessage( |
||||||
|
name: this.group.name, |
||||||
|
message: recentMessages[recentMessageKeys[index]], |
||||||
|
) |
||||||
|
)), |
||||||
|
if (!this.group.isClosed) |
||||||
|
Container( |
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0), |
||||||
|
child: Row( |
||||||
|
children: [ |
||||||
|
GestureDetector( |
||||||
|
onTap: isOnline ? () async { |
||||||
|
if (recordShow) { |
||||||
|
recordShow = false; |
||||||
|
textFocus.requestFocus(); |
||||||
|
} else { |
||||||
|
_generateRecordPath(); |
||||||
|
setState(() { |
||||||
|
menuShow = false; |
||||||
|
emojiShow = false; |
||||||
|
recordShow = true; |
||||||
|
textFocus.unfocus(); |
||||||
|
}); |
||||||
|
} |
||||||
|
} : null, |
||||||
|
child: Container( |
||||||
|
width: 20.0, |
||||||
|
child: Icon(Icons.mic_rounded, color: isOnline ? color.primary : Color(0xFFADB0BB))), |
||||||
|
), |
||||||
|
SizedBox(width: 10.0), |
||||||
|
Expanded( |
||||||
|
child: Container( |
||||||
|
height: 40, |
||||||
|
decoration: BoxDecoration( |
||||||
|
color: color.surface, |
||||||
|
borderRadius: BorderRadius.circular(15.0), |
||||||
|
), |
||||||
|
child: TextField( |
||||||
|
enabled: isOnline, |
||||||
|
style: TextStyle(fontSize: 14.0), |
||||||
|
textInputAction: TextInputAction.send, |
||||||
|
onChanged: (value) { |
||||||
|
if (value.length == 0 && sendShow) { |
||||||
|
setState(() { |
||||||
|
sendShow = false; |
||||||
|
}); |
||||||
|
} else { |
||||||
|
if (!sendShow) { |
||||||
|
setState(() { |
||||||
|
sendShow = true; |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
onSubmitted: (_v) => _sendMessage(), |
||||||
|
decoration: InputDecoration( |
||||||
|
hintText: 'Aa', |
||||||
|
border: InputBorder.none, |
||||||
|
contentPadding: EdgeInsets.only( |
||||||
|
left: 15.0, right: 15.0, bottom: 7.0), |
||||||
|
), |
||||||
|
controller: textController, |
||||||
|
focusNode: textFocus, |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
SizedBox(width: 10.0), |
||||||
|
GestureDetector( |
||||||
|
onTap: isOnline ? () { |
||||||
|
if (emojiShow) { |
||||||
|
textFocus.requestFocus(); |
||||||
|
} else { |
||||||
|
setState(() { |
||||||
|
menuShow = false; |
||||||
|
recordShow = false; |
||||||
|
emojiShow = true; |
||||||
|
textFocus.unfocus(); |
||||||
|
}); |
||||||
|
} |
||||||
|
} : null, |
||||||
|
child: Container( |
||||||
|
width: 20.0, |
||||||
|
child: Icon( |
||||||
|
emojiShow |
||||||
|
? Icons.keyboard_rounded |
||||||
|
: Icons.emoji_emotions_rounded, |
||||||
|
color: isOnline ? color.primary : Color(0xFFADB0BB))), |
||||||
|
), |
||||||
|
SizedBox(width: 10.0), |
||||||
|
sendShow |
||||||
|
? GestureDetector( |
||||||
|
onTap: isOnline ? _sendMessage : null, |
||||||
|
child: Container( |
||||||
|
width: 50.0, |
||||||
|
height: 30.0, |
||||||
|
decoration: BoxDecoration( |
||||||
|
color: Color(0xFF6174FF), |
||||||
|
borderRadius: BorderRadius.circular(10.0), |
||||||
|
), |
||||||
|
child: Center( |
||||||
|
child: Icon(Icons.send, |
||||||
|
color: Colors.white, size: 20.0))), |
||||||
|
) |
||||||
|
: GestureDetector( |
||||||
|
onTap: isOnline ? () { |
||||||
|
if (menuShow) { |
||||||
|
textFocus.requestFocus(); |
||||||
|
} else { |
||||||
|
setState(() { |
||||||
|
emojiShow = false; |
||||||
|
recordShow = false; |
||||||
|
menuShow = true; |
||||||
|
textFocus.unfocus(); |
||||||
|
}); |
||||||
|
} |
||||||
|
} : null, |
||||||
|
child: Container( |
||||||
|
width: 20.0, |
||||||
|
child: Icon(Icons.add_circle_rounded, |
||||||
|
color: isOnline ? color.primary : Color(0xFFADB0BB))), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
if (emojiShow && isOnline) Emoji(action: _selectEmoji), |
||||||
|
if (recordShow && isOnline) |
||||||
|
Container( |
||||||
|
height: 100.0, |
||||||
|
child: AudioRecorder( |
||||||
|
path: Global.recordPath + _recordName, onStop: _sendRecord), |
||||||
|
), |
||||||
|
if (menuShow && isOnline) |
||||||
|
Container( |
||||||
|
height: 100.0, |
||||||
|
child: Wrap( |
||||||
|
spacing: 20.0, |
||||||
|
runSpacing: 20.0, |
||||||
|
alignment: WrapAlignment.center, |
||||||
|
children: <Widget>[ |
||||||
|
ExtensionButton( |
||||||
|
icon: Icons.image_rounded, |
||||||
|
text: lang.album, |
||||||
|
action: _sendImage, |
||||||
|
bgColor: color.surface, |
||||||
|
iconColor: color.primary), |
||||||
|
ExtensionButton( |
||||||
|
icon: Icons.folder_rounded, |
||||||
|
text: lang.file, |
||||||
|
action: _sendFile, |
||||||
|
bgColor: color.surface, |
||||||
|
iconColor: color.primary), |
||||||
|
ExtensionButton( |
||||||
|
icon: Icons.person_rounded, |
||||||
|
text: lang.contact, |
||||||
|
action: () => _sendContact(color, lang, context.read<ChatProvider>().friends.values), |
||||||
|
bgColor: color.surface, |
||||||
|
iconColor: color.primary), |
||||||
|
], |
||||||
|
), |
||||||
|
) |
||||||
|
], |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class ExtensionButton extends StatelessWidget { |
||||||
|
final String text; |
||||||
|
final IconData icon; |
||||||
|
final Function action; |
||||||
|
final Color bgColor; |
||||||
|
final Color iconColor; |
||||||
|
|
||||||
|
const ExtensionButton({ |
||||||
|
Key key, |
||||||
|
this.icon, |
||||||
|
this.text, |
||||||
|
this.action, |
||||||
|
this.bgColor, |
||||||
|
this.iconColor, |
||||||
|
}) : super(key: key); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return GestureDetector( |
||||||
|
onTap: action, |
||||||
|
child: Column( |
||||||
|
crossAxisAlignment: CrossAxisAlignment.center, |
||||||
|
mainAxisAlignment: MainAxisAlignment.center, |
||||||
|
children: [ |
||||||
|
Container( |
||||||
|
padding: const EdgeInsets.all(10.0), |
||||||
|
decoration: BoxDecoration( |
||||||
|
color: bgColor, |
||||||
|
borderRadius: BorderRadius.circular(15.0), |
||||||
|
), |
||||||
|
child: Icon(icon, color: iconColor, size: 36.0)), |
||||||
|
SizedBox(height: 5.0), |
||||||
|
Text(text, style: TextStyle(fontSize: 14.0)), |
||||||
|
], |
||||||
|
)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Widget _menuItem(Color color, int value, IconData icon, String text) { |
||||||
|
return PopupMenuItem<int>( |
||||||
|
value: value, |
||||||
|
child: Row( |
||||||
|
children: [ |
||||||
|
Icon(icon, color: color), |
||||||
|
Padding( |
||||||
|
padding: const EdgeInsets.only(left: 20.0, right: 10.0), |
||||||
|
child: Text(text, style: TextStyle(color: Colors.black, fontSize: 16.0)), |
||||||
|
) |
||||||
|
] |
||||||
|
), |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,121 @@ |
|||||||
|
import 'package:esse/l10n/localizations.dart'; |
||||||
|
import 'package:esse/utils/relative_time.dart'; |
||||||
|
import 'package:esse/widgets/avatar.dart'; |
||||||
|
import 'package:esse/global.dart'; |
||||||
|
|
||||||
|
enum GroupType { |
||||||
|
Encrypted, |
||||||
|
Common, |
||||||
|
Open, |
||||||
|
} |
||||||
|
|
||||||
|
enum CheckType { |
||||||
|
Allow, |
||||||
|
None, |
||||||
|
Deny, |
||||||
|
Wait, |
||||||
|
} |
||||||
|
|
||||||
|
extension GroupTypeExtension on GroupType { |
||||||
|
int toInt() { |
||||||
|
switch (this) { |
||||||
|
case GroupType.Encrypted: |
||||||
|
return 0; |
||||||
|
case GroupType.Common: |
||||||
|
return 1; |
||||||
|
case GroupType.Encrypted: |
||||||
|
return 2; |
||||||
|
default: |
||||||
|
return 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static GroupType fromInt(int s) { |
||||||
|
switch (s) { |
||||||
|
case 0: |
||||||
|
return GroupType.Encrypted; |
||||||
|
case 1: |
||||||
|
return GroupType.Common; |
||||||
|
case 2: |
||||||
|
return GroupType.Open; |
||||||
|
default: |
||||||
|
return GroupType.Encrypted; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension CheckTypeExtension on CheckType { |
||||||
|
List lang(AppLocalizations lang) { |
||||||
|
switch (this) { |
||||||
|
case CheckType.Allow: |
||||||
|
return [lang.groupCheckTypeAllow, true]; |
||||||
|
case CheckType.None: |
||||||
|
return [lang.groupCheckTypeNone, false]; |
||||||
|
case CheckType.Deny: |
||||||
|
return [lang.groupCheckTypeDeny, false]; |
||||||
|
default: |
||||||
|
return ['', false]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static CheckType fromInt(int s) { |
||||||
|
switch (s) { |
||||||
|
case 0: |
||||||
|
return CheckType.Allow; |
||||||
|
case 1: |
||||||
|
return CheckType.None; |
||||||
|
case 2: |
||||||
|
return CheckType.Deny; |
||||||
|
default: |
||||||
|
return CheckType.Deny; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class GroupChat { |
||||||
|
int id; |
||||||
|
String owner; |
||||||
|
String gid; |
||||||
|
GroupType type; |
||||||
|
String addr; |
||||||
|
String name; |
||||||
|
String bio; |
||||||
|
bool isTop; |
||||||
|
bool isOk; |
||||||
|
bool isClosed; |
||||||
|
bool isNeedAgree; |
||||||
|
RelativeTime lastTime; |
||||||
|
String lastContent; |
||||||
|
bool lastReaded; |
||||||
|
bool online = false; |
||||||
|
|
||||||
|
GroupChat.fromList(List params) { |
||||||
|
this.id = params[0]; |
||||||
|
this.owner = params[1]; |
||||||
|
this.gid = params[2]; |
||||||
|
this.type = GroupTypeExtension.fromInt(params[3]); |
||||||
|
this.addr = params[4]; |
||||||
|
this.name = params[5]; |
||||||
|
this.bio = params[6]; |
||||||
|
this.isTop = params[7] == "1"; |
||||||
|
this.isOk = params[8] == "1"; |
||||||
|
this.isClosed = params[9] == "1"; |
||||||
|
this.isNeedAgree = params[10] == "1"; |
||||||
|
this.lastTime = RelativeTime.fromInt(params[11]); |
||||||
|
this.lastContent = params[12]; |
||||||
|
this.lastReaded = params[13] == "1"; |
||||||
|
this.online = params[14] == "1"; |
||||||
|
} |
||||||
|
|
||||||
|
Avatar showAvatar({double width = 45.0, bool needOnline = true}) { |
||||||
|
final avatar = Global.avatarPath + this.gid + '.png'; |
||||||
|
return Avatar( |
||||||
|
width: width, |
||||||
|
name: this.name, |
||||||
|
avatarPath: avatar, |
||||||
|
online: this.online, |
||||||
|
needOnline: needOnline, |
||||||
|
hasNew: !this.lastReaded, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,295 @@ |
|||||||
|
use group_chat_types::{GroupInfo, GroupType}; |
||||||
|
use rand::Rng; |
||||||
|
use std::time::{SystemTime, UNIX_EPOCH}; |
||||||
|
use tdn::types::{ |
||||||
|
group::GroupId, |
||||||
|
primitive::{new_io_error, PeerAddr, Result}, |
||||||
|
rpc::{json, RpcParam}, |
||||||
|
}; |
||||||
|
use tdn_storage::local::{DStorage, DsValue}; |
||||||
|
|
||||||
|
pub(super) struct GroupChatKey(Vec<u8>); |
||||||
|
|
||||||
|
impl GroupChatKey { |
||||||
|
pub fn new(value: Vec<u8>) -> Self { |
||||||
|
Self(value) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn key(&self) -> &[u8] { |
||||||
|
&self.0 |
||||||
|
} |
||||||
|
|
||||||
|
pub fn hash(&self) -> Vec<u8> { |
||||||
|
vec![] // TODO
|
||||||
|
} |
||||||
|
|
||||||
|
pub fn from_hex(s: impl ToString) -> Result<Self> { |
||||||
|
let s = s.to_string(); |
||||||
|
if s.len() % 2 != 0 { |
||||||
|
return Err(new_io_error("Hex is invalid")); |
||||||
|
} |
||||||
|
let mut value = vec![]; |
||||||
|
|
||||||
|
for i in 0..(s.len() / 2) { |
||||||
|
let res = u8::from_str_radix(&s[2 * i..2 * i + 2], 16) |
||||||
|
.map_err(|_e| new_io_error("Hex is invalid"))?; |
||||||
|
value.push(res); |
||||||
|
} |
||||||
|
|
||||||
|
Ok(Self(value)) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn to_hex(&self) -> String { |
||||||
|
let mut hex = String::new(); |
||||||
|
hex.extend(self.0.iter().map(|byte| format!("{:02x?}", byte))); |
||||||
|
hex |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Group Chat Model.
|
||||||
|
pub(super) struct GroupChat { |
||||||
|
/// db auto-increment id.
|
||||||
|
pub id: i64, |
||||||
|
/// group chat owner.
|
||||||
|
pub owner: GroupId, |
||||||
|
/// group chat id.
|
||||||
|
pub g_id: GroupId, |
||||||
|
/// group chat type.
|
||||||
|
g_type: GroupType, |
||||||
|
/// group chat server addresse.
|
||||||
|
g_addr: PeerAddr, |
||||||
|
/// group chat name.
|
||||||
|
g_name: String, |
||||||
|
/// group chat simple intro.
|
||||||
|
g_bio: String, |
||||||
|
/// group chat is set top sessions.
|
||||||
|
is_top: bool, |
||||||
|
/// group chat is created ok.
|
||||||
|
is_ok: bool, |
||||||
|
/// group chat is closed.
|
||||||
|
is_closed: bool, |
||||||
|
/// group chat need manager agree.
|
||||||
|
is_need_agree: bool, |
||||||
|
/// group chat encrypted-key.
|
||||||
|
key: GroupChatKey, |
||||||
|
/// group chat lastest message time. (only ESSE used)
|
||||||
|
last_datetime: i64, |
||||||
|
/// group chat lastest message content. (only ESSE used)
|
||||||
|
last_content: String, |
||||||
|
/// group chat lastest message readed. (only ESSE used)
|
||||||
|
last_readed: bool, |
||||||
|
/// group chat created time.
|
||||||
|
datetime: i64, |
||||||
|
/// group chat is online.
|
||||||
|
online: bool, |
||||||
|
/// is deleted.
|
||||||
|
is_deleted: bool, |
||||||
|
} |
||||||
|
|
||||||
|
impl GroupChat { |
||||||
|
pub fn new( |
||||||
|
owner: GroupId, |
||||||
|
g_type: GroupType, |
||||||
|
g_addr: PeerAddr, |
||||||
|
g_name: String, |
||||||
|
g_bio: String, |
||||||
|
is_need_agree: bool, |
||||||
|
) -> Self { |
||||||
|
let g_id = GroupId(rand::thread_rng().gen::<[u8; 32]>()); |
||||||
|
|
||||||
|
let start = SystemTime::now(); |
||||||
|
let datetime = start |
||||||
|
.duration_since(UNIX_EPOCH) |
||||||
|
.map(|s| s.as_secs()) |
||||||
|
.unwrap_or(0) as i64; // safe for all life.
|
||||||
|
|
||||||
|
let key = GroupChatKey(vec![]); |
||||||
|
|
||||||
|
Self { |
||||||
|
owner, |
||||||
|
g_id, |
||||||
|
g_type, |
||||||
|
g_addr, |
||||||
|
g_name, |
||||||
|
g_bio, |
||||||
|
is_need_agree, |
||||||
|
key, |
||||||
|
datetime, |
||||||
|
id: 0, |
||||||
|
is_top: true, |
||||||
|
is_ok: false, |
||||||
|
is_closed: false, |
||||||
|
last_datetime: datetime, |
||||||
|
last_content: Default::default(), |
||||||
|
last_readed: true, |
||||||
|
online: false, |
||||||
|
is_deleted: false, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn to_group_info(self, avatar: Vec<u8>) -> GroupInfo { |
||||||
|
match self.g_type { |
||||||
|
GroupType::Common | GroupType::Open => GroupInfo::Common( |
||||||
|
self.owner, |
||||||
|
self.g_id, |
||||||
|
self.g_type, |
||||||
|
self.is_need_agree, |
||||||
|
self.g_name, |
||||||
|
self.g_bio, |
||||||
|
avatar, |
||||||
|
), |
||||||
|
GroupType::Encrypted => GroupInfo::Common( |
||||||
|
// TODO encrypted
|
||||||
|
self.owner, |
||||||
|
self.g_id, |
||||||
|
self.g_type, |
||||||
|
self.is_need_agree, |
||||||
|
self.g_name, |
||||||
|
self.g_bio, |
||||||
|
avatar, |
||||||
|
), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn to_rpc(&self) -> RpcParam { |
||||||
|
json!([ |
||||||
|
self.id, |
||||||
|
self.owner.to_hex(), |
||||||
|
self.g_id.to_hex(), |
||||||
|
self.g_type.to_u32(), |
||||||
|
self.g_addr.to_hex(), |
||||||
|
self.g_name, |
||||||
|
self.g_bio, |
||||||
|
if self.is_top { "1" } else { "0" }, |
||||||
|
if self.is_ok { "1" } else { "0" }, |
||||||
|
if self.is_closed { "1" } else { "0" }, |
||||||
|
if self.is_need_agree { "1" } else { "0" }, |
||||||
|
self.last_datetime, |
||||||
|
self.last_content, |
||||||
|
if self.last_readed { "1" } else { "0" }, |
||||||
|
if self.online { "1" } else { "0" }, |
||||||
|
]) |
||||||
|
} |
||||||
|
|
||||||
|
fn from_values(mut v: Vec<DsValue>, contains_deleted: bool) -> Self { |
||||||
|
let is_deleted = if contains_deleted { |
||||||
|
v.pop().unwrap().as_bool() |
||||||
|
} else { |
||||||
|
false |
||||||
|
}; |
||||||
|
|
||||||
|
Self { |
||||||
|
is_deleted, |
||||||
|
online: false, |
||||||
|
datetime: v.pop().unwrap().as_i64(), |
||||||
|
last_readed: v.pop().unwrap().as_bool(), |
||||||
|
last_content: v.pop().unwrap().as_string(), |
||||||
|
last_datetime: v.pop().unwrap().as_i64(), |
||||||
|
key: GroupChatKey::from_hex(v.pop().unwrap().as_string()) |
||||||
|
.unwrap_or(GroupChatKey::new(vec![])), |
||||||
|
is_closed: v.pop().unwrap().as_bool(), |
||||||
|
is_need_agree: v.pop().unwrap().as_bool(), |
||||||
|
is_ok: v.pop().unwrap().as_bool(), |
||||||
|
is_top: v.pop().unwrap().as_bool(), |
||||||
|
g_bio: v.pop().unwrap().as_string(), |
||||||
|
g_name: v.pop().unwrap().as_string(), |
||||||
|
g_addr: PeerAddr::from_hex(v.pop().unwrap().as_string()).unwrap_or(Default::default()), |
||||||
|
g_type: GroupType::from_u32(v.pop().unwrap().as_i64() as u32), |
||||||
|
g_id: GroupId::from_hex(v.pop().unwrap().as_string()).unwrap_or(Default::default()), |
||||||
|
owner: GroupId::from_hex(v.pop().unwrap().as_string()).unwrap_or(Default::default()), |
||||||
|
id: v.pop().unwrap().as_i64(), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn get(db: &DStorage, gid: &GroupId) -> Result<Option<GroupChat>> { |
||||||
|
let sql = format!("SELECT id, owner, gcd, gtype, addr, name, bio, is_top, is_ok, is_need_agree, is_closed, key, last_datetime, last_content, last_readed, datetime FROM groups WHERE gcd = '{}' AND is_deleted = false", gid.to_hex()); |
||||||
|
let mut matrix = db.query(&sql)?; |
||||||
|
if matrix.len() > 0 { |
||||||
|
let values = matrix.pop().unwrap(); // safe unwrap()
|
||||||
|
return Ok(Some(GroupChat::from_values(values, false))); |
||||||
|
} |
||||||
|
Ok(None) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn insert(&mut self, db: &DStorage) -> Result<()> { |
||||||
|
let sql = format!("INSERT INTO groups (owner, gcd, gtype, addr, name, bio, is_top, is_ok, is_need_agree, is_closed, key, last_datetime, last_content, last_readed, datetime, is_deleted) VALUES ('{}', '{}', {}, '{}', '{}', '{}', {}, {}, {}, {}, '{}', {}, '{}', {}, {}, false)", |
||||||
|
self.owner.to_hex(), |
||||||
|
self.g_id.to_hex(), |
||||||
|
self.g_type.to_u32(), |
||||||
|
self.g_addr.to_hex(), |
||||||
|
self.g_name, |
||||||
|
self.g_bio, |
||||||
|
if self.is_top { 1 } else { 0 }, |
||||||
|
if self.is_ok { 1 } else { 0 }, |
||||||
|
if self.is_need_agree { 1 } else { 0 }, |
||||||
|
if self.is_closed { 1 } else { 0 }, |
||||||
|
self.key.to_hex(), |
||||||
|
self.last_datetime, |
||||||
|
self.last_content, |
||||||
|
if self.last_readed { 1 } else { 0 }, |
||||||
|
self.datetime, |
||||||
|
); |
||||||
|
println!("{}", sql); |
||||||
|
let id = db.insert(&sql)?; |
||||||
|
self.id = id; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn ok(&mut self, db: &DStorage) -> Result<usize> { |
||||||
|
self.is_ok = true; |
||||||
|
let sql = format!("UPDATE groups SET is_ok=1 WHERE id = {}", self.id); |
||||||
|
db.update(&sql) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Group Member Model.
|
||||||
|
pub(super) struct Member { |
||||||
|
/// db auto-increment id.
|
||||||
|
id: i64, |
||||||
|
/// group's db id.
|
||||||
|
fid: i64, |
||||||
|
/// member's Did(GroupId)
|
||||||
|
m_id: GroupId, |
||||||
|
/// member's addresse.
|
||||||
|
m_addr: PeerAddr, |
||||||
|
/// member's name.
|
||||||
|
m_name: String, |
||||||
|
/// member's remark.
|
||||||
|
m_remark: String, |
||||||
|
/// is group chat manager.
|
||||||
|
is_manager: bool, |
||||||
|
/// member's joined time.
|
||||||
|
datetime: i64, |
||||||
|
} |
||||||
|
|
||||||
|
/// Group Chat message type.
|
||||||
|
pub(super) enum MessageType { |
||||||
|
String, |
||||||
|
Image, |
||||||
|
File, |
||||||
|
Contact, |
||||||
|
Emoji, |
||||||
|
Record, |
||||||
|
Phone, |
||||||
|
Video, |
||||||
|
} |
||||||
|
|
||||||
|
/// Group Chat Message Model.
|
||||||
|
pub(super) struct Message { |
||||||
|
/// db auto-increment id.
|
||||||
|
id: i64, |
||||||
|
/// group's db id.
|
||||||
|
fid: i64, |
||||||
|
/// member's db id.
|
||||||
|
m_id: i64, |
||||||
|
/// group message consensus height.
|
||||||
|
height: i64, |
||||||
|
/// message type.
|
||||||
|
m_type: MessageType, |
||||||
|
/// message content.
|
||||||
|
m_content: String, |
||||||
|
/// message is delivery.
|
||||||
|
m_delivery: bool, |
||||||
|
/// message created time.
|
||||||
|
datetime: i64, |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
#[rustfmt::skip] |
||||||
|
pub(super) const GROUP_CHAT_VERSIONS: [&str; 4] = [ |
||||||
|
"CREATE TABLE IF NOT EXISTS groups( |
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, |
||||||
|
owner TEXT NOT NULL, |
||||||
|
gcd TEXT NOT NULL, |
||||||
|
gtype INTEGER NOT NULL, |
||||||
|
addr TEXT NOT NULL, |
||||||
|
name TEXT NOT NULL, |
||||||
|
bio TEXT NOT NULL, |
||||||
|
is_top INTEGER NOT NULL, |
||||||
|
is_ok INTEGER NOT NULL, |
||||||
|
is_need_agree INTEGER NOT NULL, |
||||||
|
is_closed INTEGER NOT NULL, |
||||||
|
key TEXT NOT NULL, |
||||||
|
last_datetime INTEGER, |
||||||
|
last_content TEXT, |
||||||
|
last_readed INTEGER, |
||||||
|
datetime INTEGER NOT NULL, |
||||||
|
is_deleted INTEGER NOT NULL);", |
||||||
|
"CREATE TABLE IF NOT EXISTS requests( |
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, |
||||||
|
fid INTEGER NOT NULL, |
||||||
|
mid TEXT NOT NULL, |
||||||
|
name TEXT NOT NULL, |
||||||
|
remark TEXT, |
||||||
|
is_ok INTEGER NOT NULL, |
||||||
|
is_over INTEGER NOT NULL, |
||||||
|
datetime INTEGER NOT NULL, |
||||||
|
is_deleted INTEGER NOT NULL);", |
||||||
|
"CREATE TABLE IF NOT EXISTS members( |
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, |
||||||
|
fid INTEGER NOT NULL, |
||||||
|
mid TEXT NOT NULL, |
||||||
|
name TEXT NOT NULL, |
||||||
|
remark TEXT, |
||||||
|
is_manager INTEGER NOT NULL, |
||||||
|
datetime INTEGER NOT NULL, |
||||||
|
is_deleted INTEGER NOT NULL);", |
||||||
|
"CREATE TABLE IF NOT EXISTS messages( |
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, |
||||||
|
fid INTEGER NOT NULL, |
||||||
|
mid INTEGER NOT NULL, |
||||||
|
height INTEGER NOT NULL, |
||||||
|
m_type INTEGER NOT NULL, |
||||||
|
content TEXT NOT NULL, |
||||||
|
is_delivery INTEGER NOT NULL, |
||||||
|
datetime INTEGER NOT NULL, |
||||||
|
is_deleted INTEGER NOT NULL);", |
||||||
|
]; |
Loading…
Reference in new issue