Merge branch 'main' into weblate-fluffychat-translations

This commit is contained in:
Krille-chan 2026-02-25 10:31:11 +01:00 committed by GitHub
commit f0841fea9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 665 additions and 483 deletions

View file

@ -20,7 +20,5 @@
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict> </dict>
</plist> </plist>

View file

@ -2,15 +2,15 @@ import UIKit
import Flutter import Flutter
@main @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self)
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
} }

View file

@ -112,5 +112,26 @@
<true/> <true/>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
</dict> </dict>
</plist> </plist>

View file

@ -440,25 +440,16 @@ class ChatListController extends State<ChatList>
PopupMenuItem( PopupMenuItem(
value: ChatContextAction.open, value: ChatContextAction.open,
child: Row( child: Row(
mainAxisSize: .min,
spacing: 12.0, spacing: 12.0,
children: [ children: [
Avatar(mxContent: room.avatar, name: displayname), Avatar(mxContent: room.avatar, name: displayname, size: 24),
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 128), constraints: const BoxConstraints(maxWidth: 200),
child: Text( child: Text(displayname, maxLines: 1, overflow: .ellipsis),
displayname,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
), ),
], ],
), ),
), ),
const PopupMenuDivider(),
if (space != null) if (space != null)
PopupMenuItem( PopupMenuItem(
value: ChatContextAction.goToSpace, value: ChatContextAction.goToSpace,

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/l10n/l10n.dart';
@ -81,6 +82,9 @@ class LoginController extends State<Login> {
password: passwordController.text, password: passwordController.text,
initialDeviceDisplayName: PlatformInfos.clientName, initialDeviceDisplayName: PlatformInfos.clientName,
); );
if (mounted) {
context.go('/backup');
}
} on MatrixException catch (exception) { } on MatrixException catch (exception) {
setState(() => passwordError = exception.errorMessage); setState(() => passwordError = exception.errorMessage);
return setState(() => loading = false); return setState(() => loading = false);

View file

@ -37,14 +37,11 @@ class SignInPage extends StatelessWidget {
? L10n.of(context).createNewAccount ? L10n.of(context).createNewAccount
: L10n.of(context).login, : L10n.of(context).login,
), ),
bottom: PreferredSize( ),
preferredSize: const Size.fromHeight(56 + 60), body: Padding(
child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0),
padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
mainAxisSize: .min, spacing: 16,
crossAxisAlignment: .center,
spacing: 12,
children: [ children: [
SelectableText( SelectableText(
signUp signUp
@ -66,24 +63,20 @@ class SignInPage extends StatelessWidget {
borderSide: BorderSide.none, borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99), borderRadius: BorderRadius.circular(99),
), ),
errorText: state.publicHomeservers.error errorText: state.publicHomeservers.error?.toLocalizedString(
?.toLocalizedString(context),
prefixIcon: const Icon(Icons.search_outlined),
hintText: L10n.of(
context, context,
).searchOrEnterHomeserverAddress, ),
prefixIcon: const Icon(Icons.search_outlined),
hintText: L10n.of(context).searchOrEnterHomeserverAddress,
), ),
), ),
], if (state.publicHomeservers.connectionState ==
), ConnectionState.done)
), Expanded(
),
),
body: state.publicHomeservers.connectionState == ConnectionState.done
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Material( child: Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius), borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
color: theme.colorScheme.surfaceContainerLow, color: theme.colorScheme.surfaceContainerLow,
child: RadioGroup<PublicHomeserverData>( child: RadioGroup<PublicHomeserverData>(
@ -93,7 +86,7 @@ class SignInPage extends StatelessWidget {
itemCount: publicHomeservers.length, itemCount: publicHomeservers.length,
itemBuilder: (context, i) { itemBuilder: (context, i) {
final server = publicHomeservers[i]; final server = publicHomeservers[i];
final homepage = server.homepage; final website = server.website;
return RadioListTile.adaptive( return RadioListTile.adaptive(
value: server, value: server,
radioScaleFactor: radioScaleFactor:
@ -106,8 +99,10 @@ class SignInPage extends StatelessWidget {
: 1, : 1,
title: Row( title: Row(
children: [ children: [
Expanded(child: Text(server.name ?? 'Unknown')), Expanded(
if (homepage != null) child: Text(server.name ?? 'Unknown'),
),
if (website != null)
SizedBox.square( SizedBox.square(
dimension: 32, dimension: 32,
child: IconButton( child: IconButton(
@ -116,7 +111,7 @@ class SignInPage extends StatelessWidget {
size: 16, size: 16,
), ),
onPressed: () => onPressed: () =>
launchUrlString(homepage), launchUrlString(website),
), ),
), ),
], ],
@ -140,7 +135,8 @@ class SignInPage extends StatelessWidget {
.colorScheme .colorScheme
.tertiaryContainer, .tertiaryContainer,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding:
const EdgeInsets.symmetric(
horizontal: 6.0, horizontal: 6.0,
vertical: 3.0, vertical: 3.0,
), ),
@ -165,7 +161,8 @@ class SignInPage extends StatelessWidget {
.colorScheme .colorScheme
.secondaryContainer, .secondaryContainer,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding:
const EdgeInsets.symmetric(
horizontal: 6.0, horizontal: 6.0,
vertical: 3.0, vertical: 3.0,
), ),
@ -194,7 +191,11 @@ class SignInPage extends StatelessWidget {
), ),
), ),
) )
: Center(child: CircularProgressIndicator.adaptive()), else
Center(child: CircularProgressIndicator.adaptive()),
],
),
),
bottomNavigationBar: AnimatedSize( bottomNavigationBar: AnimatedSize(
duration: FluffyThemes.animationDuration, duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve, curve: FluffyThemes.animationCurve,
@ -203,12 +204,14 @@ class SignInPage extends StatelessWidget {
!publicHomeservers.contains(selectedHomserver) !publicHomeservers.contains(selectedHomserver)
? const SizedBox.shrink() ? const SizedBox.shrink()
: Material( : Material(
elevation: 8,
shadowColor: theme.appBarTheme.shadowColor,
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: SafeArea( child: SafeArea(
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: theme.colorScheme.onPrimary,
),
onPressed: onPressed:
state.loginLoading.connectionState == state.loginLoading.connectionState ==
ConnectionState.waiting ConnectionState.waiting

View file

@ -7,7 +7,7 @@ int sortHomeservers(PublicHomeserverData a, PublicHomeserverData b) {
int _calcHomeserverScore(PublicHomeserverData homeserver) { int _calcHomeserverScore(PublicHomeserverData homeserver) {
var score = 0; var score = 0;
if (homeserver.description?.isNotEmpty == true) score++; 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.languages?.length ?? 0);
score += (homeserver.features?.length ?? 0); score += (homeserver.features?.length ?? 0);
score += (homeserver.onlineStatus ?? 0); score += (homeserver.onlineStatus ?? 0);

View file

@ -1,7 +1,7 @@
class PublicHomeserverData { class PublicHomeserverData {
final String? name; final String? name;
final String? clientDomain; final String? clientDomain;
final String? homepage; final String? website;
final String? isp; final String? isp;
final String? staffJur; final String? staffJur;
final String? rules; final String? rules;
@ -26,7 +26,7 @@ class PublicHomeserverData {
PublicHomeserverData({ PublicHomeserverData({
this.name, this.name,
this.clientDomain, this.clientDomain,
this.homepage, this.website,
this.isp, this.isp,
this.staffJur, this.staffJur,
this.rules, this.rules,
@ -53,7 +53,7 @@ class PublicHomeserverData {
return PublicHomeserverData( return PublicHomeserverData(
name: json['name'], name: json['name'],
clientDomain: json['client_domain'], clientDomain: json['client_domain'],
homepage: json['homepage'], website: json['website'],
isp: json['isp'], isp: json['isp'],
staffJur: json['staff_jur'], staffJur: json['staff_jur'],
rules: json['rules'], rules: json['rules'],

View file

@ -1,10 +1,10 @@
import 'package:fluffychat/config/setting_keys.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher_string.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/l10n/l10n.dart';
import 'package:fluffychat/pages/sign_in/view_model/model/public_homeserver_data.dart'; import 'package:fluffychat/pages/sign_in/view_model/model/public_homeserver_data.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart';
@ -70,6 +70,7 @@ Future<void> connectToHomeserverFlow(
if (context.mounted) { if (context.mounted) {
setState(AsyncSnapshot.withData(ConnectionState.done, true)); setState(AsyncSnapshot.withData(ConnectionState.done, true));
context.go('/backup');
} }
} catch (e, s) { } catch (e, s) {
setState(AsyncSnapshot.withError(ConnectionState.done, e, s)); setState(AsyncSnapshot.withError(ConnectionState.done, e, s));

View file

@ -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,
),
],
),
),
);
}
}

View file

@ -6,7 +6,9 @@ import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.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_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart';
import '../../config/themes.dart'; import '../../config/themes.dart';
import '../../utils/url_launcher.dart'; import '../../utils/url_launcher.dart';
import '../avatar.dart'; import '../avatar.dart';
@ -90,15 +92,8 @@ class PublicRoomDialog extends StatelessWidget {
final roomLink = roomAlias ?? chunk?.roomId; final roomLink = roomAlias ?? chunk?.roomId;
var copied = false; var copied = false;
return AlertDialog.adaptive( return AlertDialog.adaptive(
title: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 256),
child: Text(
chunk?.name ?? roomAlias?.localpart ?? chunk?.roomId ?? 'Unknown',
textAlign: TextAlign.center,
),
),
content: ConstrainedBox( content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256), constraints: const BoxConstraints(maxWidth: 256),
child: FutureBuilder<PublishedRoomsChunk>( child: FutureBuilder<PublishedRoomsChunk>(
future: _search(context), future: _search(context),
builder: (context, snapshot) { builder: (context, snapshot) {
@ -109,10 +104,38 @@ class PublicRoomDialog extends StatelessWidget {
final topic = profile?.topic; final topic = profile?.topic;
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: Column(
spacing: 8, spacing: 16,
mainAxisSize: .min, mainAxisSize: .min,
crossAxisAlignment: .stretch, crossAxisAlignment: .stretch,
children: [ children: [
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) if (roomLink != null)
HoverBuilder( HoverBuilder(
builder: (context, hovered) => StatefulBuilder( builder: (context, hovered) => StatefulBuilder(
@ -120,7 +143,9 @@ class PublicRoomDialog extends StatelessWidget {
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
Clipboard.setData(ClipboardData(text: roomLink)); Clipboard.setData(
ClipboardData(text: roomLink),
);
setState(() { setState(() {
copied = true; copied = true;
}); });
@ -134,9 +159,10 @@ class PublicRoomDialog extends StatelessWidget {
right: 4.0, right: 4.0,
), ),
child: AnimatedScale( child: AnimatedScale(
duration: duration: FluffyThemes
FluffyThemes.animationDuration, .animationDuration,
curve: FluffyThemes.animationCurve, curve: FluffyThemes
.animationCurve,
scale: hovered scale: hovered
? 1.33 ? 1.33
: copied : copied
@ -147,16 +173,17 @@ class PublicRoomDialog extends StatelessWidget {
? Icons.check_circle ? Icons.check_circle
: Icons.copy, : Icons.copy,
size: 12, size: 12,
color: copied ? Colors.green : null, color: copied
? Colors.green
: null,
), ),
), ),
), ),
), ),
TextSpan(text: roomLink), TextSpan(text: roomLink),
], ],
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium
fontSize: 10, ?.copyWith(fontSize: 10),
),
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -164,34 +191,33 @@ class PublicRoomDialog extends StatelessWidget {
), ),
), ),
), ),
Center(
child: Avatar(
mxContent: avatar,
name: profile?.name ?? roomLink,
size: Avatar.defaultSize * 2,
onTap: avatar != null
? () => showDialog(
context: context,
builder: (_) => MxcImageViewer(avatar),
)
: null,
),
),
if (profile?.numJoinedMembers != null) if (profile?.numJoinedMembers != null)
Text( Text(
L10n.of( L10n.of(context).countParticipants(
context, profile?.numJoinedMembers ?? 0,
).countParticipants(profile?.numJoinedMembers ?? 0), ),
style: const TextStyle(fontSize: 10), style: const TextStyle(fontSize: 10),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
],
),
),
],
),
if (topic != null && topic.isNotEmpty) if (topic != null && topic.isNotEmpty)
SelectableLinkify( ConstrainedBox(
constraints: BoxConstraints(maxHeight: 200),
child: Scrollbar(
thumbVisibility: true,
trackVisibility: true,
child: SingleChildScrollView(
child: SelectableLinkify(
text: topic, text: topic,
textScaleFactor: MediaQuery.textScalerOf( textScaleFactor: MediaQuery.textScalerOf(
context, context,
).scale(1), ).scale(1),
textAlign: TextAlign.center, textAlign: .start,
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle( linkStyle: TextStyle(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
@ -201,33 +227,73 @@ class PublicRoomDialog extends StatelessWidget {
onOpen: (url) => onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(), UrlLauncher(context, url.url).launchUrl(),
), ),
),
),
),
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),
),
),
], ],
), ),
); );
}, },
), ),
), ),
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),
),
],
); );
} }
} }

View file

@ -8,7 +8,9 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/utils/date_time_extension.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/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/avatar.dart';
import 'package:fluffychat/widgets/presence_builder.dart'; import 'package:fluffychat/widgets/presence_builder.dart';
import '../../utils/url_launcher.dart'; import '../../utils/url_launcher.dart';
@ -17,6 +19,8 @@ import '../hover_builder.dart';
import '../matrix.dart'; import '../matrix.dart';
import '../mxc_image_viewer.dart'; import '../mxc_image_viewer.dart';
// ignore: unused_import
class UserDialog extends StatelessWidget { class UserDialog extends StatelessWidget {
static Future<void> show({ static Future<void> show({
required BuildContext context, required BuildContext context,
@ -37,7 +41,6 @@ class UserDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
final dmRoomId = client.getDirectChatFromUserId(profile.userId);
final displayname = final displayname =
profile.displayName ?? profile.displayName ??
profile.userId.localpart ?? profile.userId.localpart ??
@ -46,13 +49,7 @@ class UserDialog extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final avatar = profile.avatarUrl; final avatar = profile.avatarUrl;
return AlertDialog.adaptive( return AlertDialog.adaptive(
title: ConstrainedBox( content: PresenceBuilder(
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, userId: profile.userId,
client: Matrix.of(context).client, client: Matrix.of(context).client,
builder: (context, presence) { builder: (context, presence) {
@ -62,21 +59,22 @@ class UserDialog extends StatelessWidget {
final presenceText = presence.currentlyActive == true final presenceText = presence.currentlyActive == true
? L10n.of(context).currentlyActive ? L10n.of(context).currentlyActive
: lastActiveTimestamp != null : lastActiveTimestamp != null
? L10n.of(context).lastActiveAgo( ? L10n.of(
lastActiveTimestamp.localizedTimeShort(context), context,
) ).lastActiveAgo(lastActiveTimestamp.localizedTimeShort(context))
: null; : null;
return SingleChildScrollView( return Column(
child: Column( spacing: 16,
spacing: 8,
mainAxisSize: .min, mainAxisSize: .min,
crossAxisAlignment: .stretch, crossAxisAlignment: .stretch,
children: [ children: [
Center( Row(
child: Avatar( spacing: 12,
children: [
Avatar(
mxContent: avatar, mxContent: avatar,
name: displayname, name: displayname,
size: Avatar.defaultSize * 2, size: Avatar.defaultSize * 1.5,
onTap: avatar != null onTap: avatar != null
? () => showDialog( ? () => showDialog(
context: context, context: context,
@ -84,7 +82,17 @@ class UserDialog extends StatelessWidget {
) )
: null, : null,
), ),
Expanded(
child: Column(
crossAxisAlignment: .start,
children: [
Text(
displayname,
maxLines: 1,
overflow: .ellipsis,
style: TextStyle(fontSize: 16),
), ),
const SizedBox(height: 8),
HoverBuilder( HoverBuilder(
builder: (context, hovered) => StatefulBuilder( builder: (context, hovered) => StatefulBuilder(
builder: (context, setState) => MouseRegion( builder: (context, setState) => MouseRegion(
@ -103,9 +111,12 @@ class UserDialog extends StatelessWidget {
children: [ children: [
WidgetSpan( WidgetSpan(
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 4.0), padding: const EdgeInsets.only(
right: 4.0,
),
child: AnimatedScale( child: AnimatedScale(
duration: FluffyThemes.animationDuration, duration:
FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve, curve: FluffyThemes.animationCurve,
scale: hovered scale: hovered
? 1.33 ? 1.33
@ -117,7 +128,9 @@ class UserDialog extends StatelessWidget {
? Icons.check_circle ? Icons.check_circle
: Icons.copy, : Icons.copy,
size: 12, size: 12,
color: copied ? Colors.green : null, color: copied
? Colors.green
: null,
), ),
), ),
), ),
@ -128,6 +141,8 @@ class UserDialog extends StatelessWidget {
fontSize: 10, fontSize: 10,
), ),
), ),
maxLines: 1,
overflow: .ellipsis,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
@ -137,16 +152,29 @@ class UserDialog extends StatelessWidget {
if (presenceText != null) if (presenceText != null)
Text( Text(
presenceText, presenceText,
style: const TextStyle(fontSize: 10), style: theme.textTheme.bodyMedium?.copyWith(
textAlign: TextAlign.center, fontSize: 10,
), ),
),
],
),
),
],
),
if (statusMsg != null) if (statusMsg != null)
SelectableLinkify( ConstrainedBox(
constraints: BoxConstraints(maxHeight: 200),
child: Scrollbar(
thumbVisibility: true,
trackVisibility: true,
child: SingleChildScrollView(
child: SelectableLinkify(
text: statusMsg, text: statusMsg,
textScaleFactor: MediaQuery.textScalerOf( textScaleFactor: MediaQuery.textScalerOf(
context, context,
).scale(1), ).scale(1),
textAlign: TextAlign.center, textAlign: TextAlign.start,
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle( linkStyle: TextStyle(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
@ -156,18 +184,55 @@ class UserDialog extends StatelessWidget {
onOpen: (url) => onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(), 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),
);
},
), ),
actions: [ AdaptiveIconTextButton(
if (client.userID != profile.userId) ...[ label: L10n.of(context).share,
AdaptiveDialogAction( icon: Icons.adaptive.share,
borderRadius: AdaptiveDialogAction.topRadius, onTap: () => FluffyShare.share(profile.userId, context),
bigButtons: true, ),
onPressed: () async { ],
),
AdaptiveDialogInkWell(
onTap: () async {
final router = GoRouter.of(context); final router = GoRouter.of(context);
final roomIdResult = await showFutureLoadingDialog( final roomIdResult = await showFutureLoadingDialog(
context: context, context: context,
@ -179,35 +244,14 @@ class UserDialog extends StatelessWidget {
router.go('/rooms/$roomId'); router.go('/rooms/$roomId');
}, },
child: Text( child: Text(
dmRoomId == null L10n.of(context).sendAMessage,
? L10n.of(context).startConversation style: TextStyle(color: theme.colorScheme.secondary),
: 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),
),
],
); );
} }
} }

View file

@ -177,7 +177,7 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
final onRoomKeyRequestSub = <String, StreamSubscription>{}; final onRoomKeyRequestSub = <String, StreamSubscription>{};
final onKeyVerificationRequestSub = <String, StreamSubscription>{}; final onKeyVerificationRequestSub = <String, StreamSubscription>{};
final onNotification = <String, StreamSubscription>{}; final onNotification = <String, StreamSubscription>{};
final onLoginStateChanged = <String, StreamSubscription<LoginState>>{}; final onLogoutSub = <String, StreamSubscription<LoginState>>{};
final onUiaRequest = <String, StreamSubscription<UiaRequest>>{}; final onUiaRequest = <String, StreamSubscription<UiaRequest>>{};
String? _cachedPassword; String? _cachedPassword;
@ -255,15 +255,17 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
context, context,
); );
}); });
onLoginStateChanged[name] ??= c.onLoginStateChanged.stream.listen((state) { onLogoutSub[name] ??= c.onLoginStateChanged.stream
.where((state) => state == LoginState.loggedOut)
.listen((state) {
final loggedInWithMultipleClients = widget.clients.length > 1; final loggedInWithMultipleClients = widget.clients.length > 1;
if (state == LoginState.loggedOut) {
_cancelSubs(c.clientName); _cancelSubs(c.clientName);
widget.clients.remove(c); widget.clients.remove(c);
ClientManager.removeClientNameFromStore(c.clientName, store); ClientManager.removeClientNameFromStore(c.clientName, store);
InitWithRestoreExtension.deleteSessionBackup(name); InitWithRestoreExtension.deleteSessionBackup(name);
}
if (loggedInWithMultipleClients && state != LoginState.loggedIn) { if (loggedInWithMultipleClients) {
ScaffoldMessenger.of( ScaffoldMessenger.of(
FluffyChatApp.router.routerDelegate.navigatorKey.currentContext ?? FluffyChatApp.router.routerDelegate.navigatorKey.currentContext ??
context, context,
@ -274,10 +276,6 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
if (state != LoginState.loggedIn) { if (state != LoginState.loggedIn) {
FluffyChatApp.router.go('/rooms'); FluffyChatApp.router.go('/rooms');
} }
} else {
FluffyChatApp.router.go(
state == LoginState.loggedIn ? '/backup' : '/home',
);
} }
}); });
onUiaRequest[name] ??= c.onUiaRequest.stream.listen(uiaRequestHandler); onUiaRequest[name] ??= c.onUiaRequest.stream.listen(uiaRequestHandler);
@ -296,8 +294,8 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
onRoomKeyRequestSub.remove(name); onRoomKeyRequestSub.remove(name);
onKeyVerificationRequestSub[name]?.cancel(); onKeyVerificationRequestSub[name]?.cancel();
onKeyVerificationRequestSub.remove(name); onKeyVerificationRequestSub.remove(name);
onLoginStateChanged[name]?.cancel(); onLogoutSub[name]?.cancel();
onLoginStateChanged.remove(name); onLogoutSub.remove(name);
onNotification[name]?.cancel(); onNotification[name]?.cancel();
onNotification.remove(name); onNotification.remove(name);
} }
@ -373,7 +371,7 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
onRoomKeyRequestSub.values.map((s) => s.cancel()); onRoomKeyRequestSub.values.map((s) => s.cancel());
onKeyVerificationRequestSub.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()); onNotification.values.map((s) => s.cancel());
client.httpClient.close(); client.httpClient.close();

View file

@ -45,40 +45,22 @@ Future<void> showMemberActionsPopupMenu({
children: [ children: [
Avatar( Avatar(
name: displayname, name: displayname,
size: 30,
mxContent: user.avatarUrl, mxContent: user.avatarUrl,
presenceUserId: user.id, presenceUserId: user.id,
presenceBackgroundColor: theme.colorScheme.surfaceContainer, presenceBackgroundColor: theme.colorScheme.surfaceContainer,
), ),
Column(
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 128), constraints: const BoxConstraints(maxWidth: 200),
child: Text( child: Text(
displayname, 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, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
], ],
), ),
],
), ),
),
const PopupMenuDivider(),
if (onMention != null) if (onMention != null)
PopupMenuItem( PopupMenuItem(
value: _MemberActions.mention, value: _MemberActions.mention,

View file

@ -165,10 +165,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.1"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@ -1112,18 +1112,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.17" version: "0.12.18"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.1" version: "0.13.0"
matrix: matrix:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1877,26 +1877,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test name: test
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.26.3" version: "1.29.0"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.7" version: "0.7.9"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.12" version: "0.6.15"
timezone: timezone:
dependency: transitive dependency: transitive
description: description: