Переглянути джерело

login/register
offLine detect push login route
home route & drawer
logout in drawer
message (not complete)

ignalxy 4 роки тому
батько
коміт
9872d49fa4

BIN
imgs/avatar.jpg


+ 103 - 13
lib/common/api.dart

@@ -15,18 +15,16 @@ class Api {
 
   static init() async {
     if (!Global.isRelease) {
-      (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
-          (client) {
+      (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;
+        client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
       };
     }
 
-    // debug(await Dio().get('http://10.122.237.112:80/'));
+    debug(await Dio().get('http://10.122.237.112/account/login'));
   }
 
   Future<String?> login({required String username, String? password, String? token}) async {
@@ -41,14 +39,15 @@ class Api {
     var data = {"username": username, "token": token ?? Global.emptyToken, "password": password};
     debug('Api login data: $data');
     try {
-      var r = await dio.post("/account/login/", data: FormData.fromMap(data),
+      var r = await dio.post(
+        "/account/login/", data: FormData.fromMap(data),
         // options: _options,
       );
       debug('Api login response: $r');
       var json = jsonDecode(r.data);
       _token = json["token"].toString();
       debug('Api login new token: $_token');
-    } on DioError catch(e) {
+    } on DioError catch (e) {
       var data = e.response?.data;
       var status = e.response?.statusCode;
       debug('Api login error, statusCode: $status, data: $data');
@@ -59,21 +58,112 @@ class Api {
   }
 
   Future<bool> logout() async {
+    debug('Api logout begin');
+    bool _result = true;
     try {
       await dio.post("/account/logout/");
-      return true;
-    } on DioError catch(e) {
-      return false;
+    } on DioError catch (e) {
+      _result = false;
     }
+    debug('Api logout end');
+    return _result;
   }
 
-  Future<bool> register({required String username, required String password}) async {
+  Future<bool> register(String username, String password) async {
+    debug('Api register begin');
+    bool _result = true;
     try {
       var data = {"username": username, "password": password, "password2": password};
       await dio.post('/account/register/', data: FormData.fromMap(data));
-    } on DioError catch(e) {
+    } on DioError catch (e) {
+      _result = false;
+    }
+    debug('Api register end');
+    return _result;
+  }
+
+  Future<bool> edit({String? bio, File? avatar, String? phone}) async {
+    debug('Api edit begin');
+    bool _result = Global.profile.isLogin;
+    if (_result) {
+      try {
+        Map<String, dynamic> data = {"bio": bio, "phone": phone};
+        if (avatar != null) {
+          var path = avatar.path;
+          var name = path.substring(path.lastIndexOf("/") + 1, path.length);
+          data["file"] = await MultipartFile.fromFile(path, filename: name);
+        }
+        var form = FormData.fromMap(data);
+        await dio.post('/account/edit/${Global.profile.username}/', data: form);
+      } on DioError catch (e) {
+        _result = false;
+      }
+    }
+    debug('Api edit end');
+    return _result;
+  }
+
+  Future<List<String>?> friendList() async {
+    debug('Api friendList begin');
+    List<String>? friends;
+    try {
+      var r = await dio.post('/friends/friends_list/');
+      friends = jsonDecode(r.data);
+    } on DioError catch (e) {
+      debug('get friend list failed!');
+    }
+    debug('Api friendList end');
+    return friends;
+  }
+
+  Future<bool> addFriend(String username) async {
+    debug("add friend begin");
+    try {
+      await dio.post('/friends/add/', data: FormData.fromMap({"username": username}));
+    } on DioError catch (e) {
+      debug("add friend failed: ${ () {
+        switch (e.response?.statusCode) {
+          case null: return "unknown error";
+          case 421: return "already friend";
+          case 422:case 400: return "failed";
+        }
+      } ()}");
       return false;
     }
+    debug("add friend end");
     return true;
   }
-}
+
+  Future<List<String>?> friendRequest() async {
+    debug('Api friendRequest begin');
+    List<String>? apps;
+    try {
+      var r = await dio.post('/friends/get_requests/');
+      apps = jsonDecode(r.data);
+    } on DioError catch (e) {
+      debug('get friend request failed!');
+    }
+    debug('Api friendRequest end');
+    return apps;
+  }
+
+  Future<bool> deleteFriend(String username) async {
+    debug("delete friend begin");
+    bool _result = true;
+    try {
+      await dio.post('/friends/delete/', data: FormData.fromMap({"username": username}));
+    } on DioError catch (e) {
+      debug("delete friend failed: ${ () {
+        switch (e.response?.statusCode) {
+          case null: return "unknown error";
+          case 422:case 400: return "failed";
+        }
+      } ()}");
+      _result = false;
+    }
+    debug("delete friend end");
+    return _result;
+  }
+
+
+}

+ 7 - 2
lib/common/global.dart

@@ -4,7 +4,7 @@ import 'dart:convert';
 import 'package:dio/dio.dart';
 import 'package:e2ee_chat/entities/profile.dart';
 import 'package:e2ee_chat/common/api.dart';
-import 'package:e2ee_chat/models/user_model.dart';
+import 'package:e2ee_chat/models/login_model.dart';
 import 'package:e2ee_chat/objectbox.g.dart';
 import 'package:flutter/material.dart';
 import 'package:objectbox/objectbox.dart';
@@ -27,6 +27,11 @@ void debug(Object? o) {
 class Global {
   static GlobalKey<NavigatorState> navigatorKey = GlobalKey();
 
+  static final Image defaultAvatar = Image.asset(
+    "imgs/avatar.jpg",
+    width: 80,
+  );
+
   static final int profileId = 1;
   static Profile profile = Profile();
 
@@ -51,7 +56,7 @@ class Global {
 
   static listenIsLogin() async {
     while (true) {
-      if (!profile.isLogin && (profile.isLogout || !await UserModel().login())) {
+      if (!profile.isLogin && (profile.isLogout || !await LoginModel().login())) {
         debug("offline detected! navigate to login route");
         await Global.navigatorKey.currentState?.pushNamed("login");
       }

+ 13 - 0
lib/entities/content.dart

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

+ 13 - 0
lib/entities/friend.dart

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

+ 13 - 0
lib/entities/message.dart

@@ -0,0 +1,13 @@
+import 'package:objectbox/objectbox.dart';
+
+import 'friend.dart';
+import 'user.dart';
+
+@Entity()
+class Message {
+  int id = 0;
+  final from = ToOne<User>();
+  final to = ToOne<User>();
+  final String text = "";
+
+}

+ 12 - 0
lib/entities/user.dart

@@ -0,0 +1,12 @@
+import 'package:objectbox/objectbox.dart';
+
+import 'friend.dart';
+
+@Entity()
+class User {
+  User({required this.id, required this.username});
+  int id;
+  @Unique()
+  final String username;
+  final friends = ToMany<Friend>();
+}

+ 2 - 2
lib/main.dart

@@ -9,7 +9,7 @@ import 'common/global.dart';
 import 'l10n/localization_intl.dart';
 import 'models/locale_model.dart';
 import 'models/theme_model.dart';
-import 'models/user_model.dart';
+import 'models/login_model.dart';
 import 'routes/language_route.dart';
 import 'routes/login.dart';
 import 'routes/theme_change.dart';
@@ -27,7 +27,7 @@ class MyApp extends StatelessWidget {
       providers: <SingleChildWidget>[
         ChangeNotifierProvider.value(value: ThemeModel()),
         ChangeNotifierProvider.value(value: LocaleModel()),
-        ChangeNotifierProvider.value(value: UserModel()),
+        ChangeNotifierProvider.value(value: LoginModel()),
       ],
       child: Consumer2<ThemeModel, LocaleModel>(
         builder: (BuildContext context, themeModel, localeModel, Widget? child) {

+ 10 - 1
lib/models/user_model.dart → lib/models/login_model.dart

@@ -6,7 +6,7 @@ import 'package:provider/provider.dart';
 
 import 'profile.dart';
 
-class UserModel extends ProfileChangeNotifier {
+class LoginModel extends ProfileChangeNotifier {
 
   /*
   UserModel._();
@@ -52,4 +52,13 @@ class UserModel extends ProfileChangeNotifier {
       notifyListeners();
     }
   }
+
+  Future<bool> loginOrRegister(String username, String password) async {
+    if (await login(username: username, password: password)) {
+      return true;
+    } else if (await Api().register(username, password)) {
+      return login(username: username, password: password);
+    }
+    return false;
+  }
 }

+ 78 - 0
lib/models/message_model.dart

@@ -0,0 +1,78 @@
+import 'package:e2ee_chat/common/global.dart';
+import 'package:flutter/cupertino.dart';
+
+enum DTMessageType {
+  text,
+  image
+}
+
+class MessageListModel extends ChangeNotifier {
+  List items = <MessageItemModel>[];
+
+  int get count => items.length;
+}
+
+class MessageItemModel {
+  MessageItemModel({required this.chatId,
+    required this.userId,
+    required this.userName,
+    required this.chatName,
+    required this.message,
+    required this.messageType,
+    required this.time,
+    this.unReadCount = 0,
+    this.isSingle = false,
+    this.avatar,
+    this.isGroup = false,
+    this.isDisturbing = false,
+    this.isSpecialAttention = false,
+    this.isAtYou = false,
+    this.isAtAll = false,
+    this.isStick = false});
+
+  /// 聊天 Id
+  String chatId;
+
+  /// 用户名称
+  String userName;
+
+  /// 用户Id
+  String userId;
+
+  /// 聊天名称
+  String chatName;
+
+  /// 消息体
+  String message;
+
+  /// message type
+  DTMessageType messageType;
+
+  /// 时间
+  int time;
+
+  /// 未读数量
+  int unReadCount;
+
+  /// 单聊
+  bool isSingle;
+  Image? avatar;
+
+  /// 群聊信息
+  bool isGroup;
+
+  /// 消息免打扰
+  bool isDisturbing;
+
+  /// 是否为置顶
+  bool isStick;
+
+  /// 特别关注
+  bool isSpecialAttention;
+
+  /// 是否 @ 你
+  bool isAtYou;
+
+  /// 是否 @ 全部
+  bool isAtAll;
+}

+ 116 - 4
lib/objectbox-model.json

@@ -5,7 +5,7 @@
   "entities": [
     {
       "id": "2:1438354990151910015",
-      "lastPropertyId": "6:4818206049188692305",
+      "lastPropertyId": "7:4763561980566319174",
       "name": "Profile",
       "properties": [
         {
@@ -28,14 +28,126 @@
           "id": "4:5923665807684456265",
           "name": "isLogin",
           "type": 1
+        },
+        {
+          "id": "7:4763561980566319174",
+          "name": "isLogout",
+          "type": 1
+        }
+      ],
+      "relations": []
+    },
+    {
+      "id": "3:2132486004932842474",
+      "lastPropertyId": "1:8861377786907255369",
+      "name": "Content",
+      "properties": [
+        {
+          "id": "1:8861377786907255369",
+          "name": "id",
+          "type": 6,
+          "flags": 1
+        }
+      ],
+      "relations": []
+    },
+    {
+      "id": "4:5044745765388820377",
+      "lastPropertyId": "3:214180290351070734",
+      "name": "Friend",
+      "properties": [
+        {
+          "id": "1:5034484793468406763",
+          "name": "id",
+          "type": 6,
+          "flags": 1
+        },
+        {
+          "id": "2:754684519442805673",
+          "name": "user1Id",
+          "type": 11,
+          "flags": 520,
+          "indexId": "2:3790366672903506902",
+          "relationTarget": "User"
+        },
+        {
+          "id": "3:214180290351070734",
+          "name": "user2Id",
+          "type": 11,
+          "flags": 520,
+          "indexId": "3:1460593015267560936",
+          "relationTarget": "User"
+        }
+      ],
+      "relations": [
+        {
+          "id": "1:5078486750954543598",
+          "name": "messages",
+          "targetId": "5:8880211362757189177"
+        }
+      ]
+    },
+    {
+      "id": "5:8880211362757189177",
+      "lastPropertyId": "3:7721473969164711791",
+      "name": "Message",
+      "properties": [
+        {
+          "id": "1:8816894358087325317",
+          "name": "id",
+          "type": 6,
+          "flags": 1
+        },
+        {
+          "id": "2:7982216815340584307",
+          "name": "fromId",
+          "type": 11,
+          "flags": 520,
+          "indexId": "4:6495021264194887400",
+          "relationTarget": "User"
+        },
+        {
+          "id": "3:7721473969164711791",
+          "name": "toId",
+          "type": 11,
+          "flags": 520,
+          "indexId": "5:5329740598468773345",
+          "relationTarget": "User"
         }
       ],
       "relations": []
+    },
+    {
+      "id": "6:6066676571331973763",
+      "lastPropertyId": "2:2230815448876041069",
+      "name": "User",
+      "properties": [
+        {
+          "id": "1:5757871925438229578",
+          "name": "id",
+          "type": 6,
+          "flags": 1
+        },
+        {
+          "id": "2:2230815448876041069",
+          "name": "username",
+          "type": 9,
+          "flags": 2080,
+          "indexId": "6:2181766053148811865"
+        }
+      ],
+      "relations": [
+        {
+          "id": "2:3118435353931894829",
+          "name": "friends",
+          "targetId": "4:5044745765388820377"
+        }
+      ]
     }
   ],
-  "lastEntityId": "2:1438354990151910015",
-  "lastIndexId": "1:2185831944762227781",
-  "lastRelationId": "0:0",
+  "lastEntityId": "6:6066676571331973763",
+  "lastIndexId": "6:2181766053148811865",
+  "lastRelationId": "2:3118435353931894829",
   "lastSequenceId": "0:0",
   "modelVersion": 5,
   "modelVersionParserMinimum": 5,

+ 288 - 6
lib/objectbox.g.dart

@@ -9,7 +9,11 @@ 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 'entities/content.dart';
+import 'entities/friend.dart';
+import 'entities/message.dart';
 import 'entities/profile.dart';
+import 'entities/user.dart';
 
 export 'package:objectbox/objectbox.dart'; // so that callers only have to import this file
 
@@ -17,7 +21,7 @@ final _entities = <ModelEntity>[
   ModelEntity(
       id: const IdUid(2, 1438354990151910015),
       name: 'Profile',
-      lastPropertyId: const IdUid(6, 4818206049188692305),
+      lastPropertyId: const IdUid(7, 4763561980566319174),
       flags: 0,
       properties: <ModelProperty>[
         ModelProperty(
@@ -39,9 +43,114 @@ final _entities = <ModelEntity>[
             id: const IdUid(4, 5923665807684456265),
             name: 'isLogin',
             type: 1,
+            flags: 0),
+        ModelProperty(
+            id: const IdUid(7, 4763561980566319174),
+            name: 'isLogout',
+            type: 1,
             flags: 0)
       ],
       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(4, 5044745765388820377),
+      name: 'Friend',
+      lastPropertyId: const IdUid(3, 214180290351070734),
+      flags: 0,
+      properties: <ModelProperty>[
+        ModelProperty(
+            id: const IdUid(1, 5034484793468406763),
+            name: 'id',
+            type: 6,
+            flags: 1),
+        ModelProperty(
+            id: const IdUid(2, 754684519442805673),
+            name: 'user1Id',
+            type: 11,
+            flags: 520,
+            indexId: const IdUid(2, 3790366672903506902),
+            relationTarget: 'User'),
+        ModelProperty(
+            id: const IdUid(3, 214180290351070734),
+            name: 'user2Id',
+            type: 11,
+            flags: 520,
+            indexId: const IdUid(3, 1460593015267560936),
+            relationTarget: 'User')
+      ],
+      relations: <ModelRelation>[
+        ModelRelation(
+            id: const IdUid(1, 5078486750954543598),
+            name: 'messages',
+            targetId: const IdUid(5, 8880211362757189177))
+      ],
+      backlinks: <ModelBacklink>[]),
+  ModelEntity(
+      id: const IdUid(5, 8880211362757189177),
+      name: 'Message',
+      lastPropertyId: const IdUid(3, 7721473969164711791),
+      flags: 0,
+      properties: <ModelProperty>[
+        ModelProperty(
+            id: const IdUid(1, 8816894358087325317),
+            name: 'id',
+            type: 6,
+            flags: 1),
+        ModelProperty(
+            id: const IdUid(2, 7982216815340584307),
+            name: 'fromId',
+            type: 11,
+            flags: 520,
+            indexId: const IdUid(4, 6495021264194887400),
+            relationTarget: 'User'),
+        ModelProperty(
+            id: const IdUid(3, 7721473969164711791),
+            name: 'toId',
+            type: 11,
+            flags: 520,
+            indexId: const IdUid(5, 5329740598468773345),
+            relationTarget: 'User')
+      ],
+      relations: <ModelRelation>[],
+      backlinks: <ModelBacklink>[]),
+  ModelEntity(
+      id: const IdUid(6, 6066676571331973763),
+      name: 'User',
+      lastPropertyId: const IdUid(2, 2230815448876041069),
+      flags: 0,
+      properties: <ModelProperty>[
+        ModelProperty(
+            id: const IdUid(1, 5757871925438229578),
+            name: 'id',
+            type: 6,
+            flags: 1),
+        ModelProperty(
+            id: const IdUid(2, 2230815448876041069),
+            name: 'username',
+            type: 9,
+            flags: 2080,
+            indexId: const IdUid(6, 2181766053148811865))
+      ],
+      relations: <ModelRelation>[
+        ModelRelation(
+            id: const IdUid(2, 3118435353931894829),
+            name: 'friends',
+            targetId: const IdUid(4, 5044745765388820377))
+      ],
       backlinks: <ModelBacklink>[])
 ];
 
@@ -65,9 +174,9 @@ Future<Store> openStore(
 ModelDefinition getObjectBoxModel() {
   final model = ModelInfo(
       entities: _entities,
-      lastEntityId: const IdUid(2, 1438354990151910015),
-      lastIndexId: const IdUid(1, 2185831944762227781),
-      lastRelationId: const IdUid(0, 0),
+      lastEntityId: const IdUid(6, 6066676571331973763),
+      lastIndexId: const IdUid(6, 2181766053148811865),
+      lastRelationId: const IdUid(2, 3118435353931894829),
       lastSequenceId: const IdUid(0, 0),
       retiredEntityUids: const [3444477729893015694],
       retiredIndexUids: const [],
@@ -95,11 +204,12 @@ ModelDefinition getObjectBoxModel() {
           final usernameOffset = object.username == null
               ? null
               : fbb.writeString(object.username!);
-          fbb.startTable(7);
+          fbb.startTable(8);
           fbb.addInt64(0, object.id);
           fbb.addInt64(1, object.theme);
           fbb.addOffset(2, usernameOffset);
           fbb.addBool(3, object.isLogin);
+          fbb.addBool(6, object.isLogout);
           fbb.finish(fbb.endTable());
           return object.id;
         },
@@ -113,9 +223,126 @@ ModelDefinition getObjectBoxModel() {
             ..username =
                 const fb.StringReader().vTableGetNullable(buffer, rootOffset, 8)
             ..isLogin =
-                const fb.BoolReader().vTableGet(buffer, rootOffset, 10, false);
+                const fb.BoolReader().vTableGet(buffer, rootOffset, 10, false)
+            ..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;
+        }),
+    Friend: EntityDefinition<Friend>(
+        model: _entities[2],
+        toOneRelations: (Friend object) => [object.user1, object.user2],
+        toManyRelations: (Friend object) =>
+            {RelInfo<Friend>.toMany(1, object.id): object.messages},
+        getId: (Friend object) => object.id,
+        setId: (Friend object, int id) {
+          object.id = id;
+        },
+        objectToFB: (Friend object, fb.Builder fbb) {
+          fbb.startTable(4);
+          fbb.addInt64(0, object.id);
+          fbb.addInt64(1, object.user1.targetId);
+          fbb.addInt64(2, object.user2.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 = Friend()
+            ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0);
+          object.user1.targetId =
+              const fb.Int64Reader().vTableGet(buffer, rootOffset, 6, 0);
+          object.user1.attach(store);
+          object.user2.targetId =
+              const fb.Int64Reader().vTableGet(buffer, rootOffset, 8, 0);
+          object.user2.attach(store);
+          InternalToManyAccess.setRelInfo(object.messages, store,
+              RelInfo<Friend>.toMany(1, object.id), store.box<Friend>());
+          return object;
+        }),
+    Message: EntityDefinition<Message>(
+        model: _entities[3],
+        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(4);
+          fbb.addInt64(0, object.id);
+          fbb.addInt64(1, object.from.targetId);
+          fbb.addInt64(2, object.to.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 = Message()
+            ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 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);
+          return object;
+        }),
+    User: EntityDefinition<User>(
+        model: _entities[4],
+        toOneRelations: (User object) => [],
+        toManyRelations: (User object) =>
+            {RelInfo<User>.toMany(2, object.id): object.friends},
+        getId: (User object) => object.id,
+        setId: (User object, int id) {
+          object.id = id;
+        },
+        objectToFB: (User object, fb.Builder fbb) {
+          final usernameOffset = fbb.writeString(object.username);
+          fbb.startTable(3);
+          fbb.addInt64(0, object.id);
+          fbb.addOffset(1, usernameOffset);
+          fbb.finish(fbb.endTable());
+          return object.id;
+        },
+        objectFromFB: (Store store, ByteData fbData) {
+          final buffer = fb.BufferContext(fbData);
+          final rootOffset = buffer.derefObject(0);
+
+          final object = User(
+              id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0),
+              username:
+                  const fb.StringReader().vTableGet(buffer, rootOffset, 6, ''));
+          InternalToManyAccess.setRelInfo(object.friends, store,
+              RelInfo<User>.toMany(2, object.id), store.box<User>());
+          return object;
         })
   };
 
@@ -138,4 +365,59 @@ class Profile_ {
   /// see [Profile.isLogin]
   static final isLogin =
       QueryBooleanProperty<Profile>(_entities[0].properties[3]);
+
+  /// see [Profile.isLogout]
+  static final isLogout =
+      QueryBooleanProperty<Profile>(_entities[0].properties[4]);
+}
+
+/// [Content] entity fields to define ObjectBox queries.
+class Content_ {
+  /// see [Content.id]
+  static final id = QueryIntegerProperty<Content>(_entities[1].properties[0]);
+}
+
+/// [Friend] entity fields to define ObjectBox queries.
+class Friend_ {
+  /// see [Friend.id]
+  static final id = QueryIntegerProperty<Friend>(_entities[2].properties[0]);
+
+  /// see [Friend.user1]
+  static final user1 =
+      QueryRelationToOne<Friend, User>(_entities[2].properties[1]);
+
+  /// see [Friend.user2]
+  static final user2 =
+      QueryRelationToOne<Friend, User>(_entities[2].properties[2]);
+
+  /// see [Friend.messages]
+  static final messages =
+      QueryRelationToMany<Friend, Message>(_entities[2].relations[0]);
+}
+
+/// [Message] entity fields to define ObjectBox queries.
+class Message_ {
+  /// see [Message.id]
+  static final id = QueryIntegerProperty<Message>(_entities[3].properties[0]);
+
+  /// see [Message.from]
+  static final from =
+      QueryRelationToOne<Message, User>(_entities[3].properties[1]);
+
+  /// see [Message.to]
+  static final to =
+      QueryRelationToOne<Message, User>(_entities[3].properties[2]);
+}
+
+/// [User] entity fields to define ObjectBox queries.
+class User_ {
+  /// see [User.id]
+  static final id = QueryIntegerProperty<User>(_entities[4].properties[0]);
+
+  /// see [User.username]
+  static final username = QueryStringProperty<User>(_entities[4].properties[1]);
+
+  /// see [User.friends]
+  static final friends =
+      QueryRelationToMany<User, Friend>(_entities[4].relations[0]);
 }

+ 30 - 17
lib/routes/home.dart

@@ -2,8 +2,11 @@ import 'package:e2ee_chat/common/global.dart';
 import 'package:e2ee_chat/l10n/localization_intl.dart';
 import 'package:e2ee_chat/common/api.dart';
 import 'package:e2ee_chat/models/theme_model.dart';
-import 'package:e2ee_chat/models/user_model.dart';
+import 'package:e2ee_chat/models/login_model.dart';
 import 'package:e2ee_chat/widgets/empty.dart';
+import 'package:e2ee_chat/widgets/friends.dart';
+import 'package:e2ee_chat/widgets/group.dart';
+import 'package:e2ee_chat/widgets/message.dart';
 import 'package:e2ee_chat/widgets/mydrawer.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
@@ -14,7 +17,6 @@ class HomeRoute extends StatefulWidget {
 }
 
 class _HomeRouteState extends State<HomeRoute> {
-
   int _index = 0;
 
   @override
@@ -34,36 +36,47 @@ class _HomeRouteState extends State<HomeRoute> {
           }, icon: Icon(Icons.add))
         ],
       ),
+      drawer: MyDrawer(),
       bottomNavigationBar: BottomNavigationBar(
         items: [
           BottomNavigationBarItem(icon: Icon(Icons.chat_bubble), label: "message"),
           BottomNavigationBarItem(icon: Icon(Icons.people), label: "friends"),
-          BottomNavigationBarItem(icon: Icon(Icons.person), label: "mine"),
+          BottomNavigationBarItem(icon: Icon(Icons.person), label: "group"),
         ],
         currentIndex: _index,
         fixedColor: Provider.of<ThemeModel>(context).theme,
         onTap: _onItemTapped,
       ),
-      body: _buildBody(), // 构建主页面
+      body: RefreshIndicator(
+        child: _buildChild(),
+        onRefresh: () async {
+          // TODO: pull to refresh
+        },
+      ), // 构建主页面
     );
   }
 
-  Widget _buildBody() {
-    if (_index == 2) {
-      UserModel userModel = Provider.of<UserModel>(context);
-      return Center(
-        child: ElevatedButton(
-          child: Text(GmLocalizations.of(context).logout),
-          onPressed: () => userModel.logout()
-          ,)
-        ,);
-    }
-    return EmptyWidget();
-  }
-
   void _onItemTapped(int value) {
     setState(() {
       _index = value;
     });
   }
+
+  Widget _buildChild() {
+    switch (_index) {
+      case 0:
+        return MessageList();
+      case 1:
+        return FriendRoute();
+      case 2:
+        return GroupRoute();
+    }
+    return EmptyWidget();
+
+  }
+
+  Widget _buildGroupList() {
+    // TODO: group list
+    return EmptyWidget();
+  }
 }

+ 4 - 2
lib/routes/login.dart

@@ -2,7 +2,7 @@ import 'package:dio/dio.dart';
 import 'package:e2ee_chat/common/global.dart';
 import 'package:e2ee_chat/l10n/localization_intl.dart';
 import 'package:e2ee_chat/models/theme_model.dart';
-import 'package:e2ee_chat/models/user_model.dart';
+import 'package:e2ee_chat/models/login_model.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 import 'package:fluttertoast/fluttertoast.dart';
@@ -81,6 +81,8 @@ class _LoginRouteState extends State<LoginRoute> {
                   child: ElevatedButton(
                     style: ButtonStyle(
                       foregroundColor: MaterialStateProperty.all<Color>(Provider.of<ThemeModel>(context).theme),
+                      //TODO: something wrong with text color
+                      textStyle: MaterialStateProperty.all<TextStyle>(TextStyle(color: Colors.white)),
                     ),
                     onPressed: _onLogin,
                     child: Text(GmLocalizations.of(context).login),
@@ -101,7 +103,7 @@ class _LoginRouteState extends State<LoginRoute> {
       try {
         // user = await Git(context).login(_unameController.text, _pwdController.text);
         // 因为登录页返回后,首页会build,所以我们传false,更新user后不触发更新
-        await UserModel().login(username: _unameController.text, password: _pwdController.text);
+        await LoginModel().loginOrRegister(_unameController.text, _pwdController.text);
         debug('isLogin: ${Global.profile.isLogin}');
       } on DioError catch(e) {
         //登录失败则提示

+ 14 - 0
lib/widgets/friends.dart

@@ -0,0 +1,14 @@
+import 'package:e2ee_chat/widgets/empty.dart';
+import 'package:flutter/cupertino.dart';
+
+class FriendRoute extends StatefulWidget {
+  @override
+  _FriendRouteState createState() => _FriendRouteState();
+}
+
+class _FriendRouteState extends State<FriendRoute> {
+  @override
+  Widget build(BuildContext context) {
+    return EmptyWidget();
+  }
+}

+ 14 - 0
lib/widgets/group.dart

@@ -0,0 +1,14 @@
+import 'package:e2ee_chat/widgets/empty.dart';
+import 'package:flutter/cupertino.dart';
+
+class GroupRoute extends StatefulWidget {
+  @override
+  _GroupRouteState createState() => _GroupRouteState();
+}
+
+class _GroupRouteState extends State<GroupRoute> {
+  @override
+  Widget build(BuildContext context) {
+    return EmptyWidget();
+  }
+}

+ 168 - 0
lib/widgets/message.dart

@@ -0,0 +1,168 @@
+import 'package:e2ee_chat/common/global.dart';
+import 'package:e2ee_chat/models/message_model.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_slidable/flutter_slidable.dart';
+import 'package:provider/provider.dart';
+
+class MessageList extends StatefulWidget {
+  @override
+  _MessageListState createState() => _MessageListState();
+}
+
+class _MessageListState extends State<MessageList> {
+  @override
+  Widget build(BuildContext context) {
+    return ChangeNotifierProvider<MessageListModel>(
+      create: (context) {
+        return MessageListModel();
+      },
+      child: Builder(
+        builder: (context) {
+          var provider = Provider.of<MessageListModel>(context);
+          return ListView.builder(itemCount: provider.count, itemBuilder: (context, index) {
+            var _item = provider.items[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: MessageItem(_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 MessageItem extends StatelessWidget {
+  final MessageItemModel model;
+
+  MessageItem(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.message,
+                          style: TextStyle(fontSize: 28),
+                        )
+                      ]),
+                      overflow: TextOverflow.ellipsis,
+                    ),
+                  ),
+                  (model.unReadCount > 0 && !model.isDisturbing)
+                      ? 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.isDisturbing
+                      ? Row(
+                    children: <Widget>[
+                      Icon(Icons.visibility_off),
+                      model.unReadCount > 0
+                          ? Icon(Icons.chat_bubble, color: Colors.red,) // TODO: 小红点
+                          : Container()
+                    ],
+                  )
+                      : Container()
+                ],
+              )
+            ],
+          ),
+        )
+      ],
+    );
+  }
+
+  String _formatDate() {
+    DateTime dateTime =
+    DateTime.fromMillisecondsSinceEpoch(model.time);
+    return "${dateTime.hour}:${dateTime.minute}";
+  }
+}

+ 7 - 7
lib/widgets/mydrawer.dart

@@ -1,3 +1,5 @@
+import 'package:e2ee_chat/common/global.dart';
+import 'package:e2ee_chat/models/login_model.dart';
 import 'package:flutter/material.dart';
 
 class MyDrawer extends StatelessWidget {
@@ -22,14 +24,11 @@ class MyDrawer extends StatelessWidget {
                   Padding(
                     padding: const EdgeInsets.symmetric(horizontal: 16.0),
                     child: ClipOval(
-                      child: Image.asset(
-                        "imgs/avatar.png",
-                        width: 80,
-                      ),
+                      child: Global.defaultAvatar, //TODO: avatar
                     ),
                   ),
                   Text(
-                    "Wendux",
+                    Global.profile.username!,
                     style: TextStyle(fontWeight: FontWeight.bold),
                   )
                 ],
@@ -39,8 +38,9 @@ class MyDrawer extends StatelessWidget {
               child: ListView(
                 children: <Widget>[
                   ListTile(
-                    leading: const Icon(Icons.add),
-                    title: const Text('Add account'),
+                    leading: const Icon(Icons.logout),
+                    title: const Text('logout'),
+                    onTap: () => LoginModel().logout(),
                   ),
                   ListTile(
                     leading: const Icon(Icons.settings),

+ 15 - 8
pubspec.lock

@@ -42,7 +42,7 @@ packages:
       name: build
       url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.0.2"
+    version: "2.0.3"
   build_config:
     dependency: transitive
     description:
@@ -63,21 +63,21 @@ packages:
       name: build_resolvers
       url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.0.3"
+    version: "2.0.4"
   build_runner:
     dependency: "direct dev"
     description:
       name: build_runner
       url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.0.5"
+    version: "2.0.6"
   build_runner_core:
     dependency: transitive
     description:
       name: build_runner_core
       url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "7.0.0"
+    version: "7.0.1"
   built_collection:
     dependency: transitive
     description:
@@ -91,7 +91,7 @@ packages:
       name: built_value
       url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "8.1.0"
+    version: "8.1.1"
   characters:
     dependency: transitive
     description:
@@ -221,6 +221,13 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "4.2.0"
+  flutter_slidable:
+    dependency: "direct main"
+    description:
+      name: flutter_slidable
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.6.0"
   flutter_test:
     dependency: "direct dev"
     description: flutter
@@ -342,21 +349,21 @@ packages:
       name: objectbox
       url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.1"
   objectbox_flutter_libs:
     dependency: "direct main"
     description:
       name: objectbox_flutter_libs
       url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.1"
   objectbox_generator:
     dependency: "direct dev"
     description:
       name: objectbox_generator
       url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.1"
   package_config:
     dependency: transitive
     description:

+ 3 - 1
pubspec.yaml

@@ -36,6 +36,7 @@ dependencies:
   dio: ^4.0.0
   crypto: ^3.0.1
   permission_handler: ^8.1.2
+  flutter_slidable: ^0.6.0
 
   # The following adds the Cupertino Icons font to your application.
   # Use with the CupertinoIcons class for iOS style icons.
@@ -61,7 +62,8 @@ flutter:
   uses-material-design: true
 
   # To add assets to your application, add an assets section, like this:
-  # assets:
+  assets:
+    - imgs/avatar.jpg
   #   - images/a_dot_burr.jpeg
   #   - images/a_dot_ham.jpeg