mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-11-27 14:59:29 +01:00
feat: Allow loading of multiple clients in main.dart
This commit is contained in:
parent
64cef3429e
commit
deab7c83e3
@ -1561,6 +1561,12 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"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": "Open in maps",
|
||||||
"@openInMaps": {
|
"@openInMaps": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -21,6 +21,6 @@
|
|||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
<key>MinimumOSVersion</key>
|
||||||
<string>8.0</string>
|
<string>9.0</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -217,12 +217,12 @@ class AppRoutes {
|
|||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
stackedRoutes: [
|
stackedRoutes: [
|
||||||
VWidget(
|
VWidget(
|
||||||
path: '/login',
|
path: 'login',
|
||||||
widget: Login(),
|
widget: Login(),
|
||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
),
|
),
|
||||||
VWidget(
|
VWidget(
|
||||||
path: '/signup',
|
path: 'signup',
|
||||||
widget: SignupPage(),
|
widget: SignupPage(),
|
||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
),
|
),
|
||||||
@ -296,6 +296,23 @@ class AppRoutes {
|
|||||||
widget: DevicesSettings(),
|
widget: DevicesSettings(),
|
||||||
buildTransition: _dynamicTransition,
|
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(
|
VWidget(
|
||||||
|
@ -8,6 +8,8 @@ import 'app_config.dart';
|
|||||||
|
|
||||||
abstract class FluffyThemes {
|
abstract class FluffyThemes {
|
||||||
static const double columnWidth = 360.0;
|
static const double columnWidth = 360.0;
|
||||||
|
static bool isColumnMode(BuildContext context) =>
|
||||||
|
MediaQuery.of(context).size.width > columnWidth * 2;
|
||||||
|
|
||||||
static const fallbackTextStyle =
|
static const fallbackTextStyle =
|
||||||
TextStyle(fontFamily: 'NotoSans', fontFamilyFallback: ['NotoEmoji']);
|
TextStyle(fontFamily: 'NotoSans', fontFamilyFallback: ['NotoEmoji']);
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:adaptive_theme/adaptive_theme.dart';
|
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:matrix/matrix.dart';
|
||||||
import 'package:fluffychat/config/routes.dart';
|
import 'package:fluffychat/config/routes.dart';
|
||||||
import 'package:fluffychat/utils/platform_infos.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:universal_html/html.dart' as html;
|
||||||
import 'package:vrouter/vrouter.dart';
|
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/lock_screen.dart';
|
||||||
import 'widgets/matrix.dart';
|
import 'widgets/matrix.dart';
|
||||||
import 'config/themes.dart';
|
import 'config/themes.dart';
|
||||||
@ -35,27 +33,10 @@ void main() async {
|
|||||||
FlutterError.onError = (FlutterErrorDetails details) =>
|
FlutterError.onError = (FlutterErrorDetails details) =>
|
||||||
Zone.current.handleUncaughtError(details.exception, details.stack);
|
Zone.current.handleUncaughtError(details.exception, details.stack);
|
||||||
|
|
||||||
final client = Client(
|
final clients = await ClientManager.getClients();
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (PlatformInfos.isMobile) {
|
if (PlatformInfos.isMobile) {
|
||||||
BackgroundPush.clientOnly(client);
|
BackgroundPush.clientOnly(clients.first);
|
||||||
}
|
}
|
||||||
|
|
||||||
final queryParameters = <String, String>{};
|
final queryParameters = <String, String>{};
|
||||||
@ -68,24 +49,24 @@ void main() async {
|
|||||||
() => runApp(PlatformInfos.isMobile
|
() => runApp(PlatformInfos.isMobile
|
||||||
? AppLock(
|
? AppLock(
|
||||||
builder: (args) => FluffyChatApp(
|
builder: (args) => FluffyChatApp(
|
||||||
client: client,
|
clients: clients,
|
||||||
queryParameters: queryParameters,
|
queryParameters: queryParameters,
|
||||||
),
|
),
|
||||||
lockScreen: LockScreen(),
|
lockScreen: LockScreen(),
|
||||||
enabled: false,
|
enabled: false,
|
||||||
)
|
)
|
||||||
: FluffyChatApp(client: client, queryParameters: queryParameters)),
|
: FluffyChatApp(clients: clients, queryParameters: queryParameters)),
|
||||||
SentryController.captureException,
|
SentryController.captureException,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class FluffyChatApp extends StatefulWidget {
|
class FluffyChatApp extends StatefulWidget {
|
||||||
final Widget testWidget;
|
final Widget testWidget;
|
||||||
final Client client;
|
final List<Client> clients;
|
||||||
final Map<String, String> queryParameters;
|
final Map<String, String> queryParameters;
|
||||||
|
|
||||||
const FluffyChatApp(
|
const FluffyChatApp(
|
||||||
{Key key, this.testWidget, @required this.client, this.queryParameters})
|
{Key key, this.testWidget, @required this.clients, this.queryParameters})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
/// getInitialLink may rereturn the value multiple times if this view is
|
/// 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>();
|
final GlobalKey<MatrixState> _matrix = GlobalKey<MatrixState>();
|
||||||
GlobalKey<VRouterState> _router;
|
GlobalKey<VRouterState> _router;
|
||||||
bool columnMode;
|
bool columnMode;
|
||||||
String _initialUrl = '/';
|
String _initialUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initialUrl =
|
||||||
|
widget.clients.any((client) => client.isLogged()) ? '/rooms' : '/home';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AdaptiveTheme(
|
return AdaptiveTheme(
|
||||||
@ -159,8 +148,8 @@ class _FluffyChatAppState extends State<FluffyChatApp> {
|
|||||||
key: _matrix,
|
key: _matrix,
|
||||||
context: context,
|
context: context,
|
||||||
router: _router,
|
router: _router,
|
||||||
client: widget.client,
|
clients: widget.clients,
|
||||||
child: WaitForInitPage(child),
|
child: child,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -32,6 +32,7 @@ import 'send_location_dialog.dart';
|
|||||||
import 'sticker_picker_dialog.dart';
|
import 'sticker_picker_dialog.dart';
|
||||||
import '../utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart';
|
import '../utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart';
|
||||||
import '../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
|
import '../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
|
||||||
|
import '../utils/account_bundles.dart';
|
||||||
|
|
||||||
class Chat extends StatefulWidget {
|
class Chat extends StatefulWidget {
|
||||||
final Widget sideView;
|
final Widget sideView;
|
||||||
@ -45,6 +46,8 @@ class Chat extends StatefulWidget {
|
|||||||
class ChatController extends State<Chat> {
|
class ChatController extends State<Chat> {
|
||||||
Room room;
|
Room room;
|
||||||
|
|
||||||
|
Client sendingClient;
|
||||||
|
|
||||||
Timeline timeline;
|
Timeline timeline;
|
||||||
|
|
||||||
MatrixState matrix;
|
MatrixState matrix;
|
||||||
@ -222,6 +225,14 @@ class ChatController extends State<Chat> {
|
|||||||
|
|
||||||
TextEditingController sendController = TextEditingController();
|
TextEditingController sendController = TextEditingController();
|
||||||
|
|
||||||
|
void setSendingClient(Client c) => setState(() {
|
||||||
|
sendingClient = c;
|
||||||
|
});
|
||||||
|
|
||||||
|
void setActiveClient(Client c) => setState(() {
|
||||||
|
Matrix.of(context).setActiveClient(c);
|
||||||
|
});
|
||||||
|
|
||||||
Future<void> send() async {
|
Future<void> send() async {
|
||||||
if (sendController.text.trim().isEmpty) return;
|
if (sendController.text.trim().isEmpty) return;
|
||||||
var parseCommands = true;
|
var parseCommands = true;
|
||||||
@ -447,19 +458,51 @@ class ChatController extends State<Chat> {
|
|||||||
for (final event in selectedEvents) {
|
for (final event in selectedEvents) {
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () =>
|
future: () async {
|
||||||
event.status > 0 ? event.redactEvent() : event.remove());
|
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());
|
setState(() => selectedEvents.clear());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Client> get currentRoomBundle {
|
||||||
|
final clients = matrix.currentBundle;
|
||||||
|
clients.removeWhere((c) => c.getRoomById(roomId) == null);
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
|
||||||
bool get canRedactSelectedEvents {
|
bool get canRedactSelectedEvents {
|
||||||
|
final clients = matrix.currentBundle;
|
||||||
for (final event in selectedEvents) {
|
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;
|
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 {
|
void forwardEventsAction() async {
|
||||||
if (selectedEvents.length == 1) {
|
if (selectedEvents.length == 1) {
|
||||||
Matrix.of(context).shareContent = selectedEvents.first.content;
|
Matrix.of(context).shareContent = selectedEvents.first.content;
|
||||||
@ -584,6 +627,13 @@ class ChatController extends State<Chat> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
void editSelectedEventAction() {
|
void editSelectedEventAction() {
|
||||||
|
final client = currentRoomBundle.firstWhere(
|
||||||
|
(cl) => selectedEvents.first.senderId == cl.userID,
|
||||||
|
orElse: () => null);
|
||||||
|
if (client == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSendingClient(client);
|
||||||
setState(() {
|
setState(() {
|
||||||
pendingText = sendController.text;
|
pendingText = sendController.text;
|
||||||
editEvent = selectedEvents.first;
|
editEvent = selectedEvents.first;
|
||||||
@ -689,6 +739,19 @@ class ChatController extends State<Chat> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onInputBarChanged(String text) {
|
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?.cancel();
|
||||||
typingCoolDown = Timer(Duration(seconds: 2), () {
|
typingCoolDown = Timer(Duration(seconds: 2), () {
|
||||||
typingCoolDown = null;
|
typingCoolDown = null;
|
||||||
|
@ -19,6 +19,7 @@ import 'package:uni_links/uni_links.dart';
|
|||||||
import 'package:vrouter/vrouter.dart';
|
import 'package:vrouter/vrouter.dart';
|
||||||
import '../main.dart';
|
import '../main.dart';
|
||||||
import '../widgets/matrix.dart';
|
import '../widgets/matrix.dart';
|
||||||
|
import '../../utils/account_bundles.dart';
|
||||||
import '../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
|
import '../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
|
||||||
import '../utils/url_launcher.dart';
|
import '../utils/url_launcher.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Matrix.of(context).navigatorContext = context;
|
Matrix.of(context).navigatorContext = context;
|
||||||
@ -424,3 +512,5 @@ class ChatListController extends State<ChatList> {
|
|||||||
return ChatListView(this);
|
return ChatListView(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum EditBundleAction { addToBundle, removeFromBundle }
|
||||||
|
@ -47,13 +47,13 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
|||||||
showFutureLoadingDialog(
|
showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () async {
|
||||||
if (Matrix.of(context).client.homeserver == null) {
|
if (Matrix.of(context).getLoginClient().homeserver == null) {
|
||||||
await Matrix.of(context).client.checkHomeserver(
|
await Matrix.of(context).getLoginClient().checkHomeserver(
|
||||||
await Store()
|
await Store()
|
||||||
.getItem(HomeserverPickerController.ssoHomeserverKey),
|
.getItem(HomeserverPickerController.ssoHomeserverKey),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await Matrix.of(context).client.login(
|
await Matrix.of(context).getLoginClient().login(
|
||||||
LoginType.mLoginToken,
|
LoginType.mLoginToken,
|
||||||
token: token,
|
token: token,
|
||||||
initialDeviceDisplayName: PlatformInfos.clientName,
|
initialDeviceDisplayName: PlatformInfos.clientName,
|
||||||
@ -117,7 +117,7 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
});
|
});
|
||||||
final wellKnown =
|
final wellKnown =
|
||||||
await Matrix.of(context).client.checkHomeserver(homeserver);
|
await Matrix.of(context).getLoginClient().checkHomeserver(homeserver);
|
||||||
|
|
||||||
var jitsi = wellKnown?.additionalProperties
|
var jitsi = wellKnown?.additionalProperties
|
||||||
?.tryGet<Map<String, dynamic>>('im.vector.riot.jitsi')
|
?.tryGet<Map<String, dynamic>>('im.vector.riot.jitsi')
|
||||||
@ -177,13 +177,13 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
|||||||
.any((flow) => flow['type'] == AuthenticationTypes.sso);
|
.any((flow) => flow['type'] == AuthenticationTypes.sso);
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getLoginTypes() async {
|
Future<Map<String, dynamic>> getLoginTypes() async {
|
||||||
_rawLoginTypes ??= await Matrix.of(context).client.request(
|
_rawLoginTypes ??= await Matrix.of(context).getLoginClient().request(
|
||||||
RequestType.GET,
|
RequestType.GET,
|
||||||
'/client/r0/login',
|
'/client/r0/login',
|
||||||
);
|
);
|
||||||
if (registrationSupported == null) {
|
if (registrationSupported == null) {
|
||||||
try {
|
try {
|
||||||
await Matrix.of(context).client.register();
|
await Matrix.of(context).getLoginClient().register();
|
||||||
registrationSupported = true;
|
registrationSupported = true;
|
||||||
} on MatrixException catch (e) {
|
} on MatrixException catch (e) {
|
||||||
registrationSupported = e.requireAdditionalAuthentication ?? false;
|
registrationSupported = e.requireAdditionalAuthentication ?? false;
|
||||||
@ -200,14 +200,14 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
|||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
// We store the homserver in the local storage instead of a redirect
|
// We store the homserver in the local storage instead of a redirect
|
||||||
// parameter because of possible CSRF attacks.
|
// parameter because of possible CSRF attacks.
|
||||||
Store().setItem(
|
Store().setItem(ssoHomeserverKey,
|
||||||
ssoHomeserverKey, Matrix.of(context).client.homeserver.toString());
|
Matrix.of(context).getLoginClient().homeserver.toString());
|
||||||
}
|
}
|
||||||
final redirectUrl = kIsWeb
|
final redirectUrl = kIsWeb
|
||||||
? AppConfig.webBaseUrl + '/#/'
|
? AppConfig.webBaseUrl + '/#/'
|
||||||
: AppConfig.appOpenUrlScheme.toLowerCase() + '://login';
|
: AppConfig.appOpenUrlScheme.toLowerCase() + '://login';
|
||||||
final url =
|
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) {
|
if (PlatformInfos.isMobile) {
|
||||||
browser ??= ChromeSafariBrowser();
|
browser ??= ChromeSafariBrowser();
|
||||||
browser.open(url: Uri.parse(url));
|
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;
|
bool _initialized = false;
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ class LoginController extends State<Login> {
|
|||||||
} else {
|
} else {
|
||||||
identifier = AuthenticationUserIdentifier(user: username);
|
identifier = AuthenticationUserIdentifier(user: username);
|
||||||
}
|
}
|
||||||
await matrix.client.login(LoginType.mLoginPassword,
|
await matrix.getLoginClient().login(LoginType.mLoginPassword,
|
||||||
identifier: identifier,
|
identifier: identifier,
|
||||||
// To stay compatible with older server versions
|
// To stay compatible with older server versions
|
||||||
// ignore: deprecated_member_use
|
// ignore: deprecated_member_use
|
||||||
@ -98,12 +98,13 @@ class LoginController extends State<Login> {
|
|||||||
setState(() => usernameError = null);
|
setState(() => usernameError = null);
|
||||||
if (!userId.isValidMatrixId) return;
|
if (!userId.isValidMatrixId) return;
|
||||||
try {
|
try {
|
||||||
final oldHomeserver = Matrix.of(context).client.homeserver;
|
final oldHomeserver = Matrix.of(context).getLoginClient().homeserver;
|
||||||
var newDomain = Uri.https(userId.domain, '');
|
var newDomain = Uri.https(userId.domain, '');
|
||||||
Matrix.of(context).client.homeserver = newDomain;
|
Matrix.of(context).getLoginClient().homeserver = newDomain;
|
||||||
DiscoveryInformation wellKnownInformation;
|
DiscoveryInformation wellKnownInformation;
|
||||||
try {
|
try {
|
||||||
wellKnownInformation = await Matrix.of(context).client.getWellknown();
|
wellKnownInformation =
|
||||||
|
await Matrix.of(context).getLoginClient().getWellknown();
|
||||||
if (wellKnownInformation.mHomeserver?.baseUrl?.toString()?.isNotEmpty ??
|
if (wellKnownInformation.mHomeserver?.baseUrl?.toString()?.isNotEmpty ??
|
||||||
false) {
|
false) {
|
||||||
newDomain = wellKnownInformation.mHomeserver.baseUrl;
|
newDomain = wellKnownInformation.mHomeserver.baseUrl;
|
||||||
@ -120,8 +121,8 @@ class LoginController extends State<Login> {
|
|||||||
.checkHomeserver(newDomain)
|
.checkHomeserver(newDomain)
|
||||||
.catchError((e) => null),
|
.catchError((e) => null),
|
||||||
);
|
);
|
||||||
if (Matrix.of(context).client.homeserver == null) {
|
if (Matrix.of(context).getLoginClient().homeserver == null) {
|
||||||
Matrix.of(context).client.homeserver = oldHomeserver;
|
Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
|
||||||
// okay, the server we checked does not appear to be a matrix server
|
// okay, the server we checked does not appear to be a matrix server
|
||||||
Logs().v(
|
Logs().v(
|
||||||
'$newDomain is not running a homeserver, asking to use $oldHomeserver');
|
'$newDomain is not running a homeserver, asking to use $oldHomeserver');
|
||||||
@ -178,11 +179,12 @@ class LoginController extends State<Login> {
|
|||||||
Matrix.of(context).client.generateUniqueTransactionId();
|
Matrix.of(context).client.generateUniqueTransactionId();
|
||||||
final response = await showFutureLoadingDialog(
|
final response = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () => Matrix.of(context).client.requestTokenToResetPasswordEmail(
|
future: () =>
|
||||||
clientSecret,
|
Matrix.of(context).getLoginClient().requestTokenToResetPasswordEmail(
|
||||||
input.single,
|
clientSecret,
|
||||||
sendAttempt++,
|
input.single,
|
||||||
),
|
sendAttempt++,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (response.error != null) return;
|
if (response.error != null) return;
|
||||||
final ok = await showOkAlertDialog(
|
final ok = await showOkAlertDialog(
|
||||||
@ -211,7 +213,7 @@ class LoginController extends State<Login> {
|
|||||||
if (password == null) return;
|
if (password == null) return;
|
||||||
final success = await showFutureLoadingDialog(
|
final success = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () => Matrix.of(context).client.changePassword(
|
future: () => Matrix.of(context).getLoginClient().changePassword(
|
||||||
password.single,
|
password.single,
|
||||||
auth: AuthenticationThreePidCreds(
|
auth: AuthenticationThreePidCreds(
|
||||||
type: AuthenticationTypes.emailIdentity,
|
type: AuthenticationTypes.emailIdentity,
|
||||||
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
import 'package:vrouter/vrouter.dart';
|
||||||
|
|
||||||
class SettingsAccount extends StatefulWidget {
|
class SettingsAccount extends StatefulWidget {
|
||||||
const SettingsAccount({Key key}) : super(key: key);
|
const SettingsAccount({Key key}) : super(key: key);
|
||||||
@ -144,6 +145,8 @@ class SettingsAccountController extends State<SettingsAccount> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void addAccountAction() => VRouter.of(context).to('add');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final client = Matrix.of(context).client;
|
final client = Matrix.of(context).client;
|
||||||
|
@ -37,7 +37,7 @@ class SignupPageController extends State<SignupPage> {
|
|||||||
setState(() => loading = true);
|
setState(() => loading = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final client = Matrix.of(context).client;
|
final client = Matrix.of(context).getLoginClient();
|
||||||
await client.uiaRequestBackground(
|
await client.uiaRequestBackground(
|
||||||
(auth) => client.register(
|
(auth) => client.register(
|
||||||
username: usernameController.text,
|
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:fluffychat/widgets/avatar.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
import 'package:fluffychat/pages/chat_list.dart';
|
import 'package:fluffychat/pages/chat_list.dart';
|
||||||
import 'package:fluffychat/widgets/connection_status_header.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:flutter/material.dart';
|
||||||
import 'package:vrouter/vrouter.dart';
|
import 'package:vrouter/vrouter.dart';
|
||||||
import '../../widgets/matrix.dart';
|
import '../../widgets/matrix.dart';
|
||||||
|
import '../../utils/account_bundles.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import '../../utils/stream_extension.dart';
|
import '../../utils/stream_extension.dart';
|
||||||
|
|
||||||
@ -17,6 +23,59 @@ class ChatListView extends StatelessWidget {
|
|||||||
|
|
||||||
const ChatListView(this.controller, {Key key}) : super(key: key);
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return StreamBuilder<Object>(
|
return StreamBuilder<Object>(
|
||||||
@ -216,12 +275,98 @@ class ChatListView extends StatelessWidget {
|
|||||||
child: Icon(CupertinoIcons.chat_bubble),
|
child: Icon(CupertinoIcons.chat_bubble),
|
||||||
)
|
)
|
||||||
: null,
|
: 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
|
drawer: controller.spaces.isEmpty
|
||||||
? null
|
? null
|
||||||
: Drawer(
|
: Drawer(
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: controller.spaces.length + 1,
|
itemCount: controller.spaces.length,
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
|
@ -37,9 +37,10 @@ class ChatView extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
controller.matrix = Matrix.of(context);
|
controller.matrix ??= Matrix.of(context);
|
||||||
final client = controller.matrix.client;
|
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) {
|
if (controller.room == null) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@ -147,10 +148,7 @@ class ChatView extends StatelessWidget {
|
|||||||
: Text(controller.selectedEvents.length.toString()),
|
: Text(controller.selectedEvents.length.toString()),
|
||||||
actions: controller.selectMode
|
actions: controller.selectMode
|
||||||
? <Widget>[
|
? <Widget>[
|
||||||
if (controller.selectedEvents.length == 1 &&
|
if (controller.canEditSelectedEvents)
|
||||||
controller.selectedEvents.first.status > 0 &&
|
|
||||||
controller.selectedEvents.first.senderId ==
|
|
||||||
client.userID)
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.edit_outlined),
|
icon: Icon(Icons.edit_outlined),
|
||||||
tooltip: L10n.of(context).edit,
|
tooltip: L10n.of(context).edit,
|
||||||
@ -680,6 +678,14 @@ class ChatView extends StatelessWidget {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: EncryptionButton(controller.room),
|
child: EncryptionButton(controller.room),
|
||||||
),
|
),
|
||||||
|
if (controller.matrix.isMultiAccount &&
|
||||||
|
controller.matrix.currentBundle.length >
|
||||||
|
1)
|
||||||
|
Container(
|
||||||
|
height: 56,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: _ChatAccountPicker(controller),
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
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(
|
imageUrl: Uri.parse(
|
||||||
identityProvider.icon)
|
identityProvider.icon)
|
||||||
.getDownloadLink(
|
.getDownloadLink(
|
||||||
Matrix.of(context).client)
|
Matrix.of(context)
|
||||||
|
.getLoginClient())
|
||||||
.toString(),
|
.toString(),
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
@ -128,7 +129,7 @@ class HomeserverPickerView extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: _LoginButton(
|
child: _LoginButton(
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
VRouter.of(context).to('/login'),
|
VRouter.of(context).to('login'),
|
||||||
icon: Icon(Icons.login_outlined),
|
icon: Icon(Icons.login_outlined),
|
||||||
labelText: L10n.of(context).login,
|
labelText: L10n.of(context).login,
|
||||||
),
|
),
|
||||||
|
@ -19,7 +19,7 @@ class LoginView extends StatelessWidget {
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: Text(
|
title: Text(
|
||||||
L10n.of(context).logInTo(Matrix.of(context)
|
L10n.of(context).logInTo(Matrix.of(context)
|
||||||
.client
|
.getLoginClient()
|
||||||
.homeserver
|
.homeserver
|
||||||
.toString()
|
.toString()
|
||||||
.replaceFirst('https://', '')),
|
.replaceFirst('https://', '')),
|
||||||
|
@ -20,6 +20,13 @@ class SettingsAccountView extends StatelessWidget {
|
|||||||
withScrolling: true,
|
withScrolling: true,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
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(
|
ListTile(
|
||||||
trailing: Icon(Icons.edit_outlined),
|
trailing: Icon(Icons.edit_outlined),
|
||||||
title: Text(L10n.of(context).editDisplayname),
|
title: Text(L10n.of(context).editDisplayname),
|
||||||
@ -38,6 +45,7 @@ class SettingsAccountView extends StatelessWidget {
|
|||||||
title: Text(L10n.of(context).devices),
|
title: Text(L10n.of(context).devices),
|
||||||
onTap: () => VRouter.of(context).to('devices'),
|
onTap: () => VRouter.of(context).to('devices'),
|
||||||
),
|
),
|
||||||
|
Divider(height: 1),
|
||||||
ListTile(
|
ListTile(
|
||||||
trailing: Icon(Icons.exit_to_app_outlined),
|
trailing: Icon(Icons.exit_to_app_outlined),
|
||||||
title: Text(L10n.of(context).logout),
|
title: Text(L10n.of(context).logout),
|
||||||
|
@ -38,7 +38,7 @@ class SignupPageView extends StatelessWidget {
|
|||||||
labelText: L10n.of(context).username,
|
labelText: L10n.of(context).username,
|
||||||
prefixText: '@',
|
prefixText: '@',
|
||||||
suffixText:
|
suffixText:
|
||||||
':${Matrix.of(context).client.homeserver.host}'),
|
':${Matrix.of(context).getLoginClient().homeserver.host}'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Divider(),
|
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 {
|
class LoadingView extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (Matrix.of(context).loginState != null) {
|
if (Matrix.of(context)
|
||||||
|
.widget
|
||||||
|
.clients
|
||||||
|
.every((client) => client.loginState != null)) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
(_) => VRouter.of(context).to(
|
(_) => VRouter.of(context).to(
|
||||||
Matrix.of(context).loginState == LoginState.loggedIn
|
Matrix.of(context)
|
||||||
|
.widget
|
||||||
|
.clients
|
||||||
|
.any((client) => client.loginState == LoginState.loggedIn)
|
||||||
? '/rooms'
|
? '/rooms'
|
||||||
: '/home',
|
: '/home',
|
||||||
queryParameters: VRouter.of(context).queryParameters,
|
queryParameters: VRouter.of(context).queryParameters,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:fluffychat/config/themes.dart';
|
import 'package:fluffychat/config/themes.dart';
|
||||||
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class OnePageCard extends StatelessWidget {
|
class OnePageCard extends StatelessWidget {
|
||||||
@ -12,7 +13,8 @@ class OnePageCard extends StatelessWidget {
|
|||||||
static num breakpoint = FluffyThemes.columnWidth * 2;
|
static num breakpoint = FluffyThemes.columnWidth * 2;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MediaQuery.of(context).size.width <= breakpoint
|
return MediaQuery.of(context).size.width <= breakpoint ||
|
||||||
|
Matrix.of(context).client.isLogged()
|
||||||
? child
|
? child
|
||||||
: Container(
|
: Container(
|
||||||
decoration: BoxDecoration(
|
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 'dart:convert';
|
||||||
|
|
||||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||||
|
import 'package:fluffychat/utils/client_manager.dart';
|
||||||
import 'package:matrix/encryption.dart';
|
import 'package:matrix/encryption.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.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 '../utils/platform_infos.dart';
|
||||||
import '../config/app_config.dart';
|
import '../config/app_config.dart';
|
||||||
import '../config/setting_keys.dart';
|
import '../config/setting_keys.dart';
|
||||||
|
import '../utils/account_bundles.dart';
|
||||||
import '../utils/background_push.dart';
|
import '../utils/background_push.dart';
|
||||||
import 'package:vrouter/vrouter.dart';
|
import 'package:vrouter/vrouter.dart';
|
||||||
|
|
||||||
@ -35,7 +37,7 @@ class Matrix extends StatefulWidget {
|
|||||||
|
|
||||||
final BuildContext context;
|
final BuildContext context;
|
||||||
|
|
||||||
final Client client;
|
final List<Client> clients;
|
||||||
|
|
||||||
final Map<String, String> queryParameters;
|
final Map<String, String> queryParameters;
|
||||||
|
|
||||||
@ -43,7 +45,7 @@ class Matrix extends StatefulWidget {
|
|||||||
this.child,
|
this.child,
|
||||||
@required this.router,
|
@required this.router,
|
||||||
@required this.context,
|
@required this.context,
|
||||||
@required this.client,
|
@required this.clients,
|
||||||
this.queryParameters,
|
this.queryParameters,
|
||||||
Key key,
|
Key key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
@ -57,12 +59,98 @@ class Matrix extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MatrixState extends State<Matrix> with WidgetsBindingObserver {
|
class MatrixState extends State<Matrix> with WidgetsBindingObserver {
|
||||||
Client get client => widget.client;
|
int activeClient = 0;
|
||||||
|
String activeBundle;
|
||||||
Store store = Store();
|
Store store = Store();
|
||||||
BuildContext navigatorContext;
|
BuildContext navigatorContext;
|
||||||
|
|
||||||
BackgroundPush _backgroundPush;
|
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;
|
Map<String, dynamic> get shareContent => _shareContent;
|
||||||
set shareContent(Map<String, dynamic> content) {
|
set shareContent(Map<String, dynamic> content) {
|
||||||
_shareContent = content;
|
_shareContent = content;
|
||||||
@ -78,8 +166,8 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
void _initWithStore() async {
|
void _initWithStore() async {
|
||||||
try {
|
try {
|
||||||
await client.init();
|
|
||||||
if (client.isLogged()) {
|
if (client.isLogged()) {
|
||||||
|
// TODO: Figure out how this works in multi account
|
||||||
final statusMsg = await store.getItem(SettingKeys.ownStatusMessage);
|
final statusMsg = await store.getItem(SettingKeys.ownStatusMessage);
|
||||||
if (statusMsg?.isNotEmpty ?? false) {
|
if (statusMsg?.isNotEmpty ?? false) {
|
||||||
Logs().v('Send cached status message: "$statusMsg"');
|
Logs().v('Send cached status message: "$statusMsg"');
|
||||||
@ -97,15 +185,15 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription onRoomKeyRequestSub;
|
final onRoomKeyRequestSub = <String, StreamSubscription>{};
|
||||||
StreamSubscription onKeyVerificationRequestSub;
|
final onKeyVerificationRequestSub = <String, StreamSubscription>{};
|
||||||
StreamSubscription onJitsiCallSub;
|
final onJitsiCallSub = <String, StreamSubscription>{};
|
||||||
StreamSubscription onNotification;
|
final onNotification = <String, StreamSubscription>{};
|
||||||
StreamSubscription<LoginState> onLoginStateChanged;
|
final onLoginStateChanged = <String, StreamSubscription<LoginState>>{};
|
||||||
StreamSubscription<UiaRequest> onUiaRequest;
|
final onUiaRequest = <String, StreamSubscription<UiaRequest>>{};
|
||||||
StreamSubscription<html.Event> onFocusSub;
|
StreamSubscription<html.Event> onFocusSub;
|
||||||
StreamSubscription<html.Event> onBlurSub;
|
StreamSubscription<html.Event> onBlurSub;
|
||||||
StreamSubscription<Presence> onOwnPresence;
|
final onOwnPresence = <String, StreamSubscription<Presence>>{};
|
||||||
|
|
||||||
String _cachedPassword;
|
String _cachedPassword;
|
||||||
String get cachedPassword {
|
String get cachedPassword {
|
||||||
@ -165,14 +253,12 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
|
|||||||
return uiaRequest
|
return uiaRequest
|
||||||
.cancel(Exception(L10n.of(context).serverRequiresEmail));
|
.cancel(Exception(L10n.of(context).serverRequiresEmail));
|
||||||
}
|
}
|
||||||
final clientSecret =
|
final clientSecret = client.generateUniqueTransactionId();
|
||||||
Matrix.of(context).client.generateUniqueTransactionId();
|
final currentThreepidCreds = await client.requestTokenToRegisterEmail(
|
||||||
final currentThreepidCreds =
|
clientSecret,
|
||||||
await Matrix.of(context).client.requestTokenToRegisterEmail(
|
emailInput.single,
|
||||||
clientSecret,
|
0,
|
||||||
emailInput.single,
|
);
|
||||||
0,
|
|
||||||
);
|
|
||||||
final auth = AuthenticationThreePidCreds(
|
final auth = AuthenticationThreePidCreds(
|
||||||
session: uiaRequest.session,
|
session: uiaRequest.session,
|
||||||
type: AuthenticationTypes.emailIdentity,
|
type: AuthenticationTypes.emailIdentity,
|
||||||
@ -289,21 +375,14 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LoginState loginState;
|
void _registerSubs(String name) {
|
||||||
|
final c = getClientByName(name);
|
||||||
void initMatrix() {
|
if (c == null) {
|
||||||
// Display the app lock
|
Logs().w(
|
||||||
if (PlatformInfos.isMobile) {
|
'Attempted to register subscriptions for non-existing client $name');
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
return;
|
||||||
FlutterSecureStorage().read(key: SettingKeys.appLockKey).then((lock) {
|
|
||||||
if (lock?.isNotEmpty ?? false) {
|
|
||||||
AppLock.of(widget.context).enable();
|
|
||||||
AppLock.of(widget.context).showLockScreen();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
onKeyVerificationRequestSub ??= client.onKeyVerificationRequest.stream
|
onKeyVerificationRequestSub[name] ??= c.onKeyVerificationRequest.stream
|
||||||
.listen((KeyVerification request) async {
|
.listen((KeyVerification request) async {
|
||||||
var hidPopup = false;
|
var hidPopup = false;
|
||||||
request.onUpdate = () {
|
request.onUpdate = () {
|
||||||
@ -334,51 +413,95 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
|
|||||||
await request.rejectVerification();
|
await request.rejectVerification();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_initWithStore();
|
onLoginStateChanged[name] ??= c.onLoginStateChanged.stream.listen((state) {
|
||||||
|
final loggedInWithMultipleClients = widget.clients.length > 1;
|
||||||
if (kIsWeb) {
|
if (state != LoginState.loggedIn) {
|
||||||
onFocusSub = html.window.onFocus.listen((_) => webHasFocus = true);
|
_cancelSubs(c.clientName);
|
||||||
onBlurSub = html.window.onBlur.listen((_) => webHasFocus = false);
|
widget.clients.remove(c);
|
||||||
}
|
}
|
||||||
onLoginStateChanged ??= client.onLoginStateChanged.stream.listen((state) {
|
if (loggedInWithMultipleClients) {
|
||||||
if (loginState != state) {
|
// TODO: display a nicer toast
|
||||||
loginState = state;
|
showOkAlertDialog(
|
||||||
final isInLoginRoutes = {'/home', '/login', '/signup'}
|
useRootNavigator: false,
|
||||||
.contains(widget.router.currentState.url);
|
context: navigatorContext,
|
||||||
if (widget.router.currentState.url == '/' ||
|
title: 'Login state of client $name changed',
|
||||||
(state == LoginState.loggedIn) == isInLoginRoutes) {
|
message: 'New login state: $state',
|
||||||
|
okLabel: L10n.of(widget.context).ok,
|
||||||
|
);
|
||||||
|
if (state != LoginState.loggedIn) {
|
||||||
widget.router.currentState.to(
|
widget.router.currentState.to(
|
||||||
loginState == LoginState.loggedIn ? '/rooms' : '/home',
|
'/rooms',
|
||||||
queryParameters: widget.router.currentState.queryParameters,
|
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
|
// Cache and resend status message
|
||||||
onOwnPresence ??= client.onPresence.stream.listen((presence) {
|
onOwnPresence[name] ??= c.onPresence.stream.listen((presence) {
|
||||||
if (client.isLogged() &&
|
if (c.isLogged() &&
|
||||||
client.userID == presence.senderId &&
|
c.userID == presence.senderId &&
|
||||||
presence.presence?.statusMsg != null) {
|
presence.presence?.statusMsg != null) {
|
||||||
Logs().v('Update status message: "${presence.presence.statusMsg}"');
|
Logs().v('Update status message: "${presence.presence.statusMsg}"');
|
||||||
store.setItem(
|
store.setItem(
|
||||||
SettingKeys.ownStatusMessage, presence.presence.statusMsg);
|
SettingKeys.ownStatusMessage, presence.presence.statusMsg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
onUiaRequest[name] ??= c.onUiaRequest.stream.listen(_onUiaRequest);
|
||||||
onUiaRequest ??= client.onUiaRequest.stream.listen(_onUiaRequest);
|
|
||||||
if (PlatformInfos.isWeb || PlatformInfos.isLinux) {
|
if (PlatformInfos.isWeb || PlatformInfos.isLinux) {
|
||||||
client.onSync.stream.first.then((s) {
|
c.onSync.stream.first.then((s) {
|
||||||
html.Notification.requestPermission();
|
html.Notification.requestPermission();
|
||||||
onNotification ??= client.onEvent.stream
|
onNotification[name] ??= c.onEvent.stream
|
||||||
.where((e) =>
|
.where((e) =>
|
||||||
e.type == EventUpdateType.timeline &&
|
e.type == EventUpdateType.timeline &&
|
||||||
[EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted]
|
[EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted]
|
||||||
.contains(e.content['type']) &&
|
.contains(e.content['type']) &&
|
||||||
e.content['sender'] != client.userID)
|
e.content['sender'] != c.userID)
|
||||||
.listen(_showLocalNotification);
|
.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) {
|
if (PlatformInfos.isMobile) {
|
||||||
_backgroundPush = BackgroundPush(
|
_backgroundPush = BackgroundPush(
|
||||||
@ -445,11 +568,12 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
|
||||||
onRoomKeyRequestSub?.cancel();
|
onRoomKeyRequestSub.values.map((s) => s.cancel());
|
||||||
onKeyVerificationRequestSub?.cancel();
|
onKeyVerificationRequestSub.values.map((s) => s.cancel());
|
||||||
onLoginStateChanged?.cancel();
|
onLoginStateChanged.values.map((s) => s.cancel());
|
||||||
onOwnPresence?.cancel();
|
onOwnPresence.values.map((s) => s.cancel());
|
||||||
onNotification?.cancel();
|
onNotification.values.map((s) => s.cancel());
|
||||||
|
|
||||||
onFocusSub?.cancel();
|
onFocusSub?.cancel();
|
||||||
onBlurSub?.cancel();
|
onBlurSub?.cancel();
|
||||||
_backgroundPush?.onLogin?.cancel();
|
_backgroundPush?.onLogin?.cancel();
|
||||||
@ -491,3 +615,9 @@ class FixedThreepidCreds extends ThreepidCreds {
|
|||||||
return data;
|
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 file_selector_macos
|
||||||
import firebase_core
|
import firebase_core
|
||||||
import flutter_local_notifications
|
import flutter_local_notifications
|
||||||
|
import geolocator_apple
|
||||||
import package_info_plus_macos
|
import package_info_plus_macos
|
||||||
import path_provider_macos
|
import path_provider_macos
|
||||||
import share_plus_macos
|
import share_plus_macos
|
||||||
@ -22,6 +23,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
|
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||||
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
|
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
|
@ -23,6 +23,8 @@ PODS:
|
|||||||
- FMDB (2.7.5):
|
- FMDB (2.7.5):
|
||||||
- FMDB/standard (= 2.7.5)
|
- FMDB/standard (= 2.7.5)
|
||||||
- FMDB/standard (2.7.5)
|
- FMDB/standard (2.7.5)
|
||||||
|
- geolocator_apple (1.2.0):
|
||||||
|
- FlutterMacOS
|
||||||
- GoogleDataTransport (8.4.0):
|
- GoogleDataTransport (8.4.0):
|
||||||
- GoogleUtilities/Environment (~> 7.2)
|
- GoogleUtilities/Environment (~> 7.2)
|
||||||
- nanopb (~> 2.30908.0)
|
- nanopb (~> 2.30908.0)
|
||||||
@ -59,6 +61,7 @@ DEPENDENCIES:
|
|||||||
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
|
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
|
||||||
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- 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`)
|
- 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`)
|
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
|
||||||
- share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_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
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
|
||||||
FlutterMacOS:
|
FlutterMacOS:
|
||||||
:path: Flutter/ephemeral
|
:path: Flutter/ephemeral
|
||||||
|
geolocator_apple:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos
|
||||||
package_info_plus_macos:
|
package_info_plus_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos
|
||||||
path_provider_macos:
|
path_provider_macos:
|
||||||
@ -114,6 +119,7 @@ SPEC CHECKSUMS:
|
|||||||
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
|
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
|
||||||
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
|
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
|
geolocator_apple: 821be05bbdb1b49500e029ebcbf2d6acf2dfb966
|
||||||
GoogleDataTransport: cd9db2180fcecd8da1b561aea31e3e56cf834aa7
|
GoogleDataTransport: cd9db2180fcecd8da1b561aea31e3e56cf834aa7
|
||||||
GoogleUtilities: 284cddc7fffc14ae1907efb6f78ab95c1fccaedc
|
GoogleUtilities: 284cddc7fffc14ae1907efb6f78ab95c1fccaedc
|
||||||
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
|
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
|
||||||
@ -128,4 +134,4 @@ SPEC CHECKSUMS:
|
|||||||
|
|
||||||
PODFILE CHECKSUM: c7161fcf45d4fd9025dc0f48a76d6e64e52f8176
|
PODFILE CHECKSUM: c7161fcf45d4fd9025dc0f48a76d6e64e52f8176
|
||||||
|
|
||||||
COCOAPODS: 1.10.1
|
COCOAPODS: 1.11.0
|
||||||
|
@ -269,6 +269,7 @@
|
|||||||
"${BUILT_PRODUCTS_DIR}/audioplayers/audioplayers.framework",
|
"${BUILT_PRODUCTS_DIR}/audioplayers/audioplayers.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/file_selector_macos/file_selector_macos.framework",
|
"${BUILT_PRODUCTS_DIR}/file_selector_macos/file_selector_macos.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.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}/nanopb/nanopb.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/package_info_plus_macos/package_info_plus_macos.framework",
|
"${BUILT_PRODUCTS_DIR}/package_info_plus_macos/package_info_plus_macos.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/path_provider_macos/path_provider_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}/audioplayers.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_selector_macos.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}/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}/nanopb.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus_macos.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus_macos.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_macos.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_macos.framework",
|
||||||
|
Loading…
Reference in New Issue
Block a user