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: