diff --git a/lib/apps/group_chat/add.dart b/lib/apps/group_chat/add.dart index d7bca7e..34b4aff 100644 --- a/lib/apps/group_chat/add.dart +++ b/lib/apps/group_chat/add.dart @@ -18,6 +18,7 @@ import 'package:esse/provider.dart'; import 'package:esse/apps/chat/models.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 GroupAddPage extends StatefulWidget { @@ -134,7 +135,15 @@ class _GroupAddPageState extends State { } _create() { - // + final addr = _createAddrController.text.trim(); + final name = _createNameController.text.trim(); + final bio = _createBioController.text.trim(); + context.read().create(addr, name, bio, _groupNeedAgree); + setState(() { + _createNameController.text = ''; + _createBioController.text = ''; + _groupNeedAgree = false; + }); } @override @@ -175,12 +184,14 @@ class _GroupAddPageState extends State { final isDesktop = isDisplayDesktop(context); final color = Theme.of(context).colorScheme; final lang = AppLocalizations.of(context); - final provider = context.watch(); - final requests = provider.requests; + final provider = context.watch(); + final checks = provider.createCheckType.lang(lang); + final checkLang = checks[0]; + final checkOk = checks[1]; + provider.createSupported; - final account = context.read().activedAccount; - - final requestKeys = requests.keys.toList().reversed.toList(); // it had sorted. + final groups = provider.groups; + final createKeys = provider.createKeys; return SafeArea( child: DefaultTabController( @@ -269,18 +280,18 @@ class _GroupAddPageState extends State { const SizedBox(height: 20.0), const Divider(height: 1.0, color: Color(0x40ADB0BB)), const SizedBox(height: 10.0), - if (requests.isNotEmpty) - Container( - width: 600.0, - child: ListView.builder( - itemCount: requestKeys.length, - shrinkWrap: true, - physics: ClampingScrollPhysics(), - scrollDirection: Axis.vertical, - itemBuilder: (BuildContext context, int index) => - _RequestItem(request: requests[requestKeys[index]]), - ), - ) + // if (requests.isNotEmpty) + // Container( + // width: 600.0, + // child: ListView.builder( + // itemCount: requestKeys.length, + // shrinkWrap: true, + // physics: ClampingScrollPhysics(), + // scrollDirection: Axis.vertical, + // itemBuilder: (BuildContext context, int index) => + // _RequestItem(request: requests[requestKeys[index]]), + // ), + // ) ], ), ), @@ -324,7 +335,7 @@ class _GroupAddPageState extends State { }), ), ), - if (_addrOnline) + if (checkOk) Container( padding: const EdgeInsets.only(left: 8.0), child: Icon(Icons.cloud_done_rounded, @@ -346,7 +357,8 @@ class _GroupAddPageState extends State { ))), ])), const SizedBox(height: 8.0), - Text('Error Message here', style: TextStyle(fontSize: 14.0, color: Colors.red)), + Text(checkLang, style: TextStyle(fontSize: 14.0, + color: checkOk ? Colors.green : Colors.red)), Container( width: 600.0, padding: const EdgeInsets.all(10.0), @@ -438,16 +450,15 @@ class _GroupAddPageState extends State { const SizedBox(height: 20.0), const Divider(height: 1.0, color: Color(0x40ADB0BB)), const SizedBox(height: 10.0), - if (requests.isNotEmpty) Container( width: 600.0, child: ListView.builder( - itemCount: requestKeys.length, + itemCount: createKeys.length, shrinkWrap: true, physics: ClampingScrollPhysics(), scrollDirection: Axis.vertical, itemBuilder: (BuildContext context, int index) => - _RequestItem(request: requests[requestKeys[index]]), + _CreateItem(group: groups[createKeys[index]]), ), ) ], @@ -678,3 +689,70 @@ class _RequestItem extends StatelessWidget { ); } } + +class _CreateItem extends StatelessWidget { + final GroupChat group; + const _CreateItem({Key key, this.group}) : super(key: key); + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).colorScheme; + final lang = AppLocalizations.of(context); + + return SizedBox( + height: 55.0, + child: Row( + children: [ + Container( + width: 45.0, + height: 45.0, + margin: const EdgeInsets.only(right: 15.0), + child: group.showAvatar(), + ), + Expanded( + child: Container( + height: 55.0, + child: Row( + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(group.name, maxLines: 1, overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 16.0)), + Text(group.bio, maxLines: 1, overflow: TextOverflow.ellipsis, + style: TextStyle(color: Color(0xFFADB0BB), + fontSize: 12.0)), + ], + ), + ), + SizedBox(width: 10.0), + group.isOk + ? Container( + child: Text( + lang.added, + style: TextStyle(color: Color(0xFFADB0BB), fontSize: 14.0), + )) + : InkWell( + onTap: () => context.read().reSend(group.id), + hoverColor: Colors.transparent, + child: Container( + height: 35.0, + padding: const EdgeInsets.symmetric(horizontal: 10.0), + decoration: BoxDecoration( + border: Border.all(color: color.primary), + borderRadius: BorderRadius.circular(10.0)), + child: Center(child: Text(lang.send, + style: TextStyle(fontSize: 14.0, color: color.primary))), + ) + ), + ] + ) + ), + ), + ], + ), + ); + } +} diff --git a/lib/apps/group_chat/detail.dart b/lib/apps/group_chat/detail.dart new file mode 100644 index 0000000..6f30b87 --- /dev/null +++ b/lib/apps/group_chat/detail.dart @@ -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 { + 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().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().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().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().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((contact) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () async { + context.read().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(); + final recentMessages = provider.activedMessages; + final recentMessageKeys = recentMessages.keys.toList().reversed.toList(); + + final meName = context.read().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().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( + 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(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( + context, listen: false).groupClose(this.group.id); + if (!isDesktop) { + Navigator.pop(context); + } + }, + ), + ] + ); + }, + ); + } else if (value == 5) { + Provider.of(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( + context, listen: false).groupDelete(this.group.id); + if (!isDesktop) { + Navigator.pop(context); + } + }, + ), + ] + ); + }, + ); + } + }, + itemBuilder: (context) { + return >[ + _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: [ + 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().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( + 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)), + ) + ] + ), + ); +} diff --git a/lib/apps/group_chat/models.dart b/lib/apps/group_chat/models.dart index e69de29..c6a2af7 100644 --- a/lib/apps/group_chat/models.dart +++ b/lib/apps/group_chat/models.dart @@ -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, + ); + } +} diff --git a/lib/apps/group_chat/page.dart b/lib/apps/group_chat/page.dart index 0b5c350..8764e5c 100644 --- a/lib/apps/group_chat/page.dart +++ b/lib/apps/group_chat/page.dart @@ -7,6 +7,8 @@ import 'package:esse/l10n/localizations.dart'; import 'package:esse/provider.dart'; import 'package:esse/apps/group_chat/add.dart'; +import 'package:esse/apps/group_chat/models.dart'; +import 'package:esse/apps/group_chat/provider.dart'; class GroupChatList extends StatefulWidget { @override @@ -18,9 +20,15 @@ class _GroupChatListState extends State { Widget build(BuildContext context) { final color = Theme.of(context).colorScheme; final isDesktop = isDisplayDesktop(context); + final provider = context.watch(); + final orderKeys = provider.orderKeys; + final groups = provider.groups; return Scaffold( - body: const Center(child: Text('TODO group list!')), + body: ListView.builder( + itemCount: orderKeys.length, + itemBuilder: (BuildContext ctx, int index) => ListChat(group: groups[orderKeys[index]]), + ), floatingActionButton: FloatingActionButton( onPressed: () { final widget = GroupAddPage(); @@ -36,3 +44,89 @@ class _GroupChatListState extends State { ); } } + +class ListChat extends StatelessWidget { + final GroupChat group; + const ListChat({Key key, this.group}) : super(key: key); + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).colorScheme; + final lang = AppLocalizations.of(context); + final isDesktop = isDisplayDesktop(context); + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + context.read().updateActivedGroup(group.id); + // if (!isDesktop) { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (_) => GroupChatPage(), + // ), + // ); + // } else { + // context.read().updateActivedApp(GroupChatDetail()); + // } + }, + child: Container( + height: 55.0, + child: Row( + children: [ + Container( + width: 45.0, + height: 45.0, + margin: const EdgeInsets.only(left: 20.0, right: 15.0), + child: group.showAvatar(), + ), + Expanded( + child: Container( + height: 55.0, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text(group.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 16.0)) + ), + Container( + margin: const EdgeInsets.only(left: 15.0, right: 20.0), + child: Text(group.lastTime.toString(), + style: const TextStyle(color: Color(0xFFADB0BB), fontSize: 12.0), + ), + ) + ]), + const SizedBox(height: 4.0), + Row( + children: [ + Expanded( + child: Text(group.lastContent, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: Color(0xFFADB0BB), fontSize: 12.0)), + ), + if (group.isClosed) + Container( + margin: const EdgeInsets.only(left: 15.0, right: 20.0), + child: Text(lang.unfriended, + style: TextStyle(color: color.primary, fontSize: 12.0), + ), + ) + ]), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/apps/group_chat/provider.dart b/lib/apps/group_chat/provider.dart index 86da659..3e0db77 100644 --- a/lib/apps/group_chat/provider.dart +++ b/lib/apps/group_chat/provider.dart @@ -1,10 +1,22 @@ +import "dart:collection"; + import 'package:flutter/material.dart'; import 'package:esse/rpc.dart'; import 'package:esse/apps/group_chat/models.dart'; class GroupChatProvider extends ChangeNotifier { - Map groups = {}; + List createSupported = [GroupType.Encrypted, GroupType.Common, GroupType.Open]; + CheckType createCheckType = CheckType.Wait; + + Map groups = {}; + List createKeys = []; + List orderKeys = []; + SplayTreeMap requests = SplayTreeMap(); + + int actived; + + GroupChat get activedGroup => this.groups[this.actived]; GroupChatProvider() { // rpc. @@ -13,6 +25,7 @@ class GroupChatProvider extends ChangeNotifier { // rpc.addListener('group-chat-offline', _online, false); rpc.addListener('group-chat-check', _check, false); rpc.addListener('group-chat-create', _create, false); + rpc.addListener('group-chat-result', _result, false); // rpc.addListener('group-chat-update', _update, false); // rpc.addListener('group-chat-join', _join, true); // rpc.addListener('group-chat-agree', _agree, true); @@ -37,12 +50,20 @@ class GroupChatProvider extends ChangeNotifier { rpc.send('group-chat-list', []); } + updateActivedGroup(int id) { + this.actived = id; + } + check(String addr) { rpc.send('group-chat-check', [addr]); } - create() { - rpc.send('group-chat-create', []); + create(String addr, String name, String bio, bool needAgree) { + rpc.send('group-chat-create', [addr, name, bio, needAgree]); + } + + reSend(int id) { + // } _list(List params) { @@ -56,10 +77,34 @@ class GroupChatProvider extends ChangeNotifier { } _check(List params) { - // + this.createSupported.clear(); + this.createCheckType = CheckTypeExtension.fromInt(params[0]); + params[1].forEach((param) { + this.createSupported.add(GroupTypeExtension.fromInt(param)); + }); + notifyListeners(); } _create(List params) { - // + final gc = GroupChat.fromList(params); + if (gc.isOk) { + this.orderKeys.add(gc.id); + } else { + this.createKeys.add(gc.id); + } + this.groups[gc.id] = gc; + + notifyListeners(); + } + + _result(List params) { + final id = params[0]; + this.groups[id].isOk = params[1]; + this.groups[id].online = true; + if (params[1]) { + //this.createKeys.remove(id); + this.orderKeys.add(id); + } + notifyListeners(); } } diff --git a/lib/l10n/localizations.dart b/lib/l10n/localizations.dart index febad78..e8613d3 100644 --- a/lib/l10n/localizations.dart +++ b/lib/l10n/localizations.dart @@ -162,6 +162,9 @@ abstract class AppLocalizations { String get assistantBio; String get groupChat; String get groupChatBio; + String get groupCheckTypeAllow; + String get groupCheckTypeNone; + String get groupCheckTypeDeny; } class _AppLocalizationsDelegate diff --git a/lib/l10n/localizations_en.dart b/lib/l10n/localizations_en.dart index faf8fd7..5c950dd 100644 --- a/lib/l10n/localizations_en.dart +++ b/lib/l10n/localizations_en.dart @@ -234,4 +234,10 @@ class AppLocalizationsEn extends AppLocalizations { String get groupChat => 'Group Chats'; @override String get groupChatBio => 'Multiple group chats'; + @override + String get groupCheckTypeAllow => 'You can create a new group chat'; + @override + String get groupCheckTypeNone => 'Restricted, the allowed number is full'; + @override + String get groupCheckTypeDeny => 'No permission to create here'; } diff --git a/lib/l10n/localizations_zh.dart b/lib/l10n/localizations_zh.dart index 3a004fb..5de509d 100644 --- a/lib/l10n/localizations_zh.dart +++ b/lib/l10n/localizations_zh.dart @@ -234,4 +234,10 @@ class AppLocalizationsZh extends AppLocalizations { String get groupChat => '群聊'; @override String get groupChatBio => '各种各样的群聊'; + @override + String get groupCheckTypeAllow => '可以创建新的群聊'; + @override + String get groupCheckTypeNone => '创建被限制,允许数目已满'; + @override + String get groupCheckTypeDeny => '没有权限在此创建群聊'; } diff --git a/src/apps.rs b/src/apps.rs index 2db3e38..caa8a8d 100644 --- a/src/apps.rs +++ b/src/apps.rs @@ -1,8 +1,12 @@ -use tdn::types::{ - group::GroupId, - message::RecvType, - primitive::{HandleResult, Result}, - rpc::RpcHandler, +use std::sync::Arc; +use tdn::{ + smol::lock::RwLock, + types::{ + group::GroupId, + message::RecvType, + primitive::{HandleResult, Result}, + rpc::RpcHandler, + }, }; use crate::layer::Layer; @@ -25,13 +29,13 @@ pub(crate) fn app_rpc_inject(handler: &mut RpcHandler) { } pub(crate) async fn app_layer_handle( - layer: &mut Layer, + layer: &Arc>, fgid: GroupId, mgid: GroupId, msg: RecvType, ) -> Result { match fgid { - group_chat::GROUP_ID => group_chat::layer_handle(mgid, msg), + group_chat::GROUP_ID => group_chat::layer_handle(layer, mgid, msg).await, _ => chat::layer_handle(layer, fgid, mgid, msg).await, } } diff --git a/src/apps/chat/layer.rs b/src/apps/chat/layer.rs index 7b3c64d..f3b1dfd 100644 --- a/src/apps/chat/layer.rs +++ b/src/apps/chat/layer.rs @@ -1,9 +1,13 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; -use tdn::types::{ - group::{EventId, GroupId}, - message::{RecvType, SendType}, - primitive::{new_io_error, DeliveryType, HandleResult, PeerAddr, Result}, +use std::sync::Arc; +use tdn::{ + smol::lock::RwLock, + types::{ + group::{EventId, GroupId}, + message::{RecvType, SendType}, + primitive::{new_io_error, DeliveryType, HandleResult, PeerAddr, Result}, + }, }; use tdn_did::{user::User, Proof}; @@ -64,12 +68,14 @@ pub(crate) enum LayerEvent { } pub(crate) async fn handle( - layer: &mut Layer, + arc_layer: &Arc>, fgid: GroupId, mgid: GroupId, msg: RecvType, ) -> Result { let mut results = HandleResult::new(); + let mut layer = arc_layer.write().await; + match msg { RecvType::Connect(addr, data) => { let request: LayerRequest = postcard::from_bytes(&data) @@ -91,7 +97,7 @@ pub(crate) async fn handle( // 4. online to UI. results.rpcs.push(rpc::friend_online(mgid, fid, addr)); // 5. connected. - let msg = conn_res_message(layer, &mgid, addr).await?; + let msg = conn_res_message(&layer, &mgid, addr).await?; results.layers.push((mgid, fgid, msg)); layer.group.write().await.status( &mgid, @@ -146,7 +152,7 @@ pub(crate) async fn handle( friend.online = true; results.rpcs.push(rpc::friend_info(mgid, &friend)); // 4. connected. - let msg = conn_agree_message(layer, 0, &mgid, addr).await?; + let msg = conn_agree_message(&mut layer, 0, &mgid, addr).await?; results.layers.push((mgid, fgid, msg)); layer.group.write().await.status( &mgid, @@ -157,11 +163,13 @@ pub(crate) async fn handle( } } RecvType::Leave(addr) => { + let group_pin = layer.group.clone(); + let mut group_lock = group_pin.write().await; for (mgid, running) in &mut layer.runnings { let peers = running.peer_leave(&addr); for (fgid, fid) in peers { results.rpcs.push(rpc::friend_offline(*mgid, fid)); - layer.group.write().await.status( + group_lock.status( &mgid, StatusEvent::SessionFriendOffline(fgid), &mut results, @@ -303,7 +311,7 @@ pub(crate) async fn handle( // 5. online to UI. results.rpcs.push(rpc::friend_online(mgid, fid, addr)); // 6. connected. - let msg = conn_res_message(layer, &mgid, addr).await?; + let msg = conn_res_message(&layer, &mgid, addr).await?; results.layers.push((mgid, fgid, msg)); layer.group.write().await.status( &mgid, @@ -354,7 +362,7 @@ pub(crate) async fn handle( drop(db); } - let msg = conn_res_message(layer, &mgid, addr).await?; + let msg = conn_res_message(&layer, &mgid, addr).await?; results.layers.push((mgid, fgid, msg)); } LayerResponse::Reject => { @@ -377,7 +385,7 @@ pub(crate) async fn handle( } } RecvType::Event(addr, bytes) => { - return LayerEvent::handle(fgid, mgid, layer, addr, bytes).await; + return LayerEvent::handle(fgid, mgid, &mut layer, addr, bytes).await; } RecvType::Stream(_uid, _stream, _bytes) => { // TODO stream diff --git a/src/apps/group_chat/layer.rs b/src/apps/group_chat/layer.rs index c446191..d8b4e45 100644 --- a/src/apps/group_chat/layer.rs +++ b/src/apps/group_chat/layer.rs @@ -1,14 +1,28 @@ -use tdn::types::{ - group::GroupId, - message::RecvType, - primitive::{new_io_error, HandleResult, Result}, +use std::sync::Arc; +use tdn::{ + smol::lock::RwLock, + types::{ + group::GroupId, + message::RecvType, + primitive::{new_io_error, HandleResult, Result}, + }, }; use group_chat_types::GroupResult; //use group_chat_types::{Event, GroupConnect, GroupEvent, GroupInfo, GroupResult, GroupType}; -pub(crate) fn handle(_mgid: GroupId, msg: RecvType) -> Result { - let results = HandleResult::new(); +use crate::layer::Layer; +use crate::storage::group_chat_db; + +use super::models::GroupChat; +use super::rpc; + +pub(crate) async fn handle( + layer: &Arc>, + mgid: GroupId, + msg: RecvType, +) -> Result { + let mut results = HandleResult::new(); match msg { RecvType::Connect(_addr, _data) => { @@ -21,8 +35,20 @@ pub(crate) fn handle(_mgid: GroupId, msg: RecvType) -> Result { let res: GroupResult = postcard::from_bytes(&data) .map_err(|_e| new_io_error("Deseralize result failure"))?; match res { - GroupResult::Check(is_ok, supported) => { - println!("check: {}, supported: {:?}", is_ok, supported); + GroupResult::Check(ct, supported) => { + println!("check: {:?}, supported: {:?}", ct, supported); + results.rpcs.push(rpc::create_check(mgid, ct, supported)) + } + GroupResult::Create(gcd, ok) => { + println!("Create result: {}", ok); + if ok { + // TODO get gc by gcd. + let db = group_chat_db(layer.read().await.base(), &mgid)?; + if let Some(mut gc) = GroupChat::get(&db, &gcd)? { + gc.ok(&db)?; + results.rpcs.push(rpc::create_result(mgid, gc.id, ok)) + } + } } _ => { // diff --git a/src/apps/group_chat/models.rs b/src/apps/group_chat/models.rs index e69de29..b35bd9a 100644 --- a/src/apps/group_chat/models.rs +++ b/src/apps/group_chat/models.rs @@ -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); + +impl GroupChatKey { + pub fn new(value: Vec) -> Self { + Self(value) + } + + pub fn key(&self) -> &[u8] { + &self.0 + } + + pub fn hash(&self) -> Vec { + vec![] // TODO + } + + pub fn from_hex(s: impl ToString) -> Result { + 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) -> 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, 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> { + 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 { + 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, +} diff --git a/src/apps/group_chat/rpc.rs b/src/apps/group_chat/rpc.rs index 31a1f04..07f6a46 100644 --- a/src/apps/group_chat/rpc.rs +++ b/src/apps/group_chat/rpc.rs @@ -3,15 +3,28 @@ use tdn::types::{ group::GroupId, message::SendType, primitive::{new_io_error, HandleResult, PeerAddr}, - rpc::{json, RpcHandler, RpcParam}, + rpc::{json, rpc_response, RpcHandler, RpcParam}, }; +use tdn_did::Proof; -use group_chat_types::GroupConnect; -//use group_chat_types::{Event, GroupConnect, GroupEvent, GroupInfo, GroupResult, GroupType}; +use group_chat_types::{CheckType, GroupConnect, GroupInfo, GroupType}; -//use crate::group::GroupEvent; -use super::add_layer; use crate::rpc::RpcState; +use crate::storage::group_chat_db; + +use super::add_layer; +use super::models::GroupChat; + +#[inline] +pub(crate) fn create_check(mgid: GroupId, ct: CheckType, supported: Vec) -> RpcParam { + let s: Vec = supported.iter().map(|v| v.to_u32()).collect(); + rpc_response(0, "group-chat-check", json!([ct.to_u32(), s]), mgid) +} + +#[inline] +pub(crate) fn create_result(mgid: GroupId, gid: i64, ok: bool) -> RpcParam { + rpc_response(0, "group-chat-result", json!([gid, ok]), mgid) +} pub(crate) fn new_rpc_handler(handler: &mut RpcHandler) { handler.add_method("group-chat-echo", |_, params, _| async move { @@ -32,4 +45,39 @@ pub(crate) fn new_rpc_handler(handler: &mut RpcHandler) { Ok(results) }, ); + + handler.add_method( + "group-chat-create", + |gid: GroupId, params: Vec, state: Arc| async move { + let addr = PeerAddr::from_hex(params[0].as_str()?) + .map_err(|_e| new_io_error("PeerAddr invalid!"))?; + let name = params[1].as_str()?.to_owned(); + let bio = params[2].as_str()?.to_owned(); + let need_agree = params[3].as_bool()?; + let avatar = vec![]; + println!("Create: {}, {}, {}", name, bio, need_agree); + + let db = group_chat_db(state.layer.read().await.base(), &gid)?; + let mut gc = GroupChat::new(gid, GroupType::Common, addr, name, bio, need_agree); + let _gcd = gc.g_id; + + // save db + gc.insert(&db)?; + + // TODO save avatar + + let mut results = HandleResult::new(); + // TODO add to rpcs. + results.rpcs.push(json!(gc.to_rpc())); + let info = gc.to_group_info(avatar); + + // TODO create proof. + let proof: Proof = Default::default(); + + let data = postcard::to_allocvec(&GroupConnect::Create(info, proof)).unwrap_or(vec![]); + let s = SendType::Connect(0, addr, None, None, data); + add_layer(&mut results, gid, s); + Ok(results) + }, + ); } diff --git a/src/layer.rs b/src/layer.rs index e758637..c02d8e4 100644 --- a/src/layer.rs +++ b/src/layer.rs @@ -11,7 +11,6 @@ use tdn::{ }; use tdn_did::user::User; -use crate::apps::app_layer_handle; use crate::apps::chat::conn_req_message; use crate::apps::chat::Friend; use crate::group::Group; @@ -31,22 +30,6 @@ pub(crate) struct Layer { pub group: Arc>, } -impl Layer { - pub async fn handle( - &mut self, - fgid: GroupId, - mgid: GroupId, - msg: RecvType, - ) -> Result { - // 1. check to account is online. if not online, nothing. - if !self.runnings.contains_key(&mgid) { - return Err(new_io_error("running account not found.")); - } - - app_layer_handle(self, fgid, mgid, msg).await - } -} - impl Layer { pub async fn init(base: PathBuf, addr: PeerAddr, group: Arc>) -> Result { Ok(Layer { diff --git a/src/migrate.rs b/src/migrate.rs index 20c754c..5f56a02 100644 --- a/src/migrate.rs +++ b/src/migrate.rs @@ -5,12 +5,14 @@ pub mod consensus; mod account; mod file; +mod group_chat; mod service; mod session; use account::ACCOUNT_VERSIONS; use consensus::CONSENSUS_VERSIONS; use file::FILE_VERSIONS; +use group_chat::GROUP_CHAT_VERSIONS; use service::SERVICE_VERSIONS; use session::SESSION_VERSIONS; @@ -34,6 +36,9 @@ pub(crate) const SERVICE_DB: &'static str = "service.db"; /// Account's assistant database name pub(crate) const ASSISTANT_DB: &'static str = "assistant.db"; +/// Account's assistant database name +pub(crate) const GROUP_CHAT_DB: &'static str = "group_chat.db"; + pub(crate) fn main_migrate(path: &PathBuf) -> std::io::Result<()> { let mut db_path = path.clone(); db_path.push(ACCOUNT_DB); @@ -57,8 +62,24 @@ pub(crate) fn main_migrate(path: &PathBuf) -> std::io::Result<()> { ))?; } - let matrix = db.query("select db_name, version from migrates")?; + let mut account_matrix = db.query(&format!( + "select version from migrates where db_name = '{}'", + ACCOUNT_DB + ))?; + let account_version = account_matrix.pop().unwrap().pop().unwrap().as_i64() as usize; + if account_version != ACCOUNT_VERSIONS.len() { + // 2. migrate. + for i in &ACCOUNT_VERSIONS[account_version..] { + db.execute(i)?; + } + db.update(&format!( + "UPDATE migrates SET version = {} where db_name = '{}'", + ACCOUNT_VERSIONS.len(), + ACCOUNT_DB, + ))?; + } + let matrix = db.query("select db_name, version from migrates")?; for mut values in matrix { let db_version = values.pop().unwrap().as_i64() as usize; let db_name = values.pop().unwrap().as_string(); @@ -83,6 +104,7 @@ pub(crate) fn main_migrate(path: &PathBuf) -> std::io::Result<()> { FILE_DB => FILE_VERSIONS.as_ref(), SERVICE_DB => SERVICE_VERSIONS.as_ref(), ASSISTANT_DB => ASSISTANT_VERSIONS.as_ref(), + GROUP_CHAT_DB => GROUP_CHAT_VERSIONS.as_ref(), _ => { continue; } @@ -154,6 +176,12 @@ pub(crate) fn main_migrate(path: &PathBuf) -> std::io::Result<()> { ASSISTANT_DB, ))?; + db.update(&format!( + "UPDATE migrates SET version = {} where db_name = '{}'", + GROUP_CHAT_VERSIONS.len(), + GROUP_CHAT_DB, + ))?; + db.close()?; } @@ -199,5 +227,13 @@ pub(crate) fn account_init_migrate(path: &PathBuf) -> std::io::Result<()> { for i in &ASSISTANT_VERSIONS { db.execute(i)?; } + db.close()?; + + let mut db_path = path.clone(); + db_path.push(GROUP_CHAT_DB); + let db = DStorage::open(db_path)?; + for i in &GROUP_CHAT_VERSIONS { + db.execute(i)?; + } db.close() } diff --git a/src/migrate/account.rs b/src/migrate/account.rs index 16e1483..d2c0135 100644 --- a/src/migrate/account.rs +++ b/src/migrate/account.rs @@ -1,5 +1,5 @@ #[rustfmt::skip] -pub(super) const ACCOUNT_VERSIONS: [&str; 7] = [ +pub(super) const ACCOUNT_VERSIONS: [&str; 8] = [ "CREATE TABLE IF NOT EXISTS accounts( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, gid TEXT NOT NULL, @@ -19,4 +19,5 @@ pub(super) const ACCOUNT_VERSIONS: [&str; 7] = [ "INSERT INTO migrates (db_name, version) values ('session.db', 3)", "INSERT INTO migrates (db_name, version) values ('file.db', 1)", "INSERT INTO migrates (db_name, version) values ('assistant.db', 0)", + "INSERT INTO migrates (db_name, version) values ('group_chat.db', 0)", ]; diff --git a/src/migrate/group_chat.rs b/src/migrate/group_chat.rs new file mode 100644 index 0000000..8f5a067 --- /dev/null +++ b/src/migrate/group_chat.rs @@ -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);", +]; diff --git a/src/server.rs b/src/server.rs index d4fd600..c0ed383 100644 --- a/src/server.rs +++ b/src/server.rs @@ -13,6 +13,7 @@ use tdn::{ }; use crate::account::Account; +use crate::apps::app_layer_handle; use crate::group::Group; use crate::layer::Layer; use crate::migrate::main_migrate; @@ -81,7 +82,12 @@ pub async fn start(db_path: String) -> Result<()> { } } ReceiveMessage::Layer(fgid, tgid, l_msg) => { - if let Ok(handle_result) = layer.write().await.handle(fgid, tgid, l_msg).await { + // 1. check to account is online. if not online, nothing. + if !layer.read().await.runnings.contains_key(&tgid) { + continue; + } + + if let Ok(handle_result) = app_layer_handle(&layer, fgid, tgid, l_msg).await { handle(handle_result, now_rpc_uid, true, &sender).await; } } diff --git a/src/storage.rs b/src/storage.rs index f91cd30..6f5bd77 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -11,7 +11,8 @@ use tdn::types::{ use tdn_storage::local::DStorage; use crate::migrate::{ - account_init_migrate, ACCOUNT_DB, ASSISTANT_DB, CONSENSUS_DB, FILE_DB, SERVICE_DB, SESSION_DB, + account_init_migrate, ACCOUNT_DB, ASSISTANT_DB, CONSENSUS_DB, FILE_DB, GROUP_CHAT_DB, + SERVICE_DB, SESSION_DB, }; const FILES_DIR: &'static str = "files"; @@ -303,12 +304,14 @@ pub(crate) fn _write_emoji(base: &PathBuf, gid: &GroupId) -> Result<()> { Ok(()) } +#[inline] pub(crate) fn account_db(base: &PathBuf) -> Result { let mut db_path = base.clone(); db_path.push(ACCOUNT_DB); DStorage::open(db_path) } +#[inline] pub(crate) fn consensus_db(base: &PathBuf, gid: &GroupId) -> Result { let mut db_path = base.clone(); db_path.push(gid.to_hex()); @@ -316,6 +319,7 @@ pub(crate) fn consensus_db(base: &PathBuf, gid: &GroupId) -> Result { DStorage::open(db_path) } +#[inline] pub(crate) fn session_db(base: &PathBuf, gid: &GroupId) -> Result { let mut db_path = base.clone(); db_path.push(gid.to_hex()); @@ -323,6 +327,7 @@ pub(crate) fn session_db(base: &PathBuf, gid: &GroupId) -> Result { DStorage::open(db_path) } +#[inline] pub(crate) fn _file_db(base: &PathBuf, gid: &GroupId) -> Result { let mut db_path = base.clone(); db_path.push(gid.to_hex()); @@ -330,6 +335,7 @@ pub(crate) fn _file_db(base: &PathBuf, gid: &GroupId) -> Result { DStorage::open(db_path) } +#[inline] pub(crate) fn _service_db(base: &PathBuf, gid: &GroupId) -> Result { let mut db_path = base.clone(); db_path.push(gid.to_hex()); @@ -337,6 +343,7 @@ pub(crate) fn _service_db(base: &PathBuf, gid: &GroupId) -> Result { DStorage::open(db_path) } +#[inline] pub(crate) fn assistant_db(base: &PathBuf, gid: &GroupId) -> Result { let mut db_path = base.clone(); db_path.push(gid.to_hex()); @@ -344,6 +351,14 @@ pub(crate) fn assistant_db(base: &PathBuf, gid: &GroupId) -> Result { DStorage::open(db_path) } +#[inline] +pub(crate) fn group_chat_db(base: &PathBuf, gid: &GroupId) -> Result { + let mut db_path = base.clone(); + db_path.push(gid.to_hex()); + db_path.push(GROUP_CHAT_DB); + DStorage::open(db_path) +} + /// account independent db and storage directory. pub(crate) async fn account_init(base: &PathBuf, gid: &GroupId) -> Result<()> { let mut db_path = base.clone();