diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index d57061dd6..ab8e063fe 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 7f38bb254..6bea1100c 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -2,15 +2,15 @@ import UIKit import Flutter @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - if #available(iOS 10.0, *) { - UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate - } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index c94401f48..51765d1c9 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -112,5 +112,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index e55b8251d..d8751f9cb 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -440,25 +440,16 @@ class ChatListController extends State PopupMenuItem( value: ChatContextAction.open, child: Row( - mainAxisSize: .min, spacing: 12.0, children: [ - Avatar(mxContent: room.avatar, name: displayname), + Avatar(mxContent: room.avatar, name: displayname, size: 24), ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 128), - child: Text( - displayname, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), + constraints: const BoxConstraints(maxWidth: 200), + child: Text(displayname, maxLines: 1, overflow: .ellipsis), ), ], ), ), - const PopupMenuDivider(), if (space != null) PopupMenuItem( value: ChatContextAction.goToSpace, diff --git a/lib/pages/login/login.dart b/lib/pages/login/login.dart index c0a3e10ab..b1eecd0b8 100644 --- a/lib/pages/login/login.dart +++ b/lib/pages/login/login.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/l10n/l10n.dart'; @@ -81,6 +82,9 @@ class LoginController extends State { password: passwordController.text, initialDeviceDisplayName: PlatformInfos.clientName, ); + if (mounted) { + context.go('/backup'); + } } on MatrixException catch (exception) { setState(() => passwordError = exception.errorMessage); return setState(() => loading = false); diff --git a/lib/pages/sign_in/sign_in_page.dart b/lib/pages/sign_in/sign_in_page.dart index e86d39bb1..77f09df98 100644 --- a/lib/pages/sign_in/sign_in_page.dart +++ b/lib/pages/sign_in/sign_in_page.dart @@ -37,164 +37,165 @@ class SignInPage extends StatelessWidget { ? L10n.of(context).createNewAccount : L10n.of(context).login, ), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(56 + 60), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: .min, - crossAxisAlignment: .center, - spacing: 12, - children: [ - SelectableText( - signUp - ? L10n.of(context).signUpGreeting - : L10n.of(context).signInGreeting, - textAlign: .center, + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + spacing: 16, + children: [ + SelectableText( + signUp + ? L10n.of(context).signUpGreeting + : L10n.of(context).signInGreeting, + textAlign: .center, + ), + TextField( + readOnly: + state.publicHomeservers.connectionState == + ConnectionState.waiting, + controller: viewModel.filterTextController, + autocorrect: false, + keyboardType: TextInputType.url, + decoration: InputDecoration( + filled: true, + fillColor: theme.colorScheme.secondaryContainer, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(99), ), - TextField( - readOnly: - state.publicHomeservers.connectionState == - ConnectionState.waiting, - controller: viewModel.filterTextController, - autocorrect: false, - keyboardType: TextInputType.url, - decoration: InputDecoration( - filled: true, - fillColor: theme.colorScheme.secondaryContainer, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(99), + errorText: state.publicHomeservers.error?.toLocalizedString( + context, + ), + prefixIcon: const Icon(Icons.search_outlined), + hintText: L10n.of(context).searchOrEnterHomeserverAddress, + ), + ), + if (state.publicHomeservers.connectionState == + ConnectionState.done) + Expanded( + child: Material( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + clipBehavior: Clip.hardEdge, + color: theme.colorScheme.surfaceContainerLow, + child: RadioGroup( + groupValue: state.selectedHomeserver, + onChanged: viewModel.selectHomeserver, + child: ListView.builder( + itemCount: publicHomeservers.length, + itemBuilder: (context, i) { + final server = publicHomeservers[i]; + final website = server.website; + return RadioListTile.adaptive( + value: server, + radioScaleFactor: + FluffyThemes.isColumnMode(context) || + { + TargetPlatform.iOS, + TargetPlatform.macOS, + }.contains(theme.platform) + ? 2 + : 1, + title: Row( + children: [ + Expanded( + child: Text(server.name ?? 'Unknown'), + ), + if (website != null) + SizedBox.square( + dimension: 32, + child: IconButton( + icon: const Icon( + Icons.open_in_new_outlined, + size: 16, + ), + onPressed: () => + launchUrlString(website), + ), + ), + ], + ), + subtitle: Column( + spacing: 4.0, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (server.features?.isNotEmpty == true) + Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: [ + ...?server.languages?.map( + (language) => Material( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + color: theme + .colorScheme + .tertiaryContainer, + child: Padding( + padding: + const EdgeInsets.symmetric( + horizontal: 6.0, + vertical: 3.0, + ), + child: Text( + language, + style: TextStyle( + fontSize: 10, + color: theme + .colorScheme + .onTertiaryContainer, + ), + ), + ), + ), + ), + ...server.features!.map( + (feature) => Material( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + color: theme + .colorScheme + .secondaryContainer, + child: Padding( + padding: + const EdgeInsets.symmetric( + horizontal: 6.0, + vertical: 3.0, + ), + child: Text( + feature, + style: TextStyle( + fontSize: 10, + color: theme + .colorScheme + .onSecondaryContainer, + ), + ), + ), + ), + ), + ], + ), + Text( + server.description ?? 'A matrix homeserver', + ), + ], + ), + ); + }, ), - errorText: state.publicHomeservers.error - ?.toLocalizedString(context), - prefixIcon: const Icon(Icons.search_outlined), - hintText: L10n.of( - context, - ).searchOrEnterHomeserverAddress, ), ), - ], - ), - ), + ) + else + Center(child: CircularProgressIndicator.adaptive()), + ], ), ), - body: state.publicHomeservers.connectionState == ConnectionState.done - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Material( - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - clipBehavior: Clip.hardEdge, - color: theme.colorScheme.surfaceContainerLow, - child: RadioGroup( - groupValue: state.selectedHomeserver, - onChanged: viewModel.selectHomeserver, - child: ListView.builder( - itemCount: publicHomeservers.length, - itemBuilder: (context, i) { - final server = publicHomeservers[i]; - final homepage = server.homepage; - return RadioListTile.adaptive( - value: server, - radioScaleFactor: - FluffyThemes.isColumnMode(context) || - { - TargetPlatform.iOS, - TargetPlatform.macOS, - }.contains(theme.platform) - ? 2 - : 1, - title: Row( - children: [ - Expanded(child: Text(server.name ?? 'Unknown')), - if (homepage != null) - SizedBox.square( - dimension: 32, - child: IconButton( - icon: const Icon( - Icons.open_in_new_outlined, - size: 16, - ), - onPressed: () => - launchUrlString(homepage), - ), - ), - ], - ), - subtitle: Column( - spacing: 4.0, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - if (server.features?.isNotEmpty == true) - Wrap( - spacing: 4.0, - runSpacing: 4.0, - children: [ - ...?server.languages?.map( - (language) => Material( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, - ), - color: theme - .colorScheme - .tertiaryContainer, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 6.0, - vertical: 3.0, - ), - child: Text( - language, - style: TextStyle( - fontSize: 10, - color: theme - .colorScheme - .onTertiaryContainer, - ), - ), - ), - ), - ), - ...server.features!.map( - (feature) => Material( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, - ), - color: theme - .colorScheme - .secondaryContainer, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 6.0, - vertical: 3.0, - ), - child: Text( - feature, - style: TextStyle( - fontSize: 10, - color: theme - .colorScheme - .onSecondaryContainer, - ), - ), - ), - ), - ), - ], - ), - Text( - server.description ?? 'A matrix homeserver', - ), - ], - ), - ); - }, - ), - ), - ), - ) - : Center(child: CircularProgressIndicator.adaptive()), bottomNavigationBar: AnimatedSize( duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, @@ -203,12 +204,14 @@ class SignInPage extends StatelessWidget { !publicHomeservers.contains(selectedHomserver) ? const SizedBox.shrink() : Material( - elevation: 8, - shadowColor: theme.appBarTheme.shadowColor, child: Padding( padding: const EdgeInsets.all(16.0), child: SafeArea( child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + ), onPressed: state.loginLoading.connectionState == ConnectionState.waiting diff --git a/lib/pages/sign_in/utils/sort_homeservers.dart b/lib/pages/sign_in/utils/sort_homeservers.dart index 7c54a01b8..18f30dfce 100644 --- a/lib/pages/sign_in/utils/sort_homeservers.dart +++ b/lib/pages/sign_in/utils/sort_homeservers.dart @@ -7,7 +7,7 @@ int sortHomeservers(PublicHomeserverData a, PublicHomeserverData b) { int _calcHomeserverScore(PublicHomeserverData homeserver) { var score = 0; if (homeserver.description?.isNotEmpty == true) score++; - if (homeserver.homepage?.isNotEmpty == true) score++; + if (homeserver.website?.isNotEmpty == true) score++; score += (homeserver.languages?.length ?? 0); score += (homeserver.features?.length ?? 0); score += (homeserver.onlineStatus ?? 0); diff --git a/lib/pages/sign_in/view_model/model/public_homeserver_data.dart b/lib/pages/sign_in/view_model/model/public_homeserver_data.dart index b8046b821..4d484eb10 100644 --- a/lib/pages/sign_in/view_model/model/public_homeserver_data.dart +++ b/lib/pages/sign_in/view_model/model/public_homeserver_data.dart @@ -1,7 +1,7 @@ class PublicHomeserverData { final String? name; final String? clientDomain; - final String? homepage; + final String? website; final String? isp; final String? staffJur; final String? rules; @@ -26,7 +26,7 @@ class PublicHomeserverData { PublicHomeserverData({ this.name, this.clientDomain, - this.homepage, + this.website, this.isp, this.staffJur, this.rules, @@ -53,7 +53,7 @@ class PublicHomeserverData { return PublicHomeserverData( name: json['name'], clientDomain: json['client_domain'], - homepage: json['homepage'], + website: json['website'], isp: json['isp'], staffJur: json['staff_jur'], rules: json['rules'], diff --git a/lib/utils/sign_in_flows/check_homeserver.dart b/lib/utils/sign_in_flows/check_homeserver.dart index 1466215b5..ef0bf94bb 100644 --- a/lib/utils/sign_in_flows/check_homeserver.dart +++ b/lib/utils/sign_in_flows/check_homeserver.dart @@ -1,10 +1,10 @@ -import 'package:fluffychat/config/setting_keys.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/sign_in/view_model/model/public_homeserver_data.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; @@ -70,6 +70,7 @@ Future connectToHomeserverFlow( if (context.mounted) { setState(AsyncSnapshot.withData(ConnectionState.done, true)); + context.go('/backup'); } } catch (e, s) { setState(AsyncSnapshot.withError(ConnectionState.done, e, s)); diff --git a/lib/widgets/adaptive_dialogs/adaptive_dialog_action.dart b/lib/widgets/adaptive_dialogs/adaptive_dialog_action.dart index 40fc3f96d..5848cf545 100644 --- a/lib/widgets/adaptive_dialogs/adaptive_dialog_action.dart +++ b/lib/widgets/adaptive_dialogs/adaptive_dialog_action.dart @@ -82,3 +82,77 @@ class AdaptiveDialogAction extends StatelessWidget { } } } + +class AdaptiveDialogInkWell extends StatelessWidget { + final Widget child; + final VoidCallback onTap; + final EdgeInsets padding; + + const AdaptiveDialogInkWell({ + super.key, + required this.onTap, + required this.child, + this.padding = const EdgeInsets.all(16), + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + if ({TargetPlatform.iOS, TargetPlatform.macOS}.contains(theme.platform)) { + return CupertinoButton( + onPressed: onTap, + borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), + color: theme.colorScheme.surfaceBright, + padding: padding, + child: child, + ); + } + return Material( + color: theme.colorScheme.surfaceBright, + borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), + child: InkWell( + borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), + onTap: onTap, + child: Padding( + padding: padding, + child: Center(child: child), + ), + ), + ); + } +} + +class AdaptiveIconTextButton extends StatelessWidget { + final String label; + final IconData icon; + final VoidCallback onTap; + const AdaptiveIconTextButton({ + super.key, + required this.label, + required this.icon, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).colorScheme.secondary; + return Expanded( + child: AdaptiveDialogInkWell( + padding: EdgeInsets.all(8.0), + onTap: onTap, + child: Column( + mainAxisSize: .min, + children: [ + Icon(icon, color: color), + Text( + label, + style: TextStyle(fontSize: 12, color: color), + maxLines: 1, + overflow: .ellipsis, + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/adaptive_dialogs/public_room_dialog.dart b/lib/widgets/adaptive_dialogs/public_room_dialog.dart index 4d5ac8587..975507564 100644 --- a/lib/widgets/adaptive_dialogs/public_room_dialog.dart +++ b/lib/widgets/adaptive_dialogs/public_room_dialog.dart @@ -6,7 +6,9 @@ import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import '../../config/themes.dart'; import '../../utils/url_launcher.dart'; import '../avatar.dart'; @@ -90,15 +92,8 @@ class PublicRoomDialog extends StatelessWidget { final roomLink = roomAlias ?? chunk?.roomId; var copied = false; return AlertDialog.adaptive( - title: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 256), - child: Text( - chunk?.name ?? roomAlias?.localpart ?? chunk?.roomId ?? 'Unknown', - textAlign: TextAlign.center, - ), - ), content: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256), + constraints: const BoxConstraints(maxWidth: 256), child: FutureBuilder( future: _search(context), builder: (context, snapshot) { @@ -109,125 +104,196 @@ class PublicRoomDialog extends StatelessWidget { final topic = profile?.topic; return SingleChildScrollView( child: Column( - spacing: 8, + spacing: 16, mainAxisSize: .min, crossAxisAlignment: .stretch, children: [ - if (roomLink != null) - HoverBuilder( - builder: (context, hovered) => StatefulBuilder( - builder: (context, setState) => MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: roomLink)); - setState(() { - copied = true; - }); - }, - child: RichText( - text: TextSpan( - children: [ - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only( - right: 4.0, - ), - child: AnimatedScale( - duration: - FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - scale: hovered - ? 1.33 - : copied - ? 1.25 - : 1.0, - child: Icon( - copied - ? Icons.check_circle - : Icons.copy, - size: 12, - color: copied ? Colors.green : null, + Row( + spacing: 12, + children: [ + Avatar( + mxContent: avatar, + name: profile?.name ?? roomLink, + size: Avatar.defaultSize * 1.5, + onTap: avatar != null + ? () => showDialog( + context: context, + builder: (_) => MxcImageViewer(avatar), + ) + : null, + ), + Expanded( + child: Column( + crossAxisAlignment: .start, + children: [ + Text( + profile?.name ?? + roomLink ?? + profile?.roomId ?? + ' - ', + maxLines: 1, + overflow: .ellipsis, + style: TextStyle(fontSize: 16), + ), + const SizedBox(height: 8), + if (roomLink != null) + HoverBuilder( + builder: (context, hovered) => StatefulBuilder( + builder: (context, setState) => MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + Clipboard.setData( + ClipboardData(text: roomLink), + ); + setState(() { + copied = true; + }); + }, + child: RichText( + text: TextSpan( + children: [ + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only( + right: 4.0, + ), + child: AnimatedScale( + duration: FluffyThemes + .animationDuration, + curve: FluffyThemes + .animationCurve, + scale: hovered + ? 1.33 + : copied + ? 1.25 + : 1.0, + child: Icon( + copied + ? Icons.check_circle + : Icons.copy, + size: 12, + color: copied + ? Colors.green + : null, + ), + ), + ), + ), + TextSpan(text: roomLink), + ], + style: theme.textTheme.bodyMedium + ?.copyWith(fontSize: 10), ), + textAlign: TextAlign.center, ), ), ), - TextSpan(text: roomLink), - ], - style: theme.textTheme.bodyMedium?.copyWith( - fontSize: 10, ), ), - textAlign: TextAlign.center, + + if (profile?.numJoinedMembers != null) + Text( + L10n.of(context).countParticipants( + profile?.numJoinedMembers ?? 0, + ), + style: const TextStyle(fontSize: 10), + textAlign: TextAlign.center, + ), + ], + ), + ), + ], + ), + if (topic != null && topic.isNotEmpty) + ConstrainedBox( + constraints: BoxConstraints(maxHeight: 200), + child: Scrollbar( + thumbVisibility: true, + trackVisibility: true, + child: SingleChildScrollView( + child: SelectableLinkify( + text: topic, + textScaleFactor: MediaQuery.textScalerOf( + context, + ).scale(1), + textAlign: .start, + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: theme.colorScheme.primary, + decoration: TextDecoration.underline, + decorationColor: theme.colorScheme.primary, ), + onOpen: (url) => + UrlLauncher(context, url.url).launchUrl(), ), ), ), ), - Center( - child: Avatar( - mxContent: avatar, - name: profile?.name ?? roomLink, - size: Avatar.defaultSize * 2, - onTap: avatar != null - ? () => showDialog( - context: context, - builder: (_) => MxcImageViewer(avatar), - ) - : null, + + Row( + mainAxisAlignment: .spaceBetween, + spacing: 4, + children: [ + AdaptiveIconTextButton( + label: L10n.of(context).report, + icon: Icons.gavel_outlined, + onTap: () async { + Navigator.of(context).pop(); + final reason = await showTextInputDialog( + context: context, + title: L10n.of(context).whyDoYouWantToReportThis, + okLabel: L10n.of(context).report, + cancelLabel: L10n.of(context).cancel, + hintText: L10n.of(context).reason, + ); + if (reason == null || reason.isEmpty) return; + await showFutureLoadingDialog( + context: context, + future: () => Matrix.of(context).client.reportRoom( + chunk?.roomId ?? roomAlias!, + reason, + ), + ); + }, + ), + AdaptiveIconTextButton( + label: L10n.of(context).copy, + icon: Icons.copy_outlined, + onTap: () => + Clipboard.setData(ClipboardData(text: roomLink!)), + ), + AdaptiveIconTextButton( + label: L10n.of(context).share, + icon: Icons.adaptive.share, + onTap: () => FluffyShare.share( + 'https://matrix.to/#/$roomLink', + context, + ), + ), + ], + ), + AdaptiveDialogInkWell( + onTap: () => _joinRoom(context), + child: Text( + chunk?.joinRule == 'knock' && + Matrix.of( + context, + ).client.getRoomById(chunk!.roomId) == + null + ? L10n.of(context).knock + : chunk?.roomType == 'm.space' + ? L10n.of(context).joinSpace + : L10n.of(context).joinRoom, + style: TextStyle(color: theme.colorScheme.secondary), ), ), - if (profile?.numJoinedMembers != null) - Text( - L10n.of( - context, - ).countParticipants(profile?.numJoinedMembers ?? 0), - style: const TextStyle(fontSize: 10), - textAlign: TextAlign.center, - ), - if (topic != null && topic.isNotEmpty) - SelectableLinkify( - text: topic, - textScaleFactor: MediaQuery.textScalerOf( - context, - ).scale(1), - textAlign: TextAlign.center, - options: const LinkifyOptions(humanize: false), - linkStyle: TextStyle( - color: theme.colorScheme.primary, - decoration: TextDecoration.underline, - decorationColor: theme.colorScheme.primary, - ), - onOpen: (url) => - UrlLauncher(context, url.url).launchUrl(), - ), ], ), ); }, ), ), - actions: [ - AdaptiveDialogAction( - bigButtons: true, - borderRadius: AdaptiveDialogAction.topRadius, - onPressed: () => _joinRoom(context), - child: Text( - chunk?.joinRule == 'knock' && - Matrix.of(context).client.getRoomById(chunk!.roomId) == null - ? L10n.of(context).knock - : chunk?.roomType == 'm.space' - ? L10n.of(context).joinSpace - : L10n.of(context).joinRoom, - ), - ), - AdaptiveDialogAction( - bigButtons: true, - borderRadius: AdaptiveDialogAction.bottomRadius, - onPressed: Navigator.of(context).pop, - child: Text(L10n.of(context).close), - ), - ], ); } } diff --git a/lib/widgets/adaptive_dialogs/user_dialog.dart b/lib/widgets/adaptive_dialogs/user_dialog.dart index 503615197..a6dd97af4 100644 --- a/lib/widgets/adaptive_dialogs/user_dialog.dart +++ b/lib/widgets/adaptive_dialogs/user_dialog.dart @@ -8,7 +8,9 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; +import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/presence_builder.dart'; import '../../utils/url_launcher.dart'; @@ -17,6 +19,8 @@ import '../hover_builder.dart'; import '../matrix.dart'; import '../mxc_image_viewer.dart'; +// ignore: unused_import + class UserDialog extends StatelessWidget { static Future show({ required BuildContext context, @@ -37,7 +41,6 @@ class UserDialog extends StatelessWidget { @override Widget build(BuildContext context) { final client = Matrix.of(context).client; - final dmRoomId = client.getDirectChatFromUserId(profile.userId); final displayname = profile.displayName ?? profile.userId.localpart ?? @@ -46,168 +49,209 @@ class UserDialog extends StatelessWidget { final theme = Theme.of(context); final avatar = profile.avatarUrl; return AlertDialog.adaptive( - title: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 256), - child: Center(child: Text(displayname, textAlign: TextAlign.center)), - ), - content: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256), - child: PresenceBuilder( - userId: profile.userId, - client: Matrix.of(context).client, - builder: (context, presence) { - if (presence == null) return const SizedBox.shrink(); - final statusMsg = presence.statusMsg; - final lastActiveTimestamp = presence.lastActiveTimestamp; - final presenceText = presence.currentlyActive == true - ? L10n.of(context).currentlyActive - : lastActiveTimestamp != null - ? L10n.of(context).lastActiveAgo( - lastActiveTimestamp.localizedTimeShort(context), - ) - : null; - return SingleChildScrollView( - child: Column( - spacing: 8, - mainAxisSize: .min, - crossAxisAlignment: .stretch, + content: PresenceBuilder( + userId: profile.userId, + client: Matrix.of(context).client, + builder: (context, presence) { + if (presence == null) return const SizedBox.shrink(); + final statusMsg = presence.statusMsg; + final lastActiveTimestamp = presence.lastActiveTimestamp; + final presenceText = presence.currentlyActive == true + ? L10n.of(context).currentlyActive + : lastActiveTimestamp != null + ? L10n.of( + context, + ).lastActiveAgo(lastActiveTimestamp.localizedTimeShort(context)) + : null; + return Column( + spacing: 16, + mainAxisSize: .min, + crossAxisAlignment: .stretch, + children: [ + Row( + spacing: 12, children: [ - Center( - child: Avatar( - mxContent: avatar, - name: displayname, - size: Avatar.defaultSize * 2, - onTap: avatar != null - ? () => showDialog( - context: context, - builder: (_) => MxcImageViewer(avatar), - ) - : null, - ), + Avatar( + mxContent: avatar, + name: displayname, + size: Avatar.defaultSize * 1.5, + onTap: avatar != null + ? () => showDialog( + context: context, + builder: (_) => MxcImageViewer(avatar), + ) + : null, ), - HoverBuilder( - builder: (context, hovered) => StatefulBuilder( - builder: (context, setState) => MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - Clipboard.setData( - ClipboardData(text: profile.userId), - ); - setState(() { - copied = true; - }); - }, - child: RichText( - text: TextSpan( - children: [ - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(right: 4.0), - child: AnimatedScale( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - scale: hovered - ? 1.33 - : copied - ? 1.25 - : 1.0, - child: Icon( - copied - ? Icons.check_circle - : Icons.copy, - size: 12, - color: copied ? Colors.green : null, + Expanded( + child: Column( + crossAxisAlignment: .start, + children: [ + Text( + displayname, + maxLines: 1, + overflow: .ellipsis, + style: TextStyle(fontSize: 16), + ), + const SizedBox(height: 8), + HoverBuilder( + builder: (context, hovered) => StatefulBuilder( + builder: (context, setState) => MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + Clipboard.setData( + ClipboardData(text: profile.userId), + ); + setState(() { + copied = true; + }); + }, + child: RichText( + text: TextSpan( + children: [ + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only( + right: 4.0, + ), + child: AnimatedScale( + duration: + FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + scale: hovered + ? 1.33 + : copied + ? 1.25 + : 1.0, + child: Icon( + copied + ? Icons.check_circle + : Icons.copy, + size: 12, + color: copied + ? Colors.green + : null, + ), + ), + ), ), + TextSpan(text: profile.userId), + ], + style: theme.textTheme.bodyMedium?.copyWith( + fontSize: 10, ), ), + maxLines: 1, + overflow: .ellipsis, + textAlign: TextAlign.center, ), - TextSpan(text: profile.userId), - ], - style: theme.textTheme.bodyMedium?.copyWith( - fontSize: 10, ), ), - textAlign: TextAlign.center, ), ), + if (presenceText != null) + Text( + presenceText, + style: theme.textTheme.bodyMedium?.copyWith( + fontSize: 10, + ), + ), + ], + ), + ), + ], + ), + + if (statusMsg != null) + ConstrainedBox( + constraints: BoxConstraints(maxHeight: 200), + child: Scrollbar( + thumbVisibility: true, + trackVisibility: true, + child: SingleChildScrollView( + child: SelectableLinkify( + text: statusMsg, + textScaleFactor: MediaQuery.textScalerOf( + context, + ).scale(1), + textAlign: TextAlign.start, + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: theme.colorScheme.primary, + decoration: TextDecoration.underline, + decorationColor: theme.colorScheme.primary, + ), + onOpen: (url) => + UrlLauncher(context, url.url).launchUrl(), ), ), ), - if (presenceText != null) - Text( - presenceText, - style: const TextStyle(fontSize: 10), - textAlign: TextAlign.center, - ), - if (statusMsg != null) - SelectableLinkify( - text: statusMsg, - textScaleFactor: MediaQuery.textScalerOf( - context, - ).scale(1), - textAlign: TextAlign.center, - options: const LinkifyOptions(humanize: false), - linkStyle: TextStyle( - color: theme.colorScheme.primary, - decoration: TextDecoration.underline, - decorationColor: theme.colorScheme.primary, - ), - onOpen: (url) => - UrlLauncher(context, url.url).launchUrl(), - ), + ), + Row( + mainAxisAlignment: .spaceBetween, + spacing: 4, + children: [ + AdaptiveIconTextButton( + label: L10n.of(context).block, + icon: Icons.block_outlined, + onTap: () { + final router = GoRouter.of(context); + Navigator.of(context).pop(); + router.go( + '/rooms/settings/security/ignorelist', + extra: profile.userId, + ); + }, + ), + AdaptiveIconTextButton( + label: L10n.of(context).report, + icon: Icons.gavel_outlined, + onTap: () async { + Navigator.of(context).pop(); + final reason = await showTextInputDialog( + context: context, + title: L10n.of(context).whyDoYouWantToReportThis, + okLabel: L10n.of(context).report, + cancelLabel: L10n.of(context).cancel, + hintText: L10n.of(context).reason, + ); + if (reason == null || reason.isEmpty) return; + await showFutureLoadingDialog( + context: context, + future: () => Matrix.of( + context, + ).client.reportUser(profile.userId, reason), + ); + }, + ), + AdaptiveIconTextButton( + label: L10n.of(context).share, + icon: Icons.adaptive.share, + onTap: () => FluffyShare.share(profile.userId, context), + ), ], ), - ); - }, - ), + AdaptiveDialogInkWell( + onTap: () async { + final router = GoRouter.of(context); + final roomIdResult = await showFutureLoadingDialog( + context: context, + future: () => client.startDirectChat(profile.userId), + ); + final roomId = roomIdResult.result; + if (roomId == null) return; + if (context.mounted) Navigator.of(context).pop(); + router.go('/rooms/$roomId'); + }, + child: Text( + L10n.of(context).sendAMessage, + style: TextStyle(color: theme.colorScheme.secondary), + ), + ), + ], + ); + }, ), - actions: [ - if (client.userID != profile.userId) ...[ - AdaptiveDialogAction( - borderRadius: AdaptiveDialogAction.topRadius, - bigButtons: true, - onPressed: () async { - final router = GoRouter.of(context); - final roomIdResult = await showFutureLoadingDialog( - context: context, - future: () => client.startDirectChat(profile.userId), - ); - final roomId = roomIdResult.result; - if (roomId == null) return; - if (context.mounted) Navigator.of(context).pop(); - router.go('/rooms/$roomId'); - }, - child: Text( - dmRoomId == null - ? L10n.of(context).startConversation - : L10n.of(context).sendAMessage, - ), - ), - AdaptiveDialogAction( - bigButtons: true, - borderRadius: AdaptiveDialogAction.centerRadius, - onPressed: () { - final router = GoRouter.of(context); - Navigator.of(context).pop(); - router.go( - '/rooms/settings/security/ignorelist', - extra: profile.userId, - ); - }, - child: Text( - L10n.of(context).ignoreUser, - style: TextStyle(color: theme.colorScheme.error), - ), - ), - ], - AdaptiveDialogAction( - bigButtons: true, - borderRadius: AdaptiveDialogAction.bottomRadius, - onPressed: Navigator.of(context).pop, - child: Text(L10n.of(context).close), - ), - ], ); } } diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 6c66b16d1..60fd73b77 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -177,7 +177,7 @@ class MatrixState extends State with WidgetsBindingObserver { final onRoomKeyRequestSub = {}; final onKeyVerificationRequestSub = {}; final onNotification = {}; - final onLoginStateChanged = >{}; + final onLogoutSub = >{}; final onUiaRequest = >{}; String? _cachedPassword; @@ -255,31 +255,29 @@ class MatrixState extends State with WidgetsBindingObserver { context, ); }); - onLoginStateChanged[name] ??= c.onLoginStateChanged.stream.listen((state) { - final loggedInWithMultipleClients = widget.clients.length > 1; - if (state == LoginState.loggedOut) { - _cancelSubs(c.clientName); - widget.clients.remove(c); - ClientManager.removeClientNameFromStore(c.clientName, store); - InitWithRestoreExtension.deleteSessionBackup(name); - } - if (loggedInWithMultipleClients && state != LoginState.loggedIn) { - ScaffoldMessenger.of( - FluffyChatApp.router.routerDelegate.navigatorKey.currentContext ?? - context, - ).showSnackBar( - SnackBar(content: Text(L10n.of(context).oneClientLoggedOut)), - ); + onLogoutSub[name] ??= c.onLoginStateChanged.stream + .where((state) => state == LoginState.loggedOut) + .listen((state) { + final loggedInWithMultipleClients = widget.clients.length > 1; - if (state != LoginState.loggedIn) { - FluffyChatApp.router.go('/rooms'); - } - } else { - FluffyChatApp.router.go( - state == LoginState.loggedIn ? '/backup' : '/home', - ); - } - }); + _cancelSubs(c.clientName); + widget.clients.remove(c); + ClientManager.removeClientNameFromStore(c.clientName, store); + InitWithRestoreExtension.deleteSessionBackup(name); + + if (loggedInWithMultipleClients) { + ScaffoldMessenger.of( + FluffyChatApp.router.routerDelegate.navigatorKey.currentContext ?? + context, + ).showSnackBar( + SnackBar(content: Text(L10n.of(context).oneClientLoggedOut)), + ); + + if (state != LoginState.loggedIn) { + FluffyChatApp.router.go('/rooms'); + } + } + }); onUiaRequest[name] ??= c.onUiaRequest.stream.listen(uiaRequestHandler); if (PlatformInfos.isWeb || PlatformInfos.isLinux) { c.onSync.stream.first.then((s) { @@ -296,8 +294,8 @@ class MatrixState extends State with WidgetsBindingObserver { onRoomKeyRequestSub.remove(name); onKeyVerificationRequestSub[name]?.cancel(); onKeyVerificationRequestSub.remove(name); - onLoginStateChanged[name]?.cancel(); - onLoginStateChanged.remove(name); + onLogoutSub[name]?.cancel(); + onLogoutSub.remove(name); onNotification[name]?.cancel(); onNotification.remove(name); } @@ -373,7 +371,7 @@ class MatrixState extends State with WidgetsBindingObserver { onRoomKeyRequestSub.values.map((s) => s.cancel()); onKeyVerificationRequestSub.values.map((s) => s.cancel()); - onLoginStateChanged.values.map((s) => s.cancel()); + onLogoutSub.values.map((s) => s.cancel()); onNotification.values.map((s) => s.cancel()); client.httpClient.close(); diff --git a/lib/widgets/member_actions_popup_menu_button.dart b/lib/widgets/member_actions_popup_menu_button.dart index e985be01c..55eea2cb3 100644 --- a/lib/widgets/member_actions_popup_menu_button.dart +++ b/lib/widgets/member_actions_popup_menu_button.dart @@ -45,40 +45,22 @@ Future showMemberActionsPopupMenu({ children: [ Avatar( name: displayname, + size: 30, mxContent: user.avatarUrl, presenceUserId: user.id, presenceBackgroundColor: theme.colorScheme.surfaceContainer, ), - Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 128), - child: Text( - displayname, - textAlign: TextAlign.center, - style: theme.textTheme.labelLarge, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 128), - child: Text( - user.id, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 10), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 200), + child: Text( + displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), ], ), ), - const PopupMenuDivider(), if (onMention != null) PopupMenuItem( value: _MemberActions.mention, diff --git a/pubspec.lock b/pubspec.lock index 8a8077451..77050341f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" charcode: dependency: transitive description: @@ -1112,18 +1112,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" matrix: dependency: "direct main" description: @@ -1877,26 +1877,26 @@ packages: dependency: transitive description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.29.0" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.15" timezone: dependency: transitive description: