ignalxy 4 gadi atpakaļ
vecāks
revīzija
a4616d301a

+ 23 - 13
lib/common/api.dart

@@ -16,21 +16,23 @@ class Api {
   Api();
 
   static final Dio dio = Dio(BaseOptions(
-    baseUrl: 'http://10.122.237.112:80/',
+    baseUrl: 'http://mechat.shellmiao.com/',
   ));
 
-  static String WebSocketUrl='ws://10.122.234.72:80/infrastructure/ws/${Global.profile.username}/';
+  static String WebSocketUrl='ws://mechat.shellmiao.com/infrastructure/ws/${Global.profile.username}/';
 
 
 
   static final cookieJar = CookieJar();
 
   static init() async {
-    if (!Global.isRelease) {
+    if (!Global.isRelease && false) {
       (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
+        /*
         client.findProxy = (uri) {
           return "PROXY 10.122.237.112:8080";
         };
+         */
         //代理工具会提供一个抓包的自签名证书,会通不过证书校验,所以我们禁用证书校验
         client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
       };
@@ -40,6 +42,7 @@ class Api {
 
     try {
       await dio.get('/account/login/');
+      debug(await Dio().get('https://baidu.com/'));
       // await Dio().get('http://10.122.237.112/account/login/');
     } on DioError catch(e) {
       debug('Api init connect to server failed!');
@@ -135,15 +138,20 @@ class Api {
     return _result;
 }
 
+  List<String> listStringFromJson(String s) {
+    final list = <String>[];
+    for (var i in jsonDecode(s) as List<dynamic>) {
+      list.add(i.toString());
+    }
+    return list;
+  }
+
   Future<List<String>?> friendList() async {
     debug('Api friendList begin');
     List<String>? friends;
     try {
       var r = await dio.post('/friends/friends_list/');
-      friends = [];
-      for (var i in jsonDecode(r.data) as List<dynamic>) {
-        friends.add(i.toString());
-      }
+      friends = listStringFromJson(r.data);
       debug('Api friendList get: $friends');
     } on DioError catch (e) {
       debug('get friend list failed: ${e.response?.statusCode ?? "unknown"}');
@@ -191,7 +199,7 @@ class Api {
     List<String>? apps;
     try {
       var r = await dio.post('/friends/get_requests/');
-      apps = jsonDecode(r.data);
+      apps = listStringFromJson(r.data);
     } on DioError catch (e) {
       debug('get friend request failed!');
     }
@@ -240,7 +248,7 @@ class Api {
     List<String>? publicKeys;
     try {
       var r=await dio.post('/infrastructure/get_public_key/', data: FormData.fromMap({"username": username}));
-      publicKeys = jsonDecode(r.data);
+      publicKeys = listStringFromJson(r.data);
     } on DioError catch (e) {
       debug("get public key failed: ${ () {
         switch (e.response?.statusCode) {
@@ -253,7 +261,7 @@ class Api {
     return publicKeys;
   }
   Future<bool> sendMessage(String username,String plaintext) async {
-    debug("send message begin");
+    debug("send message begin: username=$username, plaintext=$plaintext");
     bool _result = true;
     try {
       await dio.post('/chat/send/', data: FormData.fromMap({"toUsername": username,"plaintext":plaintext}));
@@ -264,6 +272,7 @@ class Api {
           case 400: return "failed";
           case 422: return "the user does not exist";
           case 423: return "you are not friends";
+          default: return e;
         }
       } ()}");
       _result = false;
@@ -273,11 +282,12 @@ class Api {
   }
 
   Future<List<String>?> getFilterMessages(String username) async {
-    debug("get filter message begin");
+    debug("get filter message begin: username=$username");
     List<String>? messages;
     try {
       var r=await dio.post('/chat/filter_message/', data: FormData.fromMap({"username": username}));
-      messages = jsonDecode(r.data);
+      messages = listStringFromJson(r.data);
+      debug("get filter message: messages=$messages");
     } on DioError catch (e) {
       debug("get filter messages failed: ${ () {
         switch (e.response?.statusCode) {
@@ -297,7 +307,7 @@ class Api {
     List<String>? messages;
     try {
       var r=await dio.post('/chat/request_message/');
-      messages = jsonDecode(r.data);
+      messages = listStringFromJson(r.data);
     } on DioError catch (e) {
       debug("add public key failed: ${ () {
         switch (e.response?.statusCode) {

+ 12 - 12
lib/common/global.dart

@@ -66,18 +66,18 @@ class Global {
   }
 
   //初始化全局信息,会在APP启动时执行
-  static init() async {
+  static Future<void> init() async {
     debug('Init begin');
 
     bool status = await checkPermission();
     assert(status, "permission error");
+    final store = await openStore();
 
-    Store store = await openStore();
-    var box = store.box<Profile>();
+    Box box = store.box<Profile>();
     profile = box.get(profileId) ?? Profile();
-    store.close();
     profile.isLogin = false;
-    debug('Init profile: id: ${profile.id}, username: ${profile.username}, token: ${await profile.token}, isLogout: ${profile.isLogout}');
+    debug('Init profile: id: ${profile.id}, username: ${profile.username}, isLogout: ${profile.isLogout}');
+
 
     await Api.init();
 
@@ -91,17 +91,17 @@ class Global {
       debug('Init login failed');
     }
      */
-
+    store.close();
     debug('Global init end');
   }
 
   // 持久化Profile信息
   static saveProfile() async {
-    debug('save profile: username: ${profile.username}, isLogout: ${profile.isLogout}');
-    Store store = await openStore();
-    var box = store.box<Profile>();
-    box.removeAll();
-    box.put(profile);
-    store.close();
+    //TODO: save profile
+    // debug('save profile: username: ${profile.username}, isLogout: ${profile.isLogout}');
+    // var box = store.box<Profile>();
+    // box.removeAll();
+    // profile.id = 1;
+    // box.put(profile);
   }
 }

+ 2 - 0
lib/l10n/localization_intl.dart

@@ -27,6 +27,8 @@ class GmLocalizations {
   String get auto => Intl.message('Auto', name: 'auto');
   String get theme => Intl.message('Theme', name: 'theme');
   String get search => Intl.message('Search', name: 'search');
+  String get searchUser => Intl.message('Find User', name: 'find user');
+  String get addFriendFailed => Intl.message('Add friend failed', name: 'add friend failed');
 }
 
 //Locale代理类

+ 2 - 0
lib/main.dart

@@ -1,5 +1,6 @@
 import 'package:e2ee_chat/objectbox.g.dart';
 import 'package:e2ee_chat/view/add_friend.dart';
+import 'package:e2ee_chat/view/user_chat.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 import 'package:provider/single_child_widget.dart';
@@ -70,6 +71,7 @@ class MyApp extends StatelessWidget {
               "themes": (context) => ThemeRoute(),
               "language": (context) => LanguageRoute(),
               "add friend": (context) => AddFriendView(),
+              "user chat": (context) => UserChatView()
             },
           );
         },

+ 0 - 13
lib/model/content.dart

@@ -1,13 +0,0 @@
-import 'package:objectbox/objectbox.dart';
-
-enum ContentType {
-  text,
-  img
-}
-
-@Entity()
-class Content {
-  int id = 0;
-  final ContentType type = ContentType.text;
-  final String plaintext = "";
-}

+ 12 - 0
lib/model/friendship.dart

@@ -0,0 +1,12 @@
+import 'package:objectbox/objectbox.dart';
+
+import 'message.dart';
+import 'user.dart';
+
+@Entity()
+class FriendShip {
+  int id = 0;
+  final user0 = ToOne<User>();
+  final user1 = ToOne<User>();
+  final messages = ToMany<Message>();
+}

+ 15 - 9
lib/model/message.dart

@@ -1,25 +1,31 @@
-import 'package:e2ee_chat/model/content.dart';
+import 'dart:convert';
+
+import 'package:dash_chat/dash_chat.dart';
+import 'package:e2ee_chat/common/global.dart';
 import 'package:objectbox/objectbox.dart';
 
+import '../objectbox.g.dart';
 import 'group.dart';
 import 'user.dart';
 
 @Entity()
 class Message {
-
   int id = 0;
+  String plaintext = "";
   final from = ToOne<User>();
   final to = ToOne<User>();
-  final content = ToOne<Content>();
-  int time = 0;
+
+  @Transient()
+  ChatMessage get chatMessage => ChatMessage.fromJson(jsonDecode(plaintext));
 }
 
 @Entity()
 class GroupMessage {
   int id = 0;
-  final from = ToOne<User>();
-  final to = ToOne<Group>();
-  final content = ToOne<Content>();
-  int time = 0;
-}
+  String plaintext = "";
+  final user = ToOne<User>();
+  final group = ToOne<Group>();
 
+  @Transient()
+  ChatMessage get chatMessage => ChatMessage.fromJson(jsonDecode(plaintext));
+}

+ 1 - 0
lib/model/profile.dart

@@ -11,6 +11,7 @@ import 'user.dart';
 class Profile {
   int id = 0;
   int theme = Global.themes[0].value;
+  @Transient()
   User? user;
   bool isLogin = false;
   bool isLogout = false;

+ 7 - 9
lib/model/user.dart

@@ -1,3 +1,5 @@
+import 'package:dash_chat/dash_chat.dart';
+import 'package:e2ee_chat/common/global.dart';
 import 'package:e2ee_chat/objectbox.g.dart';
 import 'package:e2ee_chat/presenter/contact.dart';
 import 'package:objectbox/objectbox.dart';
@@ -7,7 +9,7 @@ import 'message.dart';
 
 @Entity()
 class User {
-  User(this.username, {this.bio, this.phone});
+  User(this.username, {this.bio, this.phone, this.avatar});
   int id = 0;
   @Unique()
   final String username;
@@ -16,18 +18,14 @@ class User {
   final groupMessages = ToMany<GroupMessage>();
   String? bio;
   String? phone;
+  String? avatar;
   bool isDND = false;
   bool isStick = false;
   bool isSpecialAttention = false;
 
-  static put(User user) async {
-    final store = await openStore();
-    final box = store.box<User>();
-    box.put(user);
-    store.close();
-  }
+  ChatUser toChatUser() => ChatUser(uid: username, name: username, avatar: avatar);
 
-  refreshProfile() => UserProfilePresenter(this).refresh();
+  Future<bool> refreshProfile() => UserProfilePresenter(this).refresh();
 
   // TODO: avatar
-}
+}

+ 99 - 53
lib/objectbox-model.json

@@ -5,7 +5,7 @@
   "entities": [
     {
       "id": "2:1438354990151910015",
-      "lastPropertyId": "7:4763561980566319174",
+      "lastPropertyId": "9:2124161694143580615",
       "name": "Profile",
       "properties": [
         {
@@ -32,23 +32,9 @@
       ],
       "relations": []
     },
-    {
-      "id": "3:2132486004932842474",
-      "lastPropertyId": "1:8861377786907255369",
-      "name": "Content",
-      "properties": [
-        {
-          "id": "1:8861377786907255369",
-          "name": "id",
-          "type": 6,
-          "flags": 1
-        }
-      ],
-      "relations": []
-    },
     {
       "id": "5:8880211362757189177",
-      "lastPropertyId": "6:4388824818686674479",
+      "lastPropertyId": "10:374576085041052636",
       "name": "Message",
       "properties": [
         {
@@ -58,40 +44,32 @@
           "flags": 1
         },
         {
-          "id": "2:7982216815340584307",
+          "id": "8:8678749043281739962",
           "name": "fromId",
           "type": 11,
           "flags": 520,
-          "indexId": "4:6495021264194887400",
+          "indexId": "19:8639940058127995637",
           "relationTarget": "User"
         },
         {
-          "id": "3:7721473969164711791",
+          "id": "9:5782107643295328726",
           "name": "toId",
           "type": 11,
           "flags": 520,
-          "indexId": "5:5329740598468773345",
+          "indexId": "20:304254955908714790",
           "relationTarget": "User"
         },
         {
-          "id": "5:634385368395524800",
-          "name": "time",
-          "type": 6
-        },
-        {
-          "id": "6:4388824818686674479",
-          "name": "contentId",
-          "type": 11,
-          "flags": 520,
-          "indexId": "10:205849996938840771",
-          "relationTarget": "Content"
+          "id": "10:374576085041052636",
+          "name": "plaintext",
+          "type": 9
         }
       ],
       "relations": []
     },
     {
       "id": "6:6066676571331973763",
-      "lastPropertyId": "6:3504407138147737485",
+      "lastPropertyId": "9:8156592908306452117",
       "name": "User",
       "properties": [
         {
@@ -121,6 +99,21 @@
           "id": "6:3504407138147737485",
           "name": "isSpecialAttention",
           "type": 1
+        },
+        {
+          "id": "7:1645455969656989260",
+          "name": "bio",
+          "type": 9
+        },
+        {
+          "id": "8:1358020543347548551",
+          "name": "phone",
+          "type": 9
+        },
+        {
+          "id": "9:8156592908306452117",
+          "name": "avatar",
+          "type": 9
         }
       ],
       "relations": [
@@ -168,7 +161,7 @@
     },
     {
       "id": "8:8842794649328270120",
-      "lastPropertyId": "6:5148143939128616318",
+      "lastPropertyId": "9:4521061296978960561",
       "name": "GroupMessage",
       "properties": [
         {
@@ -178,50 +171,89 @@
           "flags": 1
         },
         {
-          "id": "2:5514462395021006262",
-          "name": "fromId",
+          "id": "7:3385606654272810305",
+          "name": "userId",
           "type": 11,
           "flags": 520,
-          "indexId": "7:5268783855356873237",
+          "indexId": "17:8821791717138159999",
           "relationTarget": "User"
         },
         {
-          "id": "3:1148609827653786396",
-          "name": "toId",
+          "id": "8:3058571589698982963",
+          "name": "groupId",
           "type": 11,
           "flags": 520,
-          "indexId": "8:7769527806721188121",
+          "indexId": "18:1064437420014320687",
           "relationTarget": "Group"
         },
         {
-          "id": "5:7501600871221400279",
-          "name": "time",
-          "type": 6
+          "id": "9:4521061296978960561",
+          "name": "plaintext",
+          "type": 9
+        }
+      ],
+      "relations": []
+    },
+    {
+      "id": "9:2478855716396571806",
+      "lastPropertyId": "5:89425193393960797",
+      "name": "FriendShip",
+      "properties": [
+        {
+          "id": "1:5265335145979968165",
+          "name": "id",
+          "type": 6,
+          "flags": 1
         },
         {
-          "id": "6:5148143939128616318",
-          "name": "contentId",
+          "id": "4:6943551359011057155",
+          "name": "user0Id",
           "type": 11,
           "flags": 520,
-          "indexId": "11:6309601695170211538",
-          "relationTarget": "Content"
+          "indexId": "15:6350205891372750325",
+          "relationTarget": "User"
+        },
+        {
+          "id": "5:89425193393960797",
+          "name": "user1Id",
+          "type": 11,
+          "flags": 520,
+          "indexId": "16:5223993210314820647",
+          "relationTarget": "User"
         }
       ],
-      "relations": []
+      "relations": [
+        {
+          "id": "7:4471061252621927376",
+          "name": "messages",
+          "targetId": "5:8880211362757189177"
+        }
+      ]
     }
   ],
-  "lastEntityId": "8:8842794649328270120",
-  "lastIndexId": "11:6309601695170211538",
-  "lastRelationId": "6:6570620621500888428",
+  "lastEntityId": "9:2478855716396571806",
+  "lastIndexId": "21:4046871049442788311",
+  "lastRelationId": "7:4471061252621927376",
   "lastSequenceId": "0:0",
   "modelVersion": 5,
   "modelVersionParserMinimum": 5,
   "retiredEntityUids": [
     3444477729893015694,
-    5044745765388820377
+    5044745765388820377,
+    2132486004932842474
   ],
   "retiredIndexUids": [
-    2181766053148811865
+    2181766053148811865,
+    6495021264194887400,
+    5329740598468773345,
+    8703247978603696283,
+    459640529781791232,
+    5268783855356873237,
+    7769527806721188121,
+    7233565214869368113,
+    6309601695170211538,
+    205849996938840771,
+    4046871049442788311
   ],
   "retiredPropertyUids": [
     5463014948149082651,
@@ -234,7 +266,21 @@
     214180290351070734,
     7647214962273172849,
     6701330857882848509,
-    2182219019195059976
+    2182219019195059976,
+    7982216815340584307,
+    7721473969164711791,
+    609986407749732855,
+    8078506595736209118,
+    5514462395021006262,
+    1148609827653786396,
+    7294884137191806532,
+    7501600871221400279,
+    5148143939128616318,
+    634385368395524800,
+    4388824818686674479,
+    8861377786907255369,
+    4222660909971782394,
+    2124161694143580615
   ],
   "retiredRelationUids": [],
   "version": 1

+ 240 - 155
lib/objectbox.g.dart

@@ -9,7 +9,7 @@ import 'package:objectbox/internal.dart'; // generated code can access "internal
 import 'package:objectbox/objectbox.dart';
 import 'package:objectbox_flutter_libs/objectbox_flutter_libs.dart';
 
-import 'model/content.dart';
+import 'model/friendship.dart';
 import 'model/group.dart';
 import 'model/message.dart';
 import 'model/profile.dart';
@@ -21,7 +21,7 @@ final _entities = <ModelEntity>[
   ModelEntity(
       id: const IdUid(2, 1438354990151910015),
       name: 'Profile',
-      lastPropertyId: const IdUid(7, 4763561980566319174),
+      lastPropertyId: const IdUid(9, 2124161694143580615),
       flags: 0,
       properties: <ModelProperty>[
         ModelProperty(
@@ -47,24 +47,10 @@ final _entities = <ModelEntity>[
       ],
       relations: <ModelRelation>[],
       backlinks: <ModelBacklink>[]),
-  ModelEntity(
-      id: const IdUid(3, 2132486004932842474),
-      name: 'Content',
-      lastPropertyId: const IdUid(1, 8861377786907255369),
-      flags: 0,
-      properties: <ModelProperty>[
-        ModelProperty(
-            id: const IdUid(1, 8861377786907255369),
-            name: 'id',
-            type: 6,
-            flags: 1)
-      ],
-      relations: <ModelRelation>[],
-      backlinks: <ModelBacklink>[]),
   ModelEntity(
       id: const IdUid(5, 8880211362757189177),
       name: 'Message',
-      lastPropertyId: const IdUid(6, 4388824818686674479),
+      lastPropertyId: const IdUid(10, 374576085041052636),
       flags: 0,
       properties: <ModelProperty>[
         ModelProperty(
@@ -73,38 +59,31 @@ final _entities = <ModelEntity>[
             type: 6,
             flags: 1),
         ModelProperty(
-            id: const IdUid(2, 7982216815340584307),
+            id: const IdUid(8, 8678749043281739962),
             name: 'fromId',
             type: 11,
             flags: 520,
-            indexId: const IdUid(4, 6495021264194887400),
+            indexId: const IdUid(19, 8639940058127995637),
             relationTarget: 'User'),
         ModelProperty(
-            id: const IdUid(3, 7721473969164711791),
+            id: const IdUid(9, 5782107643295328726),
             name: 'toId',
             type: 11,
             flags: 520,
-            indexId: const IdUid(5, 5329740598468773345),
+            indexId: const IdUid(20, 304254955908714790),
             relationTarget: 'User'),
         ModelProperty(
-            id: const IdUid(5, 634385368395524800),
-            name: 'time',
-            type: 6,
-            flags: 0),
-        ModelProperty(
-            id: const IdUid(6, 4388824818686674479),
-            name: 'contentId',
-            type: 11,
-            flags: 520,
-            indexId: const IdUid(10, 205849996938840771),
-            relationTarget: 'Content')
+            id: const IdUid(10, 374576085041052636),
+            name: 'plaintext',
+            type: 9,
+            flags: 0)
       ],
       relations: <ModelRelation>[],
       backlinks: <ModelBacklink>[]),
   ModelEntity(
       id: const IdUid(6, 6066676571331973763),
       name: 'User',
-      lastPropertyId: const IdUid(6, 3504407138147737485),
+      lastPropertyId: const IdUid(9, 8156592908306452117),
       flags: 0,
       properties: <ModelProperty>[
         ModelProperty(
@@ -132,6 +111,21 @@ final _entities = <ModelEntity>[
             id: const IdUid(6, 3504407138147737485),
             name: 'isSpecialAttention',
             type: 1,
+            flags: 0),
+        ModelProperty(
+            id: const IdUid(7, 1645455969656989260),
+            name: 'bio',
+            type: 9,
+            flags: 0),
+        ModelProperty(
+            id: const IdUid(8, 1358020543347548551),
+            name: 'phone',
+            type: 9,
+            flags: 0),
+        ModelProperty(
+            id: const IdUid(9, 8156592908306452117),
+            name: 'avatar',
+            type: 9,
             flags: 0)
       ],
       relations: <ModelRelation>[
@@ -175,7 +169,7 @@ final _entities = <ModelEntity>[
   ModelEntity(
       id: const IdUid(8, 8842794649328270120),
       name: 'GroupMessage',
-      lastPropertyId: const IdUid(6, 5148143939128616318),
+      lastPropertyId: const IdUid(9, 4521061296978960561),
       flags: 0,
       properties: <ModelProperty>[
         ModelProperty(
@@ -184,33 +178,59 @@ final _entities = <ModelEntity>[
             type: 6,
             flags: 1),
         ModelProperty(
-            id: const IdUid(2, 5514462395021006262),
-            name: 'fromId',
+            id: const IdUid(7, 3385606654272810305),
+            name: 'userId',
             type: 11,
             flags: 520,
-            indexId: const IdUid(7, 5268783855356873237),
+            indexId: const IdUid(17, 8821791717138159999),
             relationTarget: 'User'),
         ModelProperty(
-            id: const IdUid(3, 1148609827653786396),
-            name: 'toId',
+            id: const IdUid(8, 3058571589698982963),
+            name: 'groupId',
             type: 11,
             flags: 520,
-            indexId: const IdUid(8, 7769527806721188121),
+            indexId: const IdUid(18, 1064437420014320687),
             relationTarget: 'Group'),
         ModelProperty(
-            id: const IdUid(5, 7501600871221400279),
-            name: 'time',
+            id: const IdUid(9, 4521061296978960561),
+            name: 'plaintext',
+            type: 9,
+            flags: 0)
+      ],
+      relations: <ModelRelation>[],
+      backlinks: <ModelBacklink>[]),
+  ModelEntity(
+      id: const IdUid(9, 2478855716396571806),
+      name: 'FriendShip',
+      lastPropertyId: const IdUid(5, 89425193393960797),
+      flags: 0,
+      properties: <ModelProperty>[
+        ModelProperty(
+            id: const IdUid(1, 5265335145979968165),
+            name: 'id',
             type: 6,
-            flags: 0),
+            flags: 1),
         ModelProperty(
-            id: const IdUid(6, 5148143939128616318),
-            name: 'contentId',
+            id: const IdUid(4, 6943551359011057155),
+            name: 'user0Id',
             type: 11,
             flags: 520,
-            indexId: const IdUid(11, 6309601695170211538),
-            relationTarget: 'Content')
+            indexId: const IdUid(15, 6350205891372750325),
+            relationTarget: 'User'),
+        ModelProperty(
+            id: const IdUid(5, 89425193393960797),
+            name: 'user1Id',
+            type: 11,
+            flags: 520,
+            indexId: const IdUid(16, 5223993210314820647),
+            relationTarget: 'User')
+      ],
+      relations: <ModelRelation>[
+        ModelRelation(
+            id: const IdUid(7, 4471061252621927376),
+            name: 'messages',
+            targetId: const IdUid(5, 8880211362757189177))
       ],
-      relations: <ModelRelation>[],
       backlinks: <ModelBacklink>[])
 ];
 
@@ -234,12 +254,28 @@ Future<Store> openStore(
 ModelDefinition getObjectBoxModel() {
   final model = ModelInfo(
       entities: _entities,
-      lastEntityId: const IdUid(8, 8842794649328270120),
-      lastIndexId: const IdUid(11, 6309601695170211538),
-      lastRelationId: const IdUid(6, 6570620621500888428),
+      lastEntityId: const IdUid(9, 2478855716396571806),
+      lastIndexId: const IdUid(21, 4046871049442788311),
+      lastRelationId: const IdUid(7, 4471061252621927376),
       lastSequenceId: const IdUid(0, 0),
-      retiredEntityUids: const [3444477729893015694, 5044745765388820377],
-      retiredIndexUids: const [2181766053148811865],
+      retiredEntityUids: const [
+        3444477729893015694,
+        5044745765388820377,
+        2132486004932842474
+      ],
+      retiredIndexUids: const [
+        2181766053148811865,
+        6495021264194887400,
+        5329740598468773345,
+        8703247978603696283,
+        459640529781791232,
+        5268783855356873237,
+        7769527806721188121,
+        7233565214869368113,
+        6309601695170211538,
+        205849996938840771,
+        4046871049442788311
+      ],
       retiredPropertyUids: const [
         5463014948149082651,
         2305308568754167021,
@@ -251,7 +287,21 @@ ModelDefinition getObjectBoxModel() {
         214180290351070734,
         7647214962273172849,
         6701330857882848509,
-        2182219019195059976
+        2182219019195059976,
+        7982216815340584307,
+        7721473969164711791,
+        609986407749732855,
+        8078506595736209118,
+        5514462395021006262,
+        1148609827653786396,
+        7294884137191806532,
+        7501600871221400279,
+        5148143939128616318,
+        634385368395524800,
+        4388824818686674479,
+        8861377786907255369,
+        4222660909971782394,
+        2124161694143580615
       ],
       retiredRelationUids: const [],
       modelVersion: 5,
@@ -268,7 +318,7 @@ ModelDefinition getObjectBoxModel() {
           object.id = id;
         },
         objectToFB: (Profile object, fb.Builder fbb) {
-          fbb.startTable(8);
+          fbb.startTable(10);
           fbb.addInt64(0, object.id);
           fbb.addInt64(1, object.theme);
           fbb.addBool(3, object.isLogin);
@@ -288,47 +338,23 @@ ModelDefinition getObjectBoxModel() {
             ..isLogout =
                 const fb.BoolReader().vTableGet(buffer, rootOffset, 16, false);
 
-          return object;
-        }),
-    Content: EntityDefinition<Content>(
-        model: _entities[1],
-        toOneRelations: (Content object) => [],
-        toManyRelations: (Content object) => {},
-        getId: (Content object) => object.id,
-        setId: (Content object, int id) {
-          object.id = id;
-        },
-        objectToFB: (Content object, fb.Builder fbb) {
-          fbb.startTable(2);
-          fbb.addInt64(0, object.id);
-          fbb.finish(fbb.endTable());
-          return object.id;
-        },
-        objectFromFB: (Store store, ByteData fbData) {
-          final buffer = fb.BufferContext(fbData);
-          final rootOffset = buffer.derefObject(0);
-
-          final object = Content()
-            ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0);
-
           return object;
         }),
     Message: EntityDefinition<Message>(
-        model: _entities[2],
-        toOneRelations: (Message object) =>
-            [object.from, object.to, object.content],
+        model: _entities[1],
+        toOneRelations: (Message object) => [object.from, object.to],
         toManyRelations: (Message object) => {},
         getId: (Message object) => object.id,
         setId: (Message object, int id) {
           object.id = id;
         },
         objectToFB: (Message object, fb.Builder fbb) {
-          fbb.startTable(7);
+          final plaintextOffset = fbb.writeString(object.plaintext);
+          fbb.startTable(11);
           fbb.addInt64(0, object.id);
-          fbb.addInt64(1, object.from.targetId);
-          fbb.addInt64(2, object.to.targetId);
-          fbb.addInt64(4, object.time);
-          fbb.addInt64(5, object.content.targetId);
+          fbb.addInt64(7, object.from.targetId);
+          fbb.addInt64(8, object.to.targetId);
+          fbb.addOffset(9, plaintextOffset);
           fbb.finish(fbb.endTable());
           return object.id;
         },
@@ -338,21 +364,18 @@ ModelDefinition getObjectBoxModel() {
 
           final object = Message()
             ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0)
-            ..time =
-                const fb.Int64Reader().vTableGet(buffer, rootOffset, 12, 0);
+            ..plaintext =
+                const fb.StringReader().vTableGet(buffer, rootOffset, 22, '');
           object.from.targetId =
-              const fb.Int64Reader().vTableGet(buffer, rootOffset, 6, 0);
+              const fb.Int64Reader().vTableGet(buffer, rootOffset, 18, 0);
           object.from.attach(store);
           object.to.targetId =
-              const fb.Int64Reader().vTableGet(buffer, rootOffset, 8, 0);
+              const fb.Int64Reader().vTableGet(buffer, rootOffset, 20, 0);
           object.to.attach(store);
-          object.content.targetId =
-              const fb.Int64Reader().vTableGet(buffer, rootOffset, 14, 0);
-          object.content.attach(store);
           return object;
         }),
     User: EntityDefinition<User>(
-        model: _entities[3],
+        model: _entities[2],
         toOneRelations: (User object) => [],
         toManyRelations: (User object) => {
               RelInfo<User>.toMany(2, object.id): object.friends,
@@ -365,12 +388,21 @@ ModelDefinition getObjectBoxModel() {
         },
         objectToFB: (User object, fb.Builder fbb) {
           final usernameOffset = fbb.writeString(object.username);
-          fbb.startTable(7);
+          final bioOffset =
+              object.bio == null ? null : fbb.writeString(object.bio!);
+          final phoneOffset =
+              object.phone == null ? null : fbb.writeString(object.phone!);
+          final avatarOffset =
+              object.avatar == null ? null : fbb.writeString(object.avatar!);
+          fbb.startTable(10);
           fbb.addInt64(0, object.id);
           fbb.addOffset(2, usernameOffset);
           fbb.addBool(3, object.isDND);
           fbb.addBool(4, object.isStick);
           fbb.addBool(5, object.isSpecialAttention);
+          fbb.addOffset(6, bioOffset);
+          fbb.addOffset(7, phoneOffset);
+          fbb.addOffset(8, avatarOffset);
           fbb.finish(fbb.endTable());
           return object.id;
         },
@@ -379,7 +411,13 @@ ModelDefinition getObjectBoxModel() {
           final rootOffset = buffer.derefObject(0);
 
           final object = User(
-              const fb.StringReader().vTableGet(buffer, rootOffset, 8, ''))
+              const fb.StringReader().vTableGet(buffer, rootOffset, 8, ''),
+              bio: const fb.StringReader()
+                  .vTableGetNullable(buffer, rootOffset, 16),
+              phone: const fb.StringReader()
+                  .vTableGetNullable(buffer, rootOffset, 18),
+              avatar: const fb.StringReader()
+                  .vTableGetNullable(buffer, rootOffset, 20))
             ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0)
             ..isDND =
                 const fb.BoolReader().vTableGet(buffer, rootOffset, 10, false)
@@ -396,7 +434,7 @@ ModelDefinition getObjectBoxModel() {
           return object;
         }),
     Group: EntityDefinition<Group>(
-        model: _entities[4],
+        model: _entities[3],
         toOneRelations: (Group object) => [],
         toManyRelations: (Group object) => {
               RelInfo<Group>.toMany(3, object.id): object.members,
@@ -425,21 +463,20 @@ ModelDefinition getObjectBoxModel() {
           return object;
         }),
     GroupMessage: EntityDefinition<GroupMessage>(
-        model: _entities[5],
-        toOneRelations: (GroupMessage object) =>
-            [object.from, object.to, object.content],
+        model: _entities[4],
+        toOneRelations: (GroupMessage object) => [object.user, object.group],
         toManyRelations: (GroupMessage object) => {},
         getId: (GroupMessage object) => object.id,
         setId: (GroupMessage object, int id) {
           object.id = id;
         },
         objectToFB: (GroupMessage object, fb.Builder fbb) {
-          fbb.startTable(7);
+          final plaintextOffset = fbb.writeString(object.plaintext);
+          fbb.startTable(10);
           fbb.addInt64(0, object.id);
-          fbb.addInt64(1, object.from.targetId);
-          fbb.addInt64(2, object.to.targetId);
-          fbb.addInt64(4, object.time);
-          fbb.addInt64(5, object.content.targetId);
+          fbb.addInt64(6, object.user.targetId);
+          fbb.addInt64(7, object.group.targetId);
+          fbb.addOffset(8, plaintextOffset);
           fbb.finish(fbb.endTable());
           return object.id;
         },
@@ -449,17 +486,50 @@ ModelDefinition getObjectBoxModel() {
 
           final object = GroupMessage()
             ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0)
-            ..time =
-                const fb.Int64Reader().vTableGet(buffer, rootOffset, 12, 0);
-          object.from.targetId =
-              const fb.Int64Reader().vTableGet(buffer, rootOffset, 6, 0);
-          object.from.attach(store);
-          object.to.targetId =
-              const fb.Int64Reader().vTableGet(buffer, rootOffset, 8, 0);
-          object.to.attach(store);
-          object.content.targetId =
-              const fb.Int64Reader().vTableGet(buffer, rootOffset, 14, 0);
-          object.content.attach(store);
+            ..plaintext =
+                const fb.StringReader().vTableGet(buffer, rootOffset, 20, '');
+          object.user.targetId =
+              const fb.Int64Reader().vTableGet(buffer, rootOffset, 16, 0);
+          object.user.attach(store);
+          object.group.targetId =
+              const fb.Int64Reader().vTableGet(buffer, rootOffset, 18, 0);
+          object.group.attach(store);
+          return object;
+        }),
+    FriendShip: EntityDefinition<FriendShip>(
+        model: _entities[5],
+        toOneRelations: (FriendShip object) => [object.user0, object.user1],
+        toManyRelations: (FriendShip object) =>
+            {RelInfo<FriendShip>.toMany(7, object.id): object.messages},
+        getId: (FriendShip object) => object.id,
+        setId: (FriendShip object, int id) {
+          object.id = id;
+        },
+        objectToFB: (FriendShip object, fb.Builder fbb) {
+          fbb.startTable(6);
+          fbb.addInt64(0, object.id);
+          fbb.addInt64(3, object.user0.targetId);
+          fbb.addInt64(4, object.user1.targetId);
+          fbb.finish(fbb.endTable());
+          return object.id;
+        },
+        objectFromFB: (Store store, ByteData fbData) {
+          final buffer = fb.BufferContext(fbData);
+          final rootOffset = buffer.derefObject(0);
+
+          final object = FriendShip()
+            ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0);
+          object.user0.targetId =
+              const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0);
+          object.user0.attach(store);
+          object.user1.targetId =
+              const fb.Int64Reader().vTableGet(buffer, rootOffset, 12, 0);
+          object.user1.attach(store);
+          InternalToManyAccess.setRelInfo(
+              object.messages,
+              store,
+              RelInfo<FriendShip>.toMany(7, object.id),
+              store.box<FriendShip>());
           return object;
         })
   };
@@ -485,97 +555,112 @@ class Profile_ {
       QueryBooleanProperty<Profile>(_entities[0].properties[3]);
 }
 
-/// [Content] entity fields to define ObjectBox queries.
-class Content_ {
-  /// see [Content.id]
-  static final id = QueryIntegerProperty<Content>(_entities[1].properties[0]);
-}
-
 /// [Message] entity fields to define ObjectBox queries.
 class Message_ {
   /// see [Message.id]
-  static final id = QueryIntegerProperty<Message>(_entities[2].properties[0]);
+  static final id = QueryIntegerProperty<Message>(_entities[1].properties[0]);
 
   /// see [Message.from]
   static final from =
-      QueryRelationToOne<Message, User>(_entities[2].properties[1]);
+      QueryRelationToOne<Message, User>(_entities[1].properties[1]);
 
   /// see [Message.to]
   static final to =
-      QueryRelationToOne<Message, User>(_entities[2].properties[2]);
-
-  /// see [Message.time]
-  static final time = QueryIntegerProperty<Message>(_entities[2].properties[3]);
+      QueryRelationToOne<Message, User>(_entities[1].properties[2]);
 
-  /// see [Message.content]
-  static final content =
-      QueryRelationToOne<Message, Content>(_entities[2].properties[4]);
+  /// see [Message.plaintext]
+  static final plaintext =
+      QueryStringProperty<Message>(_entities[1].properties[3]);
 }
 
 /// [User] entity fields to define ObjectBox queries.
 class User_ {
   /// see [User.id]
-  static final id = QueryIntegerProperty<User>(_entities[3].properties[0]);
+  static final id = QueryIntegerProperty<User>(_entities[2].properties[0]);
 
   /// see [User.username]
-  static final username = QueryStringProperty<User>(_entities[3].properties[1]);
+  static final username = QueryStringProperty<User>(_entities[2].properties[1]);
 
   /// see [User.isDND]
-  static final isDND = QueryBooleanProperty<User>(_entities[3].properties[2]);
+  static final isDND = QueryBooleanProperty<User>(_entities[2].properties[2]);
 
   /// see [User.isStick]
-  static final isStick = QueryBooleanProperty<User>(_entities[3].properties[3]);
+  static final isStick = QueryBooleanProperty<User>(_entities[2].properties[3]);
 
   /// see [User.isSpecialAttention]
   static final isSpecialAttention =
-      QueryBooleanProperty<User>(_entities[3].properties[4]);
+      QueryBooleanProperty<User>(_entities[2].properties[4]);
+
+  /// see [User.bio]
+  static final bio = QueryStringProperty<User>(_entities[2].properties[5]);
+
+  /// see [User.phone]
+  static final phone = QueryStringProperty<User>(_entities[2].properties[6]);
+
+  /// see [User.avatar]
+  static final avatar = QueryStringProperty<User>(_entities[2].properties[7]);
 
   /// see [User.friends]
   static final friends =
-      QueryRelationToMany<User, User>(_entities[3].relations[0]);
+      QueryRelationToMany<User, User>(_entities[2].relations[0]);
 
   /// see [User.messages]
   static final messages =
-      QueryRelationToMany<User, Message>(_entities[3].relations[1]);
+      QueryRelationToMany<User, Message>(_entities[2].relations[1]);
 
   /// see [User.groupMessages]
   static final groupMessages =
-      QueryRelationToMany<User, GroupMessage>(_entities[3].relations[2]);
+      QueryRelationToMany<User, GroupMessage>(_entities[2].relations[2]);
 }
 
 /// [Group] entity fields to define ObjectBox queries.
 class Group_ {
   /// see [Group.id]
-  static final id = QueryIntegerProperty<Group>(_entities[4].properties[0]);
+  static final id = QueryIntegerProperty<Group>(_entities[3].properties[0]);
 
   /// see [Group.members]
   static final members =
-      QueryRelationToMany<Group, User>(_entities[4].relations[0]);
+      QueryRelationToMany<Group, User>(_entities[3].relations[0]);
 
   /// see [Group.messages]
   static final messages =
-      QueryRelationToMany<Group, GroupMessage>(_entities[4].relations[1]);
+      QueryRelationToMany<Group, GroupMessage>(_entities[3].relations[1]);
 }
 
 /// [GroupMessage] entity fields to define ObjectBox queries.
 class GroupMessage_ {
   /// see [GroupMessage.id]
   static final id =
-      QueryIntegerProperty<GroupMessage>(_entities[5].properties[0]);
+      QueryIntegerProperty<GroupMessage>(_entities[4].properties[0]);
 
-  /// see [GroupMessage.from]
-  static final from =
-      QueryRelationToOne<GroupMessage, User>(_entities[5].properties[1]);
+  /// see [GroupMessage.user]
+  static final user =
+      QueryRelationToOne<GroupMessage, User>(_entities[4].properties[1]);
 
-  /// see [GroupMessage.to]
-  static final to =
-      QueryRelationToOne<GroupMessage, Group>(_entities[5].properties[2]);
+  /// see [GroupMessage.group]
+  static final group =
+      QueryRelationToOne<GroupMessage, Group>(_entities[4].properties[2]);
 
-  /// see [GroupMessage.time]
-  static final time =
-      QueryIntegerProperty<GroupMessage>(_entities[5].properties[3]);
+  /// see [GroupMessage.plaintext]
+  static final plaintext =
+      QueryStringProperty<GroupMessage>(_entities[4].properties[3]);
+}
+
+/// [FriendShip] entity fields to define ObjectBox queries.
+class FriendShip_ {
+  /// see [FriendShip.id]
+  static final id =
+      QueryIntegerProperty<FriendShip>(_entities[5].properties[0]);
 
-  /// see [GroupMessage.content]
-  static final content =
-      QueryRelationToOne<GroupMessage, Content>(_entities[5].properties[4]);
+  /// see [FriendShip.user0]
+  static final user0 =
+      QueryRelationToOne<FriendShip, User>(_entities[5].properties[1]);
+
+  /// see [FriendShip.user1]
+  static final user1 =
+      QueryRelationToOne<FriendShip, User>(_entities[5].properties[2]);
+
+  /// see [FriendShip.messages]
+  static final messages =
+      QueryRelationToMany<FriendShip, Message>(_entities[5].relations[0]);
 }

+ 16 - 0
lib/presenter/chat_list.dart

@@ -0,0 +1,16 @@
+import 'package:e2ee_chat/common/api.dart';
+import 'package:e2ee_chat/common/global.dart';
+import 'package:e2ee_chat/model/message.dart';
+import 'package:e2ee_chat/model/user.dart';
+import 'package:flutter/cupertino.dart';
+
+import 'login.dart';
+import 'user_chat.dart';
+
+enum DTMessageType { text, image }
+
+class ChatListPresenter extends ChangeNotifier {
+  List<UserChatPresenter> get chatList {
+    return [];
+  }
+}

+ 0 - 1
lib/presenter/contact.dart

@@ -24,7 +24,6 @@ class UserProfilePresenter extends ChangeNotifier {
 
   @override
   void notifyListeners() {
-    User.put(user);
     super.notifyListeners();
   }
 }

+ 9 - 5
lib/presenter/contact_list.dart

@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
 import 'package:lpinyin/lpinyin.dart';
 import 'package:e2ee_chat/common/api.dart';
 
+import '../objectbox.g.dart';
 import 'login.dart';
 
 String _getTag(String name) {
@@ -19,9 +20,10 @@ String _getTag(String name) {
   return tag;
 }
 
-class ContactListPresenter extends LoginPresenter {
+class ContactListPresenter extends ChangeNotifier {
   List<ContactInfo> get contacts {
-    var list = <ContactInfo>[];
+    final User? user = LoginPresenter().user;
+    final list = <ContactInfo>[];
     try {
       for (var i in user!.friends) {
         list.add(ContactInfo(name: i.username, tag: _getTag(i.username)));
@@ -29,28 +31,30 @@ class ContactListPresenter extends LoginPresenter {
     } catch (e) {
       debug(e);
     }
-    list.add(ContactInfo(name: '新的朋友', tag: '↑', bgColor: Colors.orange, iconData: Icons.person_add));
     SuspensionUtil.sortListBySuspensionTag(list);
     SuspensionUtil.setShowSuspensionStatus(list);
+    list.add(ContactInfo(name: '新的朋友', tag: '↑', bgColor: Colors.orange, iconData: Icons.person_add));
     return list;
   }
 
   Future<bool> freshContacts() async {
     debug('contact list presenter fresh contacts begin');
     bool _result = false;
-    var _user = user;
+    var _user = LoginPresenter().user;
     if (_user != null) {
       try {
         var list = (await Api().friendList())!;
+        debug('contact list presenter fresh contacts friends: $list');
         _user.friends.clear();
         list.forEach((username) {
           var contact = User(username);
           contact.refreshProfile();
           _user.friends.add(contact);
         });
+        notifyListeners();
         _result = true;
       } catch (e) {
-        debug('contact list presenter fresh contacts failed');
+        debug('contact list presenter fresh contacts failed: error $e');
       }
     }
     debug('contact list presenter fresh contacts end');

+ 34 - 3
lib/presenter/login.dart

@@ -1,16 +1,44 @@
 import 'package:dio/dio.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
 import 'package:provider/provider.dart';
 
 import 'package:e2ee_chat/common/global.dart';
 import 'package:e2ee_chat/common/api.dart';
 import '../model/user.dart';
 
+import '../objectbox.g.dart';
 import 'profile.dart';
 
-class LoginPresenter extends ProfilePresenter {
+/*
+
+
+  static Future<void> put(User user) async {
+    debug('put user ${user.id} ${user.username}');
+    final store = await openStore();
+    final box = store.box<User>();
+    box.put(user);
+    store.close();
+  }
 
-  // TODO: online keep status; A logout, B logout?
+  static Future<User?> query(String username) async {
+    final store = await openStore();
+    final box = store.box<User>();
+    int id = 0;
+    box.getAll().forEach((user) {
+      if (user.username == username) id = user.id;
+    });
+    User? user = box.get(id);
+    // Query<User> query = box.query(User_.username.equals(username)).build();
+    // final user = query.findFirst();
+    // query.close();
+    debug('query user with username=$username get id=$id');
+    store.close();
+    return user;
+  }
+ */
+
+class LoginPresenter extends ProfilePresenter {
   bool get isLogin => profile.isLogin;
 
   User? get user => profile.user;
@@ -18,6 +46,7 @@ class LoginPresenter extends ProfilePresenter {
   Future<bool> login({String? username, String? password}) async {
     debug('UserModel login begin');
     if (username != null) {
+
       profile.user = User(username);
     }
     bool _result = false;
@@ -36,6 +65,8 @@ class LoginPresenter extends ProfilePresenter {
         debug('UserModel login success!');
         profile.isLogin = true;
         profile.isLogout = false;
+        // await User.put(profile.user!);
+        profile.setToken(_token);
         notifyListeners();
       }
     }
@@ -59,4 +90,4 @@ class LoginPresenter extends ProfilePresenter {
     }
     return false;
   }
-}
+}

+ 2 - 0
lib/presenter/profile.dart

@@ -1,7 +1,9 @@
 import 'package:e2ee_chat/model/profile.dart';
+import 'package:e2ee_chat/model/user.dart';
 import 'package:flutter/cupertino.dart';
 
 import '../common/global.dart';
+import '../objectbox.g.dart';
 
 class ProfilePresenter extends ChangeNotifier {
   Profile get profile => Global.profile;

+ 0 - 80
lib/presenter/session.dart

@@ -1,80 +0,0 @@
-import 'package:e2ee_chat/common/global.dart';
-import 'package:e2ee_chat/model/message.dart';
-import 'package:e2ee_chat/model/user.dart';
-import 'package:flutter/cupertino.dart';
-
-import 'login.dart';
-
-enum DTMessageType { text, image }
-
-class SessionListModel extends ChangeNotifier {
-  User? get user => LoginPresenter().user;
-
-  List<SessionModel> get sessions {
-    final _user = user;
-    var list = <SessionModel>[];
-    if (_user != null) {
-      var friends = _user.friends;
-      for (var i in friends) {
-        list.add(SessionModel(i));
-      }
-    }
-    return list;
-  }
-
-  int get count => sessions.length;
-}
-
-class SessionModel extends SessionListModel {
-  // TODO: 仅支持单聊
-
-  SessionModel(this.contact);
-
-  final User contact;
-
-  /// 消息体
-  Message? get lastMessage {
-    final _user = user;
-    Message? message;
-    if (_user != null) {
-      try {
-        message = _user.messages.lastWhere((msg) => msg.from.target == _user || msg.to.target == _user);
-      } catch (e) {
-        debug("no message");
-      }
-    }
-    return message;
-  }
-
-  /// 未读数量
-  int get unReadCount {
-    // TODO: 未读消息数量
-    return 0;
-  }
-
-  get chatName => contact.username;
-
-  /// 单聊
-  get isSingle => true;
-
-  /// 群聊信息
-  get isGroup => false;
-
-  /// 消息免打扰
-  get isDND => contact.isDND;
-
-  /// 是否为置顶
-  get isStick => contact.isStick;
-
-  /// 特别关注
-  get isSpecialAttention => contact.isSpecialAttention;
-
-  // TODO: 是否 @ 你
-  get isAtYou => false;
-
-  // TODO: 是否 @ 全部
-  get isAtAll => false;
-
-  // TODO: user avatar
-  get avatar => Global.defaultAvatar;
-}

+ 162 - 0
lib/presenter/user_chat.dart

@@ -0,0 +1,162 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:dash_chat/dash_chat.dart';
+import 'package:e2ee_chat/common/api.dart';
+import 'package:e2ee_chat/common/global.dart';
+import 'package:e2ee_chat/model/friendship.dart';
+import 'package:e2ee_chat/model/message.dart';
+import 'package:e2ee_chat/model/user.dart';
+import 'package:e2ee_chat/objectbox.g.dart';
+import 'package:e2ee_chat/presenter/login.dart';
+import 'package:flutter/material.dart';
+import 'package:objectbox/objectbox.dart';
+import 'package:rxdart/rxdart.dart';
+
+import 'chat_list.dart';
+
+class UserChatPresenter extends ChatListPresenter {
+  UserChatPresenter({this.to}) {
+    _listenUserChat();
+  }
+
+  User? get from => LoginPresenter().user;
+  User? to;
+
+  FriendShip get friendShip => FriendShip()
+    ..user0.target = from
+    ..user1.target = to;
+
+  ToMany<Message> get messages => friendShip.messages;
+
+  List<ChatMessage>? _chatMessages;
+  List<ChatMessage> get chatMessages => _chatMessages ?? [];
+
+  bool _disposed = false;
+
+
+  @override
+  void dispose() {
+    _disposed = true;
+    super.dispose();
+  }
+
+
+  Future<void> _listenUserChat() async {
+    while (!_disposed) {
+      if (to != null) {
+        if (_chatMessages == null) {
+            debug("init chatMessages");
+          _chatMessages = await retrieveAllChatMessages();
+          notifyListeners();
+        } else {
+          await flushChatMessages();
+        }
+      }
+      await Future.delayed(Duration(seconds: 1));
+    }
+  }
+
+  Future<void> flushChatMessages() async {
+    final _user = LoginPresenter().user;
+    if (_user != null) {
+      List<String>? _list = await Api().getLastMessages(_user.username);
+      if (_list != null) {
+        debug("flush chat message");
+        for (final i in _list) {
+          chatMessages.add(chatMessageFromMessagePackage(i));
+        }
+        notifyListeners();
+      }
+    }
+    // messages.forEach((message) => list.add(message.chatMessage));
+  }
+
+  ChatMessage chatMessageFromMessagePackage(String plaintext) {
+    // Map<String, dynamic> json0 = jsonDecode(s);
+    // String plaintext = json0["plaintext"];
+    debug('plaintext: $plaintext');
+    return ChatMessage.fromJson(jsonDecode(plaintext));
+  }
+
+  Future<List<ChatMessage>> retrieveAllChatMessages() async {
+    final list = <ChatMessage>[];
+    final _user = LoginPresenter().user;
+    final _to = to;
+    if (_user != null && _to != null) {
+      List<String>? _list = await Api().getFilterMessages(_to.username);
+      if (_list != null) {
+        for (final i in _list) {
+          list.add(chatMessageFromMessagePackage(i));
+        }
+      }
+    }
+    return list;
+    // messages.forEach((message) => list.add(message.chatMessage));
+  }
+
+  Future<bool> sendMessage(ChatMessage chatMessage) async {
+    debug('user chat presenter send msg begin');
+    bool _result = true;
+    chatMessages.add(chatMessage);
+    notifyListeners();
+    try {
+      final plaintext = json.encode(chatMessage);
+      var message = Message();
+      message.from.target = from!;
+      message.to.target = to!;
+      message.plaintext = plaintext;
+      _result = await Api().sendMessage(to!.username, json.encode(chatMessage));
+    } catch (e) {
+      debug(e);
+      _result = false;
+    }
+    if (!_result) {
+      debug('user chat presenter send msg failed');
+    }
+    return _result;
+  }
+
+  /// 消息体
+  Message? get lastMessage {
+    Message? message;
+    try {
+      message = messages.last;
+    } catch (e) {
+      debug("no message");
+    }
+    return message;
+  }
+
+  /// 未读数量
+  int get unReadCount {
+    // TODO: 未读消息数量
+    return 0;
+  }
+
+  get chatName => to?.username;
+
+  /// 单聊
+  get isSingle => true;
+
+  /// 群聊信息
+  get isGroup => false;
+
+  /// 消息免打扰
+  get isDND => to?.isDND;
+
+  /// 是否为置顶
+  get isStick => to?.isStick;
+
+  /// 特别关注
+  get isSpecialAttention => to?.isSpecialAttention;
+
+  // TODO: 是否 @ 你
+  get isAtYou => false;
+
+  // TODO: 是否 @ 全部
+  get isAtAll => false;
+
+  // TODO: user avatar
+  get avatar => Global.defaultAvatar;
+}

+ 12 - 19
lib/view/add_friend.dart

@@ -1,4 +1,5 @@
 import 'package:dio/dio.dart';
+import 'package:e2ee_chat/common/api.dart';
 import 'package:e2ee_chat/common/global.dart';
 import 'package:e2ee_chat/l10n/localization_intl.dart';
 import 'package:e2ee_chat/presenter/login.dart';
@@ -19,7 +20,7 @@ class _AddFriendViewState extends State<AddFriendView> {
   @override
   Widget build(BuildContext context) {
     return Scaffold(
-      appBar: AppBar(title: Text(GmLocalizations.of(context).login)),
+      appBar: AppBar(title: Text(GmLocalizations.of(context).searchUser)),
       body: Padding(
         padding: const EdgeInsets.all(16.0),
         child: Form(
@@ -47,7 +48,7 @@ class _AddFriendViewState extends State<AddFriendView> {
                       //TODO: something wrong with text color
                       textStyle: MaterialStateProperty.all<TextStyle>(TextStyle(color: Colors.white)),
                     ),
-                    onPressed: _onLogin,
+                    onPressed: _findUser,
                     child: Text(GmLocalizations.of(context).search),
                   ),
                 ),
@@ -59,27 +60,19 @@ class _AddFriendViewState extends State<AddFriendView> {
     );
   }
 
-  void _onLogin() async {
+  void _findUser() async {
+
+
     // 提交前,先验证各个表单字段是否合法
     if ((_formKey.currentState as FormState).validate()) {
       Fluttertoast.showToast(msg: GmLocalizations.of(context).loading);
-      try {
-        // TODO: get user details
-        debug('isLogin: ${Global.profile.isLogin}');
-      } on DioError catch (e) {
-        //登录失败则提示
-        if (e.response?.statusCode == 401) {
-          Fluttertoast.showToast(msg: GmLocalizations.of(context).userNameOrPasswordWrong);
-        } else {
-          Fluttertoast.showToast(msg: e.toString());
-        }
-      } finally {
-        if (Global.profile.isLogin) {
-          debug('Login!');
-          // 返回
-          Navigator.of(context).pop();
-        }
+      bool result = await Api().addFriend(_unameController.text);
+      if (!result) {
+        Fluttertoast.showToast(msg: "发送好友申请失败");
+      } else {
+        Fluttertoast.showToast(msg: "好友申请已发送");
       }
     }
+    // TODO: get user details
   }
 }

+ 71 - 0
lib/view/chat_list.dart

@@ -0,0 +1,71 @@
+import 'package:e2ee_chat/common/global.dart';
+import 'package:e2ee_chat/presenter/chat_list.dart';
+import 'package:e2ee_chat/widgets/chat_list_item.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_slidable/flutter_slidable.dart';
+import 'package:provider/provider.dart';
+
+class ChatList extends StatefulWidget {
+  @override
+  _ChatListState createState() => _ChatListState();
+}
+
+class _ChatListState extends State<ChatList> {
+  @override
+  Widget build(BuildContext context) {
+    return ChangeNotifierProvider<ChatListPresenter>(
+      create: (context) {
+        return ChatListPresenter();
+      },
+      child: Builder(
+        builder: (context) {
+          final provider = Provider.of<ChatListPresenter>(context);
+          final list = provider.chatList;
+          return RefreshIndicator(
+              child: ListView.builder(
+                  itemCount: list.length,
+                  itemBuilder: (context, index) {
+                    final _item = list[index];
+                    return Slidable(
+                      actionPane: SlidableDrawerActionPane(),
+                      actionExtentRatio: 0.25,
+                      secondaryActions: <Widget>[
+                        IconSlideAction(
+                          caption: '取消置顶',
+                          color: Colors.black45,
+                          icon: Icons.more_horiz,
+                          onTap: () {},
+                        ),
+                        IconSlideAction(
+                          caption: '删除',
+                          color: Colors.redAccent,
+                          icon: Icons.delete,
+                          onTap: () => {},
+                        ),
+                      ],
+                      child: Padding(
+                        padding: EdgeInsets.only(left: 36, bottom: 20, top: 20),
+                        child: Row(
+                          crossAxisAlignment: CrossAxisAlignment.start,
+                          children: <Widget>[
+                            Expanded(child: ChatListItem(_item)),
+                            _item.isStick
+                                ? Container(
+                                    width: 40,
+                                    height: 48,
+                                    margin: EdgeInsets.only(top: 10, right: 10),
+                                    child: Icon(Icons.star))
+                                : Padding(
+                                    padding: EdgeInsets.symmetric(horizontal: 24),
+                                  )
+                          ],
+                        ),
+                      ),
+                    );
+                  }),
+              onRefresh: () async {});
+        },
+      ),
+    );
+  }
+}

+ 77 - 0
lib/view/contact_list.dart

@@ -0,0 +1,77 @@
+import 'package:e2ee_chat/azlistview/azlistview.dart';
+import 'package:e2ee_chat/common/global.dart';
+import 'package:e2ee_chat/model/message.dart';
+import 'package:e2ee_chat/model/user.dart';
+import 'package:e2ee_chat/presenter/contact_list.dart';
+import 'package:e2ee_chat/presenter/chat_list.dart';
+import 'package:e2ee_chat/presenter/login.dart';
+import 'package:e2ee_chat/presenter/user_chat.dart';
+import 'package:e2ee_chat/view/user_chat.dart';
+import 'package:e2ee_chat/widgets/utils.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+class ContactListView extends StatefulWidget {
+  @override
+  _ContactListViewState createState() => _ContactListViewState();
+}
+
+class _ContactListViewState extends State<ContactListView> {
+  @override
+  void initState() {
+    super.initState();
+    ContactListPresenter().freshContacts().then((_) => setState(() {}));
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ChangeNotifierProvider<ContactListPresenter>(
+      create: (context) => ContactListPresenter(),
+      child: Builder(
+        builder: (context) {
+          final provider = Provider.of<ContactListPresenter>(context);
+          final contacts = provider.contacts;
+          return RefreshIndicator(
+              child: AzListView(
+                data: contacts,
+                itemCount: contacts.length,
+                itemBuilder: (context, index) {
+                  ContactInfo info = contacts[index];
+                  return Utils.getWeChatListItem(context, info, defHeaderBgColor: Color(0xFFE5E5E5), onTap: () {
+                    // TODO: fuck
+                    Navigator.of(context).pushNamed("user chat", arguments: info.name);
+                  });
+                },
+                physics: BouncingScrollPhysics(),
+                susItemBuilder: (BuildContext context, int index) {
+                  ContactInfo model = contacts[index];
+                  if ('↑' == model.getSuspensionTag()) {
+                    return Container();
+                  }
+                  return Utils.getSusItem(context, model.getSuspensionTag());
+                },
+                indexBarData: ['↑', '☆', ...kIndexBarData],
+                indexBarOptions: IndexBarOptions(
+                  needRebuild: true,
+                  ignoreDragCancel: true,
+                  downTextStyle: TextStyle(fontSize: 12, color: Colors.white),
+                  downItemDecoration: BoxDecoration(shape: BoxShape.circle, color: Colors.green),
+                  indexHintWidth: 120 / 2,
+                  indexHintHeight: 100 / 2,
+                  indexHintDecoration: BoxDecoration(
+                    image: DecorationImage(
+                      image: AssetImage(Utils.getImgPath('ic_index_bar_bubble_gray')),
+                      fit: BoxFit.contain,
+                    ),
+                  ),
+                  indexHintAlignment: Alignment.centerRight,
+                  indexHintChildAlignment: Alignment(-0.25, 0.0),
+                  indexHintOffset: Offset(-20, 0),
+                ),
+              ),
+              onRefresh: () async {});
+        },
+      ),
+    );
+  }
+}

+ 0 - 177
lib/view/contacts.dart

@@ -1,177 +0,0 @@
-import 'package:e2ee_chat/azlistview/azlistview.dart';
-import 'package:e2ee_chat/common/global.dart';
-import 'package:e2ee_chat/model/message.dart';
-import 'package:e2ee_chat/presenter/contact_list.dart';
-import 'package:e2ee_chat/presenter/session.dart';
-import 'package:e2ee_chat/widgets/utils.dart';
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-
-class ContactListPage extends StatefulWidget {
-  @override
-  _ContactListPageState createState() => _ContactListPageState();
-}
-
-class _ContactListPageState extends State<ContactListPage> {
-
-  @override
-  void initState() {
-    super.initState();
-    ContactListPresenter().freshContacts().then((_) => setState(() {}));
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return ChangeNotifierProvider<ContactListPresenter>(
-      create: (context) => ContactListPresenter(),
-      child: Builder(
-        builder: (context) {
-          var provider = Provider.of<ContactListPresenter>(context);
-          var contacts = provider.contacts;
-          return AzListView(
-              data: contacts,
-              itemCount: contacts.length,
-              itemBuilder: (context, index) {
-                var info = contacts[index];
-                return Utils.getWeChatListItem(
-                  context,
-                  info,
-                  defHeaderBgColor: Color(0xFFE5E5E5),
-                );
-              },
-            physics: BouncingScrollPhysics(),
-            susItemBuilder: (BuildContext context, int index) {
-              ContactInfo model = contacts[index];
-              if ('↑' == model.getSuspensionTag()) {
-                return Container();
-              }
-              return Utils.getSusItem(context, model.getSuspensionTag());
-            },
-            indexBarData: ['↑', '☆', ...kIndexBarData],
-            indexBarOptions: IndexBarOptions(
-              needRebuild: true,
-              ignoreDragCancel: true,
-              downTextStyle: TextStyle(fontSize: 12, color: Colors.white),
-              downItemDecoration:
-              BoxDecoration(shape: BoxShape.circle, color: Colors.green),
-              indexHintWidth: 120 / 2,
-              indexHintHeight: 100 / 2,
-              indexHintDecoration: BoxDecoration(
-                image: DecorationImage(
-                  image: AssetImage(Utils.getImgPath('ic_index_bar_bubble_gray')),
-                  fit: BoxFit.contain,
-                ),
-              ),
-              indexHintAlignment: Alignment.centerRight,
-              indexHintChildAlignment: Alignment(-0.25, 0.0),
-              indexHintOffset: Offset(-20, 0),
-            ),
-          );
-        },
-      ),
-    );
-  }
-}
-
-class ContactItem extends StatelessWidget {
-  final SessionModel model;
-
-  ContactItem(this.model);
-
-  @override
-  Widget build(BuildContext context) {
-    return Row(
-      mainAxisAlignment: MainAxisAlignment.spaceBetween,
-      children: <Widget>[
-        model.avatar ?? Global.defaultAvatar,
-        Expanded(
-          child: Column(
-            children: <Widget>[
-              Padding(
-                padding: EdgeInsets.symmetric(vertical: 8),
-                child: Row(
-                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                  children: <Widget>[
-                    Expanded(
-                      child: Text(
-                        model.chatName,
-                        style: TextStyle(fontSize: 34),
-                        overflow: TextOverflow.ellipsis,
-                      ),
-                    ),
-                    Text(
-                      _formatDate(),
-                      style: TextStyle(fontSize: 26),
-                      overflow: TextOverflow.ellipsis,
-                    ),
-                  ],
-                ),
-              ),
-              Row(
-                mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                children: <Widget>[
-                  Expanded(
-                    child: RichText(
-                      text: TextSpan(children: [
-                        TextSpan(
-                          text: model.isAtYou ? "[@你]" : "",
-                          style: TextStyle(fontSize: 28),
-                        ),
-                        TextSpan(
-                          text: model.isSpecialAttention ? "[特别关注]" : "",
-                          style: TextStyle(fontSize: 28),
-                        ),
-                        TextSpan(
-                          text: model.isAtAll ? "[@所有人]" : "",
-                          style: TextStyle(fontSize: 28),
-                        ),
-                        TextSpan(
-                          text: model.lastMessage?.content.target?.plaintext ?? "",
-                          style: TextStyle(fontSize: 28),
-                        )
-                      ]),
-                      overflow: TextOverflow.ellipsis,
-                    ),
-                  ),
-                  (model.unReadCount > 0 && !model.isDND)
-                      ? Container(
-                          width: 32,
-                          height: 32,
-                          alignment: Alignment.center,
-                          decoration: BoxDecoration(borderRadius: BorderRadius.circular(20)),
-                          child: Text(
-                            model.unReadCount.toString(),
-                            style: TextStyle(color: Colors.white, fontSize: 26),
-                          ))
-                      : Container(),
-                  model.isDND
-                      ? Row(
-                          children: <Widget>[
-                            Icon(Icons.visibility_off),
-                            model.unReadCount > 0
-                                ? Icon(
-                                    Icons.chat_bubble,
-                                    color: Colors.red,
-                                  ) // TODO: 小红点
-                                : Container()
-                          ],
-                        )
-                      : Container()
-                ],
-              )
-            ],
-          ),
-        )
-      ],
-    );
-  }
-
-  String _formatDate() {
-    try {
-      DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(model.lastMessage!.time);
-      return "${dateTime.hour}:${dateTime.minute}";
-    } catch (e) {
-      return "";
-    }
-  }
-}

+ 24 - 32
lib/view/home.dart

@@ -7,9 +7,9 @@ import 'package:e2ee_chat/widgets/mydrawer.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 
-import 'contacts.dart';
+import 'contact_list.dart';
 import 'group.dart';
-import 'session.dart';
+import 'chat_list.dart';
 
 class HomeRoute extends StatefulWidget {
   @override
@@ -32,23 +32,20 @@ class _HomeRouteState extends State<HomeRoute> {
         title: Text(gm.home),
         actions: <Widget>[
           PopupMenuButton(itemBuilder: (context) {
-            return <PopupMenuItem>[PopupMenuItem(child: Container(
-              child: TextButton(
-                onPressed: () => Navigator.of(context).pushNamed("add friend"),
-                  child: Row(
-                    children: [
-                      Icon(Icons.person_add),
-                      Text("添加朋友"),
-                    ],
-                  )
-              ),
-            ))];
+            return <PopupMenuItem>[
+              PopupMenuItem(
+                  child: Container(
+                child: TextButton(
+                    onPressed: () => Navigator.of(context).pushNamed("add friend"),
+                    child: Row(
+                      children: [
+                        Icon(Icons.person_add),
+                        Text("添加朋友"),
+                      ],
+                    )),
+              ))
+            ];
           }),
-          IconButton(
-              onPressed: () {
-                // TODO: 弹出下拉菜单
-              },
-              icon: Icon(Icons.add))
         ],
       ),
       drawer: MyDrawer(),
@@ -73,21 +70,16 @@ class _HomeRouteState extends State<HomeRoute> {
   }
 
   Widget _buildBody() {
-    _buildChild() {
-      switch (_index) {
-        case 0:
-          return SessionList();
-        case 1:
-          return ContactListPage();
-        case 2:
-          return GroupRoute();
-      }
-      return EmptyWidget();
+    switch (_index) {
+      case 0:
+        return ChatList();
+      case 1:
+        return ContactListView();
+      case 2:
+        return GroupRoute();
     }
-    return RefreshIndicator(
-        child: _buildChild(),
-        onRefresh: _onRefresh
-    );
+    return EmptyWidget();
+    // return RefreshIndicator(child: _buildChild(), onRefresh: _onRefresh);
   }
 
   Future<void> _onRefresh() async {

+ 0 - 167
lib/view/session.dart

@@ -1,167 +0,0 @@
-import 'package:e2ee_chat/common/global.dart';
-import 'package:e2ee_chat/presenter/session.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_slidable/flutter_slidable.dart';
-import 'package:provider/provider.dart';
-
-class SessionList extends StatefulWidget {
-  @override
-  _SessionListState createState() => _SessionListState();
-}
-
-class _SessionListState extends State<SessionList> {
-  @override
-  Widget build(BuildContext context) {
-    return ChangeNotifierProvider<SessionListModel>(
-      create: (context) {
-        return SessionListModel();
-      },
-      child: Builder(
-        builder: (context) {
-          var provider = Provider.of<SessionListModel>(context);
-          return ListView.builder(
-              itemCount: provider.count,
-              itemBuilder: (context, index) {
-                var _item = provider.sessions[index];
-                return Slidable(
-                  actionPane: SlidableDrawerActionPane(),
-                  actionExtentRatio: 0.25,
-                  secondaryActions: <Widget>[
-                    IconSlideAction(
-                      caption: '取消置顶',
-                      color: Colors.black45,
-                      icon: Icons.more_horiz,
-                      onTap: () {},
-                    ),
-                    IconSlideAction(
-                      caption: '删除',
-                      color: Colors.redAccent,
-                      icon: Icons.delete,
-                      onTap: () => {},
-                    ),
-                  ],
-                  child: Padding(
-                    padding: EdgeInsets.only(left: 36, bottom: 20, top: 20),
-                    child: Row(
-                      crossAxisAlignment: CrossAxisAlignment.start,
-                      children: <Widget>[
-                        Expanded(child: SessionItem(_item)),
-                        _item.isStick
-                            ? Container(
-                                width: 40,
-                                height: 48,
-                                margin: EdgeInsets.only(top: 10, right: 10),
-                                child: Icon(Icons.star))
-                            : Padding(
-                                padding: EdgeInsets.symmetric(horizontal: 24),
-                              )
-                      ],
-                    ),
-                  ),
-                );
-              });
-        },
-      ),
-    );
-  }
-}
-
-class SessionItem extends StatelessWidget {
-  final SessionModel model;
-
-  SessionItem(this.model);
-
-  @override
-  Widget build(BuildContext context) {
-    final message = model.lastMessage;
-    return Row(
-      mainAxisAlignment: MainAxisAlignment.spaceBetween,
-      children: <Widget>[
-        model.avatar ?? Global.defaultAvatar,
-        Expanded(
-          child: Column(
-            children: <Widget>[
-              Padding(
-                padding: EdgeInsets.symmetric(vertical: 8),
-                child: Row(
-                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                  children: <Widget>[
-                    Expanded(
-                      child: Text(
-                        model.chatName,
-                        style: TextStyle(fontSize: 34),
-                        overflow: TextOverflow.ellipsis,
-                      ),
-                    ),
-                    Text(
-                      model.lastMessage != null ? _formatDate(model.lastMessage!.time) : "",
-                      style: TextStyle(fontSize: 26),
-                      overflow: TextOverflow.ellipsis,
-                    ),
-                  ],
-                ),
-              ),
-              Row(
-                mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                children: <Widget>[
-                  Expanded(
-                    child: RichText(
-                      text: TextSpan(children: [
-                        TextSpan(
-                          text: model.isAtYou ? "[@你]" : "",
-                          style: TextStyle(fontSize: 28),
-                        ),
-                        TextSpan(
-                          text: model.isSpecialAttention ? "[特别关注]" : "",
-                          style: TextStyle(fontSize: 28),
-                        ),
-                        TextSpan(
-                          text: model.isAtAll ? "[@所有人]" : "",
-                          style: TextStyle(fontSize: 28),
-                        ),
-                        TextSpan(
-                          text: model.lastMessage?.content.target?.plaintext ?? "",
-                          style: TextStyle(fontSize: 28),
-                        )
-                      ]),
-                      overflow: TextOverflow.ellipsis,
-                    ),
-                  ),
-                  (model.unReadCount > 0 && !model.isDND)
-                      ? Container(
-                          width: 32,
-                          height: 32,
-                          alignment: Alignment.center,
-                          decoration: BoxDecoration(borderRadius: BorderRadius.circular(20)),
-                          child: Text(
-                            model.unReadCount.toString(),
-                            style: TextStyle(color: Colors.white, fontSize: 26),
-                          ))
-                      : Container(),
-                  model.isDND
-                      ? Row(
-                          children: <Widget>[
-                            Icon(Icons.visibility_off),
-                            model.unReadCount > 0
-                                ? Icon(
-                                    Icons.chat_bubble,
-                                    color: Colors.red,
-                                  ) // TODO: 小红点
-                                : Container()
-                          ],
-                        )
-                      : Container()
-                ],
-              )
-            ],
-          ),
-        )
-      ],
-    );
-  }
-
-  String _formatDate(int time) {
-    DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(time);
-    return "${dateTime.hour}:${dateTime.minute}";
-  }
-}

+ 86 - 0
lib/view/user_chat.dart

@@ -0,0 +1,86 @@
+import 'dart:async';
+
+import 'package:dash_chat/dash_chat.dart';
+import 'package:e2ee_chat/common/global.dart';
+import 'package:e2ee_chat/model/message.dart';
+import 'package:e2ee_chat/model/user.dart';
+import 'package:e2ee_chat/presenter/login.dart';
+import 'package:e2ee_chat/presenter/user_chat.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import '';
+
+class UserChatView extends StatefulWidget {
+  UserChatView();
+
+  @override
+  _UserChatViewState createState() => _UserChatViewState();
+}
+
+class _UserChatViewState extends State<UserChatView> {
+  final GlobalKey<DashChatState> _chatViewKey = GlobalKey<DashChatState>();
+  final _presenter = UserChatPresenter();
+
+  @override
+  void initState() {
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final String? to = ModalRoute.of(context)?.settings.arguments.toString();
+    if (to != null) {
+      _presenter.to = User(to);
+    }
+    assert(_presenter.to != null, "error! user chat view to == null!");
+    return ChangeNotifierProvider<UserChatPresenter>(
+        create: (context) => _presenter,
+        child: Scaffold(
+          appBar: AppBar(
+            title: Text("Chat App"),
+          ),
+          body: Builder(builder: (context) {
+            final presenter = Provider.of<UserChatPresenter>(context);
+            final List<ChatMessage> messages = presenter.chatMessages;
+            final user = presenter.from!.toChatUser();
+            return DashChat(
+              key: _chatViewKey,
+              inverted: false,
+              onSend: (message) {
+                debug(message.toJson());
+                presenter.sendMessage(message);
+              },
+              sendOnEnter: true,
+              textInputAction: TextInputAction.send,
+              user: user,
+              inputDecoration: InputDecoration.collapsed(hintText: "Add message here..."),
+              dateFormat: DateFormat('yyyy-MMM-dd'),
+              timeFormat: DateFormat('HH:mm'),
+              messages: messages,
+              showUserAvatar: false,
+              showAvatarForEveryMessage: false,
+              scrollToBottom: false,
+              onPressAvatar: (ChatUser user) {
+                print("OnPressAvatar: ${user.name}");
+              },
+              onLongPressAvatar: (ChatUser user) {
+                print("OnLongPressAvatar: ${user.name}");
+              },
+              inputMaxLines: 5,
+              messageContainerPadding: EdgeInsets.only(left: 5.0, right: 5.0),
+              alwaysShowSend: true,
+              inputTextStyle: TextStyle(fontSize: 16.0),
+              inputContainerStyle: BoxDecoration(
+                border: Border.all(width: 0.0),
+                color: Colors.white,
+              ),
+              onLoadEarlier: () {
+                print("laoding...");
+              },
+              shouldShowLoadEarlier: false,
+              showTraillingBeforeSend: true,
+            );
+          }),
+        ));
+  }
+}

+ 105 - 0
lib/widgets/chat_list_item.dart

@@ -0,0 +1,105 @@
+import 'package:e2ee_chat/common/global.dart';
+import 'package:e2ee_chat/presenter/chat_list.dart';
+import 'package:e2ee_chat/presenter/user_chat.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_slidable/flutter_slidable.dart';
+import 'package:provider/provider.dart';
+
+class ChatListItem extends StatelessWidget {
+  final UserChatPresenter model;
+
+  ChatListItem(this.model);
+
+  @override
+  Widget build(BuildContext context) {
+    final message = model.lastMessage;
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+      children: <Widget>[
+        model.avatar ?? Global.defaultAvatar,
+        Expanded(
+          child: Column(
+            children: <Widget>[
+              Padding(
+                padding: EdgeInsets.symmetric(vertical: 8),
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                  children: <Widget>[
+                    Expanded(
+                      child: Text(
+                        model.chatName,
+                        style: TextStyle(fontSize: 34),
+                        overflow: TextOverflow.ellipsis,
+                      ),
+                    ),
+                    Text(
+                      _formatDate(),
+                      style: TextStyle(fontSize: 26),
+                      overflow: TextOverflow.ellipsis,
+                    ),
+                  ],
+                ),
+              ),
+              Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: <Widget>[
+                  Expanded(
+                    child: RichText(
+                      text: TextSpan(children: [
+                        TextSpan(
+                          text: model.isAtYou ? "[@你]" : "",
+                          style: TextStyle(fontSize: 28),
+                        ),
+                        TextSpan(
+                          text: model.isSpecialAttention ? "[特别关注]" : "",
+                          style: TextStyle(fontSize: 28),
+                        ),
+                        TextSpan(
+                          text: model.isAtAll ? "[@所有人]" : "",
+                          style: TextStyle(fontSize: 28),
+                        ),
+                        TextSpan(
+                          text: model.lastMessage?.plaintext ?? "",
+                          style: TextStyle(fontSize: 28),
+                        )
+                      ]),
+                      overflow: TextOverflow.ellipsis,
+                    ),
+                  ),
+                  (model.unReadCount > 0 && !model.isDND)
+                      ? Container(
+                      width: 32,
+                      height: 32,
+                      alignment: Alignment.center,
+                      decoration: BoxDecoration(borderRadius: BorderRadius.circular(20)),
+                      child: Text(
+                        model.unReadCount.toString(),
+                        style: TextStyle(color: Colors.white, fontSize: 26),
+                      ))
+                      : Container(),
+                  model.isDND
+                      ? Row(
+                    children: <Widget>[
+                      Icon(Icons.visibility_off),
+                      model.unReadCount > 0
+                          ? Icon(
+                        Icons.chat_bubble,
+                        color: Colors.red,
+                      ) // TODO: 小红点
+                          : Container()
+                    ],
+                  )
+                      : Container()
+                ],
+              )
+            ],
+          ),
+        )
+      ],
+    );
+  }
+
+  String _formatDate() {
+    return "1234";
+  }
+}

+ 12 - 18
lib/widgets/utils.dart

@@ -16,8 +16,7 @@ class Utils {
     );
   }
 
-  static Widget getSusItem(BuildContext context, String tag,
-      {double susHeight = 40}) {
+  static Widget getSusItem(BuildContext context, String tag, {double susHeight = 40}) {
     if (tag == '★') {
       tag = '★ 热门城市';
     }
@@ -38,13 +37,9 @@ class Utils {
     );
   }
 
-  static Widget getWeChatListItem(
-    BuildContext context,
-    ContactInfo model, {
-    double susHeight = 40,
-    Color? defHeaderBgColor,
-  }) {
-    return getWeChatItem(context, model, defHeaderBgColor: defHeaderBgColor);
+  static Widget getWeChatListItem(BuildContext context, ContactInfo model,
+      {double susHeight = 40, Color? defHeaderBgColor, void Function()? onTap}) {
+    return getWeChatItem(context, model, defHeaderBgColor: defHeaderBgColor, onTap: onTap);
 //    return Column(
 //      mainAxisSize: MainAxisSize.min,
 //      children: <Widget>[
@@ -58,11 +53,8 @@ class Utils {
 //    );
   }
 
-  static Widget getWeChatItem(
-    BuildContext context,
-    ContactInfo model, {
-    Color? defHeaderBgColor,
-  }) {
+  static Widget getWeChatItem(BuildContext context, ContactInfo model,
+      {Color? defHeaderBgColor, void Function()? onTap}) {
     DecorationImage? image;
 //    if (model.img != null && model.img.isNotEmpty) {
 //      image = DecorationImage(
@@ -70,6 +62,11 @@ class Utils {
 //        fit: BoxFit.contain,
 //      );
 //    }
+       void _onTap () {
+      LogUtil.e("onItemClick : $model");
+      Utils.showSnackBar(context, 'onItemClick : ${model.name}');
+    }
+
     return ListTile(
       leading: Container(
         width: 36,
@@ -89,10 +86,7 @@ class Utils {
               ),
       ),
       title: Text(model.name),
-      onTap: () {
-        LogUtil.e("onItemClick : $model");
-        Utils.showSnackBar(context, 'onItemClick : ${model.name}');
-      },
+      onTap: onTap ?? _onTap,
     );
   }
 }

+ 164 - 94
pubspec.lock

@@ -5,210 +5,238 @@ packages:
     dependency: transitive
     description:
       name: _fe_analyzer_shared
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "22.0.0"
   analyzer:
     dependency: transitive
     description:
       name: analyzer
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.7.1"
   args:
     dependency: transitive
     description:
       name: args
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.1.1"
   async:
     dependency: transitive
     description:
       name: async
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.6.1"
-  azlistview:
-    dependency: "direct main"
-    description:
-      name: azlistview
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "1.1.1"
   boolean_selector:
     dependency: transitive
     description:
       name: boolean_selector
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.1.0"
   build:
     dependency: transitive
     description:
       name: build
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.3"
   build_config:
     dependency: transitive
     description:
       name: build_config
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.0"
   build_daemon:
     dependency: transitive
     description:
       name: build_daemon
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.0.0"
   build_resolvers:
     dependency: transitive
     description:
       name: build_resolvers
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.4"
   build_runner:
     dependency: "direct dev"
     description:
       name: build_runner
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.6"
   build_runner_core:
     dependency: transitive
     description:
       name: build_runner_core
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "7.0.1"
   built_collection:
     dependency: transitive
     description:
       name: built_collection
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "5.1.0"
   built_value:
     dependency: transitive
     description:
       name: built_value
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "8.1.1"
   characters:
     dependency: transitive
     description:
       name: characters
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.1.0"
   charcode:
     dependency: transitive
     description:
       name: charcode
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.2.0"
   checked_yaml:
     dependency: transitive
     description:
       name: checked_yaml
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.1"
   cli_util:
     dependency: transitive
     description:
       name: cli_util
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "0.3.3"
   clock:
     dependency: transitive
     description:
       name: clock
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.1.0"
   code_builder:
     dependency: transitive
     description:
       name: code_builder
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "4.0.0"
   collection:
     dependency: transitive
     description:
       name: collection
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.15.0"
+  common_utils:
+    dependency: "direct main"
+    description:
+      name: common_utils
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.2"
   convert:
     dependency: transitive
     description:
       name: convert
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.0.1"
+  cookie_jar:
+    dependency: "direct main"
+    description:
+      name: cookie_jar
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.0.1"
   crypto:
     dependency: "direct main"
     description:
       name: crypto
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.0.1"
   cupertino_icons:
     dependency: "direct main"
     description:
       name: cupertino_icons
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.3"
   dart_style:
     dependency: transitive
     description:
       name: dart_style
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.1"
+  dash_chat:
+    dependency: "direct main"
+    description:
+      name: dash_chat
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.1.16"
+  decimal:
+    dependency: transitive
+    description:
+      name: decimal
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.2.0"
   dio:
     dependency: "direct main"
     description:
       name: dio
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "4.0.0"
+  dio_cookie_manager:
+    dependency: "direct main"
+    description:
+      name: dio_cookie_manager
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.0"
   fake_async:
     dependency: transitive
     description:
       name: fake_async
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.2.0"
   ffi:
     dependency: transitive
     description:
       name: ffi
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.1.2"
   file:
     dependency: transitive
     description:
       name: file
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "6.1.2"
   fixnum:
     dependency: transitive
     description:
       name: fixnum
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.0"
   flutter:
@@ -221,18 +249,25 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_parsed_text:
+    dependency: transitive
+    description:
+      name: flutter_parsed_text
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.2.1"
   flutter_secure_storage:
     dependency: "direct main"
     description:
       name: flutter_secure_storage
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "4.2.0"
   flutter_slidable:
     dependency: "direct main"
     description:
       name: flutter_slidable
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "0.6.0"
   flutter_test:
@@ -249,266 +284,287 @@ packages:
     dependency: "direct main"
     description:
       name: fluttertoast
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "8.0.7"
   frontend_server_client:
     dependency: transitive
     description:
       name: frontend_server_client
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.1.0"
   glob:
     dependency: transitive
     description:
       name: glob
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.1"
   graphs:
     dependency: transitive
     description:
       name: graphs
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.0"
   http_multi_server:
     dependency: transitive
     description:
       name: http_multi_server
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.0.1"
   http_parser:
     dependency: transitive
     description:
       name: http_parser
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "4.0.0"
   intl:
     dependency: "direct main"
     description:
       name: intl
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "0.17.0"
   io:
     dependency: transitive
     description:
       name: io
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.3"
   js:
     dependency: transitive
     description:
       name: js
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "0.6.3"
   json_annotation:
     dependency: transitive
     description:
       name: json_annotation
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "4.0.1"
   logging:
     dependency: transitive
     description:
       name: logging
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.1"
+  lpinyin:
+    dependency: "direct main"
+    description:
+      name: lpinyin
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.3"
   matcher:
     dependency: transitive
     description:
       name: matcher
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "0.12.10"
   meta:
     dependency: transitive
     description:
       name: meta
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.3.0"
   mime:
     dependency: transitive
     description:
       name: mime
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.0"
   nested:
     dependency: transitive
     description:
       name: nested
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.0"
   objectbox:
     dependency: "direct main"
     description:
       name: objectbox
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.1.1"
   objectbox_flutter_libs:
     dependency: "direct main"
     description:
       name: objectbox_flutter_libs
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.1.1"
   objectbox_generator:
     dependency: "direct dev"
     description:
       name: objectbox_generator
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.1.1"
   package_config:
     dependency: transitive
     description:
       name: package_config
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.0"
   path:
     dependency: transitive
     description:
       name: path
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.8.0"
   path_provider:
     dependency: transitive
     description:
       name: path_provider
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.2"
   path_provider_linux:
     dependency: transitive
     description:
       name: path_provider_linux
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.0"
   path_provider_macos:
     dependency: transitive
     description:
       name: path_provider_macos
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.0"
   path_provider_platform_interface:
     dependency: transitive
     description:
       name: path_provider_platform_interface
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.1"
   path_provider_windows:
     dependency: transitive
     description:
       name: path_provider_windows
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.1"
   pedantic:
     dependency: transitive
     description:
       name: pedantic
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.11.1"
   permission_handler:
     dependency: "direct main"
     description:
       name: permission_handler
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "8.1.2"
   permission_handler_platform_interface:
     dependency: transitive
     description:
       name: permission_handler_platform_interface
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.6.0"
   platform:
     dependency: transitive
     description:
       name: platform
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.0.0"
   plugin_platform_interface:
     dependency: transitive
     description:
       name: plugin_platform_interface
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.1"
   pool:
     dependency: transitive
     description:
       name: pool
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.5.0"
   process:
     dependency: transitive
     description:
       name: process
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "4.2.1"
   provider:
     dependency: "direct main"
     description:
       name: provider
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "5.0.0"
   pub_semver:
     dependency: transitive
     description:
       name: pub_semver
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.0"
   pubspec_parse:
     dependency: transitive
     description:
       name: pubspec_parse
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.0"
-  scrollable_positioned_list:
+  rational:
     dependency: transitive
+    description:
+      name: rational
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.2.1"
+  rxdart:
+    dependency: "direct main"
+    description:
+      name: rxdart
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.27.1"
+  scrollable_positioned_list:
+    dependency: "direct main"
     description:
       name: scrollable_positioned_list
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "0.1.10"
+    version: "0.2.0-nullsafety.0"
   shelf:
     dependency: transitive
     description:
       name: shelf
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.2.0"
   shelf_web_socket:
     dependency: transitive
     description:
       name: shelf_web_socket
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.1"
   sky_engine:
@@ -520,112 +576,126 @@ packages:
     dependency: transitive
     description:
       name: source_gen
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.2"
   source_span:
     dependency: transitive
     description:
       name: source_span
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.8.1"
   stack_trace:
     dependency: transitive
     description:
       name: stack_trace
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.10.0"
   stream_channel:
     dependency: transitive
     description:
       name: stream_channel
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.1.0"
   stream_transform:
     dependency: transitive
     description:
       name: stream_transform
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.0.0"
   string_scanner:
     dependency: transitive
     description:
       name: string_scanner
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.1.0"
   term_glyph:
     dependency: transitive
     description:
       name: term_glyph
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.2.0"
   test_api:
     dependency: transitive
     description:
       name: test_api
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "0.3.0"
   timing:
     dependency: transitive
     description:
       name: timing
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.0"
+  transparent_image:
+    dependency: transitive
+    description:
+      name: transparent_image
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.0"
   typed_data:
     dependency: transitive
     description:
       name: typed_data
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.3.0"
+  uuid:
+    dependency: transitive
+    description:
+      name: uuid
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.0.4"
   vector_math:
     dependency: transitive
     description:
       name: vector_math
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.1.0"
   watcher:
     dependency: transitive
     description:
       name: watcher
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.0"
   web_socket_channel:
     dependency: "direct main"
     description:
       name: web_socket_channel
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.1.0"
   win32:
     dependency: transitive
     description:
       name: win32
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.2.5"
   xdg_directories:
     dependency: transitive
     description:
       name: xdg_directories
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "0.2.0"
   yaml:
     dependency: transitive
     description:
       name: yaml
-      url: "https://pub.dartlang.org"
+      url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.1.0"
 sdks:

+ 8 - 2
pubspec.yaml

@@ -37,9 +37,10 @@ dependencies:
   dio_cookie_manager: ^2.0.0
   cookie_jar: ^3.0.1
   crypto: ^3.0.1
+  rxdart: ^0.27.1
+
   permission_handler: ^8.1.2
-  flutter_slidable: ^0.6.0
-  scrollable_positioned_list: ^0.2.0-nullsafety.0
+
   lpinyin: ^2.0.3
   common_utils: ^2.0.2
   web_socket_channel: ^2.1.0
@@ -48,8 +49,13 @@ dependencies:
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^1.0.2
 
+  flutter_slidable: ^0.6.0
+  scrollable_positioned_list: ^0.2.0-nullsafety.0
+  dash_chat: ^1.1.16
+
   intl: ^0.17.0
 
+
 dev_dependencies:
   flutter_test:
     sdk: flutter