mirror of
				https://gitlab.com/famedly/fluffychat.git
				synced 2025-11-04 06:17:26 +01:00 
			
		
		
		
	Merge branch 'krille/multi-account' into 'main'
feat: Allow loading of multiple clients in main.dart Closes #534 See merge request famedly/fluffychat!522
This commit is contained in:
		
						commit
						b7d5bd4274
					
				@ -1561,6 +1561,12 @@
 | 
			
		||||
    "type": "text",
 | 
			
		||||
    "placeholders": {}
 | 
			
		||||
  },
 | 
			
		||||
  "addAccount": "Add account",
 | 
			
		||||
  "editBundlesForAccount": "Edit bundles for this account",
 | 
			
		||||
  "addToBundle": "Add to bundle",
 | 
			
		||||
  "removeFromBundle": "Remove from this bundle",
 | 
			
		||||
  "bundleName": "Bundle name",
 | 
			
		||||
  "enableMultiAccounts": "Enable multi accounts on this device",
 | 
			
		||||
  "openInMaps": "Open in maps",
 | 
			
		||||
  "@openInMaps": {
 | 
			
		||||
    "type": "text",
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,6 @@
 | 
			
		||||
  <key>CFBundleVersion</key>
 | 
			
		||||
  <string>1.0</string>
 | 
			
		||||
  <key>MinimumOSVersion</key>
 | 
			
		||||
  <string>8.0</string>
 | 
			
		||||
  <string>9.0</string>
 | 
			
		||||
</dict>
 | 
			
		||||
</plist>
 | 
			
		||||
 | 
			
		||||
@ -217,12 +217,12 @@ class AppRoutes {
 | 
			
		||||
          buildTransition: _fadeTransition,
 | 
			
		||||
          stackedRoutes: [
 | 
			
		||||
            VWidget(
 | 
			
		||||
              path: '/login',
 | 
			
		||||
              path: 'login',
 | 
			
		||||
              widget: Login(),
 | 
			
		||||
              buildTransition: _fadeTransition,
 | 
			
		||||
            ),
 | 
			
		||||
            VWidget(
 | 
			
		||||
              path: '/signup',
 | 
			
		||||
              path: 'signup',
 | 
			
		||||
              widget: SignupPage(),
 | 
			
		||||
              buildTransition: _fadeTransition,
 | 
			
		||||
            ),
 | 
			
		||||
@ -296,6 +296,23 @@ class AppRoutes {
 | 
			
		||||
              widget: DevicesSettings(),
 | 
			
		||||
              buildTransition: _dynamicTransition,
 | 
			
		||||
            ),
 | 
			
		||||
            VWidget(
 | 
			
		||||
              path: 'add',
 | 
			
		||||
              widget: HomeserverPicker(),
 | 
			
		||||
              buildTransition: _fadeTransition,
 | 
			
		||||
              stackedRoutes: [
 | 
			
		||||
                VWidget(
 | 
			
		||||
                  path: 'login',
 | 
			
		||||
                  widget: Login(),
 | 
			
		||||
                  buildTransition: _fadeTransition,
 | 
			
		||||
                ),
 | 
			
		||||
                VWidget(
 | 
			
		||||
                  path: 'signup',
 | 
			
		||||
                  widget: SignupPage(),
 | 
			
		||||
                  buildTransition: _fadeTransition,
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        VWidget(
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,8 @@ import 'app_config.dart';
 | 
			
		||||
 | 
			
		||||
abstract class FluffyThemes {
 | 
			
		||||
  static const double columnWidth = 360.0;
 | 
			
		||||
  static bool isColumnMode(BuildContext context) =>
 | 
			
		||||
      MediaQuery.of(context).size.width > columnWidth * 2;
 | 
			
		||||
 | 
			
		||||
  static const fallbackTextStyle =
 | 
			
		||||
      TextStyle(fontFamily: 'NotoSans', fontFamilyFallback: ['NotoEmoji']);
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:adaptive_theme/adaptive_theme.dart';
 | 
			
		||||
import 'package:matrix/encryption/utils/key_verification.dart';
 | 
			
		||||
import 'package:fluffychat/utils/client_manager.dart';
 | 
			
		||||
import 'package:matrix/matrix.dart';
 | 
			
		||||
import 'package:fluffychat/config/routes.dart';
 | 
			
		||||
import 'package:fluffychat/utils/platform_infos.dart';
 | 
			
		||||
@ -18,8 +18,6 @@ import 'package:future_loading_dialog/future_loading_dialog.dart';
 | 
			
		||||
import 'package:universal_html/html.dart' as html;
 | 
			
		||||
import 'package:vrouter/vrouter.dart';
 | 
			
		||||
 | 
			
		||||
import 'utils/matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart';
 | 
			
		||||
import 'widgets/layouts/wait_for_login.dart';
 | 
			
		||||
import 'widgets/lock_screen.dart';
 | 
			
		||||
import 'widgets/matrix.dart';
 | 
			
		||||
import 'config/themes.dart';
 | 
			
		||||
@ -35,27 +33,10 @@ void main() async {
 | 
			
		||||
  FlutterError.onError = (FlutterErrorDetails details) =>
 | 
			
		||||
      Zone.current.handleUncaughtError(details.exception, details.stack);
 | 
			
		||||
 | 
			
		||||
  final client = Client(
 | 
			
		||||
    PlatformInfos.clientName,
 | 
			
		||||
    enableE2eeRecovery: true,
 | 
			
		||||
    verificationMethods: {
 | 
			
		||||
      KeyVerificationMethod.numbers,
 | 
			
		||||
      if (PlatformInfos.isMobile || PlatformInfos.isLinux)
 | 
			
		||||
        KeyVerificationMethod.emoji,
 | 
			
		||||
    },
 | 
			
		||||
    importantStateEvents: <String>{
 | 
			
		||||
      'im.ponies.room_emotes', // we want emotes to work properly
 | 
			
		||||
    },
 | 
			
		||||
    databaseBuilder: FlutterMatrixHiveStore.hiveDatabaseBuilder,
 | 
			
		||||
    supportedLoginTypes: {
 | 
			
		||||
      AuthenticationTypes.password,
 | 
			
		||||
      if (PlatformInfos.isMobile || PlatformInfos.isWeb) AuthenticationTypes.sso
 | 
			
		||||
    },
 | 
			
		||||
    compute: compute,
 | 
			
		||||
  );
 | 
			
		||||
  final clients = await ClientManager.getClients();
 | 
			
		||||
 | 
			
		||||
  if (PlatformInfos.isMobile) {
 | 
			
		||||
    BackgroundPush.clientOnly(client);
 | 
			
		||||
    BackgroundPush.clientOnly(clients.first);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  final queryParameters = <String, String>{};
 | 
			
		||||
@ -68,24 +49,24 @@ void main() async {
 | 
			
		||||
    () => runApp(PlatformInfos.isMobile
 | 
			
		||||
        ? AppLock(
 | 
			
		||||
            builder: (args) => FluffyChatApp(
 | 
			
		||||
              client: client,
 | 
			
		||||
              clients: clients,
 | 
			
		||||
              queryParameters: queryParameters,
 | 
			
		||||
            ),
 | 
			
		||||
            lockScreen: LockScreen(),
 | 
			
		||||
            enabled: false,
 | 
			
		||||
          )
 | 
			
		||||
        : FluffyChatApp(client: client, queryParameters: queryParameters)),
 | 
			
		||||
        : FluffyChatApp(clients: clients, queryParameters: queryParameters)),
 | 
			
		||||
    SentryController.captureException,
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class FluffyChatApp extends StatefulWidget {
 | 
			
		||||
  final Widget testWidget;
 | 
			
		||||
  final Client client;
 | 
			
		||||
  final List<Client> clients;
 | 
			
		||||
  final Map<String, String> queryParameters;
 | 
			
		||||
 | 
			
		||||
  const FluffyChatApp(
 | 
			
		||||
      {Key key, this.testWidget, @required this.client, this.queryParameters})
 | 
			
		||||
      {Key key, this.testWidget, @required this.clients, this.queryParameters})
 | 
			
		||||
      : super(key: key);
 | 
			
		||||
 | 
			
		||||
  /// getInitialLink may rereturn the value multiple times if this view is
 | 
			
		||||
@ -101,7 +82,15 @@ class _FluffyChatAppState extends State<FluffyChatApp> {
 | 
			
		||||
  final GlobalKey<MatrixState> _matrix = GlobalKey<MatrixState>();
 | 
			
		||||
  GlobalKey<VRouterState> _router;
 | 
			
		||||
  bool columnMode;
 | 
			
		||||
  String _initialUrl = '/';
 | 
			
		||||
  String _initialUrl;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _initialUrl =
 | 
			
		||||
        widget.clients.any((client) => client.isLogged()) ? '/rooms' : '/home';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AdaptiveTheme(
 | 
			
		||||
@ -159,8 +148,8 @@ class _FluffyChatAppState extends State<FluffyChatApp> {
 | 
			
		||||
                key: _matrix,
 | 
			
		||||
                context: context,
 | 
			
		||||
                router: _router,
 | 
			
		||||
                client: widget.client,
 | 
			
		||||
                child: WaitForInitPage(child),
 | 
			
		||||
                clients: widget.clients,
 | 
			
		||||
                child: child,
 | 
			
		||||
              );
 | 
			
		||||
            },
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,7 @@ import 'send_location_dialog.dart';
 | 
			
		||||
import 'sticker_picker_dialog.dart';
 | 
			
		||||
import '../utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart';
 | 
			
		||||
import '../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
 | 
			
		||||
import '../utils/account_bundles.dart';
 | 
			
		||||
 | 
			
		||||
class Chat extends StatefulWidget {
 | 
			
		||||
  final Widget sideView;
 | 
			
		||||
@ -45,6 +46,8 @@ class Chat extends StatefulWidget {
 | 
			
		||||
class ChatController extends State<Chat> {
 | 
			
		||||
  Room room;
 | 
			
		||||
 | 
			
		||||
  Client sendingClient;
 | 
			
		||||
 | 
			
		||||
  Timeline timeline;
 | 
			
		||||
 | 
			
		||||
  MatrixState matrix;
 | 
			
		||||
@ -222,6 +225,14 @@ class ChatController extends State<Chat> {
 | 
			
		||||
 | 
			
		||||
  TextEditingController sendController = TextEditingController();
 | 
			
		||||
 | 
			
		||||
  void setSendingClient(Client c) => setState(() {
 | 
			
		||||
        sendingClient = c;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
  void setActiveClient(Client c) => setState(() {
 | 
			
		||||
        Matrix.of(context).setActiveClient(c);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
  Future<void> send() async {
 | 
			
		||||
    if (sendController.text.trim().isEmpty) return;
 | 
			
		||||
    var parseCommands = true;
 | 
			
		||||
@ -447,19 +458,51 @@ class ChatController extends State<Chat> {
 | 
			
		||||
    for (final event in selectedEvents) {
 | 
			
		||||
      await showFutureLoadingDialog(
 | 
			
		||||
          context: context,
 | 
			
		||||
          future: () =>
 | 
			
		||||
              event.status > 0 ? event.redactEvent() : event.remove());
 | 
			
		||||
          future: () async {
 | 
			
		||||
            if (event.status > 0) {
 | 
			
		||||
              if (event.canRedact) {
 | 
			
		||||
                await event.redactEvent();
 | 
			
		||||
              } else {
 | 
			
		||||
                final client = currentRoomBundle.firstWhere(
 | 
			
		||||
                    (cl) => selectedEvents.first.senderId == cl.userID,
 | 
			
		||||
                    orElse: () => null);
 | 
			
		||||
                if (client == null) {
 | 
			
		||||
                  return;
 | 
			
		||||
                }
 | 
			
		||||
                final room = client.getRoomById(roomId);
 | 
			
		||||
                await Event.fromJson(event.toJson(), room).redactEvent();
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              await event.remove();
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
    }
 | 
			
		||||
    setState(() => selectedEvents.clear());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  List<Client> get currentRoomBundle {
 | 
			
		||||
    final clients = matrix.currentBundle;
 | 
			
		||||
    clients.removeWhere((c) => c.getRoomById(roomId) == null);
 | 
			
		||||
    return clients;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool get canRedactSelectedEvents {
 | 
			
		||||
    final clients = matrix.currentBundle;
 | 
			
		||||
    for (final event in selectedEvents) {
 | 
			
		||||
      if (event.canRedact == false) return false;
 | 
			
		||||
      if (event.canRedact == false &&
 | 
			
		||||
          !(clients.any((cl) => event.senderId == cl.userID))) return false;
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool get canEditSelectedEvents {
 | 
			
		||||
    if (selectedEvents.length != 1 || selectedEvents.first.status < 1) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    return currentRoomBundle
 | 
			
		||||
        .any((cl) => selectedEvents.first.senderId == cl.userID);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void forwardEventsAction() async {
 | 
			
		||||
    if (selectedEvents.length == 1) {
 | 
			
		||||
      Matrix.of(context).shareContent = selectedEvents.first.content;
 | 
			
		||||
@ -584,6 +627,13 @@ class ChatController extends State<Chat> {
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
  void editSelectedEventAction() {
 | 
			
		||||
    final client = currentRoomBundle.firstWhere(
 | 
			
		||||
        (cl) => selectedEvents.first.senderId == cl.userID,
 | 
			
		||||
        orElse: () => null);
 | 
			
		||||
    if (client == null) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    setSendingClient(client);
 | 
			
		||||
    setState(() {
 | 
			
		||||
      pendingText = sendController.text;
 | 
			
		||||
      editEvent = selectedEvents.first;
 | 
			
		||||
@ -689,6 +739,19 @@ class ChatController extends State<Chat> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void onInputBarChanged(String text) {
 | 
			
		||||
    final clients = currentRoomBundle;
 | 
			
		||||
    for (final client in clients) {
 | 
			
		||||
      final prefix = client.sendPrefix;
 | 
			
		||||
      if ((prefix?.isNotEmpty ?? false) &&
 | 
			
		||||
          text.toLowerCase() == '${prefix.toLowerCase()} ') {
 | 
			
		||||
        setSendingClient(client);
 | 
			
		||||
        setState(() {
 | 
			
		||||
          inputText = '';
 | 
			
		||||
          sendController.text = '';
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    typingCoolDown?.cancel();
 | 
			
		||||
    typingCoolDown = Timer(Duration(seconds: 2), () {
 | 
			
		||||
      typingCoolDown = null;
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ import 'package:uni_links/uni_links.dart';
 | 
			
		||||
import 'package:vrouter/vrouter.dart';
 | 
			
		||||
import '../main.dart';
 | 
			
		||||
import '../widgets/matrix.dart';
 | 
			
		||||
import '../../utils/account_bundles.dart';
 | 
			
		||||
import '../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
 | 
			
		||||
import '../utils/url_launcher.dart';
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
@ -409,6 +410,93 @@ class ChatListController extends State<ChatList> {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setActiveClient(Client client) {
 | 
			
		||||
    VRouter.of(context).to('/rooms');
 | 
			
		||||
    setState(() {
 | 
			
		||||
      _activeSpaceId = null;
 | 
			
		||||
      selectedRoomIds.clear();
 | 
			
		||||
      Matrix.of(context).setActiveClient(client);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setActiveBundle(String bundle) => setState(() {
 | 
			
		||||
        _activeSpaceId = null;
 | 
			
		||||
        selectedRoomIds.clear();
 | 
			
		||||
        Matrix.of(context).activeBundle = bundle;
 | 
			
		||||
        if (!Matrix.of(context)
 | 
			
		||||
            .currentBundle
 | 
			
		||||
            .any((client) => client == Matrix.of(context).client)) {
 | 
			
		||||
          Matrix.of(context)
 | 
			
		||||
              .setActiveClient(Matrix.of(context).currentBundle.first);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
  void editBundlesForAccount(String userId) async {
 | 
			
		||||
    final client = Matrix.of(context)
 | 
			
		||||
        .widget
 | 
			
		||||
        .clients[Matrix.of(context).getClientIndexByMatrixId(userId)];
 | 
			
		||||
    final action = await showConfirmationDialog<EditBundleAction>(
 | 
			
		||||
      context: context,
 | 
			
		||||
      title: L10n.of(context).editBundlesForAccount,
 | 
			
		||||
      actions: [
 | 
			
		||||
        AlertDialogAction(
 | 
			
		||||
          key: EditBundleAction.addToBundle,
 | 
			
		||||
          label: L10n.of(context).addToBundle,
 | 
			
		||||
        ),
 | 
			
		||||
        if (Matrix.of(context).activeBundle != null)
 | 
			
		||||
          AlertDialogAction(
 | 
			
		||||
            key: EditBundleAction.removeFromBundle,
 | 
			
		||||
            label: L10n.of(context).removeFromBundle,
 | 
			
		||||
          ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
    if (action == null) return;
 | 
			
		||||
    switch (action) {
 | 
			
		||||
      case EditBundleAction.addToBundle:
 | 
			
		||||
        final bundle = await showTextInputDialog(
 | 
			
		||||
            context: context,
 | 
			
		||||
            title: L10n.of(context).bundleName,
 | 
			
		||||
            textFields: [
 | 
			
		||||
              DialogTextField(hintText: L10n.of(context).bundleName)
 | 
			
		||||
            ]);
 | 
			
		||||
        if (bundle.isEmpty && bundle.single.isEmpty) return;
 | 
			
		||||
        await showFutureLoadingDialog(
 | 
			
		||||
          context: context,
 | 
			
		||||
          future: () => client.setAccountBundle(bundle.single),
 | 
			
		||||
        );
 | 
			
		||||
        break;
 | 
			
		||||
      case EditBundleAction.removeFromBundle:
 | 
			
		||||
        await showFutureLoadingDialog(
 | 
			
		||||
          context: context,
 | 
			
		||||
          future: () =>
 | 
			
		||||
              client.removeFromAccountBundle(Matrix.of(context).activeBundle),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool get displayBundles =>
 | 
			
		||||
      Matrix.of(context).hasComplexBundles &&
 | 
			
		||||
      Matrix.of(context).accountBundles.keys.length > 1;
 | 
			
		||||
 | 
			
		||||
  String get secureActiveBundle {
 | 
			
		||||
    if (Matrix.of(context).activeBundle == null ||
 | 
			
		||||
        !Matrix.of(context)
 | 
			
		||||
            .accountBundles
 | 
			
		||||
            .keys
 | 
			
		||||
            .contains(Matrix.of(context).activeBundle)) {
 | 
			
		||||
      return Matrix.of(context).accountBundles.keys.first;
 | 
			
		||||
    }
 | 
			
		||||
    return Matrix.of(context).activeBundle;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void resetActiveBundle() {
 | 
			
		||||
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
 | 
			
		||||
      setState(() {
 | 
			
		||||
        Matrix.of(context).activeBundle = null;
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    Matrix.of(context).navigatorContext = context;
 | 
			
		||||
@ -424,3 +512,5 @@ class ChatListController extends State<ChatList> {
 | 
			
		||||
    return ChatListView(this);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum EditBundleAction { addToBundle, removeFromBundle }
 | 
			
		||||
 | 
			
		||||
@ -47,13 +47,13 @@ class HomeserverPickerController extends State<HomeserverPicker> {
 | 
			
		||||
    showFutureLoadingDialog(
 | 
			
		||||
      context: context,
 | 
			
		||||
      future: () async {
 | 
			
		||||
        if (Matrix.of(context).client.homeserver == null) {
 | 
			
		||||
          await Matrix.of(context).client.checkHomeserver(
 | 
			
		||||
        if (Matrix.of(context).getLoginClient().homeserver == null) {
 | 
			
		||||
          await Matrix.of(context).getLoginClient().checkHomeserver(
 | 
			
		||||
                await Store()
 | 
			
		||||
                    .getItem(HomeserverPickerController.ssoHomeserverKey),
 | 
			
		||||
              );
 | 
			
		||||
        }
 | 
			
		||||
        await Matrix.of(context).client.login(
 | 
			
		||||
        await Matrix.of(context).getLoginClient().login(
 | 
			
		||||
              LoginType.mLoginToken,
 | 
			
		||||
              token: token,
 | 
			
		||||
              initialDeviceDisplayName: PlatformInfos.clientName,
 | 
			
		||||
@ -117,7 +117,7 @@ class HomeserverPickerController extends State<HomeserverPicker> {
 | 
			
		||||
        isLoading = true;
 | 
			
		||||
      });
 | 
			
		||||
      final wellKnown =
 | 
			
		||||
          await Matrix.of(context).client.checkHomeserver(homeserver);
 | 
			
		||||
          await Matrix.of(context).getLoginClient().checkHomeserver(homeserver);
 | 
			
		||||
 | 
			
		||||
      var jitsi = wellKnown?.additionalProperties
 | 
			
		||||
          ?.tryGet<Map<String, dynamic>>('im.vector.riot.jitsi')
 | 
			
		||||
@ -177,13 +177,13 @@ class HomeserverPickerController extends State<HomeserverPicker> {
 | 
			
		||||
          .any((flow) => flow['type'] == AuthenticationTypes.sso);
 | 
			
		||||
 | 
			
		||||
  Future<Map<String, dynamic>> getLoginTypes() async {
 | 
			
		||||
    _rawLoginTypes ??= await Matrix.of(context).client.request(
 | 
			
		||||
    _rawLoginTypes ??= await Matrix.of(context).getLoginClient().request(
 | 
			
		||||
          RequestType.GET,
 | 
			
		||||
          '/client/r0/login',
 | 
			
		||||
        );
 | 
			
		||||
    if (registrationSupported == null) {
 | 
			
		||||
      try {
 | 
			
		||||
        await Matrix.of(context).client.register();
 | 
			
		||||
        await Matrix.of(context).getLoginClient().register();
 | 
			
		||||
        registrationSupported = true;
 | 
			
		||||
      } on MatrixException catch (e) {
 | 
			
		||||
        registrationSupported = e.requireAdditionalAuthentication ?? false;
 | 
			
		||||
@ -200,14 +200,14 @@ class HomeserverPickerController extends State<HomeserverPicker> {
 | 
			
		||||
    if (kIsWeb) {
 | 
			
		||||
      // We store the homserver in the local storage instead of a redirect
 | 
			
		||||
      // parameter because of possible CSRF attacks.
 | 
			
		||||
      Store().setItem(
 | 
			
		||||
          ssoHomeserverKey, Matrix.of(context).client.homeserver.toString());
 | 
			
		||||
      Store().setItem(ssoHomeserverKey,
 | 
			
		||||
          Matrix.of(context).getLoginClient().homeserver.toString());
 | 
			
		||||
    }
 | 
			
		||||
    final redirectUrl = kIsWeb
 | 
			
		||||
        ? AppConfig.webBaseUrl + '/#/'
 | 
			
		||||
        : AppConfig.appOpenUrlScheme.toLowerCase() + '://login';
 | 
			
		||||
    final url =
 | 
			
		||||
        '${Matrix.of(context).client.homeserver?.toString()}/_matrix/client/r0/login/sso/redirect/${Uri.encodeComponent(id)}?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}';
 | 
			
		||||
        '${Matrix.of(context).getLoginClient().homeserver?.toString()}/_matrix/client/r0/login/sso/redirect/${Uri.encodeComponent(id)}?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}';
 | 
			
		||||
    if (PlatformInfos.isMobile) {
 | 
			
		||||
      browser ??= ChromeSafariBrowser();
 | 
			
		||||
      browser.open(url: Uri.parse(url));
 | 
			
		||||
@ -216,7 +216,7 @@ class HomeserverPickerController extends State<HomeserverPicker> {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void signUpAction() => VRouter.of(context).to('/signup');
 | 
			
		||||
  void signUpAction() => VRouter.of(context).to('signup');
 | 
			
		||||
 | 
			
		||||
  bool _initialized = false;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,7 @@ class LoginController extends State<Login> {
 | 
			
		||||
      } else {
 | 
			
		||||
        identifier = AuthenticationUserIdentifier(user: username);
 | 
			
		||||
      }
 | 
			
		||||
      await matrix.client.login(LoginType.mLoginPassword,
 | 
			
		||||
      await matrix.getLoginClient().login(LoginType.mLoginPassword,
 | 
			
		||||
          identifier: identifier,
 | 
			
		||||
          // To stay compatible with older server versions
 | 
			
		||||
          // ignore: deprecated_member_use
 | 
			
		||||
@ -98,12 +98,13 @@ class LoginController extends State<Login> {
 | 
			
		||||
    setState(() => usernameError = null);
 | 
			
		||||
    if (!userId.isValidMatrixId) return;
 | 
			
		||||
    try {
 | 
			
		||||
      final oldHomeserver = Matrix.of(context).client.homeserver;
 | 
			
		||||
      final oldHomeserver = Matrix.of(context).getLoginClient().homeserver;
 | 
			
		||||
      var newDomain = Uri.https(userId.domain, '');
 | 
			
		||||
      Matrix.of(context).client.homeserver = newDomain;
 | 
			
		||||
      Matrix.of(context).getLoginClient().homeserver = newDomain;
 | 
			
		||||
      DiscoveryInformation wellKnownInformation;
 | 
			
		||||
      try {
 | 
			
		||||
        wellKnownInformation = await Matrix.of(context).client.getWellknown();
 | 
			
		||||
        wellKnownInformation =
 | 
			
		||||
            await Matrix.of(context).getLoginClient().getWellknown();
 | 
			
		||||
        if (wellKnownInformation.mHomeserver?.baseUrl?.toString()?.isNotEmpty ??
 | 
			
		||||
            false) {
 | 
			
		||||
          newDomain = wellKnownInformation.mHomeserver.baseUrl;
 | 
			
		||||
@ -120,8 +121,8 @@ class LoginController extends State<Login> {
 | 
			
		||||
              .checkHomeserver(newDomain)
 | 
			
		||||
              .catchError((e) => null),
 | 
			
		||||
        );
 | 
			
		||||
        if (Matrix.of(context).client.homeserver == null) {
 | 
			
		||||
          Matrix.of(context).client.homeserver = oldHomeserver;
 | 
			
		||||
        if (Matrix.of(context).getLoginClient().homeserver == null) {
 | 
			
		||||
          Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
 | 
			
		||||
          // okay, the server we checked does not appear to be a matrix server
 | 
			
		||||
          Logs().v(
 | 
			
		||||
              '$newDomain is not running a homeserver, asking to use $oldHomeserver');
 | 
			
		||||
@ -178,11 +179,12 @@ class LoginController extends State<Login> {
 | 
			
		||||
        Matrix.of(context).client.generateUniqueTransactionId();
 | 
			
		||||
    final response = await showFutureLoadingDialog(
 | 
			
		||||
      context: context,
 | 
			
		||||
      future: () => Matrix.of(context).client.requestTokenToResetPasswordEmail(
 | 
			
		||||
            clientSecret,
 | 
			
		||||
            input.single,
 | 
			
		||||
            sendAttempt++,
 | 
			
		||||
          ),
 | 
			
		||||
      future: () =>
 | 
			
		||||
          Matrix.of(context).getLoginClient().requestTokenToResetPasswordEmail(
 | 
			
		||||
                clientSecret,
 | 
			
		||||
                input.single,
 | 
			
		||||
                sendAttempt++,
 | 
			
		||||
              ),
 | 
			
		||||
    );
 | 
			
		||||
    if (response.error != null) return;
 | 
			
		||||
    final ok = await showOkAlertDialog(
 | 
			
		||||
@ -211,7 +213,7 @@ class LoginController extends State<Login> {
 | 
			
		||||
    if (password == null) return;
 | 
			
		||||
    final success = await showFutureLoadingDialog(
 | 
			
		||||
      context: context,
 | 
			
		||||
      future: () => Matrix.of(context).client.changePassword(
 | 
			
		||||
      future: () => Matrix.of(context).getLoginClient().changePassword(
 | 
			
		||||
            password.single,
 | 
			
		||||
            auth: AuthenticationThreePidCreds(
 | 
			
		||||
              type: AuthenticationTypes.emailIdentity,
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
 | 
			
		||||
import 'package:matrix/matrix.dart';
 | 
			
		||||
import 'package:vrouter/vrouter.dart';
 | 
			
		||||
 | 
			
		||||
class SettingsAccount extends StatefulWidget {
 | 
			
		||||
  const SettingsAccount({Key key}) : super(key: key);
 | 
			
		||||
@ -144,6 +145,8 @@ class SettingsAccountController extends State<SettingsAccount> {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void addAccountAction() => VRouter.of(context).to('add');
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final client = Matrix.of(context).client;
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,7 @@ class SignupPageController extends State<SignupPage> {
 | 
			
		||||
    setState(() => loading = true);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final client = Matrix.of(context).client;
 | 
			
		||||
      final client = Matrix.of(context).getLoginClient();
 | 
			
		||||
      await client.uiaRequestBackground(
 | 
			
		||||
        (auth) => client.register(
 | 
			
		||||
          username: usernameController.text,
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,9 @@
 | 
			
		||||
import 'dart:math';
 | 
			
		||||
import 'package:async/async.dart';
 | 
			
		||||
import 'package:fluffychat/config/themes.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/widgets/avatar.dart';
 | 
			
		||||
import 'package:flutter/widgets.dart';
 | 
			
		||||
import 'package:matrix/matrix.dart';
 | 
			
		||||
import 'package:fluffychat/pages/chat_list.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/connection_status_header.dart';
 | 
			
		||||
@ -9,6 +14,7 @@ import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:vrouter/vrouter.dart';
 | 
			
		||||
import '../../widgets/matrix.dart';
 | 
			
		||||
import '../../utils/account_bundles.dart';
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
import '../../utils/stream_extension.dart';
 | 
			
		||||
 | 
			
		||||
@ -17,6 +23,59 @@ class ChatListView extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  const ChatListView(this.controller, {Key key}) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  List<BottomNavigationBarItem> getBottomBarItems(BuildContext context) {
 | 
			
		||||
    final displayClients = Matrix.of(context).currentBundle;
 | 
			
		||||
    if (displayClients.isEmpty) {
 | 
			
		||||
      displayClients.addAll(Matrix.of(context).widget.clients);
 | 
			
		||||
      controller.resetActiveBundle();
 | 
			
		||||
    }
 | 
			
		||||
    final items = displayClients.map((client) {
 | 
			
		||||
      return BottomNavigationBarItem(
 | 
			
		||||
        label: client.userID,
 | 
			
		||||
        icon: FutureBuilder<Profile>(
 | 
			
		||||
            future: client.ownProfile,
 | 
			
		||||
            builder: (context, snapshot) {
 | 
			
		||||
              return InkWell(
 | 
			
		||||
                borderRadius: BorderRadius.circular(32),
 | 
			
		||||
                onTap: () => controller.setActiveClient(client),
 | 
			
		||||
                onLongPress: () =>
 | 
			
		||||
                    controller.editBundlesForAccount(client.userID),
 | 
			
		||||
                child: Avatar(
 | 
			
		||||
                  snapshot.data?.avatarUrl,
 | 
			
		||||
                  snapshot.data?.displayName ?? client.userID.localpart,
 | 
			
		||||
                  size: 32,
 | 
			
		||||
                ),
 | 
			
		||||
              );
 | 
			
		||||
            }),
 | 
			
		||||
      );
 | 
			
		||||
    }).toList();
 | 
			
		||||
 | 
			
		||||
    if (controller.displayBundles && false) {
 | 
			
		||||
      items.insert(
 | 
			
		||||
          0,
 | 
			
		||||
          BottomNavigationBarItem(
 | 
			
		||||
              label: 'Bundles',
 | 
			
		||||
              icon: PopupMenuButton(
 | 
			
		||||
                icon: Icon(
 | 
			
		||||
                  Icons.menu,
 | 
			
		||||
                  color: Theme.of(context).textTheme.bodyText1.color,
 | 
			
		||||
                ),
 | 
			
		||||
                onSelected: controller.setActiveBundle,
 | 
			
		||||
                itemBuilder: (context) => Matrix.of(context)
 | 
			
		||||
                    .accountBundles
 | 
			
		||||
                    .keys
 | 
			
		||||
                    .map(
 | 
			
		||||
                      (bundle) => PopupMenuItem(
 | 
			
		||||
                        value: bundle,
 | 
			
		||||
                        child: Text(bundle),
 | 
			
		||||
                      ),
 | 
			
		||||
                    )
 | 
			
		||||
                    .toList(),
 | 
			
		||||
              )));
 | 
			
		||||
    }
 | 
			
		||||
    return items;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return StreamBuilder<Object>(
 | 
			
		||||
@ -216,12 +275,98 @@ class ChatListView extends StatelessWidget {
 | 
			
		||||
                          child: Icon(CupertinoIcons.chat_bubble),
 | 
			
		||||
                        )
 | 
			
		||||
                  : null,
 | 
			
		||||
              bottomNavigationBar: Matrix.of(context).isMultiAccount
 | 
			
		||||
                  ? StreamBuilder(
 | 
			
		||||
                      stream: StreamGroup.merge(Matrix.of(context)
 | 
			
		||||
                          .widget
 | 
			
		||||
                          .clients
 | 
			
		||||
                          .map((client) => client.onSync.stream.where((s) =>
 | 
			
		||||
                              s.accountData != null &&
 | 
			
		||||
                              s.accountData
 | 
			
		||||
                                  .any((e) => e.type == accountBundlesType)))),
 | 
			
		||||
                      builder: (context, _) => Material(
 | 
			
		||||
                        color: Theme.of(context)
 | 
			
		||||
                            .bottomNavigationBarTheme
 | 
			
		||||
                            .backgroundColor,
 | 
			
		||||
                        child: Column(
 | 
			
		||||
                          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                          children: [
 | 
			
		||||
                            Divider(height: 1),
 | 
			
		||||
                            Builder(builder: (context) {
 | 
			
		||||
                              final items = getBottomBarItems(context);
 | 
			
		||||
                              if (items.length == 1) {
 | 
			
		||||
                                return Padding(
 | 
			
		||||
                                  padding: const EdgeInsets.all(7.0),
 | 
			
		||||
                                  child: Column(
 | 
			
		||||
                                    mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                                    children: [
 | 
			
		||||
                                      items.single.icon,
 | 
			
		||||
                                      Text(items.single.label),
 | 
			
		||||
                                    ],
 | 
			
		||||
                                  ),
 | 
			
		||||
                                );
 | 
			
		||||
                              }
 | 
			
		||||
                              return SingleChildScrollView(
 | 
			
		||||
                                scrollDirection: Axis.horizontal,
 | 
			
		||||
                                child: SizedBox(
 | 
			
		||||
                                  width: max(
 | 
			
		||||
                                    FluffyThemes.isColumnMode(context)
 | 
			
		||||
                                        ? FluffyThemes.columnWidth
 | 
			
		||||
                                        : MediaQuery.of(context).size.width,
 | 
			
		||||
                                    Matrix.of(context).widget.clients.length *
 | 
			
		||||
                                        84.0,
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  child: BottomNavigationBar(
 | 
			
		||||
                                    elevation: 0,
 | 
			
		||||
                                    onTap: (i) => controller.setActiveClient(
 | 
			
		||||
                                        Matrix.of(context).currentBundle[i]),
 | 
			
		||||
                                    currentIndex: Matrix.of(context)
 | 
			
		||||
                                        .currentBundle
 | 
			
		||||
                                        .indexWhere(
 | 
			
		||||
                                          (client) =>
 | 
			
		||||
                                              client ==
 | 
			
		||||
                                              Matrix.of(context).client,
 | 
			
		||||
                                        ),
 | 
			
		||||
                                    showUnselectedLabels: false,
 | 
			
		||||
                                    showSelectedLabels: true,
 | 
			
		||||
                                    type: BottomNavigationBarType.shifting,
 | 
			
		||||
                                    selectedItemColor:
 | 
			
		||||
                                        Theme.of(context).primaryColor,
 | 
			
		||||
                                    items: items,
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ),
 | 
			
		||||
                              );
 | 
			
		||||
                            }),
 | 
			
		||||
                            if (controller.displayBundles)
 | 
			
		||||
                              Padding(
 | 
			
		||||
                                padding: const EdgeInsets.symmetric(
 | 
			
		||||
                                  vertical: 4.0,
 | 
			
		||||
                                  horizontal: 12,
 | 
			
		||||
                                ),
 | 
			
		||||
                                child: SizedBox(
 | 
			
		||||
                                  width: double.infinity,
 | 
			
		||||
                                  child: CupertinoSlidingSegmentedControl(
 | 
			
		||||
                                    groupValue: controller.secureActiveBundle,
 | 
			
		||||
                                    onValueChanged: controller.setActiveBundle,
 | 
			
		||||
                                    children: Map.fromEntries(Matrix.of(context)
 | 
			
		||||
                                        .accountBundles
 | 
			
		||||
                                        .keys
 | 
			
		||||
                                        .map((bundle) =>
 | 
			
		||||
                                            MapEntry(bundle, Text(bundle)))),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    )
 | 
			
		||||
                  : null,
 | 
			
		||||
              drawer: controller.spaces.isEmpty
 | 
			
		||||
                  ? null
 | 
			
		||||
                  : Drawer(
 | 
			
		||||
                      child: SafeArea(
 | 
			
		||||
                        child: ListView.builder(
 | 
			
		||||
                          itemCount: controller.spaces.length + 1,
 | 
			
		||||
                          itemCount: controller.spaces.length,
 | 
			
		||||
                          itemBuilder: (context, i) {
 | 
			
		||||
                            if (i == 0) {
 | 
			
		||||
                              return ListTile(
 | 
			
		||||
 | 
			
		||||
@ -37,9 +37,10 @@ class ChatView extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    controller.matrix = Matrix.of(context);
 | 
			
		||||
    controller.matrix ??= Matrix.of(context);
 | 
			
		||||
    final client = controller.matrix.client;
 | 
			
		||||
    controller.room ??= client.getRoomById(controller.roomId);
 | 
			
		||||
    controller.sendingClient ??= client;
 | 
			
		||||
    controller.room = controller.sendingClient.getRoomById(controller.roomId);
 | 
			
		||||
    if (controller.room == null) {
 | 
			
		||||
      return Scaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
@ -147,10 +148,7 @@ class ChatView extends StatelessWidget {
 | 
			
		||||
                : Text(controller.selectedEvents.length.toString()),
 | 
			
		||||
            actions: controller.selectMode
 | 
			
		||||
                ? <Widget>[
 | 
			
		||||
                    if (controller.selectedEvents.length == 1 &&
 | 
			
		||||
                        controller.selectedEvents.first.status > 0 &&
 | 
			
		||||
                        controller.selectedEvents.first.senderId ==
 | 
			
		||||
                            client.userID)
 | 
			
		||||
                    if (controller.canEditSelectedEvents)
 | 
			
		||||
                      IconButton(
 | 
			
		||||
                        icon: Icon(Icons.edit_outlined),
 | 
			
		||||
                        tooltip: L10n.of(context).edit,
 | 
			
		||||
@ -680,6 +678,14 @@ class ChatView extends StatelessWidget {
 | 
			
		||||
                                    alignment: Alignment.center,
 | 
			
		||||
                                    child: EncryptionButton(controller.room),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  if (controller.matrix.isMultiAccount &&
 | 
			
		||||
                                      controller.matrix.currentBundle.length >
 | 
			
		||||
                                          1)
 | 
			
		||||
                                    Container(
 | 
			
		||||
                                      height: 56,
 | 
			
		||||
                                      alignment: Alignment.center,
 | 
			
		||||
                                      child: _ChatAccountPicker(controller),
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  Expanded(
 | 
			
		||||
                                    child: Padding(
 | 
			
		||||
                                      padding: const EdgeInsets.symmetric(
 | 
			
		||||
@ -792,3 +798,58 @@ class _EditContent extends StatelessWidget {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _ChatAccountPicker extends StatelessWidget {
 | 
			
		||||
  final ChatController controller;
 | 
			
		||||
 | 
			
		||||
  const _ChatAccountPicker(this.controller, {Key key}) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  void _popupMenuButtonSelected(String mxid) {
 | 
			
		||||
    final client = controller.matrix.currentBundle
 | 
			
		||||
        .firstWhere((cl) => cl.userID == mxid, orElse: () => null);
 | 
			
		||||
    if (client == null) {
 | 
			
		||||
      Logs().w('Attempted to switch to a non-existing client $mxid');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    controller.setSendingClient(client);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    controller.matrix ??= Matrix.of(context);
 | 
			
		||||
    final clients = controller.currentRoomBundle;
 | 
			
		||||
    return Padding(
 | 
			
		||||
      padding: const EdgeInsets.all(8.0),
 | 
			
		||||
      child: FutureBuilder<Profile>(
 | 
			
		||||
        future: controller.sendingClient.ownProfile,
 | 
			
		||||
        builder: (context, snapshot) => PopupMenuButton<String>(
 | 
			
		||||
          onSelected: _popupMenuButtonSelected,
 | 
			
		||||
          itemBuilder: (BuildContext context) => clients
 | 
			
		||||
              .map((client) => PopupMenuItem<String>(
 | 
			
		||||
                    value: client.userID,
 | 
			
		||||
                    child: FutureBuilder<Profile>(
 | 
			
		||||
                      future: client.ownProfile,
 | 
			
		||||
                      builder: (context, snapshot) => ListTile(
 | 
			
		||||
                        leading: Avatar(
 | 
			
		||||
                          snapshot.data?.avatarUrl,
 | 
			
		||||
                          snapshot.data?.displayName ?? client.userID.localpart,
 | 
			
		||||
                          size: 20,
 | 
			
		||||
                        ),
 | 
			
		||||
                        title:
 | 
			
		||||
                            Text(snapshot.data?.displayName ?? client.userID),
 | 
			
		||||
                        contentPadding: EdgeInsets.all(0),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ))
 | 
			
		||||
              .toList(),
 | 
			
		||||
          child: Avatar(
 | 
			
		||||
            snapshot.data?.avatarUrl,
 | 
			
		||||
            snapshot.data?.displayName ??
 | 
			
		||||
                controller.matrix.client.userID.localpart,
 | 
			
		||||
            size: 20,
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -100,7 +100,8 @@ class HomeserverPickerView extends StatelessWidget {
 | 
			
		||||
                                              imageUrl: Uri.parse(
 | 
			
		||||
                                                      identityProvider.icon)
 | 
			
		||||
                                                  .getDownloadLink(
 | 
			
		||||
                                                      Matrix.of(context).client)
 | 
			
		||||
                                                      Matrix.of(context)
 | 
			
		||||
                                                          .getLoginClient())
 | 
			
		||||
                                                  .toString(),
 | 
			
		||||
                                              width: 24,
 | 
			
		||||
                                              height: 24,
 | 
			
		||||
@ -128,7 +129,7 @@ class HomeserverPickerView extends StatelessWidget {
 | 
			
		||||
                                    Expanded(
 | 
			
		||||
                                      child: _LoginButton(
 | 
			
		||||
                                        onPressed: () =>
 | 
			
		||||
                                            VRouter.of(context).to('/login'),
 | 
			
		||||
                                            VRouter.of(context).to('login'),
 | 
			
		||||
                                        icon: Icon(Icons.login_outlined),
 | 
			
		||||
                                        labelText: L10n.of(context).login,
 | 
			
		||||
                                      ),
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@ class LoginView extends StatelessWidget {
 | 
			
		||||
          elevation: 0,
 | 
			
		||||
          title: Text(
 | 
			
		||||
            L10n.of(context).logInTo(Matrix.of(context)
 | 
			
		||||
                .client
 | 
			
		||||
                .getLoginClient()
 | 
			
		||||
                .homeserver
 | 
			
		||||
                .toString()
 | 
			
		||||
                .replaceFirst('https://', '')),
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,13 @@ class SettingsAccountView extends StatelessWidget {
 | 
			
		||||
        withScrolling: true,
 | 
			
		||||
        child: Column(
 | 
			
		||||
          children: [
 | 
			
		||||
            ListTile(
 | 
			
		||||
              trailing: Icon(Icons.add_box_outlined),
 | 
			
		||||
              title: Text(L10n.of(context).addAccount),
 | 
			
		||||
              subtitle: Text(L10n.of(context).enableMultiAccounts),
 | 
			
		||||
              onTap: controller.addAccountAction,
 | 
			
		||||
            ),
 | 
			
		||||
            Divider(height: 1),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              trailing: Icon(Icons.edit_outlined),
 | 
			
		||||
              title: Text(L10n.of(context).editDisplayname),
 | 
			
		||||
@ -38,6 +45,7 @@ class SettingsAccountView extends StatelessWidget {
 | 
			
		||||
              title: Text(L10n.of(context).devices),
 | 
			
		||||
              onTap: () => VRouter.of(context).to('devices'),
 | 
			
		||||
            ),
 | 
			
		||||
            Divider(height: 1),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              trailing: Icon(Icons.exit_to_app_outlined),
 | 
			
		||||
              title: Text(L10n.of(context).logout),
 | 
			
		||||
 | 
			
		||||
@ -38,7 +38,7 @@ class SignupPageView extends StatelessWidget {
 | 
			
		||||
                    labelText: L10n.of(context).username,
 | 
			
		||||
                    prefixText: '@',
 | 
			
		||||
                    suffixText:
 | 
			
		||||
                        ':${Matrix.of(context).client.homeserver.host}'),
 | 
			
		||||
                        ':${Matrix.of(context).getLoginClient().homeserver.host}'),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            Divider(),
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										99
									
								
								lib/utils/account_bundles.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								lib/utils/account_bundles.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,99 @@
 | 
			
		||||
import 'package:matrix/matrix.dart';
 | 
			
		||||
 | 
			
		||||
class AccountBundles {
 | 
			
		||||
  String prefix;
 | 
			
		||||
  List<AccountBundle> bundles;
 | 
			
		||||
 | 
			
		||||
  AccountBundles({this.prefix, this.bundles});
 | 
			
		||||
 | 
			
		||||
  AccountBundles.fromJson(Map<String, dynamic> json)
 | 
			
		||||
      : prefix = json.tryGet<String>('prefix'),
 | 
			
		||||
        bundles = json['bundles'] is List
 | 
			
		||||
            ? json['bundles']
 | 
			
		||||
                .map((b) {
 | 
			
		||||
                  try {
 | 
			
		||||
                    return AccountBundle.fromJson(b);
 | 
			
		||||
                  } catch (_) {
 | 
			
		||||
                    return null;
 | 
			
		||||
                  }
 | 
			
		||||
                })
 | 
			
		||||
                .whereType<AccountBundle>()
 | 
			
		||||
                .toList()
 | 
			
		||||
            : null;
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toJson() => {
 | 
			
		||||
        if (prefix != null) 'prefix': prefix,
 | 
			
		||||
        if (bundles != null) 'bundles': bundles.map((v) => v.toJson()).toList(),
 | 
			
		||||
      };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class AccountBundle {
 | 
			
		||||
  String name;
 | 
			
		||||
  int priority;
 | 
			
		||||
 | 
			
		||||
  AccountBundle({this.name, this.priority});
 | 
			
		||||
 | 
			
		||||
  AccountBundle.fromJson(Map<String, dynamic> json)
 | 
			
		||||
      : name = json.tryGet<String>('name'),
 | 
			
		||||
        priority = json.tryGet<int>('priority');
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toJson() => <String, dynamic>{
 | 
			
		||||
        if (name != null) 'name': name,
 | 
			
		||||
        if (priority != null) 'priority': priority,
 | 
			
		||||
      };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const accountBundlesType = 'im.fluffychat.account_bundles';
 | 
			
		||||
 | 
			
		||||
extension AccountBundlesExtension on Client {
 | 
			
		||||
  List<AccountBundle> get accountBundles {
 | 
			
		||||
    List<AccountBundle> ret;
 | 
			
		||||
    if (accountData.containsKey(accountBundlesType)) {
 | 
			
		||||
      ret = AccountBundles.fromJson(accountData[accountBundlesType].content)
 | 
			
		||||
          .bundles;
 | 
			
		||||
    }
 | 
			
		||||
    ret ??= [];
 | 
			
		||||
    if (ret.isEmpty) {
 | 
			
		||||
      ret.add(AccountBundle(
 | 
			
		||||
        name: userID,
 | 
			
		||||
        priority: 0,
 | 
			
		||||
      ));
 | 
			
		||||
    }
 | 
			
		||||
    return ret;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> setAccountBundle(String name, [int priority]) async {
 | 
			
		||||
    final data =
 | 
			
		||||
        AccountBundles.fromJson(accountData[accountBundlesType]?.content ?? {});
 | 
			
		||||
    var foundBundle = false;
 | 
			
		||||
    data.bundles ??= [];
 | 
			
		||||
    for (final bundle in data.bundles) {
 | 
			
		||||
      if (bundle.name == name) {
 | 
			
		||||
        bundle.priority = priority;
 | 
			
		||||
        foundBundle = true;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (!foundBundle) {
 | 
			
		||||
      data.bundles.add(AccountBundle(name: name, priority: priority));
 | 
			
		||||
    }
 | 
			
		||||
    await setAccountData(userID, accountBundlesType, data.toJson());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> removeFromAccountBundle(String name) async {
 | 
			
		||||
    if (!accountData.containsKey(accountBundlesType)) {
 | 
			
		||||
      return; // nothing to do
 | 
			
		||||
    }
 | 
			
		||||
    final data =
 | 
			
		||||
        AccountBundles.fromJson(accountData[accountBundlesType].content);
 | 
			
		||||
    if (data.bundles == null) return;
 | 
			
		||||
    data.bundles.removeWhere((b) => b.name == name);
 | 
			
		||||
    await setAccountData(userID, accountBundlesType, data.toJson());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String get sendPrefix {
 | 
			
		||||
    final data =
 | 
			
		||||
        AccountBundles.fromJson(accountData[accountBundlesType]?.content ?? {});
 | 
			
		||||
    return data.prefix;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										72
									
								
								lib/utils/client_manager.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								lib/utils/client_manager.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,72 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/utils/platform_infos.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:matrix/encryption/utils/key_verification.dart';
 | 
			
		||||
import 'package:matrix/matrix.dart';
 | 
			
		||||
 | 
			
		||||
import 'famedlysdk_store.dart';
 | 
			
		||||
import 'matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart';
 | 
			
		||||
 | 
			
		||||
abstract class ClientManager {
 | 
			
		||||
  static const String clientNamespace = 'im.fluffychat.store.clients';
 | 
			
		||||
  static Future<List<Client>> getClients() async {
 | 
			
		||||
    final clientNames = <String>{};
 | 
			
		||||
    try {
 | 
			
		||||
      final rawClientNames = await Store().getItem(clientNamespace);
 | 
			
		||||
      if (rawClientNames != null) {
 | 
			
		||||
        final clientNamesList =
 | 
			
		||||
            (jsonDecode(rawClientNames) as List).cast<String>();
 | 
			
		||||
        clientNames.addAll(clientNamesList);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e, s) {
 | 
			
		||||
      Logs().w('Client names in store are corrupted', e, s);
 | 
			
		||||
      await Store().deleteItem(clientNamespace);
 | 
			
		||||
    }
 | 
			
		||||
    if (clientNames.isEmpty) clientNames.add(PlatformInfos.clientName);
 | 
			
		||||
    final clients = clientNames.map(createClient).toList();
 | 
			
		||||
    await Future.wait(clients.map((client) => client
 | 
			
		||||
        .init()
 | 
			
		||||
        .catchError((e, s) => Logs().e('Unable to initialize client', e, s))));
 | 
			
		||||
    if (clients.length > 1 && clients.any((c) => !c.isLogged())) {
 | 
			
		||||
      final loggedOutClients = clients.where((c) => !c.isLogged()).toList();
 | 
			
		||||
      for (final client in loggedOutClients) {
 | 
			
		||||
        clientNames.remove(client.clientName);
 | 
			
		||||
        clients.remove(client);
 | 
			
		||||
      }
 | 
			
		||||
      await Store().setItem(clientNamespace, jsonEncode(clientNames.toList()));
 | 
			
		||||
    }
 | 
			
		||||
    return clients;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static Future<void> addClientNameToStore(String clientName) async {
 | 
			
		||||
    final clientNamesList = <String>[];
 | 
			
		||||
    final rawClientNames = await Store().getItem(clientNamespace);
 | 
			
		||||
    if (rawClientNames != null) {
 | 
			
		||||
      final stored = (jsonDecode(rawClientNames) as List).cast<String>();
 | 
			
		||||
      clientNamesList.addAll(stored);
 | 
			
		||||
    }
 | 
			
		||||
    clientNamesList.add(clientName);
 | 
			
		||||
    await Store().setItem(clientNamespace, jsonEncode(clientNamesList));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static Client createClient(String clientName) => Client(
 | 
			
		||||
        clientName,
 | 
			
		||||
        enableE2eeRecovery: true,
 | 
			
		||||
        verificationMethods: {
 | 
			
		||||
          KeyVerificationMethod.numbers,
 | 
			
		||||
          if (PlatformInfos.isMobile || PlatformInfos.isLinux)
 | 
			
		||||
            KeyVerificationMethod.emoji,
 | 
			
		||||
        },
 | 
			
		||||
        importantStateEvents: <String>{
 | 
			
		||||
          'im.ponies.room_emotes', // we want emotes to work properly
 | 
			
		||||
        },
 | 
			
		||||
        databaseBuilder: FlutterMatrixHiveStore.hiveDatabaseBuilder,
 | 
			
		||||
        supportedLoginTypes: {
 | 
			
		||||
          AuthenticationTypes.password,
 | 
			
		||||
          if (PlatformInfos.isMobile || PlatformInfos.isWeb)
 | 
			
		||||
            AuthenticationTypes.sso
 | 
			
		||||
        },
 | 
			
		||||
        compute: compute,
 | 
			
		||||
      );
 | 
			
		||||
}
 | 
			
		||||
@ -7,10 +7,16 @@ import 'package:flutter/material.dart';
 | 
			
		||||
class LoadingView extends StatelessWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    if (Matrix.of(context).loginState != null) {
 | 
			
		||||
    if (Matrix.of(context)
 | 
			
		||||
        .widget
 | 
			
		||||
        .clients
 | 
			
		||||
        .every((client) => client.loginState != null)) {
 | 
			
		||||
      WidgetsBinding.instance.addPostFrameCallback(
 | 
			
		||||
        (_) => VRouter.of(context).to(
 | 
			
		||||
          Matrix.of(context).loginState == LoginState.loggedIn
 | 
			
		||||
          Matrix.of(context)
 | 
			
		||||
                  .widget
 | 
			
		||||
                  .clients
 | 
			
		||||
                  .any((client) => client.loginState == LoginState.loggedIn)
 | 
			
		||||
              ? '/rooms'
 | 
			
		||||
              : '/home',
 | 
			
		||||
          queryParameters: VRouter.of(context).queryParameters,
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import 'dart:math';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/config/themes.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/matrix.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
class OnePageCard extends StatelessWidget {
 | 
			
		||||
@ -12,7 +13,8 @@ class OnePageCard extends StatelessWidget {
 | 
			
		||||
  static num breakpoint = FluffyThemes.columnWidth * 2;
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return MediaQuery.of(context).size.width <= breakpoint
 | 
			
		||||
    return MediaQuery.of(context).size.width <= breakpoint ||
 | 
			
		||||
            Matrix.of(context).client.isLogged()
 | 
			
		||||
        ? child
 | 
			
		||||
        : Container(
 | 
			
		||||
            decoration: BoxDecoration(
 | 
			
		||||
 | 
			
		||||
@ -1,25 +0,0 @@
 | 
			
		||||
import 'package:fluffychat/pages/views/empty_page_view.dart';
 | 
			
		||||
import 'package:matrix/matrix.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import '../matrix.dart';
 | 
			
		||||
 | 
			
		||||
class WaitForInitPage extends StatelessWidget {
 | 
			
		||||
  final Widget page;
 | 
			
		||||
  const WaitForInitPage(this.page, {Key key}) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    if (Matrix.of(context).loginState == null) {
 | 
			
		||||
      return StreamBuilder<LoginState>(
 | 
			
		||||
          stream: Matrix.of(context).client.onLoginStateChanged.stream,
 | 
			
		||||
          builder: (context, snapshot) {
 | 
			
		||||
            if (!snapshot.hasData) {
 | 
			
		||||
              return EmptyPage(loading: true);
 | 
			
		||||
            }
 | 
			
		||||
            return page;
 | 
			
		||||
          });
 | 
			
		||||
    }
 | 
			
		||||
    return page;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -3,6 +3,7 @@ import 'dart:io';
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
 | 
			
		||||
import 'package:fluffychat/utils/client_manager.dart';
 | 
			
		||||
import 'package:matrix/encryption.dart';
 | 
			
		||||
import 'package:matrix/matrix.dart';
 | 
			
		||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
 | 
			
		||||
@ -23,6 +24,7 @@ import '../pages/key_verification_dialog.dart';
 | 
			
		||||
import '../utils/platform_infos.dart';
 | 
			
		||||
import '../config/app_config.dart';
 | 
			
		||||
import '../config/setting_keys.dart';
 | 
			
		||||
import '../utils/account_bundles.dart';
 | 
			
		||||
import '../utils/background_push.dart';
 | 
			
		||||
import 'package:vrouter/vrouter.dart';
 | 
			
		||||
 | 
			
		||||
@ -35,7 +37,7 @@ class Matrix extends StatefulWidget {
 | 
			
		||||
 | 
			
		||||
  final BuildContext context;
 | 
			
		||||
 | 
			
		||||
  final Client client;
 | 
			
		||||
  final List<Client> clients;
 | 
			
		||||
 | 
			
		||||
  final Map<String, String> queryParameters;
 | 
			
		||||
 | 
			
		||||
@ -43,7 +45,7 @@ class Matrix extends StatefulWidget {
 | 
			
		||||
    this.child,
 | 
			
		||||
    @required this.router,
 | 
			
		||||
    @required this.context,
 | 
			
		||||
    @required this.client,
 | 
			
		||||
    @required this.clients,
 | 
			
		||||
    this.queryParameters,
 | 
			
		||||
    Key key,
 | 
			
		||||
  }) : super(key: key);
 | 
			
		||||
@ -57,12 +59,98 @@ class Matrix extends StatefulWidget {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class MatrixState extends State<Matrix> with WidgetsBindingObserver {
 | 
			
		||||
  Client get client => widget.client;
 | 
			
		||||
  int activeClient = 0;
 | 
			
		||||
  String activeBundle;
 | 
			
		||||
  Store store = Store();
 | 
			
		||||
  BuildContext navigatorContext;
 | 
			
		||||
 | 
			
		||||
  BackgroundPush _backgroundPush;
 | 
			
		||||
 | 
			
		||||
  Client get client => widget.clients[_safeActiveClient];
 | 
			
		||||
 | 
			
		||||
  bool get isMultiAccount => widget.clients.length > 1;
 | 
			
		||||
 | 
			
		||||
  int getClientIndexByMatrixId(String matrixId) =>
 | 
			
		||||
      widget.clients.indexWhere((client) => client.userID == matrixId);
 | 
			
		||||
 | 
			
		||||
  int get _safeActiveClient {
 | 
			
		||||
    if (activeClient < 0 || activeClient >= widget.clients.length) {
 | 
			
		||||
      return 0;
 | 
			
		||||
    }
 | 
			
		||||
    return activeClient;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setActiveClient(Client cl) {
 | 
			
		||||
    final i = widget.clients.indexWhere((c) => c == cl);
 | 
			
		||||
    if (i != null) {
 | 
			
		||||
      activeClient = i;
 | 
			
		||||
    } else {
 | 
			
		||||
      Logs().w('Tried to set an unknown client ${cl.userID} as active');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  List<Client> get currentBundle {
 | 
			
		||||
    if (!hasComplexBundles) {
 | 
			
		||||
      return List.from(widget.clients);
 | 
			
		||||
    }
 | 
			
		||||
    final bundles = accountBundles;
 | 
			
		||||
    if (bundles.containsKey(activeBundle)) {
 | 
			
		||||
      return bundles[activeBundle];
 | 
			
		||||
    }
 | 
			
		||||
    return bundles.values.first;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, List<Client>> get accountBundles {
 | 
			
		||||
    final resBundles = <String, List<_AccountBundleWithClient>>{};
 | 
			
		||||
    for (var i = 0; i < widget.clients.length; i++) {
 | 
			
		||||
      final bundles = widget.clients[i].accountBundles;
 | 
			
		||||
      for (final bundle in bundles) {
 | 
			
		||||
        if (bundle.name == null) {
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
        resBundles[bundle.name] ??= [];
 | 
			
		||||
        resBundles[bundle.name].add(_AccountBundleWithClient(
 | 
			
		||||
          client: widget.clients[i],
 | 
			
		||||
          bundle: bundle,
 | 
			
		||||
        ));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (final b in resBundles.values) {
 | 
			
		||||
      b.sort((a, b) => a.bundle.priority == null
 | 
			
		||||
          ? 1
 | 
			
		||||
          : b.bundle.priority == null
 | 
			
		||||
              ? -1
 | 
			
		||||
              : a.bundle.priority.compareTo(b.bundle.priority));
 | 
			
		||||
    }
 | 
			
		||||
    return resBundles
 | 
			
		||||
        .map((k, v) => MapEntry(k, v.map((vv) => vv.client).toList()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool get hasComplexBundles => accountBundles.values.any((v) => v.length > 1);
 | 
			
		||||
 | 
			
		||||
  Client _loginClientCandidate;
 | 
			
		||||
 | 
			
		||||
  Client getLoginClient() {
 | 
			
		||||
    final multiAccount = client.isLogged();
 | 
			
		||||
    if (!multiAccount) return client;
 | 
			
		||||
    _loginClientCandidate ??= ClientManager.createClient(
 | 
			
		||||
        client.generateUniqueTransactionId())
 | 
			
		||||
      ..onLoginStateChanged
 | 
			
		||||
          .stream
 | 
			
		||||
          .where((l) => l == LoginState.loggedIn)
 | 
			
		||||
          .first
 | 
			
		||||
          .then((_) {
 | 
			
		||||
        widget.clients.add(_loginClientCandidate);
 | 
			
		||||
        ClientManager.addClientNameToStore(_loginClientCandidate.clientName);
 | 
			
		||||
        _registerSubs(_loginClientCandidate.clientName);
 | 
			
		||||
        widget.router.currentState.to('/rooms');
 | 
			
		||||
      });
 | 
			
		||||
    return _loginClientCandidate;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Client getClientByName(String name) => widget.clients
 | 
			
		||||
      .firstWhere((c) => c.clientName == name, orElse: () => null);
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> get shareContent => _shareContent;
 | 
			
		||||
  set shareContent(Map<String, dynamic> content) {
 | 
			
		||||
    _shareContent = content;
 | 
			
		||||
@ -78,8 +166,8 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
 | 
			
		||||
 | 
			
		||||
  void _initWithStore() async {
 | 
			
		||||
    try {
 | 
			
		||||
      await client.init();
 | 
			
		||||
      if (client.isLogged()) {
 | 
			
		||||
        // TODO: Figure out how this works in multi account
 | 
			
		||||
        final statusMsg = await store.getItem(SettingKeys.ownStatusMessage);
 | 
			
		||||
        if (statusMsg?.isNotEmpty ?? false) {
 | 
			
		||||
          Logs().v('Send cached status message: "$statusMsg"');
 | 
			
		||||
@ -97,15 +185,15 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  StreamSubscription onRoomKeyRequestSub;
 | 
			
		||||
  StreamSubscription onKeyVerificationRequestSub;
 | 
			
		||||
  StreamSubscription onJitsiCallSub;
 | 
			
		||||
  StreamSubscription onNotification;
 | 
			
		||||
  StreamSubscription<LoginState> onLoginStateChanged;
 | 
			
		||||
  StreamSubscription<UiaRequest> onUiaRequest;
 | 
			
		||||
  final onRoomKeyRequestSub = <String, StreamSubscription>{};
 | 
			
		||||
  final onKeyVerificationRequestSub = <String, StreamSubscription>{};
 | 
			
		||||
  final onJitsiCallSub = <String, StreamSubscription>{};
 | 
			
		||||
  final onNotification = <String, StreamSubscription>{};
 | 
			
		||||
  final onLoginStateChanged = <String, StreamSubscription<LoginState>>{};
 | 
			
		||||
  final onUiaRequest = <String, StreamSubscription<UiaRequest>>{};
 | 
			
		||||
  StreamSubscription<html.Event> onFocusSub;
 | 
			
		||||
  StreamSubscription<html.Event> onBlurSub;
 | 
			
		||||
  StreamSubscription<Presence> onOwnPresence;
 | 
			
		||||
  final onOwnPresence = <String, StreamSubscription<Presence>>{};
 | 
			
		||||
 | 
			
		||||
  String _cachedPassword;
 | 
			
		||||
  String get cachedPassword {
 | 
			
		||||
@ -165,14 +253,12 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
 | 
			
		||||
            return uiaRequest
 | 
			
		||||
                .cancel(Exception(L10n.of(context).serverRequiresEmail));
 | 
			
		||||
          }
 | 
			
		||||
          final clientSecret =
 | 
			
		||||
              Matrix.of(context).client.generateUniqueTransactionId();
 | 
			
		||||
          final currentThreepidCreds =
 | 
			
		||||
              await Matrix.of(context).client.requestTokenToRegisterEmail(
 | 
			
		||||
                    clientSecret,
 | 
			
		||||
                    emailInput.single,
 | 
			
		||||
                    0,
 | 
			
		||||
                  );
 | 
			
		||||
          final clientSecret = client.generateUniqueTransactionId();
 | 
			
		||||
          final currentThreepidCreds = await client.requestTokenToRegisterEmail(
 | 
			
		||||
            clientSecret,
 | 
			
		||||
            emailInput.single,
 | 
			
		||||
            0,
 | 
			
		||||
          );
 | 
			
		||||
          final auth = AuthenticationThreePidCreds(
 | 
			
		||||
            session: uiaRequest.session,
 | 
			
		||||
            type: AuthenticationTypes.emailIdentity,
 | 
			
		||||
@ -289,21 +375,14 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  LoginState loginState;
 | 
			
		||||
 | 
			
		||||
  void initMatrix() {
 | 
			
		||||
    // Display the app lock
 | 
			
		||||
    if (PlatformInfos.isMobile) {
 | 
			
		||||
      WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
			
		||||
        FlutterSecureStorage().read(key: SettingKeys.appLockKey).then((lock) {
 | 
			
		||||
          if (lock?.isNotEmpty ?? false) {
 | 
			
		||||
            AppLock.of(widget.context).enable();
 | 
			
		||||
            AppLock.of(widget.context).showLockScreen();
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
  void _registerSubs(String name) {
 | 
			
		||||
    final c = getClientByName(name);
 | 
			
		||||
    if (c == null) {
 | 
			
		||||
      Logs().w(
 | 
			
		||||
          'Attempted to register subscriptions for non-existing client $name');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    onKeyVerificationRequestSub ??= client.onKeyVerificationRequest.stream
 | 
			
		||||
    onKeyVerificationRequestSub[name] ??= c.onKeyVerificationRequest.stream
 | 
			
		||||
        .listen((KeyVerification request) async {
 | 
			
		||||
      var hidPopup = false;
 | 
			
		||||
      request.onUpdate = () {
 | 
			
		||||
@ -334,51 +413,95 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
 | 
			
		||||
        await request.rejectVerification();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    _initWithStore();
 | 
			
		||||
 | 
			
		||||
    if (kIsWeb) {
 | 
			
		||||
      onFocusSub = html.window.onFocus.listen((_) => webHasFocus = true);
 | 
			
		||||
      onBlurSub = html.window.onBlur.listen((_) => webHasFocus = false);
 | 
			
		||||
    }
 | 
			
		||||
    onLoginStateChanged ??= client.onLoginStateChanged.stream.listen((state) {
 | 
			
		||||
      if (loginState != state) {
 | 
			
		||||
        loginState = state;
 | 
			
		||||
        final isInLoginRoutes = {'/home', '/login', '/signup'}
 | 
			
		||||
            .contains(widget.router.currentState.url);
 | 
			
		||||
        if (widget.router.currentState.url == '/' ||
 | 
			
		||||
            (state == LoginState.loggedIn) == isInLoginRoutes) {
 | 
			
		||||
    onLoginStateChanged[name] ??= c.onLoginStateChanged.stream.listen((state) {
 | 
			
		||||
      final loggedInWithMultipleClients = widget.clients.length > 1;
 | 
			
		||||
      if (state != LoginState.loggedIn) {
 | 
			
		||||
        _cancelSubs(c.clientName);
 | 
			
		||||
        widget.clients.remove(c);
 | 
			
		||||
      }
 | 
			
		||||
      if (loggedInWithMultipleClients) {
 | 
			
		||||
        // TODO: display a nicer toast
 | 
			
		||||
        showOkAlertDialog(
 | 
			
		||||
          useRootNavigator: false,
 | 
			
		||||
          context: navigatorContext,
 | 
			
		||||
          title: 'Login state of client $name changed',
 | 
			
		||||
          message: 'New login state: $state',
 | 
			
		||||
          okLabel: L10n.of(widget.context).ok,
 | 
			
		||||
        );
 | 
			
		||||
        if (state != LoginState.loggedIn) {
 | 
			
		||||
          widget.router.currentState.to(
 | 
			
		||||
            loginState == LoginState.loggedIn ? '/rooms' : '/home',
 | 
			
		||||
            '/rooms',
 | 
			
		||||
            queryParameters: widget.router.currentState.queryParameters,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        widget.router.currentState.to(
 | 
			
		||||
          state == LoginState.loggedIn ? '/rooms' : '/home',
 | 
			
		||||
          queryParameters: widget.router.currentState.queryParameters,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Cache and resend status message
 | 
			
		||||
    onOwnPresence ??= client.onPresence.stream.listen((presence) {
 | 
			
		||||
      if (client.isLogged() &&
 | 
			
		||||
          client.userID == presence.senderId &&
 | 
			
		||||
    onOwnPresence[name] ??= c.onPresence.stream.listen((presence) {
 | 
			
		||||
      if (c.isLogged() &&
 | 
			
		||||
          c.userID == presence.senderId &&
 | 
			
		||||
          presence.presence?.statusMsg != null) {
 | 
			
		||||
        Logs().v('Update status message: "${presence.presence.statusMsg}"');
 | 
			
		||||
        store.setItem(
 | 
			
		||||
            SettingKeys.ownStatusMessage, presence.presence.statusMsg);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    onUiaRequest ??= client.onUiaRequest.stream.listen(_onUiaRequest);
 | 
			
		||||
    onUiaRequest[name] ??= c.onUiaRequest.stream.listen(_onUiaRequest);
 | 
			
		||||
    if (PlatformInfos.isWeb || PlatformInfos.isLinux) {
 | 
			
		||||
      client.onSync.stream.first.then((s) {
 | 
			
		||||
      c.onSync.stream.first.then((s) {
 | 
			
		||||
        html.Notification.requestPermission();
 | 
			
		||||
        onNotification ??= client.onEvent.stream
 | 
			
		||||
        onNotification[name] ??= c.onEvent.stream
 | 
			
		||||
            .where((e) =>
 | 
			
		||||
                e.type == EventUpdateType.timeline &&
 | 
			
		||||
                [EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted]
 | 
			
		||||
                    .contains(e.content['type']) &&
 | 
			
		||||
                e.content['sender'] != client.userID)
 | 
			
		||||
                e.content['sender'] != c.userID)
 | 
			
		||||
            .listen(_showLocalNotification);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _cancelSubs(String name) {
 | 
			
		||||
    onRoomKeyRequestSub[name]?.cancel();
 | 
			
		||||
    onRoomKeyRequestSub.remove(name);
 | 
			
		||||
    onKeyVerificationRequestSub[name]?.cancel();
 | 
			
		||||
    onKeyVerificationRequestSub.remove(name);
 | 
			
		||||
    onLoginStateChanged[name]?.cancel();
 | 
			
		||||
    onLoginStateChanged.remove(name);
 | 
			
		||||
    onOwnPresence[name]?.cancel();
 | 
			
		||||
    onOwnPresence.remove(name);
 | 
			
		||||
    onNotification[name]?.cancel();
 | 
			
		||||
    onNotification.remove(name);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void initMatrix() {
 | 
			
		||||
    // Display the app lock
 | 
			
		||||
    if (PlatformInfos.isMobile) {
 | 
			
		||||
      WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
			
		||||
        FlutterSecureStorage().read(key: SettingKeys.appLockKey).then((lock) {
 | 
			
		||||
          if (lock?.isNotEmpty ?? false) {
 | 
			
		||||
            AppLock.of(widget.context).enable();
 | 
			
		||||
            AppLock.of(widget.context).showLockScreen();
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _initWithStore();
 | 
			
		||||
 | 
			
		||||
    for (final c in widget.clients) {
 | 
			
		||||
      _registerSubs(c.clientName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (kIsWeb) {
 | 
			
		||||
      onFocusSub = html.window.onFocus.listen((_) => webHasFocus = true);
 | 
			
		||||
      onBlurSub = html.window.onBlur.listen((_) => webHasFocus = false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (PlatformInfos.isMobile) {
 | 
			
		||||
      _backgroundPush = BackgroundPush(
 | 
			
		||||
@ -445,11 +568,12 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    WidgetsBinding.instance.removeObserver(this);
 | 
			
		||||
 | 
			
		||||
    onRoomKeyRequestSub?.cancel();
 | 
			
		||||
    onKeyVerificationRequestSub?.cancel();
 | 
			
		||||
    onLoginStateChanged?.cancel();
 | 
			
		||||
    onOwnPresence?.cancel();
 | 
			
		||||
    onNotification?.cancel();
 | 
			
		||||
    onRoomKeyRequestSub.values.map((s) => s.cancel());
 | 
			
		||||
    onKeyVerificationRequestSub.values.map((s) => s.cancel());
 | 
			
		||||
    onLoginStateChanged.values.map((s) => s.cancel());
 | 
			
		||||
    onOwnPresence.values.map((s) => s.cancel());
 | 
			
		||||
    onNotification.values.map((s) => s.cancel());
 | 
			
		||||
 | 
			
		||||
    onFocusSub?.cancel();
 | 
			
		||||
    onBlurSub?.cancel();
 | 
			
		||||
    _backgroundPush?.onLogin?.cancel();
 | 
			
		||||
@ -491,3 +615,9 @@ class FixedThreepidCreds extends ThreepidCreds {
 | 
			
		||||
    return data;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AccountBundleWithClient {
 | 
			
		||||
  final Client client;
 | 
			
		||||
  final AccountBundle bundle;
 | 
			
		||||
  _AccountBundleWithClient({this.client, this.bundle});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ import audioplayers
 | 
			
		||||
import file_selector_macos
 | 
			
		||||
import firebase_core
 | 
			
		||||
import flutter_local_notifications
 | 
			
		||||
import geolocator_apple
 | 
			
		||||
import package_info_plus_macos
 | 
			
		||||
import path_provider_macos
 | 
			
		||||
import share_plus_macos
 | 
			
		||||
@ -22,6 +23,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
 | 
			
		||||
  FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
 | 
			
		||||
  FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
 | 
			
		||||
  FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
 | 
			
		||||
  GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
 | 
			
		||||
  FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
 | 
			
		||||
  PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
 | 
			
		||||
  SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,8 @@ PODS:
 | 
			
		||||
  - FMDB (2.7.5):
 | 
			
		||||
    - FMDB/standard (= 2.7.5)
 | 
			
		||||
  - FMDB/standard (2.7.5)
 | 
			
		||||
  - geolocator_apple (1.2.0):
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - GoogleDataTransport (8.4.0):
 | 
			
		||||
    - GoogleUtilities/Environment (~> 7.2)
 | 
			
		||||
    - nanopb (~> 2.30908.0)
 | 
			
		||||
@ -59,6 +61,7 @@ DEPENDENCIES:
 | 
			
		||||
  - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
 | 
			
		||||
  - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
 | 
			
		||||
  - FlutterMacOS (from `Flutter/ephemeral`)
 | 
			
		||||
  - geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos`)
 | 
			
		||||
  - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`)
 | 
			
		||||
  - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
 | 
			
		||||
  - share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`)
 | 
			
		||||
@ -89,6 +92,8 @@ EXTERNAL SOURCES:
 | 
			
		||||
    :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
 | 
			
		||||
  FlutterMacOS:
 | 
			
		||||
    :path: Flutter/ephemeral
 | 
			
		||||
  geolocator_apple:
 | 
			
		||||
    :path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos
 | 
			
		||||
  package_info_plus_macos:
 | 
			
		||||
    :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos
 | 
			
		||||
  path_provider_macos:
 | 
			
		||||
@ -114,6 +119,7 @@ SPEC CHECKSUMS:
 | 
			
		||||
  flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
 | 
			
		||||
  FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
 | 
			
		||||
  FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
 | 
			
		||||
  geolocator_apple: 821be05bbdb1b49500e029ebcbf2d6acf2dfb966
 | 
			
		||||
  GoogleDataTransport: cd9db2180fcecd8da1b561aea31e3e56cf834aa7
 | 
			
		||||
  GoogleUtilities: 284cddc7fffc14ae1907efb6f78ab95c1fccaedc
 | 
			
		||||
  nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
 | 
			
		||||
@ -128,4 +134,4 @@ SPEC CHECKSUMS:
 | 
			
		||||
 | 
			
		||||
PODFILE CHECKSUM: c7161fcf45d4fd9025dc0f48a76d6e64e52f8176
 | 
			
		||||
 | 
			
		||||
COCOAPODS: 1.10.1
 | 
			
		||||
COCOAPODS: 1.11.0
 | 
			
		||||
 | 
			
		||||
@ -269,6 +269,7 @@
 | 
			
		||||
				"${BUILT_PRODUCTS_DIR}/audioplayers/audioplayers.framework",
 | 
			
		||||
				"${BUILT_PRODUCTS_DIR}/file_selector_macos/file_selector_macos.framework",
 | 
			
		||||
				"${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework",
 | 
			
		||||
				"${BUILT_PRODUCTS_DIR}/geolocator_apple/geolocator_apple.framework",
 | 
			
		||||
				"${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework",
 | 
			
		||||
				"${BUILT_PRODUCTS_DIR}/package_info_plus_macos/package_info_plus_macos.framework",
 | 
			
		||||
				"${BUILT_PRODUCTS_DIR}/path_provider_macos/path_provider_macos.framework",
 | 
			
		||||
@ -289,6 +290,7 @@
 | 
			
		||||
				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audioplayers.framework",
 | 
			
		||||
				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_selector_macos.framework",
 | 
			
		||||
				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework",
 | 
			
		||||
				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/geolocator_apple.framework",
 | 
			
		||||
				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework",
 | 
			
		||||
				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus_macos.framework",
 | 
			
		||||
				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_macos.framework",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user