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,164 +37,165 @@ 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( spacing: 16,
mainAxisSize: .min, children: [
crossAxisAlignment: .center, SelectableText(
spacing: 12, signUp
children: [ ? L10n.of(context).signUpGreeting
SelectableText( : L10n.of(context).signInGreeting,
signUp textAlign: .center,
? L10n.of(context).signUpGreeting ),
: L10n.of(context).signInGreeting, TextField(
textAlign: .center, 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( errorText: state.publicHomeservers.error?.toLocalizedString(
readOnly: context,
state.publicHomeservers.connectionState == ),
ConnectionState.waiting, prefixIcon: const Icon(Icons.search_outlined),
controller: viewModel.filterTextController, hintText: L10n.of(context).searchOrEnterHomeserverAddress,
autocorrect: false, ),
keyboardType: TextInputType.url, ),
decoration: InputDecoration( if (state.publicHomeservers.connectionState ==
filled: true, ConnectionState.done)
fillColor: theme.colorScheme.secondaryContainer, Expanded(
border: OutlineInputBorder( child: Material(
borderSide: BorderSide.none, borderRadius: BorderRadius.circular(
borderRadius: BorderRadius.circular(99), AppConfig.borderRadius,
),
clipBehavior: Clip.hardEdge,
color: theme.colorScheme.surfaceContainerLow,
child: RadioGroup<PublicHomeserverData>(
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<PublicHomeserverData>(
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( 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,125 +104,196 @@ 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: [
if (roomLink != null) Row(
HoverBuilder( spacing: 12,
builder: (context, hovered) => StatefulBuilder( children: [
builder: (context, setState) => MouseRegion( Avatar(
cursor: SystemMouseCursors.click, mxContent: avatar,
child: GestureDetector( name: profile?.name ?? roomLink,
onTap: () { size: Avatar.defaultSize * 1.5,
Clipboard.setData(ClipboardData(text: roomLink)); onTap: avatar != null
setState(() { ? () => showDialog(
copied = true; context: context,
}); builder: (_) => MxcImageViewer(avatar),
}, )
child: RichText( : null,
text: TextSpan( ),
children: [ Expanded(
WidgetSpan( child: Column(
child: Padding( crossAxisAlignment: .start,
padding: const EdgeInsets.only( children: [
right: 4.0, Text(
), profile?.name ??
child: AnimatedScale( roomLink ??
duration: profile?.roomId ??
FluffyThemes.animationDuration, ' - ',
curve: FluffyThemes.animationCurve, maxLines: 1,
scale: hovered overflow: .ellipsis,
? 1.33 style: TextStyle(fontSize: 16),
: copied ),
? 1.25 const SizedBox(height: 8),
: 1.0, if (roomLink != null)
child: Icon( HoverBuilder(
copied builder: (context, hovered) => StatefulBuilder(
? Icons.check_circle builder: (context, setState) => MouseRegion(
: Icons.copy, cursor: SystemMouseCursors.click,
size: 12, child: GestureDetector(
color: copied ? Colors.green : null, 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( Row(
mxContent: avatar, mainAxisAlignment: .spaceBetween,
name: profile?.name ?? roomLink, spacing: 4,
size: Avatar.defaultSize * 2, children: [
onTap: avatar != null AdaptiveIconTextButton(
? () => showDialog( label: L10n.of(context).report,
context: context, icon: Icons.gavel_outlined,
builder: (_) => MxcImageViewer(avatar), onTap: () async {
) Navigator.of(context).pop();
: null, 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),
),
],
); );
} }
} }

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,168 +49,209 @@ 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), userId: profile.userId,
child: Center(child: Text(displayname, textAlign: TextAlign.center)), client: Matrix.of(context).client,
), builder: (context, presence) {
content: ConstrainedBox( if (presence == null) return const SizedBox.shrink();
constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256), final statusMsg = presence.statusMsg;
child: PresenceBuilder( final lastActiveTimestamp = presence.lastActiveTimestamp;
userId: profile.userId, final presenceText = presence.currentlyActive == true
client: Matrix.of(context).client, ? L10n.of(context).currentlyActive
builder: (context, presence) { : lastActiveTimestamp != null
if (presence == null) return const SizedBox.shrink(); ? L10n.of(
final statusMsg = presence.statusMsg; context,
final lastActiveTimestamp = presence.lastActiveTimestamp; ).lastActiveAgo(lastActiveTimestamp.localizedTimeShort(context))
final presenceText = presence.currentlyActive == true : null;
? L10n.of(context).currentlyActive return Column(
: lastActiveTimestamp != null spacing: 16,
? L10n.of(context).lastActiveAgo( mainAxisSize: .min,
lastActiveTimestamp.localizedTimeShort(context), crossAxisAlignment: .stretch,
) children: [
: null; Row(
return SingleChildScrollView( spacing: 12,
child: Column(
spacing: 8,
mainAxisSize: .min,
crossAxisAlignment: .stretch,
children: [ children: [
Center( Avatar(
child: Avatar( mxContent: avatar,
mxContent: avatar, name: displayname,
name: displayname, size: Avatar.defaultSize * 1.5,
size: Avatar.defaultSize * 2, onTap: avatar != null
onTap: avatar != null ? () => showDialog(
? () => showDialog( context: context,
context: context, builder: (_) => MxcImageViewer(avatar),
builder: (_) => MxcImageViewer(avatar), )
) : null,
: null,
),
), ),
HoverBuilder( Expanded(
builder: (context, hovered) => StatefulBuilder( child: Column(
builder: (context, setState) => MouseRegion( crossAxisAlignment: .start,
cursor: SystemMouseCursors.click, children: [
child: GestureDetector( Text(
onTap: () { displayname,
Clipboard.setData( maxLines: 1,
ClipboardData(text: profile.userId), overflow: .ellipsis,
); style: TextStyle(fontSize: 16),
setState(() { ),
copied = true; const SizedBox(height: 8),
}); HoverBuilder(
}, builder: (context, hovered) => StatefulBuilder(
child: RichText( builder: (context, setState) => MouseRegion(
text: TextSpan( cursor: SystemMouseCursors.click,
children: [ child: GestureDetector(
WidgetSpan( onTap: () {
child: Padding( Clipboard.setData(
padding: const EdgeInsets.only(right: 4.0), ClipboardData(text: profile.userId),
child: AnimatedScale( );
duration: FluffyThemes.animationDuration, setState(() {
curve: FluffyThemes.animationCurve, copied = true;
scale: hovered });
? 1.33 },
: copied child: RichText(
? 1.25 text: TextSpan(
: 1.0, children: [
child: Icon( WidgetSpan(
copied child: Padding(
? Icons.check_circle padding: const EdgeInsets.only(
: Icons.copy, right: 4.0,
size: 12, ),
color: copied ? Colors.green : null, 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( Row(
presenceText, mainAxisAlignment: .spaceBetween,
style: const TextStyle(fontSize: 10), spacing: 4,
textAlign: TextAlign.center, children: [
), AdaptiveIconTextButton(
if (statusMsg != null) label: L10n.of(context).block,
SelectableLinkify( icon: Icons.block_outlined,
text: statusMsg, onTap: () {
textScaleFactor: MediaQuery.textScalerOf( final router = GoRouter.of(context);
context, Navigator.of(context).pop();
).scale(1), router.go(
textAlign: TextAlign.center, '/rooms/settings/security/ignorelist',
options: const LinkifyOptions(humanize: false), extra: profile.userId,
linkStyle: TextStyle( );
color: theme.colorScheme.primary, },
decoration: TextDecoration.underline, ),
decorationColor: theme.colorScheme.primary, AdaptiveIconTextButton(
), label: L10n.of(context).report,
onOpen: (url) => icon: Icons.gavel_outlined,
UrlLauncher(context, url.url).launchUrl(), 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),
),
],
); );
} }
} }

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,31 +255,29 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
context, context,
); );
}); });
onLoginStateChanged[name] ??= c.onLoginStateChanged.stream.listen((state) { onLogoutSub[name] ??= c.onLoginStateChanged.stream
final loggedInWithMultipleClients = widget.clients.length > 1; .where((state) => state == LoginState.loggedOut)
if (state == LoginState.loggedOut) { .listen((state) {
_cancelSubs(c.clientName); final loggedInWithMultipleClients = widget.clients.length > 1;
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)),
);
if (state != LoginState.loggedIn) { _cancelSubs(c.clientName);
FluffyChatApp.router.go('/rooms'); widget.clients.remove(c);
} ClientManager.removeClientNameFromStore(c.clientName, store);
} else { InitWithRestoreExtension.deleteSessionBackup(name);
FluffyChatApp.router.go(
state == LoginState.loggedIn ? '/backup' : '/home', 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); onUiaRequest[name] ??= c.onUiaRequest.stream.listen(uiaRequestHandler);
if (PlatformInfos.isWeb || PlatformInfos.isLinux) { if (PlatformInfos.isWeb || PlatformInfos.isLinux) {
c.onSync.stream.first.then((s) { c.onSync.stream.first.then((s) {
@ -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( ConstrainedBox(
mainAxisSize: .min, constraints: const BoxConstraints(maxWidth: 200),
crossAxisAlignment: .start, child: Text(
children: [ displayname,
ConstrainedBox( maxLines: 1,
constraints: const BoxConstraints(maxWidth: 128), overflow: TextOverflow.ellipsis,
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,
),
),
],
), ),
], ],
), ),
), ),
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: