mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-12-28 09:02:35 +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…
Reference in New Issue
Block a user