Merge remote-tracking branch 'origin/main'
Some checks failed
Main Deploy Workflow / deploy_web (push) Has been cancelled
Main Deploy Workflow / deploy_playstore_internal (push) Has been cancelled

This commit is contained in:
Alexey 2026-03-20 14:50:02 +03:00
commit 2c8d415475
34 changed files with 495 additions and 727 deletions

View file

@ -3,7 +3,6 @@ import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:collection/collection.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@ -24,6 +23,8 @@ import 'widgets/fluffy_chat_app.dart';
ReceivePort? mainIsolateReceivePort;
bool _vodozemacInitialized = false;
void main() async {
if (PlatformInfos.isAndroid) {
final port = mainIsolateReceivePort = ReceivePort();
@ -63,7 +64,10 @@ void main() async {
final store = await AppSettings.init();
Logs().i('Welcome to ${AppSettings.applicationName.value} <3');
await vod.init(wasmPath: './assets/assets/vodozemac/');
if (!_vodozemacInitialized) {
await vod.init(wasmPath: './assets/assets/vodozemac/');
_vodozemacInitialized = true;
}
Logs().nativeColors = !PlatformInfos.isIOS;
final clients = await ClientManager.getClients(store: store);
@ -117,9 +121,6 @@ Future<void> startGui(List<Client> clients, SharedPreferences store) async {
await firstClient?.accountDataLoading;
runApp(FluffyChatApp(clients: clients, pincode: pin, store: store));
if (const String.fromEnvironment('WITH_SEMANTICS') == 'true') {
SemanticsBinding.instance.ensureSemantics();
}
}
/// Watches the lifecycle changes to start the application when it

View file

@ -204,39 +204,31 @@ class BootstrapDialogState extends State<BootstrapDialog> {
),
const SizedBox(height: 16),
if (_supportsSecureStorage)
Semantics(
identifier: 'store_in_secure_storage',
child: CheckboxListTile.adaptive(
contentPadding: const EdgeInsets.symmetric(
horizontal: 8.0,
),
value: _storeInSecureStorage,
activeColor: theme.colorScheme.primary,
onChanged: (b) {
setState(() {
_storeInSecureStorage = b;
});
},
title: Text(_getSecureStorageLocalizedName()),
subtitle: Text(
L10n.of(context).storeInSecureStorageDescription,
),
CheckboxListTile.adaptive(
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
value: _storeInSecureStorage,
activeColor: theme.colorScheme.primary,
onChanged: (b) {
setState(() {
_storeInSecureStorage = b;
});
},
title: Text(_getSecureStorageLocalizedName()),
subtitle: Text(
L10n.of(context).storeInSecureStorageDescription,
),
),
const SizedBox(height: 16),
Semantics(
identifier: 'copy_to_clipboard',
child: CheckboxListTile.adaptive(
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
value: _recoveryKeyCopied,
activeColor: theme.colorScheme.primary,
onChanged: (b) {
FluffyShare.share(key!, context);
setState(() => _recoveryKeyCopied = true);
},
title: Text(L10n.of(context).copyToClipboard),
subtitle: Text(L10n.of(context).saveKeyManuallyDescription),
),
CheckboxListTile.adaptive(
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
value: _recoveryKeyCopied,
activeColor: theme.colorScheme.primary,
onChanged: (b) {
FluffyShare.share(key!, context, copyOnly: true);
setState(() => _recoveryKeyCopied = true);
},
title: Text(L10n.of(context).copyToClipboard),
subtitle: Text(L10n.of(context).saveKeyManuallyDescription),
),
const SizedBox(height: 16),
ElevatedButton.icon(

View file

@ -397,6 +397,7 @@ class ChatInputRow extends StatelessWidget {
),
)
: IconButton(
key: Key('send_button'),
tooltip: L10n.of(context).send,
onPressed: controller.send,
style: IconButton.styleFrom(

View file

@ -385,6 +385,7 @@ class InputBar extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Autocomplete<Map<String, String?>>(
key: Key('chat_input_field'),
focusNode: focusNode,
textEditingController: controller,
optionsBuilder: getSuggestions,

View file

@ -173,23 +173,20 @@ class ClientChooserButton extends StatelessWidget {
clipBehavior: Clip.hardEdge,
borderRadius: BorderRadius.circular(99),
color: Colors.transparent,
child: Semantics(
identifier: 'accounts_and_settings',
child: PopupMenuButton<Object>(
tooltip: 'Accounts and settings',
popUpAnimationStyle: FluffyThemes.isColumnMode(context)
? AnimationStyle.noAnimation
: null, // https://github.com/flutter/flutter/issues/167180
onSelected: (o) => _clientSelected(o, context),
itemBuilder: _bundleMenuItems,
child: Center(
child: Avatar(
mxContent: snapshot.data?.avatarUrl,
name:
snapshot.data?.displayName ??
matrix.client.userID?.localpart,
size: 32,
),
child: PopupMenuButton<Object>(
key: Key('accounts_and_settings_buttons'),
tooltip: 'Accounts and settings',
popUpAnimationStyle: FluffyThemes.isColumnMode(context)
? AnimationStyle.noAnimation
: null, // https://github.com/flutter/flutter/issues/167180
onSelected: (o) => _clientSelected(o, context),
itemBuilder: _bundleMenuItems,
child: Center(
child: Avatar(
mxContent: snapshot.data?.avatarUrl,
name:
snapshot.data?.displayName ?? matrix.client.userID?.localpart,
size: 32,
),
),
),

View file

@ -81,7 +81,6 @@ class SettingsController extends State<Settings> {
context: context,
future: () => matrix.client.logout(),
);
context.go('/');
}
Future<void> setAvatarAction() async {

View file

@ -96,104 +96,98 @@ class SignInPage extends StatelessWidget {
itemBuilder: (context, i) {
final server = publicHomeservers[i];
final website = server.website;
return Semantics(
identifier: 'homeserver_tile_$i',
child: RadioListTile(
value: server,
enabled:
state.loginLoading.connectionState !=
ConnectionState.waiting,
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),
return RadioListTile(
value: server,
enabled:
state.loginLoading.connectionState !=
ConnectionState.waiting,
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',
),
],
),
],
),
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',
),
],
),
);
},
@ -217,29 +211,26 @@ class SignInPage extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SafeArea(
child: Semantics(
identifier: 'connect_to_homeserver_button',
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: theme.colorScheme.onPrimary,
),
onPressed:
state.loginLoading.connectionState ==
ConnectionState.waiting
? null
: () => connectToHomeserverFlow(
selectedHomserver,
context,
viewModel.setLoginLoading,
signUp,
),
child:
state.loginLoading.connectionState ==
ConnectionState.waiting
? const CircularProgressIndicator.adaptive()
: Text(L10n.of(context).continueText),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: theme.colorScheme.onPrimary,
),
onPressed:
state.loginLoading.connectionState ==
ConnectionState.waiting
? null
: () => connectToHomeserverFlow(
selectedHomserver,
context,
viewModel.setLoginLoading,
signUp,
),
child:
state.loginLoading.connectionState ==
ConnectionState.waiting
? const CircularProgressIndicator.adaptive()
: Text(L10n.of(context).continueText),
),
),
),

View file

@ -24,9 +24,14 @@ abstract class FluffyShare {
return;
}
await Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(L10n.of(context).copiedToClipboard)));
if (!PlatformInfos.isMobile) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
showCloseIcon: true,
content: Text(L10n.of(context).copiedToClipboard),
),
);
}
return;
}

View file

@ -41,11 +41,13 @@ Future<OkCancelResult?> showOkCancelAlertDialog({
),
actions: [
AdaptiveDialogAction(
key: Key('ok_cancel_alert_dialog_cancel_button'),
onPressed: () =>
Navigator.of(context).pop<OkCancelResult>(OkCancelResult.cancel),
child: Text(cancelLabel ?? L10n.of(context).cancel),
),
AdaptiveDialogAction(
key: Key('ok_cancel_alert_dialog_ok_button'),
onPressed: () =>
Navigator.of(context).pop<OkCancelResult>(OkCancelResult.ok),
autofocus: true,

View file

@ -85,7 +85,11 @@ class LoadingDialogState<T> extends State<LoadingDialog> {
void initState() {
super.initState();
widget.future.then(
(result) => Navigator.of(context).pop<Result<T>>(Result.value(result)),
(result) {
if (!mounted) return;
if (!Navigator.of(context).canPop()) return;
Navigator.of(context).pop<Result<T>>(Result.value(result));
},
onError: (e, s) => setState(() {
exception = e;
stackTrace = s;

View file

@ -371,7 +371,6 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
onKeyVerificationRequestSub.values.map((s) => s.cancel());
onLogoutSub.values.map((s) => s.cancel());
onNotification.values.map((s) => s.cancel());
client.httpClient.close();
linuxNotifications?.close();