diff --git a/lib/apps/group_chat/add.dart b/lib/apps/group_chat/add.dart index f71e631..4a55c3b 100644 --- a/lib/apps/group_chat/add.dart +++ b/lib/apps/group_chat/add.dart @@ -144,7 +144,7 @@ class _GroupAddPageState extends State { addr = addr.substring(2); } var name = _joinNameController.text; - context.read().join(id, addr, name, ""); + context.read().join(GroupType.Open, id, addr, name, ""); setState(() { _joinIdController.text = ''; _joinAddrController.text = ''; diff --git a/lib/apps/group_chat/detail.dart b/lib/apps/group_chat/detail.dart index 53f8d3b..ca90ba8 100644 --- a/lib/apps/group_chat/detail.dart +++ b/lib/apps/group_chat/detail.dart @@ -14,6 +14,8 @@ 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/widgets/show_contact.dart'; +import 'package:esse/rpc.dart'; import 'package:esse/global.dart'; import 'package:esse/provider.dart'; @@ -214,7 +216,7 @@ class _GroupChatDetailState extends State { return Scaffold( key: GroupChatDetail._scaffoldKey, - endDrawer: _MemberDrawerWidget(title: lang.members), + endDrawer: _MemberDrawerWidget(gid: this.group.gid, title: lang.members), drawerScrimColor: color.background, body: SafeArea( child: Column( @@ -588,24 +590,38 @@ Widget _menuItem(Color color, int value, IconData icon, String text) { } class _MemberDrawerWidget extends StatelessWidget { + final String gid; final String title; - const _MemberDrawerWidget({Key key, this.title}) : super(key: key); + const _MemberDrawerWidget({Key key, this.gid, this.title}) : super(key: key); - Widget _item(context, Member member, bool isOwner, Color color) { + Widget _meItem(Member member, bool meOwner, bool meManager, Color color, lang) { + return Container( + height: 55.0, + child: ListTile( + leading: member.showAvatar(colorSurface: false), + title: Text(lang.me, textAlign: TextAlign.left, + style: TextStyle(fontSize: 16.0, fontStyle: FontStyle.italic)), + trailing: Text(meOwner ? lang.groupOwner : (meManager ? lang.manager : ''), + style: TextStyle(color: color)), + ) + ); + } + + Widget _item(context, Member member, bool isOwner, bool meOwner, bool meManager, Color color, lang) { return Container( height: 55.0, child: ListTile( leading: member.showAvatar(colorSurface: false), title: Text(member.name, textAlign: TextAlign.left, style: TextStyle(fontSize: 16.0)), trailing: Text(member.isBlock - ? 'Blocked' : (isOwner - ? 'Owner' : (member.isManager - ? 'Manager' : '')), + ? lang.blocked : (isOwner + ? lang.groupOwner : (member.isManager + ? lang.manager : '')), style: TextStyle(color: color)), onTap: () { Navigator.pop(context); showShadowDialog(context, Icons.group_rounded, title, - MemberDetail(member: member, isGroupOwner: isOwner, isGroupManager: member.isManager), + MemberDetail(member: member, isGroupOwner: meOwner, isGroupManager: meManager), 10.0, ); } @@ -613,16 +629,33 @@ class _MemberDrawerWidget extends StatelessWidget { ); } + _action(List ids) { + rpc.send('group-chat-invite', [gid, ids]); + } + + _invite(context, String title) { + Navigator.pop(context); + showShadowDialog(context, Icons.people_rounded, title, + ContactList(callback: _action, multiple: true), + 0.0, + ); + } + @override Widget build(BuildContext context) { final color = Theme.of(context).colorScheme; final lang = AppLocalizations.of(context); final isLight = color.brightness == Brightness.light; final isDesktop = isDisplayDesktop(context); + final myId = context.read().activedAccountId; final provider = context.watch(); final members = provider.activedMembers; - final allKeys = provider.activedMemberOrder; + final all = provider.activedMemberOrder(myId); + final allKeys = all[0]; + final meId = all[1]; + final meOwner = all[2]; + final meManager = all[3]; return Drawer( child: BackdropFilter( @@ -633,15 +666,41 @@ class _MemberDrawerWidget extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 20.0), child: Column( children: [ - Text(lang.members, style: Theme.of(context).textTheme.title), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.only(left: 20.0), + child: Text("${lang.members} (${allKeys.length + 1})", + style: Theme.of(context).textTheme.title), + ), + Container( + margin: const EdgeInsets.only(right: 10.0), + padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), + decoration: BoxDecoration( + border: Border.all(color: Color(0xFF6174FF)), + borderRadius: BorderRadius.circular(25.0)), + child: TextButton(child: Row( + children: [ + Icon(Icons.add, size: 16.0), + Text(lang.invite), + ] + ), + onPressed: () => _invite(context, lang.contact), + ), + ) + ] + ), const SizedBox(height: 10.0), const Divider(height: 1.0, color: Color(0x40ADB0BB)), const SizedBox(height: 10.0), + _meItem(members[meId], meOwner, meManager, color.primary, lang), Expanded( child: ListView.builder( itemCount: allKeys.length, itemBuilder: (BuildContext ctx, int index) => _item( - context, members[allKeys[index]], index == 0, color.primary + context, members[allKeys[index]], + index == 0 && !meOwner, meOwner, meManager, color.primary, lang ), ) ) @@ -702,75 +761,84 @@ class _MemberDetailState extends State { const SizedBox(height: 10.0), _infoListTooltip(Icons.person, color.primary, widget.member.mid), _infoListTooltip(Icons.location_on, color.primary, widget.member.addr), - const SizedBox(height: 10.0), if (widget.isGroupOwner) - InkWell( - onTap: () { - Navigator.pop(context); - // TODO delete. - }, - hoverColor: Colors.transparent, - child: Container( - width: 300.0, - padding: const EdgeInsets.symmetric(vertical: 10.0), - decoration: BoxDecoration( - border: Border.all(), - borderRadius: BorderRadius.circular(10.0)), - child: Center(child: Text(widget.member.isManager ? 'Cancel Manager' : 'Set Manager', - style: TextStyle(fontSize: 14.0))), + Container( + padding: const EdgeInsets.only(top: 20.0, bottom: 10.0), + child: InkWell( + onTap: () { + Navigator.pop(context); + // TODO delete. + }, + hoverColor: Colors.transparent, + child: Container( + width: 300.0, + padding: const EdgeInsets.symmetric(vertical: 10.0), + decoration: BoxDecoration( + border: Border.all(color: color.primary), + borderRadius: BorderRadius.circular(10.0)), + child: Center(child: Text(widget.member.isManager ? 'Cancel Manager' : 'Set Manager', + style: TextStyle(fontSize: 14.0, color: color.primary))), + ) ) ), - const SizedBox(height: 10.0), if (notFriend) - InkWell( - onTap: () { - Navigator.pop(context); - // TODO delete. - }, - hoverColor: Colors.transparent, - child: Container( - width: 300.0, - padding: const EdgeInsets.symmetric(vertical: 10.0), - decoration: BoxDecoration( - border: Border.all(color: Color(0xFF6174FF)), - borderRadius: BorderRadius.circular(10.0)), - child: Center(child: Text(lang.addFriend, - style: TextStyle(fontSize: 14.0, color: Color(0xFF6174FF)))), + Container( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: InkWell( + onTap: () { + Navigator.pop(context); + // TODO delete. + }, + hoverColor: Colors.transparent, + child: Container( + width: 300.0, + padding: const EdgeInsets.symmetric(vertical: 10.0), + decoration: BoxDecoration( + border: Border.all(color: Color(0xFF6174FF)), + borderRadius: BorderRadius.circular(10.0)), + child: Center(child: Text(lang.addFriend, + style: TextStyle(fontSize: 14.0, color: Color(0xFF6174FF)))), + ) ) ), - const SizedBox(height: 10.0), if (widget.isGroupManager || widget.isGroupOwner) - InkWell( - onTap: () { - Navigator.pop(context); - // TODO delete. - }, - hoverColor: Colors.transparent, - child: Container( - width: 300.0, - padding: const EdgeInsets.symmetric(vertical: 10.0), - decoration: BoxDecoration( - border: Border.all(color: Colors.red), - borderRadius: BorderRadius.circular(10.0)), - child: Center(child: Text(lang.delete, - style: TextStyle(fontSize: 14.0, color: Colors.red))), + Container( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: InkWell( + onTap: () { + Navigator.pop(context); + // TODO delete. + }, + hoverColor: Colors.transparent, + child: Container( + width: 300.0, + padding: const EdgeInsets.symmetric(vertical: 10.0), + decoration: BoxDecoration( + border: Border.all(color: Colors.red), + borderRadius: BorderRadius.circular(10.0)), + child: Center(child: Text(lang.delete, + style: TextStyle(fontSize: 14.0, color: Colors.red))), + ) ) ), - InkWell( - onTap: () { - Navigator.pop(context); - context.read().memberUpdate( - widget.member.id, !widget.member.isBlock); - }, - hoverColor: Colors.transparent, - child: Container( - width: 300.0, - padding: const EdgeInsets.symmetric(vertical: 10.0), - decoration: BoxDecoration( - border: Border.all(color: Colors.red), - borderRadius: BorderRadius.circular(10.0)), - child: Center(child: Text(widget.member.isBlock ? 'Blocked' : 'Block', - style: TextStyle(fontSize: 14.0, color: Colors.red))), + Container( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: InkWell( + onTap: () { + Navigator.pop(context); + context.read().memberUpdate( + widget.member.id, !widget.member.isBlock); + }, + hoverColor: Colors.transparent, + child: Container( + width: 300.0, + padding: const EdgeInsets.symmetric(vertical: 10.0), + decoration: BoxDecoration( + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(10.0)), + child: Center(child: Text(widget.member.isBlock ? lang.blocked : lang.block, + style: TextStyle(fontSize: 14.0, color: Colors.black))), + ) ) ), ] diff --git a/lib/apps/group_chat/provider.dart b/lib/apps/group_chat/provider.dart index 33bdcb3..930ee28 100644 --- a/lib/apps/group_chat/provider.dart +++ b/lib/apps/group_chat/provider.dart @@ -33,27 +33,40 @@ class GroupChatProvider extends ChangeNotifier { return false; } - List get activedMemberOrder { + List activedMemberOrder(String myId) { + int meId = 0; + bool meOwner = false; + bool meManager = false; List allKeys = []; List managers = []; List commons = []; List blocks = []; this.activedMembers.forEach((i, m) { - if (m.isBlock) { - blocks.add(i); - } else { + if (m.mid == myId) { + meId = i; if (m.isManager) { + meManager = true; if (m.mid == this.activedGroup.owner) { - allKeys.add(i); - } else { - managers.add(i); + meOwner = true; } + } + } else { + if (m.isBlock) { + blocks.add(i); } else { - commons.add(i); + if (m.isManager) { + if (m.mid == this.activedGroup.owner) { + allKeys.add(i); + } else { + managers.add(i); + } + } else { + commons.add(i); + } } } }); - return allKeys + managers + commons + blocks; + return [allKeys + managers + commons + blocks, meId, meOwner, meManager]; } GroupChatProvider() { @@ -117,8 +130,8 @@ class GroupChatProvider extends ChangeNotifier { rpc.send('group-chat-resend', [id, myName]); } - join(String gid, String gaddr, String name, String remark, [String key = '']) { - rpc.send('group-chat-join', [gid, gaddr, name, remark, key]); + join(GroupType gtype, String gid, String gaddr, String name, String remark, [String proof = '', String key = '']) { + rpc.send('group-chat-join', [gtype.toInt(), gid, gaddr, name, remark, proof, key]); } requestHandle(String gid, int id, int rid, bool ok) { @@ -150,6 +163,12 @@ class GroupChatProvider extends ChangeNotifier { // rpc.send('group-chat-readd', [id]); } + invite(String gid, List ids) { + print(gid); + print(ids); + rpc.send('group-chat-invite', [ids]); + } + memberUpdate(int id, bool isBlock) { rpc.send('group-chat-member-update', [id, isBlock]); } diff --git a/lib/apps/primitives.dart b/lib/apps/primitives.dart index 512cf57..610de8a 100644 --- a/lib/apps/primitives.dart +++ b/lib/apps/primitives.dart @@ -11,6 +11,7 @@ enum MessageType { Record, Phone, Video, + Invite, } // use 00-99 @@ -33,6 +34,8 @@ extension MessageTypeExtension on MessageType { return 6; case MessageType.Video: return 7; + case MessageType.Invite: + return 8; default: return 0; } @@ -56,6 +59,8 @@ extension MessageTypeExtension on MessageType { return MessageType.Phone; case 7: return MessageType.Video; + case 8: + return MessageType.Invite; default: return MessageType.String; } diff --git a/lib/l10n/localizations.dart b/lib/l10n/localizations.dart index f71d08b..8d49a80 100644 --- a/lib/l10n/localizations.dart +++ b/lib/l10n/localizations.dart @@ -82,6 +82,11 @@ abstract class AppLocalizations { String get create; String get exit; String get loadMore; + String get me; + String get manager; + String get block; + String get blocked; + String get invite; // theme String get themeDark; @@ -175,6 +180,7 @@ abstract class AppLocalizations { String get groupChatBio; String get groupJoin; String get groupCreate; + String get groupOwner; String get groupTypeEncrypted; String get groupTypeEncryptedInfo; String get groupTypePrivate; diff --git a/lib/l10n/localizations_en.dart b/lib/l10n/localizations_en.dart index 09f3538..e969b00 100644 --- a/lib/l10n/localizations_en.dart +++ b/lib/l10n/localizations_en.dart @@ -88,6 +88,16 @@ class AppLocalizationsEn extends AppLocalizations { String get exit => 'Exit'; @override String get loadMore => 'Load more...'; + @override + String get me => 'Me'; + @override + String get manager => 'Manager'; + @override + String get block => 'Block'; + @override + String get blocked => 'Blocked'; + @override + String get invite => 'Invite'; // theme @override @@ -261,6 +271,8 @@ class AppLocalizationsEn extends AppLocalizations { @override String get groupCreate => 'Create Group'; @override + String get groupOwner => 'Owner'; + @override String get groupTypeEncrypted => 'Encrypted'; @override String get groupTypeEncryptedInfo => "Encrypted: It can only be joined by the invitation of members, and the manager's consent is optional, Members hold a zero-knowledge proof about the key to enter; Group information and messages are encrypted and stored on the server, and the server NOT has secret key, INVISIBLE information."; diff --git a/lib/l10n/localizations_zh.dart b/lib/l10n/localizations_zh.dart index 3b81098..2f65be3 100644 --- a/lib/l10n/localizations_zh.dart +++ b/lib/l10n/localizations_zh.dart @@ -88,6 +88,16 @@ class AppLocalizationsZh extends AppLocalizations { String get exit => '退出'; @override String get loadMore => '加载更多'; + @override + String get me => '我'; + @override + String get manager => '管理员'; + @override + String get block => '拉黑'; + @override + String get blocked => '已拉黑'; + @override + String get invite => '邀请'; // theme @override @@ -261,6 +271,8 @@ class AppLocalizationsZh extends AppLocalizations { @override String get groupCreate => '新建群'; @override + String get groupOwner => '群主'; + @override String get groupTypeEncrypted => '加密'; @override String get groupTypeEncryptedInfo => '加密的群聊:只能通过群成员邀请加入,可选是否需要管理员同意,成员需持有密钥的零知识证明方可进入;群信息和消息全部加密存储在服务端,服务端无密钥,不可见信息。'; diff --git a/lib/widgets/chat_message.dart b/lib/widgets/chat_message.dart index 8db4e4d..a82b90b 100644 --- a/lib/widgets/chat_message.dart +++ b/lib/widgets/chat_message.dart @@ -308,6 +308,26 @@ class ChatMessage extends StatelessWidget { } } + Widget _showInvite(context, lang, color) { + // contact [name, gid, addr, avatar] + //final infos = message.showContact(); + //final gid = 'EG' + infos[1].toUpperCase(); + + final width = MediaQuery.of(context).size.width * 0.6; + // text + return Container( + constraints: BoxConstraints(minWidth: 50, maxWidth: width), + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 14.0), + decoration: BoxDecoration( + color: message.isMe ? Color(0xFF6174FF) : color.primaryVariant, + borderRadius: BorderRadius.circular(15.0), + ), + child: Text(message.content, + style: TextStyle( + color: message.isMe ? Colors.white : Color(0xFF1C1939), + fontSize: 14.0))); + } + Widget _infoListTooltip(icon, color, text) { return Container( width: 300.0, @@ -338,6 +358,8 @@ class ChatMessage extends StatelessWidget { return _showContact(context, lang, color); } else if (message.type == MessageType.Record) { return _showRecord(); + } else if (message.type == MessageType.Invite) { + return _showInvite(context, lang, color); } return _showText(context, color, isDesktop); } diff --git a/lib/widgets/show_contact.dart b/lib/widgets/show_contact.dart index 907c9d4..2ce41d2 100644 --- a/lib/widgets/show_contact.dart +++ b/lib/widgets/show_contact.dart @@ -1,12 +1,65 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:esse/l10n/localizations.dart'; -import 'package:esse/models/friend.dart'; -import 'package:esse/widgets/shadow_dialog.dart'; +import 'package:esse/apps/chat/models.dart' show Friend; +import 'package:esse/apps/chat/provider.dart'; -openContact(BuildContext context, ColorScheme color, AppLocalizations lang, List orders, Map friends, Function callback) { - showShadowDialog( - context, Icons.person_rounded, lang.contact, Column( +class ContactList extends StatefulWidget { + final Function callback; + final bool multiple; + const ContactList({Key key, this.callback, this.multiple}): super(key: key); + + @override + _ContactListState createState() => _ContactListState(); +} + +class _ContactListState extends State { + List _checks = []; + Map _friends = {}; + List _keys = []; + + @override + initState() { + super.initState(); + new Future.delayed(Duration.zero, () { + final provider = context.read(); + _friends = provider.friends; + _keys = provider.orderKeys; + _checks = List.generate(_keys.length, (_) => false); + setState(() {}); + }); + } + + Widget _friend(int i, Friend friend) { + return Container( + height: 55.0, + child: ListTile( + leading: friend.showAvatar(), + title: Text(friend.name), + trailing: Checkbox( + value: _checks[i], + onChanged: (bool value) { + setState(() { + _checks[i] = value; + }); + }, + ), + ) + ); + } + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).colorScheme; + final lang = AppLocalizations.of(context); + + double maxHeight = (MediaQuery.of(context).size.height - 300); + if (maxHeight < 100.0) { + maxHeight = 100.0; + } + + return Column( children: [ Container( height: 40.0, @@ -29,28 +82,38 @@ openContact(BuildContext context, ColorScheme color, AppLocalizations lang, List ), ), ), - SizedBox(height: 15.0), - Column( - children: orders.map((id) { - final contact = friends[id]; - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => callback(context, contact.id), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 14.0), - child: Row( - children: [ - contact.showAvatar(needOnline: false), - SizedBox(width: 15.0), - Text(contact.name, style: TextStyle(fontSize: 16.0)), - ], - ), - ), - ); - }).toList() + const SizedBox(height: 16.0), + Container( + height: maxHeight, + child: SingleChildScrollView( + child: Column(children: List.generate(_keys.length, (i) => + _friend(i, _friends[_keys[i]]) + )) + ) + ), + const Divider(height: 1.0, color: Color(0x40ADB0BB)), + const SizedBox(height: 10.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton(child: Text(lang.cancel, style: TextStyle(color: color.onSurface)), + onPressed: () => Navigator.pop(context), + ), + TextButton(child: Text(lang.ok), + onPressed: () { + Navigator.pop(context); + List ids = []; + _keys.asMap().forEach((i, value) { + if (_checks[i]) { + ids.add(value); + } + }); + widget.callback(ids); + }, + ), + ] ) ] - ) - ); - + ); + } } diff --git a/pubspec.lock b/pubspec.lock index 5e77623..d7854a7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,14 +21,14 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.7.0" audio_session: dependency: transitive description: name: audio_session url: "https://pub.dartlang.org" source: hosted - version: "0.1.0" + version: "0.1.2" boolean_selector: dependency: transitive description: @@ -64,6 +64,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + collision: + dependency: transitive + description: + name: collision + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.3" convert: dependency: "direct main" description: @@ -77,7 +84,7 @@ packages: name: crop url: "https://pub.dartlang.org" source: hosted - version: "0.5.1" + version: "0.5.1+1" cross_file: dependency: transitive description: @@ -154,7 +161,7 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.1" file: dependency: transitive description: @@ -168,7 +175,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.2+2" file_selector: dependency: "direct main" description: @@ -181,7 +188,7 @@ packages: description: path: "plugins/file_selector/file_selector_linux" ref: HEAD - resolved-ref: f2d8aa3820fb87316516670bf4d51a74de8ac0dd + resolved-ref: e48abe7c3e9ebfe0b81622167c5201d4e783bb81 url: "git://github.com/google/flutter-desktop-embedding.git" source: git version: "0.0.2" @@ -190,7 +197,7 @@ packages: description: path: "plugins/file_selector/file_selector_macos" ref: HEAD - resolved-ref: f2d8aa3820fb87316516670bf4d51a74de8ac0dd + resolved-ref: e48abe7c3e9ebfe0b81622167c5201d4e783bb81 url: "git://github.com/google/flutter-desktop-embedding.git" source: git version: "0.0.4" @@ -213,7 +220,7 @@ packages: description: path: "plugins/file_selector/file_selector_windows" ref: HEAD - resolved-ref: f2d8aa3820fb87316516670bf4d51a74de8ac0dd + resolved-ref: e48abe7c3e9ebfe0b81622167c5201d4e783bb81 url: "git://github.com/google/flutter-desktop-embedding.git" source: git version: "0.0.2" @@ -245,14 +252,14 @@ packages: name: flutter_localized_locales url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" flutter_test: dependency: "direct dev" description: flutter @@ -302,7 +309,7 @@ packages: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.7.5+2" + version: "0.7.5+3" image_picker_for_web: dependency: transitive description: @@ -342,7 +349,7 @@ packages: name: just_audio url: "https://pub.dartlang.org" source: hosted - version: "0.7.4+1" + version: "0.7.5" just_audio_platform_interface: dependency: transitive description: @@ -370,7 +377,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.4.0" nested: dependency: transitive description: @@ -398,7 +405,7 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" path_provider_linux: dependency: transitive description: @@ -524,14 +531,14 @@ packages: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.26.0" + version: "0.27.1" shared_preferences: dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" shared_preferences_linux: dependency: transitive description: @@ -620,7 +627,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.0" typed_data: dependency: transitive description: diff --git a/src/apps/chat/layer.rs b/src/apps/chat/layer.rs index 0c124c8..d12c1e6 100644 --- a/src/apps/chat/layer.rs +++ b/src/apps/chat/layer.rs @@ -445,6 +445,7 @@ impl LayerEvent { // TODO (NetworkMessage::Video, content) } + MessageType::Invite => (NetworkMessage::Invite(content.clone()), content), }; let mut msg = Message::new(&mgid, fid, true, m_type, raw, false); @@ -499,7 +500,7 @@ pub(super) fn reject_message( SendType::Event(uid, addr, data) } -pub(super) fn event_message( +pub(crate) fn event_message( layer: &mut Layer, tid: i64, me_id: GroupId, diff --git a/src/apps/chat/mod.rs b/src/apps/chat/mod.rs index d649fe1..4191cd9 100644 --- a/src/apps/chat/mod.rs +++ b/src/apps/chat/mod.rs @@ -2,8 +2,8 @@ mod layer; mod models; pub(crate) mod rpc; -pub(crate) use layer::chat_conn; pub(crate) use layer::handle as layer_handle; pub(crate) use layer::LayerEvent; +pub(crate) use layer::{chat_conn, event_message}; pub(crate) use models::{Friend, Message, MessageType, NetworkMessage, Request}; pub(crate) use rpc::new_rpc_handler; diff --git a/src/apps/chat/models.rs b/src/apps/chat/models.rs index 4df01fb..a856638 100644 --- a/src/apps/chat/models.rs +++ b/src/apps/chat/models.rs @@ -51,6 +51,7 @@ pub(crate) enum NetworkMessage { Emoji, Phone, Video, + Invite(String), None, } @@ -97,6 +98,7 @@ impl NetworkMessage { // TODO (MessageType::Video, "".to_owned()) } + NetworkMessage::Invite(content) => (MessageType::Invite, content), NetworkMessage::None => { return Ok(Message::new_with_id( hash, @@ -148,6 +150,7 @@ impl NetworkMessage { }; Ok(NetworkMessage::Record(bytes, time)) } + MessageType::Invite => Ok(NetworkMessage::Invite(model.content)), MessageType::Emoji => Ok(NetworkMessage::Emoji), MessageType::Phone => Ok(NetworkMessage::Phone), MessageType::Video => Ok(NetworkMessage::Video), @@ -165,6 +168,7 @@ pub(crate) enum MessageType { Record, Phone, Video, + Invite, } impl MessageType { @@ -178,6 +182,7 @@ impl MessageType { MessageType::Record => 5, MessageType::Phone => 6, MessageType::Video => 7, + MessageType::Invite => 8, } } @@ -191,6 +196,7 @@ impl MessageType { 5 => MessageType::Record, 6 => MessageType::Phone, 7 => MessageType::Video, + 8 => MessageType::Invite, _ => MessageType::String, } } diff --git a/src/apps/group_chat/models.rs b/src/apps/group_chat/models.rs index 2f19dc3..3cf8b4f 100644 --- a/src/apps/group_chat/models.rs +++ b/src/apps/group_chat/models.rs @@ -65,7 +65,7 @@ pub(crate) struct GroupChat { /// group chat id. pub g_id: GroupId, /// group chat type. - g_type: GroupType, + pub g_type: GroupType, /// group chat server addresse. pub g_addr: PeerAddr, /// group chat name. diff --git a/src/apps/group_chat/rpc.rs b/src/apps/group_chat/rpc.rs index d75f51f..2cc7d08 100644 --- a/src/apps/group_chat/rpc.rs +++ b/src/apps/group_chat/rpc.rs @@ -9,9 +9,9 @@ use tdn_did::Proof; use group_chat_types::{CheckType, Event, GroupType, JoinProof, LayerEvent}; -use crate::apps::chat::MessageType; +use crate::apps::chat::{Friend, MessageType}; use crate::rpc::RpcState; -use crate::storage::group_chat_db; +use crate::storage::{chat_db, group_chat_db}; use super::add_layer; use super::models::{to_network_message, GroupChat, GroupChatKey, Member, Message, Request}; @@ -236,11 +236,14 @@ pub(crate) fn new_rpc_handler(handler: &mut RpcHandler) { handler.add_method( "group-chat-join", |gid: GroupId, params: Vec, state: Arc| async move { - let gcd = GroupId::from_hex(params[0].as_str()?)?; - let gaddr = PeerAddr::from_hex(params[1].as_str()?)?; - let gname = params[2].as_str()?.to_owned(); - let gremark = params[3].as_str()?.to_owned(); - let gkey = params[4].as_str()?; + let _gtype = GroupType::from_u32(params[0].as_i64()? as u32); + let gcd = GroupId::from_hex(params[1].as_str()?)?; + let gaddr = PeerAddr::from_hex(params[2].as_str()?)?; + let gname = params[3].as_str()?.to_owned(); + let gremark = params[4].as_str()?.to_owned(); + let gproof = params[5].as_str()?; + let _proof = Proof::from_hex(gproof).unwrap_or(Proof::default()); + let gkey = params[6].as_str()?; let key = GroupChatKey::from_hex(gkey).unwrap_or(GroupChatKey::new(vec![])); let mut request = Request::new_by_me(gcd, gaddr, gname, gremark, key); @@ -259,6 +262,75 @@ pub(crate) fn new_rpc_handler(handler: &mut RpcHandler) { }, ); + handler.add_method( + "group-chat-invite", + |gid: GroupId, params: Vec, state: Arc| async move { + let id = params[0].as_i64()?; + let gcd = GroupId::from_hex(params[1].as_str()?)?; + let ids: Vec = params[2] + .as_array()? + .iter() + .filter_map(|v| v.as_i64()) + .collect(); + + // + let group_lock = state.group.read().await; + let base = group_lock.base().clone(); + + let chat = chat_db(&base, &gid)?; + let group_db = group_chat_db(&base, &gid)?; + + let mut invites = vec![]; + for id in ids { + let friend = Friend::get_id(&chat, id)??; + if Member::get_id(&group_db, &id, &friend.gid).is_err() { + let proof = group_lock.prove_addr(&gid, &friend.gid.into())?; + invites.push((friend.id, friend.gid, friend.addr, proof)); + } + } + drop(chat); + + let gc = GroupChat::get_id(&group_db, &id)??; + let tmp_name = gc.g_name.replace(";", "-;"); + + let mut results = HandleResult::new(); + let mut layer_lock = state.layer.write().await; + for (fid, fgid, mut faddr, proof) in invites { + let contact_values = format!( + "{};;{};;{};;{};;{}", + gc.g_type.to_u32(), + gcd.to_hex(), + gc.g_addr.to_hex(), + tmp_name, + proof.to_hex(), + ); + + // check if encrypted group type. need online. + if gc.g_type == GroupType::Encrypted { + if let Ok(addr) = layer_lock.running(&gid)?.online(&fgid) { + faddr = addr; + } else { + continue; + } + } + + let (msg, nw) = crate::apps::chat::LayerEvent::from_message( + &base, + gid, + fid, + MessageType::Invite, + contact_values, + ) + .await?; + let event = crate::apps::chat::LayerEvent::Message(msg.hash, nw); + let s = + crate::apps::chat::event_message(&mut layer_lock, msg.id, gid, faddr, &event); + results.layers.push((gid, fgid, s)); + } + Ok(results) + }, + ); + handler.add_method( "group-chat-request-handle", |gid: GroupId, params: Vec, state: Arc| async move {