Browse Source

group check & create

pull/18/head
Sun 4 years ago
parent
commit
cdcf3c8aba
  1. 124
      lib/apps/group_chat/add.dart
  2. 588
      lib/apps/group_chat/detail.dart
  3. 121
      lib/apps/group_chat/models.dart
  4. 96
      lib/apps/group_chat/page.dart
  5. 55
      lib/apps/group_chat/provider.dart
  6. 3
      lib/l10n/localizations.dart
  7. 6
      lib/l10n/localizations_en.dart
  8. 6
      lib/l10n/localizations_zh.dart
  9. 10
      src/apps.rs
  10. 24
      src/apps/chat/layer.rs
  11. 36
      src/apps/group_chat/layer.rs
  12. 295
      src/apps/group_chat/models.rs
  13. 58
      src/apps/group_chat/rpc.rs
  14. 17
      src/layer.rs
  15. 38
      src/migrate.rs
  16. 3
      src/migrate/account.rs
  17. 50
      src/migrate/group_chat.rs
  18. 8
      src/server.rs
  19. 17
      src/storage.rs

124
lib/apps/group_chat/add.dart

@ -18,6 +18,7 @@ import 'package:esse/provider.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<GroupAddPage> { @@ -134,7 +135,15 @@ class _GroupAddPageState extends State<GroupAddPage> {
}
_create() {
//
final addr = _createAddrController.text.trim();
final name = _createNameController.text.trim();
final bio = _createBioController.text.trim();
context.read<GroupChatProvider>().create(addr, name, bio, _groupNeedAgree);
setState(() {
_createNameController.text = '';
_createBioController.text = '';
_groupNeedAgree = false;
});
}
@override
@ -175,12 +184,14 @@ class _GroupAddPageState extends State<GroupAddPage> { @@ -175,12 +184,14 @@ class _GroupAddPageState extends State<GroupAddPage> {
final isDesktop = isDisplayDesktop(context);
final color = Theme.of(context).colorScheme;
final lang = AppLocalizations.of(context);
final provider = context.watch<ChatProvider>();
final requests = provider.requests;
final provider = context.watch<GroupChatProvider>();
final checks = provider.createCheckType.lang(lang);
final checkLang = checks[0];
final checkOk = checks[1];
provider.createSupported;
final account = context.read<AccountProvider>().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<GroupAddPage> { @@ -269,18 +280,18 @@ class _GroupAddPageState extends State<GroupAddPage> {
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<GroupAddPage> { @@ -324,7 +335,7 @@ class _GroupAddPageState extends State<GroupAddPage> {
}),
),
),
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<GroupAddPage> { @@ -346,7 +357,8 @@ class _GroupAddPageState extends State<GroupAddPage> {
))),
])),
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<GroupAddPage> { @@ -438,16 +450,15 @@ class _GroupAddPageState extends State<GroupAddPage> {
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 { @@ -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<GroupChatProvider>().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))),
)
),
]
)
),
),
],
),
);
}
}

588
lib/apps/group_chat/detail.dart

@ -0,0 +1,588 @@ @@ -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)),
)
]
),
);
}

121
lib/apps/group_chat/models.dart

@ -0,0 +1,121 @@ @@ -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,
);
}
}

96
lib/apps/group_chat/page.dart

@ -7,6 +7,8 @@ import 'package:esse/l10n/localizations.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<GroupChatList> { @@ -18,9 +20,15 @@ class _GroupChatListState extends State<GroupChatList> {
Widget build(BuildContext context) {
final color = Theme.of(context).colorScheme;
final isDesktop = isDisplayDesktop(context);
final provider = context.watch<GroupChatProvider>();
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<GroupChatList> { @@ -36,3 +44,89 @@ class _GroupChatListState extends State<GroupChatList> {
);
}
}
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<GroupChatProvider>().updateActivedGroup(group.id);
// if (!isDesktop) {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (_) => GroupChatPage(),
// ),
// );
// } else {
// context.read<AccountProvider>().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),
),
)
]),
],
),
),
),
],
),
),
);
}
}

55
lib/apps/group_chat/provider.dart

@ -1,10 +1,22 @@ @@ -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<int, int> groups = {};
List<GroupType> createSupported = [GroupType.Encrypted, GroupType.Common, GroupType.Open];
CheckType createCheckType = CheckType.Wait;
Map<int, GroupChat> groups = {};
List<int> createKeys = [];
List<int> orderKeys = [];
SplayTreeMap<int, GroupChat> requests = SplayTreeMap();
int actived;
GroupChat get activedGroup => this.groups[this.actived];
GroupChatProvider() {
// rpc.
@ -13,6 +25,7 @@ class GroupChatProvider extends ChangeNotifier { @@ -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 { @@ -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 { @@ -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();
}
}

3
lib/l10n/localizations.dart

@ -162,6 +162,9 @@ abstract class AppLocalizations { @@ -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

6
lib/l10n/localizations_en.dart

@ -234,4 +234,10 @@ class AppLocalizationsEn extends AppLocalizations { @@ -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';
}

6
lib/l10n/localizations_zh.dart

@ -234,4 +234,10 @@ class AppLocalizationsZh extends AppLocalizations { @@ -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 => '没有权限在此创建群聊';
}

10
src/apps.rs

@ -1,8 +1,12 @@ @@ -1,8 +1,12 @@
use tdn::types::{
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<RpcState>) { @@ -25,13 +29,13 @@ pub(crate) fn app_rpc_inject(handler: &mut RpcHandler<RpcState>) {
}
pub(crate) async fn app_layer_handle(
layer: &mut Layer,
layer: &Arc<RwLock<Layer>>,
fgid: GroupId,
mgid: GroupId,
msg: RecvType,
) -> Result<HandleResult> {
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,
}
}

24
src/apps/chat/layer.rs

@ -1,9 +1,13 @@ @@ -1,9 +1,13 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tdn::types::{
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 { @@ -64,12 +68,14 @@ pub(crate) enum LayerEvent {
}
pub(crate) async fn handle(
layer: &mut Layer,
arc_layer: &Arc<RwLock<Layer>>,
fgid: GroupId,
mgid: GroupId,
msg: RecvType,
) -> Result<HandleResult> {
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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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

36
src/apps/group_chat/layer.rs

@ -1,14 +1,28 @@ @@ -1,14 +1,28 @@
use tdn::types::{
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<HandleResult> {
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<RwLock<Layer>>,
mgid: GroupId,
msg: RecvType,
) -> Result<HandleResult> {
let mut results = HandleResult::new();
match msg {
RecvType::Connect(_addr, _data) => {
@ -21,8 +35,20 @@ pub(crate) fn handle(_mgid: GroupId, msg: RecvType) -> Result<HandleResult> { @@ -21,8 +35,20 @@ pub(crate) fn handle(_mgid: GroupId, msg: RecvType) -> Result<HandleResult> {
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))
}
}
}
_ => {
//

295
src/apps/group_chat/models.rs

@ -0,0 +1,295 @@ @@ -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,
}

58
src/apps/group_chat/rpc.rs

@ -3,15 +3,28 @@ use tdn::types::{ @@ -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<GroupType>) -> RpcParam {
let s: Vec<u32> = 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<RpcState>) {
handler.add_method("group-chat-echo", |_, params, _| async move {
@ -32,4 +45,39 @@ pub(crate) fn new_rpc_handler(handler: &mut RpcHandler<RpcState>) { @@ -32,4 +45,39 @@ pub(crate) fn new_rpc_handler(handler: &mut RpcHandler<RpcState>) {
Ok(results)
},
);
handler.add_method(
"group-chat-create",
|gid: GroupId, params: Vec<RpcParam>, state: Arc<RpcState>| 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)
},
);
}

17
src/layer.rs

@ -11,7 +11,6 @@ use tdn::{ @@ -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 { @@ -31,22 +30,6 @@ pub(crate) struct Layer {
pub group: Arc<RwLock<Group>>,
}
impl Layer {
pub async fn handle(
&mut self,
fgid: GroupId,
mgid: GroupId,
msg: RecvType,
) -> Result<HandleResult> {
// 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<RwLock<Group>>) -> Result<Layer> {
Ok(Layer {

38
src/migrate.rs

@ -5,12 +5,14 @@ pub mod consensus; @@ -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"; @@ -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<()> { @@ -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<()> { @@ -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<()> { @@ -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<()> { @@ -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()
}

3
src/migrate/account.rs

@ -1,5 +1,5 @@ @@ -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] = [ @@ -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)",
];

50
src/migrate/group_chat.rs

@ -0,0 +1,50 @@ @@ -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);",
];

8
src/server.rs

@ -13,6 +13,7 @@ use tdn::{ @@ -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<()> { @@ -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;
}
}

17
src/storage.rs

@ -11,7 +11,8 @@ use tdn::types::{ @@ -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<()> { @@ -303,12 +304,14 @@ pub(crate) fn _write_emoji(base: &PathBuf, gid: &GroupId) -> Result<()> {
Ok(())
}
#[inline]
pub(crate) fn account_db(base: &PathBuf) -> Result<DStorage> {
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<DStorage> {
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> { @@ -316,6 +319,7 @@ pub(crate) fn consensus_db(base: &PathBuf, gid: &GroupId) -> Result<DStorage> {
DStorage::open(db_path)
}
#[inline]
pub(crate) fn session_db(base: &PathBuf, gid: &GroupId) -> Result<DStorage> {
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> { @@ -323,6 +327,7 @@ pub(crate) fn session_db(base: &PathBuf, gid: &GroupId) -> Result<DStorage> {
DStorage::open(db_path)
}
#[inline]
pub(crate) fn _file_db(base: &PathBuf, gid: &GroupId) -> Result<DStorage> {
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> { @@ -330,6 +335,7 @@ pub(crate) fn _file_db(base: &PathBuf, gid: &GroupId) -> Result<DStorage> {
DStorage::open(db_path)
}
#[inline]
pub(crate) fn _service_db(base: &PathBuf, gid: &GroupId) -> Result<DStorage> {
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> { @@ -337,6 +343,7 @@ pub(crate) fn _service_db(base: &PathBuf, gid: &GroupId) -> Result<DStorage> {
DStorage::open(db_path)
}
#[inline]
pub(crate) fn assistant_db(base: &PathBuf, gid: &GroupId) -> Result<DStorage> {
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> { @@ -344,6 +351,14 @@ pub(crate) fn assistant_db(base: &PathBuf, gid: &GroupId) -> Result<DStorage> {
DStorage::open(db_path)
}
#[inline]
pub(crate) fn group_chat_db(base: &PathBuf, gid: &GroupId) -> Result<DStorage> {
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();

Loading…
Cancel
Save