diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb
index dcd9011a..223da318 100644
--- a/assets/l10n/intl_en.arb
+++ b/assets/l10n/intl_en.arb
@@ -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",
diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist
index 6b4c0f78..f2872cf4 100644
--- a/ios/Flutter/AppFrameworkInfo.plist
+++ b/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 8.0
+ 9.0
diff --git a/lib/config/routes.dart b/lib/config/routes.dart
index 813b5eb7..34c302d0 100644
--- a/lib/config/routes.dart
+++ b/lib/config/routes.dart
@@ -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(
diff --git a/lib/config/themes.dart b/lib/config/themes.dart
index eae5f19f..f9ab67f7 100644
--- a/lib/config/themes.dart
+++ b/lib/config/themes.dart
@@ -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']);
diff --git a/lib/main.dart b/lib/main.dart
index 0177df27..74b0591e 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -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: {
- '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 = {};
@@ -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 clients;
final Map 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 {
final GlobalKey _matrix = GlobalKey();
GlobalKey _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 {
key: _matrix,
context: context,
router: _router,
- client: widget.client,
- child: WaitForInitPage(child),
+ clients: widget.clients,
+ child: child,
);
},
);
diff --git a/lib/pages/chat.dart b/lib/pages/chat.dart
index 2c0e067e..bc983703 100644
--- a/lib/pages/chat.dart
+++ b/lib/pages/chat.dart
@@ -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 {
Room room;
+ Client sendingClient;
+
Timeline timeline;
MatrixState matrix;
@@ -222,6 +225,14 @@ class ChatController extends State {
TextEditingController sendController = TextEditingController();
+ void setSendingClient(Client c) => setState(() {
+ sendingClient = c;
+ });
+
+ void setActiveClient(Client c) => setState(() {
+ Matrix.of(context).setActiveClient(c);
+ });
+
Future send() async {
if (sendController.text.trim().isEmpty) return;
var parseCommands = true;
@@ -447,19 +458,51 @@ class ChatController extends State {
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 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 {
});
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 {
}
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;
diff --git a/lib/pages/chat_list.dart b/lib/pages/chat_list.dart
index 78411034..e2d51aa5 100644
--- a/lib/pages/chat_list.dart
+++ b/lib/pages/chat_list.dart
@@ -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 {
}
}
+ 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(
+ 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 {
return ChatListView(this);
}
}
+
+enum EditBundleAction { addToBundle, removeFromBundle }
diff --git a/lib/pages/homeserver_picker.dart b/lib/pages/homeserver_picker.dart
index 4e485a24..c3fa0df7 100644
--- a/lib/pages/homeserver_picker.dart
+++ b/lib/pages/homeserver_picker.dart
@@ -47,13 +47,13 @@ class HomeserverPickerController extends State {
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 {
isLoading = true;
});
final wellKnown =
- await Matrix.of(context).client.checkHomeserver(homeserver);
+ await Matrix.of(context).getLoginClient().checkHomeserver(homeserver);
var jitsi = wellKnown?.additionalProperties
?.tryGet