From 341a76412c7909ddd2904d9fc70ffa1d0620a1a7 Mon Sep 17 00:00:00 2001 From: q234rty Date: Sun, 8 Sep 2024 01:02:24 +0800 Subject: [PATCH 001/236] fix: Bypass image compression in flutter_file_picker We do our own image compression in the dark sdk anyway. Might fix #1168 --- lib/pages/chat/chat.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index ebc60d662..cd770ca5f 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -508,6 +508,7 @@ class ChatController extends State void sendFileAction() async { final result = await AppLock.of(context).pauseWhile( FilePicker.platform.pickFiles( + compressionQuality: 0, allowMultiple: false, withData: true, ), @@ -547,6 +548,7 @@ class ChatController extends State void sendImageAction() async { final result = await AppLock.of(context).pauseWhile( FilePicker.platform.pickFiles( + compressionQuality: 0, type: FileType.image, withData: true, allowMultiple: false, From 4efd73c943aa1357c6c592d63f4d635589e82477 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Mon, 9 Sep 2024 13:44:38 +0200 Subject: [PATCH 002/236] build: Update flutter to 3.24.2 --- .github/workflows/versions.env | 2 +- pubspec.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/versions.env b/.github/workflows/versions.env index a7af31db7..598275c27 100644 --- a/.github/workflows/versions.env +++ b/.github/workflows/versions.env @@ -1,2 +1,2 @@ -FLUTTER_VERSION=3.24.1 +FLUTTER_VERSION=3.24.2 JAVA_VERSION=17 diff --git a/pubspec.lock b/pubspec.lock index 33068a8bf..348f73d54 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2287,10 +2287,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" wakelock_plus: dependency: "direct main" description: From 4eb13f2ae7f812c31d83242066cf9c31eae92684 Mon Sep 17 00:00:00 2001 From: Ettore Atalan Date: Sun, 1 Sep 2024 09:50:26 +0000 Subject: [PATCH 003/236] Translated using Weblate (German) Currently translated at 99.3% (659 of 663 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/ --- assets/l10n/intl_de.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_de.arb b/assets/l10n/intl_de.arb index f9321f156..2a9eb554c 100644 --- a/assets/l10n/intl_de.arb +++ b/assets/l10n/intl_de.arb @@ -2793,5 +2793,7 @@ "sendCanceled": "Senden abgebrochen", "@sendCanceled": {}, "noChatsFoundHere": "Hier wurden noch keine Chats gefunden. Starten Sie einen neuen Chat mit jemandem, indem Sie die Schaltfläche unten verwenden. ⤵️", - "@noChatsFoundHere": {} + "@noChatsFoundHere": {}, + "whatIsAHomeserver": "Was ist ein Heimserver?", + "@whatIsAHomeserver": {} } From 49bc971836aecd8779d05d5627097e6418637899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sat, 31 Aug 2024 16:24:22 +0000 Subject: [PATCH 004/236] Translated using Weblate (Estonian) Currently translated at 100.0% (663 of 663 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/ --- assets/l10n/intl_et.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_et.arb b/assets/l10n/intl_et.arb index f702beb5a..fc6b6c7cf 100644 --- a/assets/l10n/intl_et.arb +++ b/assets/l10n/intl_et.arb @@ -2799,5 +2799,9 @@ "discoverHomeservers": "Leia koduservereid", "@discoverHomeservers": {}, "whatIsAHomeserver": "Mis on koduserver?", - "@whatIsAHomeserver": {} + "@whatIsAHomeserver": {}, + "homeserverDescription": "Sarnaselt e-postiteenuse pakkujale on kõik sinu sõnumid salvestatud koduserveris. Sa võid valida sellise koduserveri, nagu sulle meeldib ja nad kõik suudavad teiste koduserveritega suhelda. Lisateavet leiad veebisaidist https://matrix.org.", + "@homeserverDescription": {}, + "doesNotSeemToBeAValidHomeserver": "Ei tundu olema ühilduv koduserver. Kas võrguaadress on ikka õige?", + "@doesNotSeemToBeAValidHomeserver": {} } From 58996c19229b3df3a5107ad28a9f6ff56091da4e Mon Sep 17 00:00:00 2001 From: Bezruchenko Simon Date: Thu, 5 Sep 2024 06:45:04 +0000 Subject: [PATCH 005/236] Translated using Weblate (Ukrainian) Currently translated at 100.0% (663 of 663 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/ --- assets/l10n/intl_uk.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/l10n/intl_uk.arb b/assets/l10n/intl_uk.arb index 519ce5cde..b6829d707 100644 --- a/assets/l10n/intl_uk.arb +++ b/assets/l10n/intl_uk.arb @@ -1665,7 +1665,7 @@ "type": "text", "placeholders": {} }, - "yourChatBackupHasBeenSetUp": "Резервне копіювання бесіди налаштовано.", + "yourChatBackupHasBeenSetUp": "Рез. копію чату налаштовано.", "@yourChatBackupHasBeenSetUp": {}, "clearArchive": "Очистити архів", "@clearArchive": {}, From 292e20a119812e6838ce3314eaa3a4cfcbf7ca11 Mon Sep 17 00:00:00 2001 From: kdh8219 Date: Wed, 4 Sep 2024 08:06:17 +0000 Subject: [PATCH 006/236] Translated using Weblate (Korean) Currently translated at 97.4% (646 of 663 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ko/ --- assets/l10n/intl_ko.arb | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_ko.arb b/assets/l10n/intl_ko.arb index ce17351d6..443fb077f 100644 --- a/assets/l10n/intl_ko.arb +++ b/assets/l10n/intl_ko.arb @@ -303,7 +303,7 @@ "type": "text", "placeholders": {} }, - "defaultPermissionLevel": "기본 권한 레벨", + "defaultPermissionLevel": "새로 참가하는 유저들의 기본 권한 레벨", "@defaultPermissionLevel": { "type": "text", "placeholders": {} @@ -2736,5 +2736,32 @@ "joinedChats": "참여한 채팅", "@joinedChats": {}, "noMoreChatsFound": "채팅을 찾을 수 없습니다...", - "@noMoreChatsFound": {} + "@noMoreChatsFound": {}, + "moderatorLevel": "{level} - 관리자", + "@moderatorLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "adminLevel": "{level} - 운영자", + "@adminLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "invitedBy": "📩 {user}님이 나를 초대함", + "@invitedBy": { + "placeholders": { + "user": {} + } + }, + "userLevel": "{level} - 유저", + "@userLevel": { + "type": "text", + "placeholders": { + "level": {} + } + } } From 63e3ac2355e50d45e2602c478afaeb1996c97b7d Mon Sep 17 00:00:00 2001 From: Zentropivity Date: Fri, 6 Sep 2024 19:46:29 +0000 Subject: [PATCH 007/236] Translated using Weblate (Hungarian) Currently translated at 100.0% (663 of 663 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/hu/ --- assets/l10n/intl_hu.arb | 101 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_hu.arb b/assets/l10n/intl_hu.arb index 185a04816..8bd45c7f3 100644 --- a/assets/l10n/intl_hu.arb +++ b/assets/l10n/intl_hu.arb @@ -1641,7 +1641,7 @@ "type": "text", "placeholders": {} }, - "defaultPermissionLevel": "Alapértelmezett hozzáférési szint", + "defaultPermissionLevel": "Alapértelmezett hozzáférési szint új felhasználóknak", "@defaultPermissionLevel": { "type": "text", "placeholders": {} @@ -2706,5 +2706,102 @@ "gallery": "Galéria", "@gallery": {}, "commandHint_ignore": "Adott matrix ID figyelmen kívül hagyása", - "@commandHint_ignore": {} + "@commandHint_ignore": {}, + "unread": "Olvasatlan", + "@unread": {}, + "space": "Tér", + "@space": {}, + "spaces": "Terek", + "@spaces": {}, + "invitedBy": "📩 Meghívva {user} által", + "@invitedBy": { + "placeholders": { + "user": {} + } + }, + "markAsUnread": "Olvasatlannak jelölés", + "@markAsUnread": {}, + "moderatorLevel": "{level} - Moderátor", + "@moderatorLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "changeGeneralChatSettings": "Általános csevegés beállítások módosítása", + "@changeGeneralChatSettings": {}, + "updateInstalled": "🎉 {version} verziójú fejlesztés telepítve!", + "@updateInstalled": { + "type": "text", + "placeholders": { + "version": {} + } + }, + "changelog": "Változások", + "@changelog": {}, + "sendCanceled": "Visszavonás küldése", + "@sendCanceled": {}, + "noChatsFoundHere": "Itt még nincs csevegés. Kezdjen újat valakivel a lentebbi gombbal. ⤵️", + "@noChatsFoundHere": {}, + "goToSpace": "Menj a térre: {space}", + "@goToSpace": { + "type": "text", + "space": {} + }, + "changeTheCanonicalRoomAlias": "Csevegés fő, nyilvános címének változtatása", + "@changeTheCanonicalRoomAlias": {}, + "chatPermissionsDescription": "Adja meg milyen erősségi szint kell egyes csevegési akciókhoz. A 0, 50 és 100-as szintek általában felhasználókat, moderátorokat és rendszergazdákat jelölnek de bármilyen szintezés lehetséges.", + "@chatPermissionsDescription": {}, + "whatIsAHomeserver": "Mi az a Matrix-kiszolgáló?", + "@whatIsAHomeserver": {}, + "homeserverDescription": "Az összes adata a Mátrix-kiszolgálón tárolódik, pont mint egy e-mail kiszolgálón. Kiválaszthatja melyik Matrix-kiszolgálót akarja használni, miközben tud kommunikálni mindenkivel. Tudjon meg többet itt: https://matrix.org.", + "@homeserverDescription": {}, + "userLevel": "{level} - Felhasználó", + "@userLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "adminLevel": "{level} - Rendszergazda", + "@adminLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "inviteOtherUsers": "Más felhasználók meghívása a csevegésbe", + "@inviteOtherUsers": {}, + "changeTheVisibilityOfChatHistory": "Csevegési előzmények láthatóságának változtatása", + "@changeTheVisibilityOfChatHistory": {}, + "changeTheChatPermissions": "Csevegés engedélyek változtatása", + "@changeTheChatPermissions": {}, + "sendRoomNotifications": "@room értesítés küldése", + "@sendRoomNotifications": {}, + "changeTheDescriptionOfTheGroup": "Csevegés leírásának változtatása", + "@changeTheDescriptionOfTheGroup": {}, + "swipeRightToLeftToReply": "Húzza balra a válaszoláshoz", + "@swipeRightToLeftToReply": {}, + "alwaysUse24HourFormat": "true", + "@alwaysUse24HourFormat": { + "description": "Set to true to always display time of day in 24 hour format." + }, + "loginWithMatrixId": "Jelentkezzen be Matrix-ID-vel", + "@loginWithMatrixId": {}, + "discoverHomeservers": "Matrix-kiszolgálók felfedezése", + "@discoverHomeservers": {}, + "doesNotSeemToBeAValidHomeserver": "Nem tűnik kompatibilisnak a Mátrix-kiszolgálóval. Helytelen URL?", + "@doesNotSeemToBeAValidHomeserver": {}, + "countChatsAndCountParticipants": "{chats} csevegések és {participants} résztvevők", + "@countChatsAndCountParticipants": { + "type": "text", + "placeholders": { + "chats": {}, + "participants": {} + } + }, + "noMoreChatsFound": "Nincs több csevegés...", + "@noMoreChatsFound": {}, + "joinedChats": "Csatlakozott csevegések", + "@joinedChats": {} } From 09c48673825f5ace2b0c081fcf950f5c5dc511e7 Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 10 Sep 2024 07:58:39 +0000 Subject: [PATCH 008/236] Translated using Weblate (German) Currently translated at 99.3% (659 of 663 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/ --- assets/l10n/intl_de.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/l10n/intl_de.arb b/assets/l10n/intl_de.arb index 2a9eb554c..145c7beac 100644 --- a/assets/l10n/intl_de.arb +++ b/assets/l10n/intl_de.arb @@ -2794,6 +2794,6 @@ "@sendCanceled": {}, "noChatsFoundHere": "Hier wurden noch keine Chats gefunden. Starten Sie einen neuen Chat mit jemandem, indem Sie die Schaltfläche unten verwenden. ⤵️", "@noChatsFoundHere": {}, - "whatIsAHomeserver": "Was ist ein Heimserver?", + "whatIsAHomeserver": "Was ist ein Homeserver?", "@whatIsAHomeserver": {} } From a732ea62e29c76af7fa170769c3a8764559d16d4 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Wed, 11 Sep 2024 11:45:46 +0200 Subject: [PATCH 009/236] feat: New audio message design with displayed body --- lib/pages/chat/events/audio_player.dart | 235 +++++++++++++-------- lib/pages/chat/events/message_content.dart | 1 + 2 files changed, 150 insertions(+), 86 deletions(-) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index d4667bb9d..80492f284 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -4,24 +4,33 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:just_audio/just_audio.dart'; import 'package:matrix/matrix.dart'; import 'package:opus_caf_converter_dart/opus_caf_converter_dart.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/url_launcher.dart'; import '../../../utils/matrix_sdk_extensions/event_extension.dart'; class AudioPlayerWidget extends StatefulWidget { final Color color; + final double fontSize; final Event event; static String? currentId; static const int wavesCount = 40; - const AudioPlayerWidget(this.event, {this.color = Colors.black, super.key}); + const AudioPlayerWidget( + this.event, { + this.color = Colors.black, + required this.fontSize, + super.key, + }); @override AudioPlayerState createState() => AudioPlayerState(); @@ -39,8 +48,8 @@ class AudioPlayerState extends State { StreamSubscription? onPlayerError; String? statusText; - int currentPosition = 0; - double maxPosition = 0; + double currentPosition = 0; + double maxPosition = 1; MatrixFile? matrixFile; File? audioFile; @@ -58,6 +67,14 @@ class AudioPlayerState extends State { super.dispose(); } + void _startAction() { + if (status == AudioPlayerStatus.downloaded) { + _playAction(); + } else { + _downloadAction(); + } + } + Future _downloadAction() async { if (status != AudioPlayerStatus.notDownloaded) return; setState(() => status = AudioPlayerStatus.downloading); @@ -125,9 +142,7 @@ class AudioPlayerState extends State { setState(() { statusText = '${state.inMinutes.toString().padLeft(2, '0')}:${(state.inSeconds % 60).toString().padLeft(2, '0')}'; - currentPosition = ((state.inMilliseconds.toDouble() / maxPosition) * - AudioPlayerWidget.wavesCount) - .round(); + currentPosition = state.inMilliseconds.toDouble(); }); if (state.inMilliseconds.toDouble() == maxPosition) { audioPlayer.stop(); @@ -222,103 +237,151 @@ class AudioPlayerState extends State { final statusText = this.statusText ??= _durationString ?? '00:00'; final audioPlayer = this.audioPlayer; + + final body = widget.event.content.tryGet('body') ?? + widget.event.content.tryGet('filename'); + final displayBody = body != null && + body.isNotEmpty && + widget.event.content['org.matrix.msc1767.audio'] == null; + return Padding( padding: const EdgeInsets.all(12.0), - child: Row( + child: Column( mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: buttonSize, - height: buttonSize, - child: status == AudioPlayerStatus.downloading - ? CircularProgressIndicator(strokeWidth: 2, color: widget.color) - : InkWell( - borderRadius: BorderRadius.circular(64), - child: Material( - color: widget.color.withAlpha(64), - borderRadius: BorderRadius.circular(64), - child: Icon( - audioPlayer?.playerState.playing == true - ? Icons.pause_outlined - : Icons.play_arrow_outlined, - color: widget.color, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConstrainedBox( + constraints: + const BoxConstraints(maxWidth: FluffyThemes.columnWidth), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: buttonSize, + height: buttonSize, + child: status == AudioPlayerStatus.downloading + ? CircularProgressIndicator( + strokeWidth: 2, + color: widget.color, + ) + : InkWell( + borderRadius: BorderRadius.circular(64), + onLongPress: () => widget.event.saveFile(context), + onTap: _startAction, + child: Material( + color: widget.color.withAlpha(64), + borderRadius: BorderRadius.circular(64), + child: Icon( + audioPlayer?.playerState.playing == true + ? Icons.pause_outlined + : Icons.play_arrow_outlined, + color: widget.color, + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + for (var i = 0; + i < AudioPlayerWidget.wavesCount; + i++) + Expanded( + child: Container( + height: 32, + alignment: Alignment.center, + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: 1, + ), + decoration: BoxDecoration( + color: widget.color.withAlpha(96), + borderRadius: BorderRadius.circular(32), + ), + height: 32 * (waveform[i] / 1024), + ), + ), + ), + ], + ), ), - ), - onLongPress: () => widget.event.saveFile(context), - onTap: () { - if (status == AudioPlayerStatus.downloaded) { - _playAction(); - } else { - _downloadAction(); - } - }, + SizedBox( + height: 32, + child: Slider( + thumbColor: widget.event.senderId == + widget.event.room.client.userID + ? theme.colorScheme.onPrimary + : theme.colorScheme.primary, + activeColor: Colors.transparent, + inactiveColor: Colors.transparent, + max: maxPosition, + value: currentPosition, + onChanged: (position) => audioPlayer == null + ? _startAction() + : audioPlayer.seek( + Duration(milliseconds: position.round()), + ), + ), + ), + ], ), - ), - const SizedBox(width: 8), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - for (var i = 0; i < AudioPlayerWidget.wavesCount; i++) - GestureDetector( - onTapDown: (_) => audioPlayer?.seek( - Duration( - milliseconds: - (maxPosition / AudioPlayerWidget.wavesCount).round() * - i, + ), + const SizedBox(width: 8), + SizedBox( + width: 36, + child: Text( + statusText, + style: TextStyle( + color: widget.color, + fontSize: 12, ), ), - child: Container( - height: 32, - color: widget.color.withAlpha(0), - alignment: Alignment.center, - child: Opacity( - opacity: currentPosition > i ? 1 : 0.5, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 1), - decoration: BoxDecoration( - color: widget.color, - borderRadius: BorderRadius.circular(2), + ), + const SizedBox(width: 8), + Badge( + isLabelVisible: audioPlayer != null, + label: audioPlayer == null + ? null + : Text( + '${audioPlayer.speed.toString()}x', ), - width: 2, - height: 32 * (waveform[i] / 1024), - ), + backgroundColor: theme.colorScheme.secondary, + textColor: theme.colorScheme.onSecondary, + child: InkWell( + splashColor: widget.color.withAlpha(128), + borderRadius: BorderRadius.circular(64), + onTap: audioPlayer == null ? null : _toggleSpeed, + child: Icon( + Icons.mic_none_outlined, + color: widget.color, ), ), ), - ], + const SizedBox(width: 8), + ], + ), ), - const SizedBox(width: 8), - SizedBox( - width: 36, - child: Text( - statusText, + if (displayBody) + Linkify( + text: body, style: TextStyle( color: widget.color, - fontSize: 12, + fontSize: widget.fontSize, ), - ), - ), - const SizedBox(width: 8), - Badge( - isLabelVisible: audioPlayer != null, - label: audioPlayer == null - ? null - : Text( - '${audioPlayer.speed.toString()}x', - ), - backgroundColor: theme.colorScheme.secondary, - textColor: theme.colorScheme.onSecondary, - child: InkWell( - splashColor: widget.color.withAlpha(128), - borderRadius: BorderRadius.circular(64), - onTap: audioPlayer == null ? null : _toggleSpeed, - child: Icon( - Icons.mic_none_outlined, - color: widget.color, + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: widget.color.withAlpha(150), + fontSize: widget.fontSize, + decoration: TextDecoration.underline, + decorationColor: widget.color.withAlpha(150), ), + onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), ), - ), - const SizedBox(width: 8), ], ), ); diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 5aeb650a4..415e37428 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -151,6 +151,7 @@ class MessageContent extends StatelessWidget { return AudioPlayerWidget( event, color: textColor, + fontSize: fontSize, ); } return MessageDownloadContent(event, textColor); From 54306847f8bcb911c5a144726069d9a24247495b Mon Sep 17 00:00:00 2001 From: krille-chan Date: Wed, 11 Sep 2024 13:56:28 +0200 Subject: [PATCH 010/236] chore: Follow up audioplayer design --- lib/pages/chat/events/audio_player.dart | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 80492f284..6225a5c5e 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:fluffychat/config/app_config.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -244,6 +245,11 @@ class AudioPlayerState extends State { body.isNotEmpty && widget.event.content['org.matrix.msc1767.audio'] == null; + final wavePosition = + (currentPosition / maxPosition) * AudioPlayerWidget.wavesCount; + + final fontSize = 12 * AppConfig.fontSizeFactor; + return Padding( padding: const EdgeInsets.all(12.0), child: Column( @@ -300,8 +306,10 @@ class AudioPlayerState extends State { horizontal: 1, ), decoration: BoxDecoration( - color: widget.color.withAlpha(96), - borderRadius: BorderRadius.circular(32), + color: i < wavePosition + ? theme.colorScheme.onPrimaryContainer + : widget.color.withAlpha(64), + borderRadius: BorderRadius.circular(64), ), height: 32 * (waveform[i] / 1024), ), @@ -366,22 +374,24 @@ class AudioPlayerState extends State { ], ), ), - if (displayBody) + if (displayBody) ...[ + const SizedBox(height: 8), Linkify( text: body, style: TextStyle( color: widget.color, - fontSize: widget.fontSize, + fontSize: fontSize, ), options: const LinkifyOptions(humanize: false), linkStyle: TextStyle( color: widget.color.withAlpha(150), - fontSize: widget.fontSize, + fontSize: fontSize, decoration: TextDecoration.underline, decorationColor: widget.color.withAlpha(150), ), onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), ), + ], ], ), ); From b59356147007c829f2bdc737589fe117896a8a1b Mon Sep 17 00:00:00 2001 From: Peter Wallerius Date: Tue, 10 Sep 2024 20:40:09 +0000 Subject: [PATCH 011/236] Translated using Weblate (German) Currently translated at 100.0% (663 of 663 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/ --- assets/l10n/intl_de.arb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_de.arb b/assets/l10n/intl_de.arb index 145c7beac..6bf23b00b 100644 --- a/assets/l10n/intl_de.arb +++ b/assets/l10n/intl_de.arb @@ -2795,5 +2795,13 @@ "noChatsFoundHere": "Hier wurden noch keine Chats gefunden. Starten Sie einen neuen Chat mit jemandem, indem Sie die Schaltfläche unten verwenden. ⤵️", "@noChatsFoundHere": {}, "whatIsAHomeserver": "Was ist ein Homeserver?", - "@whatIsAHomeserver": {} + "@whatIsAHomeserver": {}, + "doesNotSeemToBeAValidHomeserver": "Scheint kein kompatibler Homeserver zu sein. Falsche URL?", + "@doesNotSeemToBeAValidHomeserver": {}, + "loginWithMatrixId": "Mit Matrix-ID anmelden", + "@loginWithMatrixId": {}, + "discoverHomeservers": "Server suchen", + "@discoverHomeservers": {}, + "homeserverDescription": "Alle deine Daten werden auf dem Homeserver gespeichert, so wie bei einem Email Anbieter. Du kannst aussuchen, welchen Homeserver du benutzen willst und kannst trotzdem mit allen kommunizieren. Erfahre mehr auf https:/matrix.org.", + "@homeserverDescription": {} } From 439dad0d9debe0206baffac674492286fdb6c00d Mon Sep 17 00:00:00 2001 From: fadelkon Date: Sat, 14 Sep 2024 22:01:00 +0000 Subject: [PATCH 012/236] Translated using Weblate (Catalan) Currently translated at 100.0% (663 of 663 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ca/ --- assets/l10n/intl_ca.arb | 203 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 199 insertions(+), 4 deletions(-) diff --git a/assets/l10n/intl_ca.arb b/assets/l10n/intl_ca.arb index 0608d4456..d1e781a84 100644 --- a/assets/l10n/intl_ca.arb +++ b/assets/l10n/intl_ca.arb @@ -1606,7 +1606,7 @@ "type": "text", "description": "Usage hint for the command /react" }, - "defaultPermissionLevel": "Nivell de permisos per defecte", + "defaultPermissionLevel": "Nivell de permisos per defecte per nous membres", "@defaultPermissionLevel": { "type": "text", "placeholders": {} @@ -2212,7 +2212,7 @@ "type": "text", "placeholders": {} }, - "hasKnocked": "{user} pica a la porta", + "hasKnocked": "🚪 {user} pica a la porta", "@hasKnocked": { "placeholders": { "user": {} @@ -2418,7 +2418,7 @@ "@screenSharingDetail": {}, "placeCall": "Truca", "@placeCall": {}, - "block": "bloca", + "block": "Bloca", "@block": {}, "blockUsername": "Ignora aquesti usuàrïi", "@blockUsername": {}, @@ -2607,5 +2607,200 @@ "commandHint_ignore": "Ignora el compte de matrix especificat", "@commandHint_ignore": {}, "commandHint_unignore": "Deixa d'ignorar el compt de matrix especificat", - "@commandHint_unignore": {} + "@commandHint_unignore": {}, + "sendCanceled": "S'ha canceŀlat l'enviament", + "@sendCanceled": {}, + "noChatsFoundHere": "Encara no hi ha xats. Obre una conversa amb algú picant al botó de sota. ⤵️", + "@noChatsFoundHere": {}, + "hideMemberChangesInPublicChatsBody": "No mostres a l'històric de conversa de les sales públiques quan algú hi entra o surt. Això facilita la lectura.", + "@hideMemberChangesInPublicChatsBody": {}, + "invitedBy": "📩 Convidadi per {user}", + "@invitedBy": { + "placeholders": { + "user": {} + } + }, + "knock": "Pica", + "@knock": {}, + "knocking": "S'està picant", + "@knocking": {}, + "restricted": "Restringit", + "@restricted": {}, + "knockRestricted": "No es pot picar a la porta", + "@knockRestricted": {}, + "goToSpace": "Ves a l'espai {space}", + "@goToSpace": { + "type": "text", + "space": {} + }, + "adminLevel": "{level} - Admin", + "@adminLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "changeGeneralChatSettings": "Canvia les opcions generals de xat", + "@changeGeneralChatSettings": {}, + "sendRoomNotifications": "Envia notificacions @room", + "@sendRoomNotifications": {}, + "changeTheDescriptionOfTheGroup": "Canvia la descripció de la sala", + "@changeTheDescriptionOfTheGroup": {}, + "changelog": "Registre de canvis", + "@changelog": {}, + "userLevel": "{level} - Usuàriï", + "@userLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "moderatorLevel": "{level} - Moderadori", + "@moderatorLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "inviteOtherUsers": "Convida més gent a la conversa", + "@inviteOtherUsers": {}, + "changeTheChatPermissions": "Canvia els permisos de la sala", + "@changeTheChatPermissions": {}, + "changeTheVisibilityOfChatHistory": "Canvia la visibilitat de l'historial de conversa", + "@changeTheVisibilityOfChatHistory": {}, + "changeTheCanonicalRoomAlias": "Canvia l'adreça principal de la sala", + "@changeTheCanonicalRoomAlias": {}, + "accessAndVisibilityDescription": "Qui pot entrar a aquesta conversa i com pot ser descoberta.", + "@accessAndVisibilityDescription": {}, + "customEmojisAndStickers": "Emojis i stickers propis", + "@customEmojisAndStickers": {}, + "accessAndVisibility": "Accés i visibilitat", + "@accessAndVisibility": {}, + "calls": "Trucades", + "@calls": {}, + "hideRedactedMessages": "Amaga els missatges estripats", + "@hideRedactedMessages": {}, + "hideInvalidOrUnknownMessageFormats": "Amaga els missatges que tinguin un format desconegut", + "@hideInvalidOrUnknownMessageFormats": {}, + "hideMemberChangesInPublicChats": "Amaga els canvis d'estat de lis membres a les sales públiques", + "@hideMemberChangesInPublicChats": {}, + "notifyMeFor": "Nofica'm que", + "@notifyMeFor": {}, + "overview": "Resum", + "@overview": {}, + "passwordRecoverySettings": "Recuperació de contrasenya", + "@passwordRecoverySettings": {}, + "userRole": "Rol d'usuàriï", + "@userRole": {}, + "minimumPowerLevel": "El nivell mínim de permisos és {level}.", + "@minimumPowerLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "gallery": "Galeria", + "@gallery": {}, + "noDatabaseEncryption": "No es pot xifrar la base de dades en aquesta plataforma", + "@noDatabaseEncryption": {}, + "usersMustKnock": "Lis membres han de picar a la porta", + "@usersMustKnock": {}, + "noOneCanJoin": "Ningú s'hi pot ficar", + "@noOneCanJoin": {}, + "userWouldLikeToChangeTheChat": "{user} vol entrar a la sala.", + "@userWouldLikeToChangeTheChat": { + "placeholders": { + "user": {} + } + }, + "customEmojisAndStickersBody": "Afegeix o comparteix emojis o stickers. Els podràs fer servir en qualsevol conversa.", + "@customEmojisAndStickersBody": {}, + "hideRedactedMessagesBody": "Si algú estripa un missatge, ja no apareixerà a l'historial de la conversa.", + "@hideRedactedMessagesBody": {}, + "searchIn": "Cerca a la sala \"{chat}\"...", + "@searchIn": { + "type": "text", + "placeholders": { + "chat": {} + } + }, + "markAsUnread": "Marca com a no llegit", + "@markAsUnread": {}, + "chatPermissionsDescription": "Defineix quin nivell de permisos cal per cada acció en aquesta sala. Els nivells 0, 50 i 100 normalment representen usuàriïs, mods i admins, però es pot canviar.", + "@chatPermissionsDescription": {}, + "updateInstalled": "🎉 S'ha actualitzat a la versió {version}!", + "@updateInstalled": { + "type": "text", + "placeholders": { + "version": {} + } + }, + "alwaysUse24HourFormat": "true", + "@alwaysUse24HourFormat": { + "description": "Set to true to always display time of day in 24 hour format." + }, + "appLockDescription": "Bloca l'app amb un pin quan no la facis servir", + "@appLockDescription": {}, + "swipeRightToLeftToReply": "Llisca de dreta esquerra per respondre", + "@swipeRightToLeftToReply": {}, + "globalChatId": "Identificador global de xat", + "@globalChatId": {}, + "createNewAddress": "Crea una adreça nova", + "@createNewAddress": {}, + "searchMore": "Cerca més...", + "@searchMore": {}, + "files": "Arxius", + "@files": {}, + "publicChatAddresses": "Adreces públiques de la sala", + "@publicChatAddresses": {}, + "unreadChatsInApp": "{appname}: {unread} converses pendents", + "@unreadChatsInApp": { + "type": "text", + "placeholders": { + "appname": {}, + "unread": {} + } + }, + "thereAreCountUsersBlocked": "Ara mateix hi ha {count} usuàriïs bloquejadis.", + "@thereAreCountUsersBlocked": { + "type": "text", + "count": {} + }, + "loginWithMatrixId": "Entra amb l'id de Matrix", + "@loginWithMatrixId": {}, + "discoverHomeservers": "Descobreix servidors", + "@discoverHomeservers": {}, + "whatIsAHomeserver": "Què és un servidor de Matrix?", + "@whatIsAHomeserver": {}, + "homeserverDescription": "Totes les teves dades s'emmagatzemen al servidor, com passa amb el e-mail. Pots triar quin servidor vols fer servir sense témer a no poder comunicar gent d'altres servidors. Llegeix-ne més a https://matrix.org.", + "@homeserverDescription": {}, + "doesNotSeemToBeAValidHomeserver": "No sembla un servidor compatible. Pot ser que la URL estigui malament?", + "@doesNotSeemToBeAValidHomeserver": {}, + "countChatsAndCountParticipants": "{chats} xats i {participants} participants", + "@countChatsAndCountParticipants": { + "type": "text", + "placeholders": { + "chats": {}, + "participants": {} + } + }, + "noMoreChatsFound": "No hi ha més xats...", + "@noMoreChatsFound": {}, + "space": "Espai", + "@space": {}, + "joinedChats": "Xats on has entrat", + "@joinedChats": {}, + "unread": "Sense llegir", + "@unread": {}, + "spaces": "Espais", + "@spaces": {}, + "noPublicLinkHasBeenCreatedYet": "No s'ha creat cap enllaç públic", + "@noPublicLinkHasBeenCreatedYet": {}, + "chatCanBeDiscoveredViaSearchOnServer": "La sala es pot descobrir amb la cerca de {server}", + "@chatCanBeDiscoveredViaSearchOnServer": { + "type": "text", + "placeholders": { + "server": {} + } + } } From 3355e66e9a5cb90818eb2a2ee2ccfd22af900908 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 16 Sep 2024 08:08:13 +0200 Subject: [PATCH 013/236] chore: Fix formatting --- lib/pages/chat/events/audio_player.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 6225a5c5e..13b2a3989 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:fluffychat/config/app_config.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -11,6 +10,7 @@ import 'package:matrix/matrix.dart'; import 'package:opus_caf_converter_dart/opus_caf_converter_dart.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; From 003c109453401523dd807f35c75871b48ff42b74 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Mon, 16 Sep 2024 16:49:34 +0200 Subject: [PATCH 014/236] build: Update to Flutter 3.24.3 --- .github/workflows/versions.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/versions.env b/.github/workflows/versions.env index 598275c27..6519d6b5c 100644 --- a/.github/workflows/versions.env +++ b/.github/workflows/versions.env @@ -1,2 +1,2 @@ -FLUTTER_VERSION=3.24.2 +FLUTTER_VERSION=3.24.3 JAVA_VERSION=17 From 75d28e16ea3d0c4ddbc365a79ac0b0b9ed80a5bb Mon Sep 17 00:00:00 2001 From: Krille Date: Wed, 18 Sep 2024 10:13:53 +0200 Subject: [PATCH 015/236] chore: Follow up waveform color --- lib/pages/chat/events/audio_player.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 13b2a3989..8b7f8b525 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -307,8 +307,8 @@ class AudioPlayerState extends State { ), decoration: BoxDecoration( color: i < wavePosition - ? theme.colorScheme.onPrimaryContainer - : widget.color.withAlpha(64), + ? widget.color + : widget.color.withAlpha(128), borderRadius: BorderRadius.circular(64), ), height: 32 * (waveform[i] / 1024), From 5152dbfa07b7d618965b94f49cf178b35863ffd8 Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Tue, 17 Sep 2024 16:14:20 +0000 Subject: [PATCH 016/236] Translated using Weblate (Basque) Currently translated at 100.0% (663 of 663 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/eu/ --- assets/l10n/intl_eu.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/l10n/intl_eu.arb b/assets/l10n/intl_eu.arb index f317b99de..d0903579c 100644 --- a/assets/l10n/intl_eu.arb +++ b/assets/l10n/intl_eu.arb @@ -2715,7 +2715,7 @@ }, "noMoreChatsFound": "Ez da beste txatik aurkitu...", "@noMoreChatsFound": {}, - "unread": "Irakurri gabe", + "unread": "Irakurtzeke", "@unread": {}, "space": "Gunea", "@space": {}, From 95ab1f9cc4f2597910caac809a2045e9e65f07bf Mon Sep 17 00:00:00 2001 From: Edgars Andersons Date: Tue, 17 Sep 2024 05:58:49 +0000 Subject: [PATCH 017/236] Translated using Weblate (Latvian) Currently translated at 100.0% (663 of 663 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/lv/ --- assets/l10n/intl_lv.arb | 99 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_lv.arb b/assets/l10n/intl_lv.arb index 5c11ff97f..044f7dee7 100644 --- a/assets/l10n/intl_lv.arb +++ b/assets/l10n/intl_lv.arb @@ -1827,7 +1827,7 @@ }, "noChatDescriptionYet": "Vēl nav izveidots tērzēšanas apraksts.", "@noChatDescriptionYet": {}, - "defaultPermissionLevel": "Noklusējuma atļauju līmenis", + "defaultPermissionLevel": "Noklusējuma atļauju līmenis jauniem lietotājiem", "@defaultPermissionLevel": { "type": "text", "placeholders": {} @@ -2686,5 +2686,100 @@ "restricted": "Ierobežots", "@restricted": {}, "knockRestricted": "Pieklauvēt ierobežotajiem", - "@knockRestricted": {} + "@knockRestricted": {}, + "sendCanceled": "Sūtīšana atcelta", + "@sendCanceled": {}, + "noChatsFoundHere": "Šeit vēl nav tērzēšanu. Jauna saruna ar kādu ir uzsākama ar zemāk esošo pogu. ⤵️", + "@noChatsFoundHere": {}, + "invitedBy": "📩 {user} uzaicināja", + "@invitedBy": { + "placeholders": { + "user": {} + } + }, + "moderatorLevel": "{level} - Moderators", + "@moderatorLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "adminLevel": "{level} - Pārvaldītājs", + "@adminLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "changeTheVisibilityOfChatHistory": "Mainīt tērzēšanas vēstures redzamību", + "@changeTheVisibilityOfChatHistory": {}, + "changeTheCanonicalRoomAlias": "Mainīt tērzēšanas galveno publisko adresi", + "@changeTheCanonicalRoomAlias": {}, + "sendRoomNotifications": "Sūtīt @istaba paziņojumus", + "@sendRoomNotifications": {}, + "changeTheDescriptionOfTheGroup": "Mainīt tērzēšanas aprakstu", + "@changeTheDescriptionOfTheGroup": {}, + "alwaysUse24HourFormat": "nē", + "@alwaysUse24HourFormat": { + "description": "Set to true to always display time of day in 24 hour format." + }, + "userLevel": "{level} - Lietotājs", + "@userLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "goToSpace": "Doties uz vietu: {space}", + "@goToSpace": { + "type": "text", + "space": {} + }, + "changeGeneralChatSettings": "Mainīt vispārējos tērzēšanas iestatījumus", + "@changeGeneralChatSettings": {}, + "inviteOtherUsers": "Uzaicināt šajā tērzēšanā citus lietotājus", + "@inviteOtherUsers": {}, + "changeTheChatPermissions": "Mainīt tērzēšanas atļaujas", + "@changeTheChatPermissions": {}, + "chatPermissionsDescription": "Noteikt, kurš spēka līmenis ir nepieciešams noteiktām darbībām šajā tērzēšanā. Spēka līmeņi 0, 50 un 100 parasti atbilst lietotājiem, moderatoriem un pārvaldītājiem, bet ir iespējams jebkāds iedalījums.", + "@chatPermissionsDescription": {}, + "doesNotSeemToBeAValidHomeserver": "Neizskatās pēc saderīga mājasservera. Nepareizs URL?", + "@doesNotSeemToBeAValidHomeserver": {}, + "loginWithMatrixId": "Pieteikties ar Matrix-Id", + "@loginWithMatrixId": {}, + "discoverHomeservers": "Atklāt mājasserverus", + "@discoverHomeservers": {}, + "whatIsAHomeserver": "Kas ir mājasserveris?", + "@whatIsAHomeserver": {}, + "homeserverDescription": "Visi lietotāja dati tiek glabāti mājasserverī, gluži kā ar e-pasta nodrošinātāju. Ir iespējams izvēlēties, kuru mājasserveri izmantot, saglabājot iespēju sazināties ar ikvienu. Vairāk var uzzināt https://matrix.org.", + "@homeserverDescription": {}, + "updateInstalled": "🎉 Atjauninājums {version} uzstādīts.", + "@updateInstalled": { + "type": "text", + "placeholders": { + "version": {} + } + }, + "changelog": "Izmaiņu žurnāls", + "@changelog": {}, + "countChatsAndCountParticipants": "{chats} tērzēšanas un {participants} dalībnieki", + "@countChatsAndCountParticipants": { + "type": "text", + "placeholders": { + "chats": {}, + "participants": {} + } + }, + "noMoreChatsFound": "Vairs nav tērzēšanu...", + "@noMoreChatsFound": {}, + "joinedChats": "Tērzēšanas, kurās piedalos", + "@joinedChats": {}, + "unread": "Nelasītas", + "@unread": {}, + "space": "Vieta", + "@space": {}, + "spaces": "Vietas", + "@spaces": {}, + "markAsUnread": "Atzīmēt kā nelasītu", + "@markAsUnread": {} } From 439092f90d7e13b9d21c91d666d1e4497f5c0376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jos=C3=A9=20m?= Date: Wed, 18 Sep 2024 06:24:40 +0000 Subject: [PATCH 018/236] Translated using Weblate (Galician) Currently translated at 100.0% (663 of 663 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/gl/ --- assets/l10n/intl_gl.arb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_gl.arb b/assets/l10n/intl_gl.arb index 55c4f8790..b5b72fb3a 100644 --- a/assets/l10n/intl_gl.arb +++ b/assets/l10n/intl_gl.arb @@ -2196,7 +2196,7 @@ "@callingPermissions": {}, "callingAccount": "Conta que chama", "@callingAccount": {}, - "callingAccountDetails": "Permítelle a FluffyChat usar a app marcador nativa de android.", + "callingAccountDetails": "Permítelle a FluffyChat usar a app de telefonía nativa de android.", "@callingAccountDetails": {}, "appearOnTopDetails": "Permítelle á app aparecer por enriba (non é preciso se xa configuraches FluffyChat como unha conta para chamadas)", "@appearOnTopDetails": {}, @@ -2690,7 +2690,7 @@ "level": {} } }, - "searchIn": "Buscar na charla \"{chat}\"...", + "searchIn": "Buscar na conversa \"{chat}\"...", "@searchIn": { "type": "text", "placeholders": { From c63a53bbcc377f3d2c0e3ffb6dc0922a05206856 Mon Sep 17 00:00:00 2001 From: Krille Date: Thu, 19 Sep 2024 16:04:16 +0200 Subject: [PATCH 019/236] build: Update to matrix dart sdk 0.33 --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 348f73d54..870b44251 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1210,10 +1210,10 @@ packages: dependency: "direct main" description: name: matrix - sha256: f47a751e01abe5a6aa17b21187724788c45e3700fbec14a04ac1e2d2097b9a81 + sha256: bf2127e89041441ccd873a6435ff289a8f45e5e83337db95493bb239b56ed4aa url: "https://pub.dev" source: hosted - version: "0.32.4" + version: "0.33.0" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index cee396750..55c052c38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: keyboard_shortcuts: ^0.1.4 latlong2: ^0.9.1 linkify: ^5.0.0 - matrix: ^0.32.3 + matrix: ^0.33.0 native_imaging: ^0.1.1 opus_caf_converter_dart: ^1.0.1 package_info_plus: ^6.0.0 From 2cad97fac761432faf1fecc14c1ecd7c4e5bff1e Mon Sep 17 00:00:00 2001 From: Krille Date: Thu, 19 Sep 2024 18:41:08 +0200 Subject: [PATCH 020/236] build: Update dependencies --- lib/utils/background_push.dart | 2 + pubspec.lock | 305 +++++++++++++++++---------------- 2 files changed, 157 insertions(+), 150 deletions(-) diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index 039dde895..748461f2a 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -323,6 +323,8 @@ class BackgroundPush { } Future setupUp() async { + // Blocked by https://codeberg.org/UnifiedPush/flutter-connector/issues/2 + // ignore: deprecated_member_use await UnifiedPush.registerAppWithDialog(matrix!.context); } diff --git a/pubspec.lock b/pubspec.lock index 870b44251..993921887 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,31 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" adaptive_dialog: dependency: "direct main" description: name: adaptive_dialog - sha256: "817ff9b4bb441434d1fcb39a8d4492e50be456cd3507e4f19c5c7455c9e279e0" + sha256: "165018c7520eb42ea8184b87601f7489c100c87abce15cd06e0a036c60f954e6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" analyzer: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.7.0" animations: dependency: "direct main" description: @@ -37,10 +42,10 @@ packages: dependency: transitive description: name: ansicolor - sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" appkit_ui_element_colors: dependency: transitive description: @@ -77,10 +82,10 @@ packages: dependency: transitive description: name: audio_session - sha256: "4012d798e25a0ebfd4ad7203fefe7e386fdfedd2243afc52fa700b8bde6a3a4f" + sha256: "343e83bc7809fbda2591a49e525d6b63213ade10c76f15813be9aed6657b3261" url: "https://pub.dev" source: hosted - version: "0.1.20" + version: "0.1.21" badges: dependency: "direct main" description: @@ -121,14 +126,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - build_config: - dependency: transitive - description: - name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 - url: "https://pub.dev" - source: hosted - version: "1.1.1" callkeep: dependency: "direct main" description: @@ -173,10 +170,10 @@ packages: dependency: "direct main" description: name: chewie - sha256: e53da939709efb9aad0f3d72a69a8d05f889168b7a138af60ce78bab5c94b135 + sha256: "335df378c025588aef400c704bd71f0daea479d4cd57c471c88c056c1144e7cd" url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.8.5" cli_util: dependency: transitive description: @@ -229,26 +226,26 @@ packages: dependency: transitive description: name: coverage - sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" + sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.2" cross_file: dependency: transitive description: name: cross_file - sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.4+1" + version: "0.3.4+2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" csslib: dependency: transitive description: @@ -269,10 +266,10 @@ packages: dependency: transitive description: name: dart_webrtc - sha256: dc97a87e8d8c168fc5401ef18eaa0e06e1ac8fc099812de27a002860d021e227 + sha256: ac7ef077084b3e54004716f1d736fcd839e1b60bc3f21f4122a35a9bb5ca2e47 url: "https://pub.dev" source: hosted - version: "1.4.7" + version: "1.4.8" dbus: dependency: transitive description: @@ -281,14 +278,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" - dependency_validator: - dependency: transitive - description: - name: dependency_validator - sha256: f727a5627aa405965fab4aef4f468e50a9b632ba0737fd2f98c932fec6d712b9 - url: "https://pub.dev" - source: hosted - version: "3.2.3" desktop_drop: dependency: "direct main" description: @@ -309,18 +298,18 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91 + sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 url: "https://pub.dev" source: hosted - version: "10.1.0" + version: "10.1.2" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" dynamic_color: dependency: "direct main" description: @@ -373,10 +362,10 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: @@ -389,10 +378,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "824f5b9f389bfc4dddac3dea76cd70c51092d9dff0b2ece7ef4f53db8547d258" + sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3" url: "https://pub.dev" source: hosted - version: "8.0.6" + version: "8.0.7" file_selector_linux: dependency: transitive description: @@ -421,10 +410,10 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" url: "https://pub.dev" source: hosted - version: "0.9.3+1" + version: "0.9.3+2" fixnum: dependency: transitive description: @@ -567,18 +556,18 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "0a9068149f0225e81642b03562e99776106edbd967816ee68bc16310d457c60e" + sha256: c500d5d9e7e553f06b61877ca6b9c8b92c570a4c8db371038702e8ce57f8a50f url: "https://pub.dev" source: hosted - version: "17.2.1+1" + version: "17.2.2" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af url: "https://pub.dev" source: hosted - version: "4.0.0+1" + version: "4.0.1" flutter_local_notifications_platform_interface: dependency: transitive description: @@ -636,10 +625,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e + sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.0.22" flutter_ringtone_player: dependency: "direct main" description: @@ -836,10 +825,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "39dd52168d6c59984454183148dc3a5776960c61083adfc708cc79a7b3ce1ba8" + sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.7" gradient_borders: dependency: transitive description: @@ -940,18 +929,18 @@ packages: dependency: transitive description: name: image_picker_android - sha256: ff39a10ab4f48f4ac70776d0494a97bf073cd2570892cd46bc8a5cac162c25db + sha256: c0a6763d50b354793d0192afd0a12560b823147d3ded7c6b77daf658fa05cc85 url: "https://pub.dev" source: hosted - version: "0.8.12+4" + version: "0.8.12+13" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3" + sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.5" image_picker_ios: dependency: transitive description: @@ -1049,10 +1038,10 @@ packages: dependency: "direct main" description: name: just_audio - sha256: ee50602364ba83fa6308f5512dd560c713ec3e1f2bc75f0db43618f0d82ef71a + sha256: d8e8aaf417d33e345299c17f6457f72bd4ba0c549dc34607abb5183a354edc4d url: "https://pub.dev" source: hosted - version: "0.9.39" + version: "0.9.40" just_audio_platform_interface: dependency: transitive description: @@ -1065,10 +1054,10 @@ packages: dependency: transitive description: name: just_audio_web - sha256: "0edb481ad4aa1ff38f8c40f1a3576013c3420bf6669b686fe661627d49bc606c" + sha256: b163878529d9b028c53a6972fcd58cae2405bcd11cbfcea620b6fb9f151429d6 url: "https://pub.dev" source: hosted - version: "0.4.11" + version: "0.4.12" keyboard_shortcuts: dependency: "direct main" description: @@ -1154,10 +1143,10 @@ packages: dependency: transitive description: name: logger - sha256: af05cc8714f356fd1f3888fb6741cbe9fbe25cdb6eedbab80e1a6db21047d4a4 + sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" logging: dependency: transitive description: @@ -1170,10 +1159,10 @@ packages: dependency: transitive description: name: macos_ui - sha256: "91c7f3427f763fd96b65831342b896b18751140e6bf55f8572fcb41f7b30bcab" + sha256: "80f6539aba5a3a1182d5225a6c27969a780bcb1d2d8135b4ffb708570cf0c854" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.9" macos_window_utils: dependency: transitive description: @@ -1182,6 +1171,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" markdown: dependency: transitive description: @@ -1234,18 +1231,18 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" msix: dependency: "direct dev" description: name: msix - sha256: "519b183d15dc9f9c594f247e2d2339d855cf0eaacc30e19b128e14f3ecc62047" + sha256: c50d6bd1aafe0d071a3c1e5a5ccb056404502935cb0a549e3178c4aae16caf33 url: "https://pub.dev" source: hosted - version: "3.16.7" + version: "3.16.8" native_imaging: dependency: "direct main" description: @@ -1354,10 +1351,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "30c5aa827a6ae95ce2853cdc5fe3971daaac00f6f081c419c013f7f57bff2f5e" + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" url: "https://pub.dev" source: hosted - version: "2.2.7" + version: "2.2.10" path_provider_foundation: dependency: transitive description: @@ -1402,10 +1399,10 @@ packages: dependency: transitive description: name: permission_handler_android - sha256: b29a799ca03be9f999aa6c39f7de5209482d638e6f857f6b93b0875c618b7e54 + sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa" url: "https://pub.dev" source: hosted - version: "12.0.7" + version: "12.0.12" permission_handler_apple: dependency: transitive description: @@ -1418,18 +1415,18 @@ packages: dependency: transitive description: name: permission_handler_html - sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" + sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851 url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.3+2" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" + sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 url: "https://pub.dev" source: hosted - version: "4.2.1" + version: "4.2.3" permission_handler_windows: dependency: transitive description: @@ -1474,10 +1471,10 @@ packages: dependency: transitive description: name: pointer_interceptor - sha256: d0a8e660d1204eaec5bd34b34cc92174690e076d2e4f893d9d68c486a13b07c4 + sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523" url: "https://pub.dev" source: hosted - version: "0.10.1+1" + version: "0.10.1+2" pointer_interceptor_ios: dependency: transitive description: @@ -1498,10 +1495,10 @@ packages: dependency: transitive description: name: pointer_interceptor_web - sha256: a6237528b46c411d8d55cdfad8fcb3269fc4cbb26060b14bff94879165887d1e + sha256: "7a7087782110f8c1827170660b09f8aa893e0e9a61431dbbe2ac3fc482e8c044" url: "https://pub.dev" source: hosted - version: "0.10.2" + version: "0.10.2+1" polylabel: dependency: transitive description: @@ -1578,10 +1575,10 @@ packages: dependency: transitive description: name: qr - sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" qr_code_scanner: dependency: "direct main" description: @@ -1594,10 +1591,10 @@ packages: dependency: transitive description: name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" random_string: dependency: transitive description: @@ -1626,18 +1623,18 @@ packages: dependency: transitive description: name: record_android - sha256: "9ccf6a206dc72b486cf37893690e70c17610e8f05dba8da1a808e73dc2f49a04" + sha256: d7af0b3119725a0f561817c72b5f5eca4d7a76d441deef519ae04e4824c0734c url: "https://pub.dev" source: hosted - version: "1.2.4" + version: "1.2.6" record_darwin: dependency: transitive description: name: record_darwin - sha256: b038c26d1066eb81f4e7433bfb85f0d450ca3fac0002a7216b83a21b775ecf21 + sha256: fe90d302acb1f3cee1ade5df9c150ca5cee33b48d8cdf1cf433bf577d7f00134 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" record_linux: dependency: transitive description: @@ -1658,10 +1655,10 @@ packages: dependency: transitive description: name: record_web - sha256: "703adb626d31e2dd86a8f6b34e306e03cd323e0c5e16e11bbc0385b07a8df97e" + sha256: "656b7a865f90651fab997c2a563364f5fd60a0b527d5dadbb915d62d84fc3867" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.3" record_windows: dependency: transitive description: @@ -1682,10 +1679,10 @@ packages: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" safe_url_check: dependency: transitive description: @@ -1730,58 +1727,58 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.2" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shelf: dependency: transitive description: @@ -1802,18 +1799,18 @@ packages: dependency: transitive description: name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -1831,10 +1828,10 @@ packages: dependency: transitive description: name: source_map_stack_trace - sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" source_maps: dependency: transitive description: @@ -1871,10 +1868,10 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + sha256: "4058172e418eb7e7f2058dcb7657d451a8fc264afa0dea4dbd0f304a57131611" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.4+3" sqflite_common_ffi: dependency: "direct main" description: @@ -1887,18 +1884,18 @@ packages: dependency: "direct main" description: name: sqlcipher_flutter_libs - sha256: "672e7f9d8a19896c3bfc44ca5f6fd8ee978970c5946817eeedf5e59c176aacf1" + sha256: a6a08d3082c1deaacc8f6670c78a9c2384991102db5b234d5293aa2c65e87f61 url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.6.4" sqlite3: dependency: transitive description: name: sqlite3 - sha256: "6d17989c0b06a5870b2190d391925186f944cb943e5262d0d3f778fcfca3bc6e" + sha256: fde692580bee3379374af1f624eb3e113ab2865ecb161dbe2d8ac2de9735dbdb url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.5" stack_trace: dependency: transitive description: @@ -1951,10 +1948,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: "51b08572b9f091f8c3eb4d9d4be253f196ff0075d5ec9b10a884026d5b55d7bc" url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.3.0+2" tar: dependency: transitive description: @@ -2079,26 +2076,26 @@ packages: dependency: "direct main" description: name: unifiedpush - sha256: ef7f3ae6139d27169604e3844379ef7929af573a2be21d9e82187f44ab7b9a32 + sha256: "6dbed5a6305ca33f1865c7a3d814ae39476b79a2d23ca76a5708f023f405730f" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.2" unifiedpush_android: dependency: transitive description: name: unifiedpush_android - sha256: "610ad746294541f56d632adf9afba5d1c164c44e23ec0dd2162a41a6ff00a00e" + sha256: "7443dece0a850ae956514f809983eb2b39fc518c2c7d24dbfe817198bec89134" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.0" unifiedpush_platform_interface: dependency: transitive description: name: unifiedpush_platform_interface - sha256: "7782b18a15d22bb184fa766ef1e0c675eef862055ff815453df7041dfd026146" + sha256: dd588d78a8b2bfc10430e30035526e98caa543d0b7364a6344b5eb4815721c6d url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" universal_html: dependency: "direct main" description: @@ -2135,10 +2132,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf + sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab url: "https://pub.dev" source: hosted - version: "6.3.3" + version: "6.3.10" url_launcher_ios: dependency: transitive description: @@ -2151,10 +2148,10 @@ packages: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.0" url_launcher_macos: dependency: transitive description: @@ -2175,26 +2172,26 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" uuid: dependency: transitive description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.0" vector_graphics: dependency: transitive description: @@ -2247,10 +2244,10 @@ packages: dependency: transitive description: name: video_player_android - sha256: fdc0331ce9f808cc2714014cb8126bd6369943affefd54f8fdab0ea0bb617b7f + sha256: "38d8fe136c427abdce68b5e8c3c08ea29d7a794b453c7a51b12ecfad4aad9437" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.7.3" video_player_avfoundation: dependency: transitive description: @@ -2271,10 +2268,10 @@ packages: dependency: transitive description: name: video_player_web - sha256: ff4d69a6614b03f055397c27a71c9d3ddea2b2a23d71b2ba0164f59ca32b8fe2 + sha256: "6dcdd298136523eaf7dfc31abaf0dfba9aa8a8dbc96670e87e9d42b6f2caf774" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" visibility_detector: dependency: transitive description: @@ -2295,10 +2292,10 @@ packages: dependency: "direct main" description: name: wakelock_plus - sha256: "14758533319a462ffb5aa3b7ddb198e59b29ac3b02da14173a1715d65d4e6e68" + sha256: bf4ee6f17a2fa373ed3753ad0e602b7603f8c75af006d5b9bdade263928c0484 url: "https://pub.dev" source: hosted - version: "1.2.5" + version: "1.2.8" wakelock_plus_platform_interface: dependency: transitive description: @@ -2323,14 +2320,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "3.0.1" webdriver: dependency: transitive description: @@ -2367,10 +2372,10 @@ packages: dependency: transitive description: name: win32_registry - sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.1.5" window_to_front: dependency: transitive description: @@ -2412,5 +2417,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" From 838dcb413e2d66ce56966e490bb2d5e612cf9617 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 22 Sep 2024 09:38:57 +0200 Subject: [PATCH 021/236] chore: Make imageviewer full window --- lib/pages/chat/events/image_bubble.dart | 1 - lib/pages/image_viewer/image_viewer_view.dart | 33 ++++++++++++++----- lib/widgets/mxc_image.dart | 3 +- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/lib/pages/chat/events/image_bubble.dart b/lib/pages/chat/events/image_bubble.dart index 3ee745eeb..9dbdceb04 100644 --- a/lib/pages/chat/events/image_bubble.dart +++ b/lib/pages/chat/events/image_bubble.dart @@ -60,7 +60,6 @@ class ImageBubble extends StatelessWidget { if (!tapToView) return; showDialog( context: context, - useRootNavigator: false, builder: (_) => ImageViewer(event), ); } diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index e4352864f..fd04183e4 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -14,38 +14,55 @@ class ImageViewerView extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.black, + backgroundColor: Colors.black.withOpacity(0.5), extendBodyBehindAppBar: true, appBar: AppBar( elevation: 0, leading: IconButton( + style: IconButton.styleFrom( + backgroundColor: Colors.black.withOpacity(0.5), + ), icon: const Icon(Icons.close), onPressed: Navigator.of(context).pop, color: Colors.white, tooltip: L10n.of(context)!.close, ), - backgroundColor: const Color(0x44000000), + backgroundColor: Colors.transparent, actions: [ IconButton( + style: IconButton.styleFrom( + backgroundColor: Colors.black.withOpacity(0.5), + ), icon: const Icon(Icons.reply_outlined), onPressed: controller.forwardAction, color: Colors.white, tooltip: L10n.of(context)!.share, ), + const SizedBox(width: 8), IconButton( + style: IconButton.styleFrom( + backgroundColor: Colors.black.withOpacity(0.5), + ), icon: const Icon(Icons.download_outlined), onPressed: () => controller.saveFileAction(context), color: Colors.white, tooltip: L10n.of(context)!.downloadFile, ), + const SizedBox(width: 8), if (PlatformInfos.isMobile) // Use builder context to correctly position the share dialog on iPad - Builder( - builder: (context) => IconButton( - onPressed: () => controller.shareFileAction(context), - tooltip: L10n.of(context)!.share, - color: Colors.white, - icon: Icon(Icons.adaptive.share_outlined), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Builder( + builder: (context) => IconButton( + style: IconButton.styleFrom( + backgroundColor: Colors.black.withOpacity(0.5), + ), + onPressed: () => controller.shareFileAction(context), + tooltip: L10n.of(context)!.share, + color: Colors.white, + icon: Icon(Icons.adaptive.share_outlined), + ), ), ), ], diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index d776cfc82..bf007f530 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -64,7 +64,8 @@ class _MxcImageState extends State { } Future _load() async { - final client = widget.client ?? Matrix.of(context).client; + final client = + widget.client ?? widget.event?.room.client ?? Matrix.of(context).client; final uri = widget.uri; final event = widget.event; From 6866a996a37306df115f0fb8d37d0057ed60bfcc Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 22 Sep 2024 14:36:35 +0200 Subject: [PATCH 022/236] chore: Follow up imageviewer --- lib/pages/chat/events/image_bubble.dart | 5 ++++- lib/pages/image_viewer/image_viewer.dart | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/pages/chat/events/image_bubble.dart b/lib/pages/chat/events/image_bubble.dart index 9dbdceb04..a2fcf23b3 100644 --- a/lib/pages/chat/events/image_bubble.dart +++ b/lib/pages/chat/events/image_bubble.dart @@ -60,7 +60,10 @@ class ImageBubble extends StatelessWidget { if (!tapToView) return; showDialog( context: context, - builder: (_) => ImageViewer(event), + builder: (_) => ImageViewer( + event, + outerContext: context, + ), ); } diff --git a/lib/pages/image_viewer/image_viewer.dart b/lib/pages/image_viewer/image_viewer.dart index 94ab19dd4..9e8cfe82c 100644 --- a/lib/pages/image_viewer/image_viewer.dart +++ b/lib/pages/image_viewer/image_viewer.dart @@ -10,8 +10,9 @@ import '../../utils/matrix_sdk_extensions/event_extension.dart'; class ImageViewer extends StatefulWidget { final Event event; + final BuildContext outerContext; - const ImageViewer(this.event, {super.key}); + const ImageViewer(this.event, {required this.outerContext, super.key}); @override ImageViewerController createState() => ImageViewerController(); @@ -20,8 +21,9 @@ class ImageViewer extends StatefulWidget { class ImageViewerController extends State { /// Forward this image to another room. void forwardAction() { - Matrix.of(context).shareContent = widget.event.content; - context.go('/rooms'); + Matrix.of(widget.outerContext).shareContent = widget.event.content; + Navigator.of(context).pop(); + widget.outerContext.go('/rooms'); } /// Save this file with a system call. From 5c9880f0b20772bcd916765a6ee1d77c8b0bccbd Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 22 Sep 2024 15:45:41 +0200 Subject: [PATCH 023/236] refactor: Load bytes from sending files later to not let app crash --- assets/l10n/intl_en.arb | 3 +- lib/pages/chat/chat.dart | 74 +--- lib/pages/chat/send_file_dialog.dart | 338 +++++++++++------- lib/pages/chat_list/chat_list.dart | 9 +- .../{resize_image.dart => resize_video.dart} | 25 +- pubspec.lock | 4 +- pubspec.yaml | 2 + 7 files changed, 242 insertions(+), 213 deletions(-) rename lib/utils/{resize_image.dart => resize_video.dart} (56%) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 8c0c677f9..b8ef839d5 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2761,5 +2761,6 @@ "discoverHomeservers": "Discover homeservers", "whatIsAHomeserver": "What is a homeserver?", "homeserverDescription": "All your data is stored on the homeserver, just like an email provider. You can choose which homeserver you want to use, while you can still communicate with everyone. Learn more at at https://matrix.org.", - "doesNotSeemToBeAValidHomeserver": "Doesn't seem to be a compatible homeserver. Wrong URL?" + "doesNotSeemToBeAValidHomeserver": "Doesn't seem to be a compatible homeserver. Wrong URL?", + "calculatingFileSize": "Calculating file size..." } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index cd770ca5f..9b934e05b 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -37,7 +37,6 @@ import 'package:fluffychat/widgets/app_lock.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/account_bundles.dart'; import '../../utils/localized_exception_extension.dart'; -import '../../utils/matrix_sdk_extensions/matrix_file_extension.dart'; import 'send_file_dialog.dart'; import 'send_location_dialog.dart'; @@ -123,36 +122,11 @@ class ChatController extends State void onDragDone(DropDoneDetails details) async { setState(() => dragging = false); if (details.files.isEmpty) return; - final result = await showFutureLoadingDialog( - context: context, - future: () async { - final clientConfig = await room.client.getConfig(); - final maxUploadSize = clientConfig.mUploadSize ?? 100 * 1024 * 1024; - final matrixFiles = await Future.wait( - details.files.map( - (xfile) async { - final length = await xfile.length(); - if (length > maxUploadSize) { - throw FileTooBigMatrixException(length, maxUploadSize); - } - return MatrixFile( - bytes: await xfile.readAsBytes(), - name: xfile.name, - mimeType: xfile.mimeType, - ).detectFileType; - }, - ), - ); - return matrixFiles; - }, - ); - final matrixFiles = result.result; - if (matrixFiles == null || matrixFiles.isEmpty) return; await showAdaptiveDialog( context: context, builder: (c) => SendFileDialog( - files: matrixFiles, + files: details.files, room: room, ), ); @@ -510,36 +484,24 @@ class ChatController extends State FilePicker.platform.pickFiles( compressionQuality: 0, allowMultiple: false, - withData: true, ), ); if (result == null || result.files.isEmpty) return; await showAdaptiveDialog( context: context, builder: (c) => SendFileDialog( - files: result.files - .map( - (xfile) => MatrixFile( - bytes: xfile.bytes!, - name: xfile.name, - ).detectFileType, - ) - .toList(), + files: result.xFiles, room: room, ), ); } void sendImageFromClipBoard(Uint8List? image) async { + if (image == null) return; await showAdaptiveDialog( context: context, builder: (c) => SendFileDialog( - files: [ - MatrixFile( - bytes: image!, - name: "image from Clipboard", - ).detectFileType, - ], + files: [XFile.fromData(image)], room: room, ), ); @@ -550,7 +512,6 @@ class ChatController extends State FilePicker.platform.pickFiles( compressionQuality: 0, type: FileType.image, - withData: true, allowMultiple: false, ), ); @@ -559,14 +520,7 @@ class ChatController extends State await showAdaptiveDialog( context: context, builder: (c) => SendFileDialog( - files: result.files - .map( - (xfile) => MatrixFile( - bytes: xfile.bytes!, - name: xfile.name, - ).detectFileType, - ) - .toList(), + files: result.xFiles, room: room, ), ); @@ -577,16 +531,11 @@ class ChatController extends State FocusScope.of(context).requestFocus(FocusNode()); final file = await ImagePicker().pickImage(source: ImageSource.camera); if (file == null) return; - final bytes = await file.readAsBytes(); + await showAdaptiveDialog( context: context, builder: (c) => SendFileDialog( - files: [ - MatrixImageFile( - bytes: bytes, - name: file.path, - ), - ], + files: [file], room: room, ), ); @@ -600,16 +549,11 @@ class ChatController extends State maxDuration: const Duration(minutes: 1), ); if (file == null) return; - final bytes = await file.readAsBytes(); + await showAdaptiveDialog( context: context, builder: (c) => SendFileDialog( - files: [ - MatrixVideoFile( - bytes: bytes, - name: file.path, - ), - ], + files: [file], room: room, ), ); diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index edcb981f3..5109a1d40 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -1,18 +1,25 @@ +import 'dart:io'; + import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:cross_file/cross_file.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; +import 'package:mime/mime.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/error_reporter.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/size_string.dart'; -import '../../utils/resize_image.dart'; +import '../../utils/resize_video.dart'; class SendFileDialog extends StatefulWidget { final Room room; - final List files; + final List files; const SendFileDialog({ required this.room, @@ -33,158 +40,233 @@ class SendFileDialogState extends State { Future _send() async { final scaffoldMessenger = ScaffoldMessenger.of(context); final l10n = L10n.of(context)!; - for (var file in widget.files) { - MatrixImageFile? thumbnail; - if (file is MatrixVideoFile && file.bytes.length > minSizeToCompress) { - await showFutureLoadingDialog( - context: context, - future: () async { - file = origImage ? file : await file.resizeVideo(); - thumbnail = await file.getVideoThumbnail(); - }, - ); - } - widget.room - .sendFileEvent( - file, - thumbnail: thumbnail, - shrinkImageMaxDimension: origImage ? null : 1600, - ) - .catchError( - (e, s) { - if (e is FileTooBigMatrixException) { - scaffoldMessenger.showSnackBar( - SnackBar(content: Text(l10n.fileIsTooBigForServer)), - ); - return null; - } - ErrorReporter(context, 'Unable to send file').onErrorCallback(e, s); - return null; - }, - ); - } + Navigator.of(context, rootNavigator: false).pop(); + showFutureLoadingDialog( + context: context, + future: () async { + final clientConfig = await widget.room.client.getConfig(); + final maxUploadSize = clientConfig.mUploadSize ?? 100 * 1024 * 1024; + + for (final xfile in widget.files) { + final MatrixFile file; + MatrixImageFile? thumbnail; + final length = await xfile.length(); + final mimeType = xfile.mimeType ?? lookupMimeType(xfile.path); + + // If file is a video, shrink it! + if (mimeType != null && + mimeType.startsWith('video') && + length > minSizeToCompress && + !origImage) { + file = await xfile.resizeVideo(); + thumbnail = await xfile.getVideoThumbnail(); + } else { + // Else we just create a MatrixFile + file = MatrixFile( + bytes: await xfile.readAsBytes(), + name: xfile.name, + mimeType: xfile.mimeType, + ).detectFileType; + } + + if (file.bytes.length > maxUploadSize) { + throw FileTooBigMatrixException(length, maxUploadSize); + } + + widget.room + .sendFileEvent( + file, + thumbnail: thumbnail, + shrinkImageMaxDimension: origImage ? null : 1600, + ) + .catchError( + (e, s) { + if (e is FileTooBigMatrixException) { + scaffoldMessenger.showSnackBar( + SnackBar(content: Text(l10n.fileIsTooBigForServer)), + ); + return null; + } + ErrorReporter(context, 'Unable to send file') + .onErrorCallback(e, s); + return null; + }, + ); + } + }, + ); + return; } + Future _calcCombinedFileSize() async { + final lengths = + await Future.wait(widget.files.map((file) => file.length())); + return lengths.fold(0, (p, length) => p + length).sizeString; + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); var sendStr = L10n.of(context)!.sendFile; - final allFilesAreImages = - widget.files.every((file) => file is MatrixImageFile); - final sizeString = widget.files - .fold(0, (p, file) => p + file.bytes.length) - .sizeString; + final uniqueMimeType = widget.files + .map((file) => file.mimeType ?? lookupMimeType(file.path)) + .toSet() + .singleOrNull; + final fileName = widget.files.length == 1 ? widget.files.single.name : L10n.of(context)!.countFiles(widget.files.length.toString()); - if (allFilesAreImages) { + if (uniqueMimeType?.startsWith('image') ?? false) { sendStr = L10n.of(context)!.sendImage; - } else if (widget.files.every((file) => file is MatrixAudioFile)) { + } else if (uniqueMimeType?.startsWith('audio') ?? false) { sendStr = L10n.of(context)!.sendAudio; - } else if (widget.files.every((file) => file is MatrixVideoFile)) { + } else if (uniqueMimeType?.startsWith('video') ?? false) { sendStr = L10n.of(context)!.sendVideo; } - Widget contentWidget; - if (allFilesAreImages) { - contentWidget = Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Material( - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - elevation: theme.appBarTheme.scrolledUnderElevation ?? 4, - shadowColor: theme.appBarTheme.shadowColor, - clipBehavior: Clip.hardEdge, - child: Image.memory( - widget.files.first.bytes, - fit: BoxFit.contain, - height: 256, - ), - ), - ), - const SizedBox(height: 16), - // Workaround for SwitchListTile.adaptive crashes in CupertinoDialog - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CupertinoSwitch( - value: origImage, - onChanged: (v) => setState(() => origImage = v), + + return FutureBuilder( + future: _calcCombinedFileSize(), + builder: (context, snapshot) { + final sizeString = + snapshot.data ?? L10n.of(context)!.calculatingFileSize; + + Widget contentWidget; + if (uniqueMimeType?.startsWith('image') ?? false) { + contentWidget = Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Material( + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + elevation: theme.appBarTheme.scrolledUnderElevation ?? 4, + shadowColor: theme.appBarTheme.shadowColor, + clipBehavior: Clip.hardEdge, + child: kIsWeb + ? Image.network( + widget.files.first.path, + fit: BoxFit.contain, + height: 256, + ) + : Image.file( + File(widget.files.first.path), + fit: BoxFit.contain, + height: 256, + ), + ), ), - const SizedBox(width: 16), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - L10n.of(context)!.sendOriginal, - style: const TextStyle(fontWeight: FontWeight.bold), + const SizedBox(height: 16), + // Workaround for SwitchListTile.adaptive crashes in CupertinoDialog + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CupertinoSwitch( + value: origImage, + onChanged: (v) => setState(() => origImage = v), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + L10n.of(context)!.sendOriginal, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text(sizeString), + ], ), - Text(sizeString), - ], - ), + ), + ], ), ], - ), - ], - ); - } else if (widget.files.every((file) => file is MatrixVideoFile)) { - contentWidget = Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(fileName), - const SizedBox(height: 16), - // Workaround for SwitchListTile.adaptive crashes in CupertinoDialog - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CupertinoSwitch( - value: origImage, - onChanged: (v) => setState(() => origImage = v), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + ); + } else { + final fileNameParts = fileName.split('.'); + contentWidget = SizedBox( + width: 256, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( children: [ - Text( - L10n.of(context)!.sendOriginal, - style: const TextStyle(fontWeight: FontWeight.bold), + Icon( + uniqueMimeType == null + ? Icons.description_outlined + : uniqueMimeType.startsWith('video') + ? Icons.video_file_outlined + : uniqueMimeType.startsWith('audio') + ? Icons.audio_file_outlined + : Icons.description_outlined, ), - Text(sizeString), + const SizedBox(width: 8), + Expanded( + child: Text( + fileNameParts.first, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (fileNameParts.length > 1) + Text('.${fileNameParts.last}'), + Text(' ($sizeString)'), ], ), - ), - ], - ), - ], - ); - } else { - contentWidget = Text('$fileName ($sizeString)'); - } - return AlertDialog.adaptive( - title: Text(sendStr), - content: contentWidget, - actions: [ - TextButton( - onPressed: () { - // just close the dialog - Navigator.of(context, rootNavigator: false).pop(); - }, - child: Text(L10n.of(context)!.cancel), - ), - TextButton( - onPressed: _send, - child: Text(L10n.of(context)!.send), - ), - ], + // Workaround for SwitchListTile.adaptive crashes in CupertinoDialog + if (uniqueMimeType != null && + uniqueMimeType.startsWith('video') && + PlatformInfos.isMobile) + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CupertinoSwitch( + value: origImage, + onChanged: (v) => setState(() => origImage = v), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + L10n.of(context)!.sendOriginal, + style: + const TextStyle(fontWeight: FontWeight.bold), + ), + Text(sizeString), + ], + ), + ), + ], + ), + ], + ), + ); + } + return AlertDialog.adaptive( + title: Text(sendStr), + content: contentWidget, + actions: [ + TextButton( + onPressed: () { + // just close the dialog + Navigator.of(context, rootNavigator: false).pop(); + }, + child: Text(L10n.of(context)!.cancel), + ), + TextButton( + onPressed: _send, + child: Text(L10n.of(context)!.send), + ), + ], + ); + }, ); } } diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 9df247e4c..bc9b3fa02 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:cross_file/cross_file.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_shortcuts/flutter_shortcuts.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; @@ -205,7 +206,13 @@ class ChatListController extends State context: context, useRootNavigator: false, builder: (c) => SendFileDialog( - files: [shareFile], + files: [ + XFile.fromData( + shareFile.bytes, + name: shareFile.name, + mimeType: shareFile.mimeType, + ), + ], room: room, ), ); diff --git a/lib/utils/resize_image.dart b/lib/utils/resize_video.dart similarity index 56% rename from lib/utils/resize_image.dart rename to lib/utils/resize_video.dart index 3bfe81a6d..768677106 100644 --- a/lib/utils/resize_image.dart +++ b/lib/utils/resize_video.dart @@ -1,28 +1,25 @@ -import 'dart:io'; - +import 'package:cross_file/cross_file.dart'; import 'package:matrix/matrix.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:video_compress/video_compress.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -extension ResizeImage on MatrixFile { +extension ResizeImage on XFile { static const int max = 1200; static const int quality = 40; Future resizeVideo() async { - final tmpDir = await getTemporaryDirectory(); - final tmpFile = File('${tmpDir.path}/$name'); MediaInfo? mediaInfo; - await tmpFile.writeAsBytes(bytes); try { - // will throw an error e.g. on Android SDK < 18 - mediaInfo = await VideoCompress.compressVideo(tmpFile.path); + if (PlatformInfos.isMobile) { + // will throw an error e.g. on Android SDK < 18 + mediaInfo = await VideoCompress.compressVideo(path); + } } catch (e, s) { Logs().w('Error while compressing video', e, s); } return MatrixVideoFile( - bytes: (await mediaInfo?.file?.readAsBytes()) ?? bytes, + bytes: (await mediaInfo?.file?.readAsBytes()) ?? await readAsBytes(), name: name, mimeType: mimeType, width: mediaInfo?.width, @@ -33,13 +30,9 @@ extension ResizeImage on MatrixFile { Future getVideoThumbnail() async { if (!PlatformInfos.isMobile) return null; - final tmpDir = await getTemporaryDirectory(); - final tmpFile = File('${tmpDir.path}/$name'); - if (await tmpFile.exists() == false) { - await tmpFile.writeAsBytes(bytes); - } + try { - final bytes = await VideoCompress.getByteThumbnail(tmpFile.path); + final bytes = await VideoCompress.getByteThumbnail(path); if (bytes == null) return null; return MatrixImageFile( bytes: bytes, diff --git a/pubspec.lock b/pubspec.lock index 993921887..3bd1a3047 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -231,7 +231,7 @@ packages: source: hosted version: "1.9.2" cross_file: - dependency: transitive + dependency: "direct main" description: name: cross_file sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" @@ -1228,7 +1228,7 @@ packages: source: hosted version: "2.0.0" mime: - dependency: transitive + dependency: "direct main" description: name: mime sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" diff --git a/pubspec.yaml b/pubspec.yaml index 55c052c38..363abfed5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: callkeep: ^0.3.2 chewie: ^1.8.1 collection: ^1.18.0 + cross_file: ^0.3.4+2 cupertino_icons: any desktop_drop: ^0.4.4 desktop_notifications: ^0.6.3 @@ -65,6 +66,7 @@ dependencies: latlong2: ^0.9.1 linkify: ^5.0.0 matrix: ^0.33.0 + mime: ^1.0.6 native_imaging: ^0.1.1 opus_caf_converter_dart: ^1.0.1 package_info_plus: ^6.0.0 From b83503585fa77c4951a50e46725fb9a0b2620e57 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 22 Sep 2024 19:18:08 +0200 Subject: [PATCH 024/236] feat: Sending multiple files at once --- assets/l10n/intl_de.arb | 2 - assets/l10n/intl_en.arb | 30 +++- lib/pages/chat/chat.dart | 10 +- lib/pages/chat/send_file_dialog.dart | 157 +++++++++++++------ lib/pages/chat_list/chat_list.dart | 1 + lib/utils/localized_exception_extension.dart | 23 ++- 6 files changed, 165 insertions(+), 58 deletions(-) diff --git a/assets/l10n/intl_de.arb b/assets/l10n/intl_de.arb index 6bf23b00b..021f77046 100644 --- a/assets/l10n/intl_de.arb +++ b/assets/l10n/intl_de.arb @@ -2271,8 +2271,6 @@ "@disableEncryptionWarning": {}, "reopenChat": "Chat wieder eröffnen", "@reopenChat": {}, - "fileIsTooBigForServer": "Der Server meldet, dass die Datei zu groß ist für eine Übermittlung ist.", - "@fileIsTooBigForServer": {}, "noBackupWarning": "Achtung! Ohne Aktivierung des Chat-Backups verlierst du den Zugriff auf deine verschlüsselten Nachrichten. Vor dem Ausloggen wird dringend empfohlen, das Chat-Backup zu aktivieren.", "@noBackupWarning": {}, "noOtherDevicesFound": "Keine anderen Geräte anwesend", diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index b8ef839d5..b25e1977d 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2422,8 +2422,13 @@ "@noBackupWarning": {}, "noOtherDevicesFound": "No other devices found", "@noOtherDevicesFound": {}, - "fileIsTooBigForServer": "The server reports that the file is too large to be sent.", - "@fileIsTooBigForServer": {}, + "fileIsTooBigForServer": "Unable to send! The server only supports attachments up to {max}.", + "@fileIsTooBigForServer": { + "type": "text", + "placeholders": { + "max": {} + } + }, "fileHasBeenSavedAt": "File has been saved at {path}", "@fileHasBeenSavedAt": { "type": "text", @@ -2762,5 +2767,24 @@ "whatIsAHomeserver": "What is a homeserver?", "homeserverDescription": "All your data is stored on the homeserver, just like an email provider. You can choose which homeserver you want to use, while you can still communicate with everyone. Learn more at at https://matrix.org.", "doesNotSeemToBeAValidHomeserver": "Doesn't seem to be a compatible homeserver. Wrong URL?", - "calculatingFileSize": "Calculating file size..." + "calculatingFileSize": "Calculating file size...", + "prepareSendingAttachment": "Prepare sending attachment...", + "sendingAttachment": "Sending attachment...", + "generatingVideoThumbnail": "Generating video thumbnail...", + "compressVideo": "Compressing video...", + "sendingAttachmentCountOfCount": "Sending attachment {index} of {length}...", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + }, + "serverLimitReached": "Server limit reached! Waiting {seconds} seconds...", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + } } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 9b934e05b..c47de7a83 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -128,6 +128,7 @@ class ChatController extends State builder: (c) => SendFileDialog( files: details.files, room: room, + outerContext: context, ), ); } @@ -483,7 +484,7 @@ class ChatController extends State final result = await AppLock.of(context).pauseWhile( FilePicker.platform.pickFiles( compressionQuality: 0, - allowMultiple: false, + allowMultiple: true, ), ); if (result == null || result.files.isEmpty) return; @@ -492,6 +493,7 @@ class ChatController extends State builder: (c) => SendFileDialog( files: result.xFiles, room: room, + outerContext: context, ), ); } @@ -503,6 +505,7 @@ class ChatController extends State builder: (c) => SendFileDialog( files: [XFile.fromData(image)], room: room, + outerContext: context, ), ); } @@ -512,7 +515,7 @@ class ChatController extends State FilePicker.platform.pickFiles( compressionQuality: 0, type: FileType.image, - allowMultiple: false, + allowMultiple: true, ), ); if (result == null || result.files.isEmpty) return; @@ -522,6 +525,7 @@ class ChatController extends State builder: (c) => SendFileDialog( files: result.xFiles, room: room, + outerContext: context, ), ); } @@ -537,6 +541,7 @@ class ChatController extends State builder: (c) => SendFileDialog( files: [file], room: room, + outerContext: context, ), ); } @@ -555,6 +560,7 @@ class ChatController extends State builder: (c) => SendFileDialog( files: [file], room: room, + outerContext: context, ), ); } diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index 5109a1d40..78856bc03 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -6,12 +6,11 @@ import 'package:flutter/material.dart'; import 'package:cross_file/cross_file.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:mime/mime.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/utils/error_reporter.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/size_string.dart'; @@ -20,10 +19,12 @@ import '../../utils/resize_video.dart'; class SendFileDialog extends StatefulWidget { final Room room; final List files; + final BuildContext outerContext; const SendFileDialog({ required this.room, required this.files, + required this.outerContext, super.key, }); @@ -38,65 +39,98 @@ class SendFileDialogState extends State { static const int minSizeToCompress = 20 * 1024; Future _send() async { - final scaffoldMessenger = ScaffoldMessenger.of(context); + final scaffoldMessenger = ScaffoldMessenger.of(widget.outerContext); final l10n = L10n.of(context)!; - Navigator.of(context, rootNavigator: false).pop(); + try { + scaffoldMessenger.showLoadingSnackBar(l10n.prepareSendingAttachment); + Navigator.of(context, rootNavigator: false).pop(); + final clientConfig = await widget.room.client.getConfig(); + final maxUploadSize = clientConfig.mUploadSize ?? 100 * 1024 * 1024; - showFutureLoadingDialog( - context: context, - future: () async { - final clientConfig = await widget.room.client.getConfig(); - final maxUploadSize = clientConfig.mUploadSize ?? 100 * 1024 * 1024; + for (final xfile in widget.files) { + final MatrixFile file; + MatrixImageFile? thumbnail; + final length = await xfile.length(); + final mimeType = xfile.mimeType ?? lookupMimeType(xfile.path); - for (final xfile in widget.files) { - final MatrixFile file; - MatrixImageFile? thumbnail; - final length = await xfile.length(); - final mimeType = xfile.mimeType ?? lookupMimeType(xfile.path); + // If file is a video, shrink it! + if (PlatformInfos.isMobile && + mimeType != null && + mimeType.startsWith('video') && + length > minSizeToCompress && + !origImage) { + scaffoldMessenger.showLoadingSnackBar(l10n.compressVideo); + file = await xfile.resizeVideo(); + scaffoldMessenger.showLoadingSnackBar(l10n.generatingVideoThumbnail); + thumbnail = await xfile.getVideoThumbnail(); + } else { + // Else we just create a MatrixFile + file = MatrixFile( + bytes: await xfile.readAsBytes(), + name: xfile.name, + mimeType: xfile.mimeType, + ).detectFileType; + } - // If file is a video, shrink it! - if (mimeType != null && - mimeType.startsWith('video') && - length > minSizeToCompress && - !origImage) { - file = await xfile.resizeVideo(); - thumbnail = await xfile.getVideoThumbnail(); - } else { - // Else we just create a MatrixFile - file = MatrixFile( - bytes: await xfile.readAsBytes(), - name: xfile.name, - mimeType: xfile.mimeType, - ).detectFileType; - } + if (file.bytes.length > maxUploadSize) { + throw FileTooBigMatrixException(length, maxUploadSize); + } + + if (widget.files.length > 1) { + scaffoldMessenger.showLoadingSnackBar( + l10n.sendingAttachmentCountOfCount( + widget.files.indexOf(xfile) + 1, + widget.files.length, + ), + ); + } else { + scaffoldMessenger.showLoadingSnackBar(l10n.sendingAttachment); + } - if (file.bytes.length > maxUploadSize) { - throw FileTooBigMatrixException(length, maxUploadSize); + try { + await widget.room.sendFileEvent( + file, + thumbnail: thumbnail, + shrinkImageMaxDimension: origImage ? null : 1600, + ); + } on MatrixException catch (e) { + final retryAfterMs = e.retryAfterMs; + if (e.error != MatrixError.M_LIMIT_EXCEEDED || retryAfterMs == null) { + rethrow; } + final retryAfterDuration = + Duration(milliseconds: retryAfterMs + 1000); + + scaffoldMessenger.showSnackBar( + SnackBar( + content: Text( + l10n.serverLimitReached(retryAfterDuration.inSeconds), + ), + ), + ); + await Future.delayed(retryAfterDuration); + + scaffoldMessenger.showLoadingSnackBar(l10n.sendingAttachment); - widget.room - .sendFileEvent( + await widget.room.sendFileEvent( file, thumbnail: thumbnail, shrinkImageMaxDimension: origImage ? null : 1600, - ) - .catchError( - (e, s) { - if (e is FileTooBigMatrixException) { - scaffoldMessenger.showSnackBar( - SnackBar(content: Text(l10n.fileIsTooBigForServer)), - ); - return null; - } - ErrorReporter(context, 'Unable to send file') - .onErrorCallback(e, s); - return null; - }, ); } - }, - ); + } + } catch (e) { + scaffoldMessenger.clearSnackBars(); + scaffoldMessenger.showSnackBar( + SnackBar( + content: Text(e.toLocalizedString(widget.outerContext)), + duration: const Duration(seconds: 30), + showCloseIcon: true, + ), + ); + rethrow; + } return; } @@ -270,3 +304,30 @@ class SendFileDialogState extends State { ); } } + +extension on ScaffoldMessengerState { + ScaffoldFeatureController showLoadingSnackBar( + String title, + ) { + clearSnackBars(); + return showSnackBar( + SnackBar( + duration: const Duration(minutes: 5), + dismissDirection: DismissDirection.none, + content: Row( + children: [ + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ), + const SizedBox(width: 16), + Text(title), + ], + ), + ), + ); + } +} diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index bc9b3fa02..5c631eb0e 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -214,6 +214,7 @@ class ChatListController extends State ), ], room: room, + outerContext: context, ), ); Matrix.of(context).shareContent = null; diff --git a/lib/utils/localized_exception_extension.dart b/lib/utils/localized_exception_extension.dart index 0dfbe3dce..c501bcbcb 100644 --- a/lib/utils/localized_exception_extension.dart +++ b/lib/utils/localized_exception_extension.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:math'; import 'package:flutter/material.dart'; @@ -10,10 +11,29 @@ import 'package:matrix/matrix.dart'; import 'uia_request_manager.dart'; extension LocalizedExceptionExtension on Object { + static String _formatFileSize(int size) { + if (size < 1024) return '$size B'; + final i = (log(size) / log(1024)).floor(); + final num = (size / pow(1024, i)); + final round = num.round(); + final numString = round < 10 + ? num.toStringAsFixed(2) + : round < 100 + ? num.toStringAsFixed(1) + : round.toString(); + return '$numString ${'kMGTPEZY'[i - 1]}B'; + } + String toLocalizedString( BuildContext context, [ ExceptionContext? exceptionContext, ]) { + if (this is FileTooBigMatrixException) { + final exception = this as FileTooBigMatrixException; + return L10n.of(context)!.fileIsTooBigForServer( + _formatFileSize(exception.maxFileSize), + ); + } if (this is MatrixException) { switch ((this as MatrixException).error) { case MatrixError.M_FORBIDDEN: @@ -30,9 +50,6 @@ extension LocalizedExceptionExtension on Object { if (this is InvalidPassphraseException) { return L10n.of(context)!.wrongRecoveryKey; } - if (this is FileTooBigMatrixException) { - return L10n.of(context)!.fileIsTooBigForServer; - } if (this is BadServerVersionsException) { final serverVersions = (this as BadServerVersionsException) .serverVersions From d53d0d80830bdb3de0515f97afcc9ae62cf38899 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 22 Sep 2024 19:20:10 +0200 Subject: [PATCH 025/236] chore: Follow up sendfile snackbars --- lib/pages/chat/send_file_dialog.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index 78856bc03..6eb249cb7 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -85,7 +85,7 @@ class SendFileDialogState extends State { ), ); } else { - scaffoldMessenger.showLoadingSnackBar(l10n.sendingAttachment); + scaffoldMessenger.clearSnackBars(); } try { From dfb2605829f1aeea7fb13485bba6a90b42feddb2 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 22 Sep 2024 19:47:47 +0200 Subject: [PATCH 026/236] chore: Follow up send file snackbars --- lib/pages/chat/send_file_dialog.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index 6eb249cb7..5092d5566 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -120,6 +120,7 @@ class SendFileDialogState extends State { ); } } + scaffoldMessenger.clearSnackBars(); } catch (e) { scaffoldMessenger.clearSnackBars(); scaffoldMessenger.showSnackBar( From 8e5474b4e408f1bee247b850ca924416d7fd29c7 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 22 Sep 2024 20:08:49 +0200 Subject: [PATCH 027/236] build: Update dependencies --- pubspec.lock | 40 ++++++++++++++++++++-------------------- pubspec.yaml | 8 ++++---- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 3bd1a3047..6fdb11e55 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -266,10 +266,10 @@ packages: dependency: transitive description: name: dart_webrtc - sha256: ac7ef077084b3e54004716f1d736fcd839e1b60bc3f21f4122a35a9bb5ca2e47 + sha256: c664ad88d5646735753add421ee2118486c100febef5e92b7f59cdbabf6a51f6 url: "https://pub.dev" source: hosted - version: "1.4.8" + version: "1.4.9" dbus: dependency: transitive description: @@ -322,10 +322,10 @@ packages: dependency: "direct main" description: name: emoji_picker_flutter - sha256: "839200a2bd1af9a65d71133a5a246dbf5b24f7e4f6f4c5390130c2e0ed5f85af" + sha256: "7c6681783e06710608df27be0e38aa4ba73ca1ccac370bb0e7a1320723ae4bca" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.1" emojis: dependency: "direct main" description: @@ -378,10 +378,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3" + sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12" url: "https://pub.dev" source: hosted - version: "8.0.7" + version: "8.1.2" file_selector_linux: dependency: transitive description: @@ -740,10 +740,10 @@ packages: dependency: "direct main" description: name: flutter_webrtc - sha256: fd5f115a08dcdc00b988bea3003c956f1b60a78a61d899cbddfb44f5d0e44d4a + sha256: f6800cc2af79018c12e955ddf8ad007891fdfbb8199b0ce3dccd0977ed2add9c url: "https://pub.dev" source: hosted - version: "0.10.8" + version: "0.11.7" frontend_server_client: dependency: transitive description: @@ -1295,18 +1295,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: cb44f49b6e690fa766f023d5b22cac6b9affe741dd792b6ac7ad4fabe0d7b097 + sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "8.0.2" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" pana: dependency: transitive description: @@ -1711,18 +1711,18 @@ packages: dependency: "direct main" description: name: share_plus - sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544 + sha256: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52" url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "10.0.2" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4" + sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.0" shared_preferences: dependency: "direct main" description: @@ -1892,10 +1892,10 @@ packages: dependency: transitive description: name: sqlite3 - sha256: fde692580bee3379374af1f624eb3e113ab2865ecb161dbe2d8ac2de9735dbdb + sha256: "45f168ae2213201b54e09429ed0c593dc2c88c924a1488d6f9c523a255d567cb" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.6" stack_trace: dependency: transitive description: @@ -2316,10 +2316,10 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.0.0" web_socket: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 363abfed5..c7f6ae089 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: emoji_picker_flutter: ^2.1.1 emojis: ^0.9.9 #fcm_shared_isolate: ^0.1.0 - file_picker: ^8.0.6 + file_picker: ^8.1.2 flutter: sdk: flutter flutter_app_badger: ^1.5.0 @@ -49,7 +49,7 @@ dependencies: git: https://github.com/krille-chan/flutter_shortcuts.git flutter_typeahead: ^5.2.0 flutter_web_auth_2: ^3.1.1 - flutter_webrtc: ^0.10.3 + flutter_webrtc: ^0.11.7 future_loading_dialog: ^0.3.0 geolocator: ^7.6.2 go_router: ^14.0.1 @@ -69,7 +69,7 @@ dependencies: mime: ^1.0.6 native_imaging: ^0.1.1 opus_caf_converter_dart: ^1.0.1 - package_info_plus: ^6.0.0 + package_info_plus: ^8.0.2 pasteboard: ^0.2.0 path: ^1.9.0 path_provider: ^2.1.2 @@ -81,7 +81,7 @@ dependencies: receive_sharing_intent: 1.4.5 # Update needs more work record: ^5.1.2 scroll_to_index: ^3.0.1 - share_plus: ^9.0.0 + share_plus: ^10.0.2 shared_preferences: ^2.2.0 # Pinned because https://github.com/flutter/flutter/issues/118401 slugify: ^2.0.0 sqflite_common_ffi: ^2.3.3 From efad71e53513f09a86661637a4152a6e9532913d Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 22 Sep 2024 20:13:15 +0200 Subject: [PATCH 028/236] chore: Follow up google services patch --- scripts/enable-android-google-services.patch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/enable-android-google-services.patch b/scripts/enable-android-google-services.patch index 4ec4b8f13..85e390db3 100644 --- a/scripts/enable-android-google-services.patch +++ b/scripts/enable-android-google-services.patch @@ -131,6 +131,6 @@ index 69c80d6e..efd32d89 100644 emojis: ^0.9.9 - #fcm_shared_isolate: ^0.1.0 + fcm_shared_isolate: ^0.1.0 - file_picker: ^8.0.6 + file_picker: ^8.1.2 flutter: sdk: flutter From 188ce96ef31680cccf34ed25734abc0f10511c68 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Fri, 27 Sep 2024 17:43:02 +0200 Subject: [PATCH 029/236] chore: Improve message info dialog --- lib/pages/chat/event_info_dialog.dart | 52 +++++++++++++++------------ 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/lib/pages/chat/event_info_dialog.dart b/lib/pages/chat/event_info_dialog.dart index c930e4b14..fd2dcc68f 100644 --- a/lib/pages/chat/event_info_dialog.dart +++ b/lib/pages/chat/event_info_dialog.dart @@ -27,7 +27,7 @@ class EventInfoDialog extends StatelessWidget { super.key, }); - String get prettyJson { + String prettyJson(MatrixEvent event) { const decoder = JsonDecoder(); const encoder = JsonEncoder.withIndent(' '); final object = decoder.convert(jsonEncode(event.toJson())); @@ -37,6 +37,7 @@ class EventInfoDialog extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final originalSource = event.originalSource; return Scaffold( appBar: AppBar( title: Text(L10n.of(context)!.messageInfo), @@ -61,48 +62,53 @@ class EventInfoDialog extends StatelessWidget { ), ), ListTile( - title: Text(L10n.of(context)!.time), + title: Text('${L10n.of(context)!.time}:'), subtitle: Text(event.originServerTs.localizedTime(context)), ), ListTile( - title: Text(L10n.of(context)!.messageType), - subtitle: Text(event.humanreadableType), + title: Text('${L10n.of(context)!.status}:'), + subtitle: Text(event.status.name), ), ListTile(title: Text('${L10n.of(context)!.sourceCode}:')), Padding( padding: const EdgeInsets.all(12.0), child: Material( borderRadius: BorderRadius.circular(AppConfig.borderRadius), - color: theme.colorScheme.inverseSurface, + color: theme.colorScheme.surfaceContainer, child: SingleChildScrollView( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(16), scrollDirection: Axis.horizontal, child: SelectableText( - prettyJson, + prettyJson(MatrixEvent.fromJson(event.toJson())), style: TextStyle( - color: theme.colorScheme.onInverseSurface, + color: theme.colorScheme.onSurface, ), ), ), ), ), + if (originalSource != null) ...[ + ListTile(title: Text('${L10n.of(context)!.encrypted}:')), + Padding( + padding: const EdgeInsets.all(12.0), + child: Material( + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + color: theme.colorScheme.surfaceContainer, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + scrollDirection: Axis.horizontal, + child: SelectableText( + prettyJson(originalSource), + style: TextStyle( + color: theme.colorScheme.onSurface, + ), + ), + ), + ), + ), + ], ], ), ); } } - -extension on Event { - String get humanreadableType { - if (type == EventTypes.Message) { - return messageType.split('m.').last; - } - if (type.startsWith('m.room.')) { - return type.split('m.room.').last; - } - if (type.startsWith('m.')) { - return type.split('m.').last; - } - return type; - } -} From c636bc573a70511422aae555dea2d943103be8cd Mon Sep 17 00:00:00 2001 From: Krille Date: Sun, 29 Sep 2024 15:35:27 +0200 Subject: [PATCH 030/236] build: Update emoji picker package --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- scripts/enable-android-google-services.patch | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 6fdb11e55..194ec0bd8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -322,10 +322,10 @@ packages: dependency: "direct main" description: name: emoji_picker_flutter - sha256: "7c6681783e06710608df27be0e38aa4ba73ca1ccac370bb0e7a1320723ae4bca" + sha256: "08567e6f914d36c32091a96cf2f51d2558c47aa2bd47a590dc4f50e42e0965f6" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "3.1.0" emojis: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c7f6ae089..8cd648565 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: desktop_notifications: ^0.6.3 device_info_plus: ^10.0.1 dynamic_color: ^1.7.0 - emoji_picker_flutter: ^2.1.1 + emoji_picker_flutter: ^3.1.0 emojis: ^0.9.9 #fcm_shared_isolate: ^0.1.0 file_picker: ^8.1.2 diff --git a/scripts/enable-android-google-services.patch b/scripts/enable-android-google-services.patch index 85e390db3..ec7139117 100644 --- a/scripts/enable-android-google-services.patch +++ b/scripts/enable-android-google-services.patch @@ -127,7 +127,7 @@ index 69c80d6e..efd32d89 100644 +++ b/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: dynamic_color: ^1.7.0 - emoji_picker_flutter: ^2.1.1 + emoji_picker_flutter: ^3.1.0 emojis: ^0.9.9 - #fcm_shared_isolate: ^0.1.0 + fcm_shared_isolate: ^0.1.0 From 6f9d58df515d9963b4dd857905b72437be3f50f7 Mon Sep 17 00:00:00 2001 From: Krille Date: Sun, 29 Sep 2024 16:34:07 +0200 Subject: [PATCH 031/236] build: Increase iOS minimum deployment target to 13.0 --- ios/Podfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Podfile b/ios/Podfile index 1f9db6f6e..fac4d8176 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '12.1' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' From 1436269c9e610148a43f2316c097ac27077cc8ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Sep 2024 06:53:51 +0000 Subject: [PATCH 032/236] build(deps): bump webrick from 1.7.0 to 1.8.2 in /ios Bumps [webrick](https://github.com/ruby/webrick) from 1.7.0 to 1.8.2. - [Release notes](https://github.com/ruby/webrick/releases) - [Commits](https://github.com/ruby/webrick/compare/v1.7.0...v1.8.2) --- updated-dependencies: - dependency-name: webrick dependency-type: indirect ... Signed-off-by: dependabot[bot] --- ios/Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 3854e9409..1b22c3e97 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -184,7 +184,7 @@ GEM unf_ext unf_ext (0.0.7.7) unicode-display_width (1.7.0) - webrick (1.7.0) + webrick (1.8.2) word_wrap (1.0.0) xcodeproj (1.19.0) CFPropertyList (>= 2.3.3, < 4.0) From 2da20f1a3929a16057b126c76ca8a06e43e7f4b4 Mon Sep 17 00:00:00 2001 From: v1s7 Date: Wed, 25 Sep 2024 19:31:25 +0000 Subject: [PATCH 033/236] Translated using Weblate (Russian) Currently translated at 98.6% (654 of 663 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ru/ --- assets/l10n/intl_ru.arb | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/assets/l10n/intl_ru.arb b/assets/l10n/intl_ru.arb index 2526b66db..136577c49 100644 --- a/assets/l10n/intl_ru.arb +++ b/assets/l10n/intl_ru.arb @@ -284,12 +284,12 @@ "type": "text", "placeholders": {} }, - "changeTheHomeserver": "Изменить сервер Matrix", + "changeTheHomeserver": "Изменить домашний сервер", "@changeTheHomeserver": { "type": "text", "placeholders": {} }, - "changeTheme": "Тема", + "changeTheme": "Персонализация", "@changeTheme": { "type": "text", "placeholders": {} @@ -559,7 +559,7 @@ "type": "text", "placeholders": {} }, - "defaultPermissionLevel": "Уровень разрешений по умолчанию", + "defaultPermissionLevel": "Уровень разрешений по умолчанию для новых пользователей", "@defaultPermissionLevel": { "type": "text", "placeholders": {} @@ -711,7 +711,7 @@ "type": "text", "placeholders": {} }, - "enterYourHomeserver": "Введите адрес вашего сервера Matrix", + "enterYourHomeserver": "Введите адрес вашего домашнего сервера", "@enterYourHomeserver": { "type": "text", "placeholders": {} @@ -1919,7 +1919,7 @@ "@scanQrCode": {}, "sendOnEnter": "Отправлять по Enter", "@sendOnEnter": {}, - "homeserver": "Сервер Matrix", + "homeserver": "Домашний сервер", "@homeserver": {}, "serverRequiresEmail": "Этот сервер должен подтвердить ваш адрес электронной почты для регистрации.", "@serverRequiresEmail": {}, @@ -2760,5 +2760,28 @@ "@goToSpace": { "type": "text", "space": {} - } + }, + "sendCanceled": "Отправка отменена", + "@sendCanceled": {}, + "noChatsFoundHere": "Не было найдено ни одного чата. Начать с кем-нибудь новый чат можно, нажав кнопку ниже. ⤵️", + "@noChatsFoundHere": {}, + "updateInstalled": "🎉 Обновление {version} успешно установлено!", + "@updateInstalled": { + "type": "text", + "placeholders": { + "version": {} + } + }, + "changelog": "Список изменений", + "@changelog": {}, + "invitedBy": "📩 Приглашен(а) {user}", + "@invitedBy": { + "placeholders": { + "user": {} + } + }, + "doesNotSeemToBeAValidHomeserver": "Этот домашний сервер выглядит несовместимым. Нет ли в ссылке опечаток?", + "@doesNotSeemToBeAValidHomeserver": {}, + "noMoreChatsFound": "Больше чатов не обнаружено...", + "@noMoreChatsFound": {} } From f3af9aa01aba6f29048230d11885bdebed88027d Mon Sep 17 00:00:00 2001 From: Rex_sa Date: Fri, 27 Sep 2024 05:43:12 +0000 Subject: [PATCH 034/236] Translated using Weblate (Arabic) Currently translated at 100.0% (670 of 670 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ar/ --- assets/l10n/intl_ar.arb | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_ar.arb b/assets/l10n/intl_ar.arb index 7adb678b1..52921a9de 100644 --- a/assets/l10n/intl_ar.arb +++ b/assets/l10n/intl_ar.arb @@ -2291,7 +2291,7 @@ "@noBackupWarning": {}, "noOtherDevicesFound": "لم يتم العثور على أجهزة أخرى", "@noOtherDevicesFound": {}, - "fileIsTooBigForServer": "أبلغ الخادم أن الملف كبير جدًا بحيث لا يمكن إرساله.", + "fileIsTooBigForServer": "تعذر الإرسال! لا يدعم الخادم سوى المرفقات التي تصل إلى {max}.", "@fileIsTooBigForServer": {}, "jumpToLastReadMessage": "الانتقال إلى آخر رسالة مقروءة", "@jumpToLastReadMessage": {}, @@ -2803,5 +2803,30 @@ "homeserverDescription": "يتم تخزين جميع بياناتك على خادم المنزل، تمامًا مثل مزود خدمة البريد الإلكتروني. يمكنك اختيار خادم البيت الذي تريد استخدامه، بينما لا يزال بإمكانك التواصل مع الجميع. اعرف المزيد على https://matrix.org.", "@homeserverDescription": {}, "doesNotSeemToBeAValidHomeserver": "لا يبدو أنه خادم منزلي متوافق. عنوان URL غير صحيح ؟", - "@doesNotSeemToBeAValidHomeserver": {} + "@doesNotSeemToBeAValidHomeserver": {}, + "calculatingFileSize": "جارٍ حساب حجم الملف...", + "@calculatingFileSize": {}, + "prepareSendingAttachment": "إعداد مرفق الإرسال...", + "@prepareSendingAttachment": {}, + "sendingAttachment": "جارٍ إرسال المرفق...", + "@sendingAttachment": {}, + "generatingVideoThumbnail": "جارٍ إنشاء صورة مصغرة للفيديو...", + "@generatingVideoThumbnail": {}, + "compressVideo": "جارٍ ضغط الفيديو...", + "@compressVideo": {}, + "sendingAttachmentCountOfCount": "جارٍ إرسال المرفق {index} من {length}...", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + }, + "serverLimitReached": "تم الوصول إلى حد الخادم! انتظر {seconds} ثانية...", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + } } From 4ac48045f12e37e3801b231819754696284632fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Fri, 27 Sep 2024 07:21:58 +0000 Subject: [PATCH 035/236] Translated using Weblate (Estonian) Currently translated at 100.0% (670 of 670 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/ --- assets/l10n/intl_et.arb | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_et.arb b/assets/l10n/intl_et.arb index fc6b6c7cf..5ea3e8942 100644 --- a/assets/l10n/intl_et.arb +++ b/assets/l10n/intl_et.arb @@ -2271,7 +2271,7 @@ "@noOtherDevicesFound": {}, "noBackupWarning": "Hoiatus! Kui sa ei lülita sisse vestluse varundust, siis sul puudub hiljem ligipääs krüptitud sõnumitele. Me tungivalt soovitame, et palun lülita vestluse varundamine sisse enne väljalogimist.", "@noBackupWarning": {}, - "fileIsTooBigForServer": "Serveri seadistuste alusel on see fail saatmiseks liiga suur.", + "fileIsTooBigForServer": "Saatmine ei õnnestu! Serveri vaid kuni {max} suurusega manuseid.", "@fileIsTooBigForServer": {}, "fileHasBeenSavedAt": "Fail on salvestatud kausta: {path}", "@fileHasBeenSavedAt": { @@ -2803,5 +2803,30 @@ "homeserverDescription": "Sarnaselt e-postiteenuse pakkujale on kõik sinu sõnumid salvestatud koduserveris. Sa võid valida sellise koduserveri, nagu sulle meeldib ja nad kõik suudavad teiste koduserveritega suhelda. Lisateavet leiad veebisaidist https://matrix.org.", "@homeserverDescription": {}, "doesNotSeemToBeAValidHomeserver": "Ei tundu olema ühilduv koduserver. Kas võrguaadress on ikka õige?", - "@doesNotSeemToBeAValidHomeserver": {} + "@doesNotSeemToBeAValidHomeserver": {}, + "prepareSendingAttachment": "Valmistume manuse saatmiseks...", + "@prepareSendingAttachment": {}, + "generatingVideoThumbnail": "Loome video pisipilti...", + "@generatingVideoThumbnail": {}, + "compressVideo": "Pakime videot väiksemaks...", + "@compressVideo": {}, + "sendingAttachmentCountOfCount": "Saadame manust: {index} pikkusega {length}...", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + }, + "serverLimitReached": "Serveri poolt lubatud ülempiir on käes. Ootame {seconds} sekundit...", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + }, + "sendingAttachment": "Saadame manust...", + "@sendingAttachment": {}, + "calculatingFileSize": "Arvutame faili suurust...", + "@calculatingFileSize": {} } From 6140956562ceb14499d74209c6cac19d79a5af7c Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Fri, 27 Sep 2024 14:37:30 +0000 Subject: [PATCH 036/236] Translated using Weblate (Basque) Currently translated at 99.5% (667 of 670 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/eu/ --- assets/l10n/intl_eu.arb | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_eu.arb b/assets/l10n/intl_eu.arb index d0903579c..aed33ec67 100644 --- a/assets/l10n/intl_eu.arb +++ b/assets/l10n/intl_eu.arb @@ -2230,7 +2230,7 @@ "@noBackupWarning": {}, "doNotShowAgain": "Ez erakutsi berriro", "@doNotShowAgain": {}, - "fileIsTooBigForServer": "Zerbitzariak dio fitxategia handiegia dela bidali ahal izateko.", + "fileIsTooBigForServer": "Ezin da bidali! Zerbitzariak gehienez {max}-ko eranskinak onartzen ditu.", "@fileIsTooBigForServer": {}, "noOtherDevicesFound": "Ez da beste gailurik aurkitu", "@noOtherDevicesFound": {}, @@ -2803,5 +2803,30 @@ "whatIsAHomeserver": "Zer da zerbitzari bat?", "@whatIsAHomeserver": {}, "doesNotSeemToBeAValidHomeserver": "Ez dirudi zerbitzaria bateragarria denik. Zuzena da URLa?", - "@doesNotSeemToBeAValidHomeserver": {} + "@doesNotSeemToBeAValidHomeserver": {}, + "calculatingFileSize": "Fitxategiaren tamaina kalkulatzen…", + "@calculatingFileSize": {}, + "prepareSendingAttachment": "Eranskinaren bidalketa prestatzen…", + "@prepareSendingAttachment": {}, + "sendingAttachment": "Eranskina bidaltzen…", + "@sendingAttachment": {}, + "compressVideo": "Bideoa konprimatzen…", + "@compressVideo": {}, + "generatingVideoThumbnail": "Bideoaren iruditxoa sortzen…", + "@generatingVideoThumbnail": {}, + "serverLimitReached": "Zerbitzariaren muga gainditu da! Itxaron {seconds} segundo…", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + }, + "sendingAttachmentCountOfCount": "{index}. eranskina bidaltzen ({length} guztira)…", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + } } From d2faa2779a5370143b5904d5664ad89c658038e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jos=C3=A9=20m?= Date: Fri, 27 Sep 2024 02:55:12 +0000 Subject: [PATCH 037/236] Translated using Weblate (Galician) Currently translated at 100.0% (670 of 670 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/gl/ --- assets/l10n/intl_gl.arb | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_gl.arb b/assets/l10n/intl_gl.arb index b5b72fb3a..b4c41870a 100644 --- a/assets/l10n/intl_gl.arb +++ b/assets/l10n/intl_gl.arb @@ -2271,7 +2271,7 @@ "@noOtherDevicesFound": {}, "noBackupWarning": "Aviso! Se non activas a copia de apoio da conversa, perderás o acceso ás túas mensaxes cifradas. É moi recomendable activar a copia de apoio da conversa antes de pechar a sesión.", "@noBackupWarning": {}, - "fileIsTooBigForServer": "O servidor informa de que o ficheiro é demasiado grande para envialo.", + "fileIsTooBigForServer": "Non se puido enviar! O servidor só permite anexos que non superen {max}.", "@fileIsTooBigForServer": {}, "fileHasBeenSavedAt": "Gardouse o ficheiro en {path}", "@fileHasBeenSavedAt": { @@ -2803,5 +2803,30 @@ "homeserverDescription": "Todos os teus datos quedan gardados no servidor de inicio, igual que co teu provedor de correo electrónico. Podes elexir o servidor que queres usar e poderás comunicarte con todos os demais. Aprende máis en https://matrix.org.", "@homeserverDescription": {}, "doesNotSeemToBeAValidHomeserver": "Non semella ser un servidor de inicio compatible. É o URL correcto?", - "@doesNotSeemToBeAValidHomeserver": {} + "@doesNotSeemToBeAValidHomeserver": {}, + "calculatingFileSize": "Calculando o tamaño do ficheiro…", + "@calculatingFileSize": {}, + "prepareSendingAttachment": "Preparando o envío…", + "@prepareSendingAttachment": {}, + "sendingAttachment": "Enviando o anexo…", + "@sendingAttachment": {}, + "generatingVideoThumbnail": "Creando miniatura do vídeo…", + "@generatingVideoThumbnail": {}, + "compressVideo": "Comprimindo o vídeo…", + "@compressVideo": {}, + "serverLimitReached": "Acadouse o límite do servidor! Agarda {seconds} segundos…", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + }, + "sendingAttachmentCountOfCount": "Enviando o anexo {index} de {length}…", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + } } From d107dc7f932aa97e0ca82233e314fefd0ca7e71d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Fri, 27 Sep 2024 16:51:50 +0000 Subject: [PATCH 038/236] Translated using Weblate (Turkish) Currently translated at 100.0% (670 of 670 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/tr/ --- assets/l10n/intl_tr.arb | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_tr.arb b/assets/l10n/intl_tr.arb index 04e74ed26..d89e4e6b0 100644 --- a/assets/l10n/intl_tr.arb +++ b/assets/l10n/intl_tr.arb @@ -2271,7 +2271,7 @@ "@noBackupWarning": {}, "noOtherDevicesFound": "Başka aygıt bulunamadı", "@noOtherDevicesFound": {}, - "fileIsTooBigForServer": "Sunucu, dosyanın gönderilemeyecek kadar büyük olduğunu bildiriyor.", + "fileIsTooBigForServer": "Gönderilemiyor! Sunucu yalnızca {max} değerine kadar olan ekleri destekliyor.", "@fileIsTooBigForServer": {}, "fileHasBeenSavedAt": "Dosya {path} konumuna kaydedildi", "@fileHasBeenSavedAt": { @@ -2803,5 +2803,30 @@ "homeserverDescription": "Tüm verileriniz tıpkı bir e-posta sağlayıcısı gibi ana sunucuda saklanır. Hangi ana sunucuyu kullanmak istediğinizi seçebilir ve herkesle iletişim kurmaya devam edebilirsiniz. https://matrix.org adresinden daha fazla bilgi edinin.", "@homeserverDescription": {}, "doesNotSeemToBeAValidHomeserver": "Uyumlu bir ana sunucu gibi görünmüyor. Yanlış URL mi?", - "@doesNotSeemToBeAValidHomeserver": {} + "@doesNotSeemToBeAValidHomeserver": {}, + "prepareSendingAttachment": "Ek gönderilmeye hazırlanıyor...", + "@prepareSendingAttachment": {}, + "serverLimitReached": "Sunucu sınırına ulaşıldı! {seconds} saniye bekleniyor...", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + }, + "calculatingFileSize": "Dosya boyutu hesaplanıyor...", + "@calculatingFileSize": {}, + "sendingAttachmentCountOfCount": "Ek {index} / {length} gönderiliyor...", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + }, + "sendingAttachment": "Ek gönderiliyor...", + "@sendingAttachment": {}, + "generatingVideoThumbnail": "Video küçük resmi oluşturuluyor...", + "@generatingVideoThumbnail": {}, + "compressVideo": "Video sıkıştırılıyor...", + "@compressVideo": {} } From 3d8011a0089408d72f57671e7e2af4aa3af08a0e Mon Sep 17 00:00:00 2001 From: Bezruchenko Simon Date: Fri, 27 Sep 2024 15:25:09 +0000 Subject: [PATCH 039/236] Translated using Weblate (Ukrainian) Currently translated at 100.0% (670 of 670 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/ --- assets/l10n/intl_uk.arb | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_uk.arb b/assets/l10n/intl_uk.arb index b6829d707..bca3c8815 100644 --- a/assets/l10n/intl_uk.arb +++ b/assets/l10n/intl_uk.arb @@ -2271,7 +2271,7 @@ "@noOtherDevicesFound": {}, "noBackupWarning": "Увага! Якщо ви не ввімкнете резервне копіювання бесіди, ви втратите доступ до своїх зашифрованих повідомлень. Наполегливо радимо ввімкнути резервне копіювання бесіди перед виходом.", "@noBackupWarning": {}, - "fileIsTooBigForServer": "Сервер повідомляє, що файл завеликий для надсилання.", + "fileIsTooBigForServer": "Не вдалося надіслати! Сервер підтримує вкладення розміром до {max}.", "@fileIsTooBigForServer": {}, "fileHasBeenSavedAt": "Файл збережено в {path}", "@fileHasBeenSavedAt": { @@ -2803,5 +2803,30 @@ "homeserverDescription": "Усі ваші дані зберігаються на домашньому сервері, так само як у постачальника послуг електронної пошти. Ви можете вибрати, який домашній сервер ви хочете використовувати, водночас ви можете спілкуватися з усіма. Докладніше на https://matrix.org.", "@homeserverDescription": {}, "doesNotSeemToBeAValidHomeserver": "Здається, це несумісний домашній сервер. Неправильна URL-адреса?", - "@doesNotSeemToBeAValidHomeserver": {} + "@doesNotSeemToBeAValidHomeserver": {}, + "calculatingFileSize": "Обчислення розміру файлу...", + "@calculatingFileSize": {}, + "prepareSendingAttachment": "Підготовка до відправлення вкладення...", + "@prepareSendingAttachment": {}, + "sendingAttachment": "Відправлення вкладення...", + "@sendingAttachment": {}, + "generatingVideoThumbnail": "Генерування мініатюри відео...", + "@generatingVideoThumbnail": {}, + "compressVideo": "Стискання відео...", + "@compressVideo": {}, + "sendingAttachmentCountOfCount": "Відправлення вкладення {index} з {length}...", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + }, + "serverLimitReached": "Досягнуто ліміт сервера! Очікування {seconds} секунд...", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + } } From 572cd7d40d97c6d5d13730ccdcfacc326ccce6ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=8E=8B=E5=8F=AB=E6=88=91=E6=9D=A5=E5=B7=A1?= =?UTF-8?q?=E5=B1=B1?= Date: Fri, 27 Sep 2024 00:06:15 +0000 Subject: [PATCH 040/236] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (670 of 670 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/zh_Hans/ --- assets/l10n/intl_zh.arb | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_zh.arb b/assets/l10n/intl_zh.arb index e73944e6a..b7389dc96 100644 --- a/assets/l10n/intl_zh.arb +++ b/assets/l10n/intl_zh.arb @@ -2267,7 +2267,7 @@ "@deviceKeys": {}, "report": "举报", "@report": {}, - "fileIsTooBigForServer": "服务器报告文件过大,无法发送。", + "fileIsTooBigForServer": "无法发送!服务器只支持最大 {max} 的文件。", "@fileIsTooBigForServer": {}, "noOtherDevicesFound": "未找到其它设备", "@noOtherDevicesFound": {}, @@ -2803,5 +2803,30 @@ "homeserverDescription": "主服务器上就像电子邮件提供商,你的所有数据都存储在上面。你可以选择你想使用哪个主服务器。在 https://matrix.org 上了解更多信息。", "@homeserverDescription": {}, "doesNotSeemToBeAValidHomeserver": "似乎不是兼容的主服务器。URL 不正确?", - "@doesNotSeemToBeAValidHomeserver": {} + "@doesNotSeemToBeAValidHomeserver": {}, + "prepareSendingAttachment": "准备发送附件…", + "@prepareSendingAttachment": {}, + "sendingAttachment": "发送附件中…", + "@sendingAttachment": {}, + "calculatingFileSize": "计算文件尺寸中…", + "@calculatingFileSize": {}, + "generatingVideoThumbnail": "生成视频缩略图中…", + "@generatingVideoThumbnail": {}, + "compressVideo": "压缩视频中…", + "@compressVideo": {}, + "sendingAttachmentCountOfCount": "正在发送附件 {index},共 {length} 个附件…", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + }, + "serverLimitReached": "达到了服务器限制!等待 {seconds} 秒…", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + } } From aff0491578f8894e96ba27bcc83bb9f333392d70 Mon Sep 17 00:00:00 2001 From: Edgars Andersons Date: Fri, 27 Sep 2024 06:57:05 +0000 Subject: [PATCH 041/236] Translated using Weblate (Latvian) Currently translated at 100.0% (670 of 670 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/lv/ --- assets/l10n/intl_lv.arb | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_lv.arb b/assets/l10n/intl_lv.arb index 044f7dee7..5bcd315ef 100644 --- a/assets/l10n/intl_lv.arb +++ b/assets/l10n/intl_lv.arb @@ -1002,7 +1002,7 @@ "username": {} } }, - "fileIsTooBigForServer": "Serveris ziņo, ka datne ir pārāk liela, lai to nosūtītu.", + "fileIsTooBigForServer": "Nevar nosūtīt. Serveris nodrošina pielikums līdz {max}.", "@fileIsTooBigForServer": {}, "homeserver": "Mājasserveris", "@homeserver": {}, @@ -2781,5 +2781,30 @@ "spaces": "Vietas", "@spaces": {}, "markAsUnread": "Atzīmēt kā nelasītu", - "@markAsUnread": {} + "@markAsUnread": {}, + "sendingAttachment": "Nosūta pielikumu...", + "@sendingAttachment": {}, + "generatingVideoThumbnail": "Izveido video sīktēlu...", + "@generatingVideoThumbnail": {}, + "sendingAttachmentCountOfCount": "Nosūta {index}. pielikumu no {length}...", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + }, + "serverLimitReached": "Sasniegts servera ierobežojums. Gaida {seconds} sekundes...", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + }, + "calculatingFileSize": "Aprēķina datnes lielumu...", + "@calculatingFileSize": {}, + "prepareSendingAttachment": "Sagatavo pielikuma nosūtīšanu...", + "@prepareSendingAttachment": {}, + "compressVideo": "Saspiež video...", + "@compressVideo": {} } From de7b85819f0355f4c38a14c3fbe1d31c9d4b24c0 Mon Sep 17 00:00:00 2001 From: Krille Date: Sun, 29 Sep 2024 17:08:01 +0200 Subject: [PATCH 042/236] chore: Group notifications on android by first space parent --- lib/utils/push_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart index 7a82b370f..66c2d0196 100644 --- a/lib/utils/push_helper.dart +++ b/lib/utils/push_helper.dart @@ -295,7 +295,7 @@ Future _tryPushHelper( ), importance: Importance.high, priority: Priority.max, - groupKey: notificationGroupId, + groupKey: event.room.spaceParents.firstOrNull?.roomId ?? 'rooms', ); const iOSPlatformChannelSpecifics = DarwinNotificationDetails(); final platformChannelSpecifics = NotificationDetails( From c1f8b13aca0aed71ea0d16a56032aba5068ee8c0 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Thu, 3 Oct 2024 12:07:34 +0200 Subject: [PATCH 043/236] fix: Public rooms always publicly visible even when turned off on creation --- lib/pages/new_group/new_group.dart | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index 83a8b478e..107d3661b 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -63,7 +63,7 @@ class NewGroupController extends State { final roomId = await client.createGroupChat( visibility: - publicGroup ? sdk.Visibility.public : sdk.Visibility.private, + groupCanBeFound ? sdk.Visibility.public : sdk.Visibility.private, preset: publicGroup ? sdk.CreateRoomPreset.publicChat : sdk.CreateRoomPreset.privateChat, @@ -77,12 +77,6 @@ class NewGroupController extends State { ], ); if (!mounted) return; - if (publicGroup && groupCanBeFound) { - await client.setRoomVisibilityOnDirectory( - roomId, - visibility: sdk.Visibility.public, - ); - } context.go('/rooms/$roomId/invite'); } catch (e, s) { sdk.Logs().d('Unable to create group', e, s); From 542b4bf9280c6007a125efe391054002faca6c78 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Thu, 3 Oct 2024 14:10:47 +0200 Subject: [PATCH 044/236] chore: Nicer representation of invited DMs --- lib/utils/matrix_sdk_extensions/matrix_locals.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/matrix_sdk_extensions/matrix_locals.dart b/lib/utils/matrix_sdk_extensions/matrix_locals.dart index a492b87ee..165130c0b 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_locals.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_locals.dart @@ -346,7 +346,7 @@ class MatrixLocals extends MatrixLocalizations { l10n.startedKeyVerification(senderName); @override - String invitedBy(String senderName) => l10n.invitedBy(senderName); + String invitedBy(String senderName) => senderName; @override String get cancelledSend => l10n.sendCanceled; From 96ecc878b4cb807d9a11d71182e9c5792ce0c04e Mon Sep 17 00:00:00 2001 From: baltevl Date: Tue, 1 Oct 2024 23:15:23 +0200 Subject: [PATCH 045/236] build: Add unifiedpush_ui package Add unifiedpush_ui as part of unifiedpush was split off into it. --- lib/utils/background_push.dart | 29 ++++++++++++++++++++++++++--- pubspec.lock | 8 ++++++++ pubspec.yaml | 1 + 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index 748461f2a..142ec7cf9 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -30,6 +30,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:http/http.dart' as http; import 'package:matrix/matrix.dart'; import 'package:unifiedpush/unifiedpush.dart'; +import 'package:unifiedpush_ui/unifiedpush_ui.dart'; import 'package:fluffychat/utils/push_helper.dart'; import 'package:fluffychat/widgets/fluffy_chat_app.dart'; @@ -323,9 +324,8 @@ class BackgroundPush { } Future setupUp() async { - // Blocked by https://codeberg.org/UnifiedPush/flutter-connector/issues/2 - // ignore: deprecated_member_use - await UnifiedPush.registerAppWithDialog(matrix!.context); + await UnifiedPushUi(matrix!.context, ["default"], UPFunctions()) + .registerAppWithDialog(); } Future _newUpEndpoint(String newEndpoint, String i) async { @@ -403,3 +403,26 @@ class BackgroundPush { ); } } + +class UPFunctions extends UnifiedPushFunctions { + final List features = [/*list of features*/]; + @override + Future getDistributor() async { + return await UnifiedPush.getDistributor(); + } + + @override + Future> getDistributors() async { + return await UnifiedPush.getDistributors(features); + } + + @override + Future registerApp(String instance) async { + await UnifiedPush.registerApp(instance, features); + } + + @override + Future saveDistributor(String distributor) async { + await UnifiedPush.saveDistributor(distributor); + } +} diff --git a/pubspec.lock b/pubspec.lock index 194ec0bd8..ef90a19f0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2096,6 +2096,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + unifiedpush_ui: + dependency: "direct main" + description: + name: unifiedpush_ui + sha256: cf86f0214f37debd41f25c0425c8489df85e27f9f8784fed571eb7a86d39ba11 + url: "https://pub.dev" + source: hosted + version: "0.1.0" universal_html: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 8cd648565..21a7b1e33 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,6 +90,7 @@ dependencies: tor_detector_web: ^1.1.0 uni_links: ^0.5.1 unifiedpush: ^5.0.1 + unifiedpush_ui: ^0.1.0 universal_html: ^2.2.4 url_launcher: ^6.2.5 video_compress: ^3.1.3 From 86c9354cfd2d2f071e5b630802871da07ce0a26b Mon Sep 17 00:00:00 2001 From: krille-chan Date: Thu, 3 Oct 2024 16:04:52 +0200 Subject: [PATCH 046/236] refactor: Reuse flutter local notifications object --- lib/utils/background_push.dart | 56 ++++++++++++++++++++++------------ lib/utils/push_helper.dart | 28 ++--------------- 2 files changed, 39 insertions(+), 45 deletions(-) diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index 142ec7cf9..a7ce8c6de 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -70,31 +70,46 @@ class BackgroundPush { bool upAction = false; - BackgroundPush._(this.client) { - firebase?.setListeners( - onMessage: (message) => pushHelper( - PushNotification.fromJson( - Map.from(message['data'] ?? message), + void _init() async { + try { + await _flutterLocalNotificationsPlugin.initialize( + const InitializationSettings( + android: AndroidInitializationSettings('notifications_icon'), + iOS: DarwinInitializationSettings(), + ), + onDidReceiveNotificationResponse: goToRoom, + ); + Logs().v('Flutter Local Notifications initialized'); + firebase?.setListeners( + onMessage: (message) => pushHelper( + PushNotification.fromJson( + Map.from(message['data'] ?? message), + ), + client: client, + l10n: l10n, + activeRoomId: matrix?.activeRoomId, + flutterLocalNotificationsPlugin: _flutterLocalNotificationsPlugin, ), - client: client, - l10n: l10n, - activeRoomId: matrix?.activeRoomId, - onSelectNotification: goToRoom, - ), - ); - if (Platform.isAndroid) { - UnifiedPush.initialize( - onNewEndpoint: _newUpEndpoint, - onRegistrationFailed: _upUnregistered, - onUnregistered: _upUnregistered, - onMessage: _onUpMessage, ); + if (Platform.isAndroid) { + await UnifiedPush.initialize( + onNewEndpoint: _newUpEndpoint, + onRegistrationFailed: _upUnregistered, + onUnregistered: _upUnregistered, + onMessage: _onUpMessage, + ); + } + } catch (e, s) { + Logs().e('Unable to initialize Flutter local notifications', e, s); } } + BackgroundPush._(this.client) { + _init(); + } + factory BackgroundPush.clientOnly(Client client) { - _instance ??= BackgroundPush._(client); - return _instance!; + return _instance ??= BackgroundPush._(client); } factory BackgroundPush( @@ -110,7 +125,7 @@ class BackgroundPush { Future cancelNotification(String roomId) async { Logs().v('Cancel notification for room', roomId); - await FlutterLocalNotificationsPlugin().cancel(roomId.hashCode); + await _flutterLocalNotificationsPlugin.cancel(roomId.hashCode); // Workaround for app icon badge not updating if (Platform.isIOS) { @@ -400,6 +415,7 @@ class BackgroundPush { client: client, l10n: l10n, activeRoomId: matrix?.activeRoomId, + flutterLocalNotificationsPlugin: _flutterLocalNotificationsPlugin, ); } } diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart index 66c2d0196..ff6c5934e 100644 --- a/lib/utils/push_helper.dart +++ b/lib/utils/push_helper.dart @@ -22,7 +22,7 @@ Future pushHelper( Client? client, L10n? l10n, String? activeRoomId, - void Function(NotificationResponse?)? onSelectNotification, + required FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin, }) async { try { await _tryPushHelper( @@ -30,22 +30,11 @@ Future pushHelper( client: client, l10n: l10n, activeRoomId: activeRoomId, - onSelectNotification: onSelectNotification, + flutterLocalNotificationsPlugin: flutterLocalNotificationsPlugin, ); } catch (e, s) { Logs().v('Push Helper has crashed!', e, s); - // Initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project - final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - await flutterLocalNotificationsPlugin.initialize( - const InitializationSettings( - android: AndroidInitializationSettings('notifications_icon'), - iOS: DarwinInitializationSettings(), - ), - onDidReceiveNotificationResponse: onSelectNotification, - onDidReceiveBackgroundNotificationResponse: onSelectNotification, - ); - l10n ??= lookupL10n(const Locale('en')); flutterLocalNotificationsPlugin.show( notification.roomId?.hashCode ?? 0, @@ -76,7 +65,7 @@ Future _tryPushHelper( Client? client, L10n? l10n, String? activeRoomId, - void Function(NotificationResponse?)? onSelectNotification, + required FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin, }) async { final isBackgroundMessage = client == null; Logs().v( @@ -91,17 +80,6 @@ Future _tryPushHelper( return; } - // Initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project - final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - await flutterLocalNotificationsPlugin.initialize( - const InitializationSettings( - android: AndroidInitializationSettings('notifications_icon'), - iOS: DarwinInitializationSettings(), - ), - onDidReceiveNotificationResponse: onSelectNotification, - //onDidReceiveBackgroundNotificationResponse: onSelectNotification, - ); - client ??= (await ClientManager.getClients( initialize: false, store: await SharedPreferences.getInstance(), From 5ee30c35c6ef6cc0e9af6ea6ca26bc5d70cd0bcc Mon Sep 17 00:00:00 2001 From: krille-chan Date: Thu, 3 Oct 2024 17:35:01 +0200 Subject: [PATCH 047/236] build: Update flutter local notifications package --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index ef90a19f0..af057910e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -556,10 +556,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: c500d5d9e7e553f06b61877ca6b9c8b92c570a4c8db371038702e8ce57f8a50f + sha256: "49eeef364fddb71515bc78d5a8c51435a68bccd6e4d68e25a942c5e47761ae71" url: "https://pub.dev" source: hosted - version: "17.2.2" + version: "17.2.3" flutter_local_notifications_linux: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 21a7b1e33..f555d4a3e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: flutter_html: ^3.0.0-beta.2 flutter_html_table: ^3.0.0-beta.2 flutter_linkify: ^6.0.0 - flutter_local_notifications: ^17.2.1+1 + flutter_local_notifications: ^17.2.3 flutter_localizations: sdk: flutter flutter_map: ^6.1.0 From ae52fcf9ecd5695bca1d3a96e6db156c6ebdc4e4 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Thu, 3 Oct 2024 19:29:56 +0200 Subject: [PATCH 048/236] refactor: Remove duplicated navigator workaround --- lib/pages/chat_list/chat_list.dart | 2 +- .../settings_security/settings_security.dart | 2 +- lib/widgets/fluffy_chat_app.dart | 12 ++++-------- lib/widgets/matrix.dart | 18 +++++++++++++----- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 5c631eb0e..581a62b3a 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -957,7 +957,7 @@ class ChatListController extends State isTorBrowser = isTor; } - Future dehydrate() => Matrix.of(context).dehydrateAction(); + Future dehydrate() => Matrix.of(context).dehydrateAction(context); } enum EditBundleAction { addToBundle, removeFromBundle } diff --git a/lib/pages/settings_security/settings_security.dart b/lib/pages/settings_security/settings_security.dart index 69069d693..a4c1c4a6d 100644 --- a/lib/pages/settings_security/settings_security.dart +++ b/lib/pages/settings_security/settings_security.dart @@ -118,7 +118,7 @@ class SettingsSecurityController extends State { ).show(context); } - Future dehydrateAction() => Matrix.of(context).dehydrateAction(); + Future dehydrateAction() => Matrix.of(context).dehydrateAction(context); @override Widget build(BuildContext context) => SettingsSecurityView(this); diff --git a/lib/widgets/fluffy_chat_app.dart b/lib/widgets/fluffy_chat_app.dart index d9d2f042a..dfab1a6c3 100644 --- a/lib/widgets/fluffy_chat_app.dart +++ b/lib/widgets/fluffy_chat_app.dart @@ -54,14 +54,10 @@ class FluffyChatApp extends StatelessWidget { clients: clients, // Need a navigator above the Matrix widget for // displaying dialogs - child: Navigator( - onGenerateRoute: (_) => MaterialPageRoute( - builder: (_) => Matrix( - clients: clients, - store: store, - child: testWidget ?? child, - ), - ), + child: Matrix( + clients: clients, + store: store, + child: testWidget ?? child, ), ), ), diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 4de23a2f5..aa3cc2c4c 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -287,13 +287,16 @@ class MatrixState extends State with WidgetsBindingObserver { if (!hidPopup && {KeyVerificationState.done, KeyVerificationState.error} .contains(request.state)) { - Navigator.of(context).pop('dialog'); + FluffyChatApp.router.pop('dialog'); } hidPopup = true; }; request.onUpdate = null; hidPopup = true; - await KeyVerificationDialog(request: request).show(context); + await KeyVerificationDialog(request: request).show( + FluffyChatApp.router.routerDelegate.navigatorKey.currentContext ?? + context, + ); }); onLoginStateChanged[name] ??= c.onLoginStateChanged.stream.listen((state) { final loggedInWithMultipleClients = widget.clients.length > 1; @@ -304,7 +307,10 @@ class MatrixState extends State with WidgetsBindingObserver { _cancelSubs(c.clientName); widget.clients.remove(c); ClientManager.removeClientNameFromStore(c.clientName, store); - ScaffoldMessenger.of(context).showSnackBar( + ScaffoldMessenger.of( + FluffyChatApp.router.routerDelegate.navigatorKey.currentContext ?? + context, + ).showSnackBar( SnackBar( content: Text(L10n.of(context)!.oneClientLoggedOut), ), @@ -362,7 +368,9 @@ class MatrixState extends State with WidgetsBindingObserver { onFcmError: (errorMsg, {Uri? link}) async { final result = await showOkCancelAlertDialog( barrierDismissible: true, - context: context, + context: FluffyChatApp + .router.routerDelegate.navigatorKey.currentContext ?? + context, title: L10n.of(context)!.pushNotificationsNotAvailable, message: errorMsg, fullyCapitalizedForMaterial: false, @@ -483,7 +491,7 @@ class MatrixState extends State with WidgetsBindingObserver { ); } - Future dehydrateAction() async { + Future dehydrateAction(BuildContext context) async { final response = await showOkCancelAlertDialog( context: context, isDestructiveAction: true, From fc959ce3e521100f95239a28cd981455ebcfe397 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Thu, 3 Oct 2024 21:20:34 +0200 Subject: [PATCH 049/236] refactor: Use file selector on linux --- lib/pages/chat/chat.dart | 28 ++++----- lib/pages/chat_details/chat_details.dart | 16 +++--- .../homeserver_picker/homeserver_picker.dart | 11 ++-- lib/pages/new_group/new_group.dart | 11 ++-- lib/pages/new_space/new_space.dart | 13 ++--- lib/pages/settings/settings.dart | 14 ++--- .../settings_emotes/settings_emotes.dart | 34 ++++------- lib/pages/settings_style/settings_style.dart | 14 ++--- lib/utils/file_selector.dart | 57 +++++++++++++++++++ pubspec.lock | 32 +++++++++++ pubspec.yaml | 1 + scripts/enable-android-google-services.patch | 2 +- 12 files changed, 144 insertions(+), 89 deletions(-) create mode 100644 lib/utils/file_selector.dart diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index c47de7a83..4e45f01c7 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -10,7 +10,6 @@ import 'package:collection/collection.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; @@ -29,11 +28,11 @@ import 'package:fluffychat/pages/chat/event_info_dialog.dart'; import 'package:fluffychat/pages/chat/recording_dialog.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/utils/error_reporter.dart'; +import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/app_lock.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/account_bundles.dart'; import '../../utils/localized_exception_extension.dart'; @@ -481,17 +480,12 @@ class ChatController extends State } void sendFileAction() async { - final result = await AppLock.of(context).pauseWhile( - FilePicker.platform.pickFiles( - compressionQuality: 0, - allowMultiple: true, - ), - ); - if (result == null || result.files.isEmpty) return; + final files = await selectFiles(context, allowMultiple: true); + if (files.isEmpty) return; await showAdaptiveDialog( context: context, builder: (c) => SendFileDialog( - files: result.xFiles, + files: files, room: room, outerContext: context, ), @@ -511,19 +505,17 @@ class ChatController extends State } void sendImageAction() async { - final result = await AppLock.of(context).pauseWhile( - FilePicker.platform.pickFiles( - compressionQuality: 0, - type: FileType.image, - allowMultiple: true, - ), + final files = await selectFiles( + context, + allowMultiple: true, + extensions: imageExtensions, ); - if (result == null || result.files.isEmpty) return; + if (files.isEmpty) return; await showAdaptiveDialog( context: context, builder: (c) => SendFileDialog( - files: result.xFiles, + files: files, room: room, outerContext: context, ), diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 6c78adda6..3b1ea8da3 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; @@ -11,9 +10,9 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat_details/chat_details_view.dart'; import 'package:fluffychat/pages/settings/settings.dart'; +import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/app_lock.dart'; import 'package:fluffychat/widgets/matrix.dart'; enum AliasActions { copy, delete, setCanonical } @@ -165,16 +164,15 @@ class ChatDetailsController extends State { name: result.path, ); } else { - final picked = await AppLock.of(context).pauseWhile( - FilePicker.platform.pickFiles( - type: FileType.image, - withData: true, - ), + final picked = await selectFiles( + context, + allowMultiple: false, + extensions: imageExtensions, ); - final pickedFile = picked?.files.firstOrNull; + final pickedFile = picked.firstOrNull; if (pickedFile == null) return; file = MatrixFile( - bytes: pickedFile.bytes!, + bytes: await pickedFile.readAsBytes(), name: pickedFile.name, ); } diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index 955aa3c56..d5fe0700a 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:go_router/go_router.dart'; @@ -15,8 +14,8 @@ import 'package:universal_html/html.dart' as html; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; +import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/app_lock.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/localized_exception_extension.dart'; @@ -201,10 +200,8 @@ class HomeserverPickerController extends State { Widget build(BuildContext context) => HomeserverPickerView(this); Future restoreBackup() async { - final picked = await AppLock.of(context).pauseWhile( - FilePicker.platform.pickFiles(withData: true), - ); - final file = picked?.files.firstOrNull; + final picked = await selectFiles(context); + final file = picked.firstOrNull; if (file == null) return; setState(() { error = null; @@ -212,7 +209,7 @@ class HomeserverPickerController extends State { }); try { final client = Matrix.of(context).getLoginClient(); - await client.importDump(String.fromCharCodes(file.bytes!)); + await client.importDump(String.fromCharCodes(await file.readAsBytes())); Matrix.of(context).initMatrix(); } catch (e) { setState(() { diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index 107d3661b..393e88372 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -2,11 +2,11 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart' as sdk; import 'package:fluffychat/pages/new_group/new_group_view.dart'; +import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/widgets/matrix.dart'; class NewGroup extends StatefulWidget { @@ -35,15 +35,16 @@ class NewGroupController extends State { void setGroupCanBeFound(bool b) => setState(() => groupCanBeFound = b); void selectPhoto() async { - final photo = await FilePicker.platform.pickFiles( - type: FileType.image, + final photo = await selectFiles( + context, + extensions: imageExtensions, allowMultiple: false, - withData: true, ); + final bytes = await photo.singleOrNull?.readAsBytes(); setState(() { avatarUrl = null; - avatar = photo?.files.singleOrNull?.bytes; + avatar = bytes; }); } diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index a5a55c8b1..0190f15ce 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -2,13 +2,13 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/new_space/new_space_view.dart'; +import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -32,15 +32,14 @@ class NewSpaceController extends State { Uri? avatarUrl; void selectPhoto() async { - final photo = await FilePicker.platform.pickFiles( - type: FileType.image, - allowMultiple: false, - withData: true, + final photo = await selectFiles( + context, + extensions: imageExtensions, ); - + final bytes = await photo.firstOrNull?.readAsBytes(); setState(() { avatarUrl = null; - avatar = photo?.files.singleOrNull?.bytes; + avatar = bytes; }); } diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index d844063b1..455de997b 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -4,14 +4,13 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/app_lock.dart'; import '../../widgets/matrix.dart'; import '../bootstrap/bootstrap_dialog.dart'; import 'settings_view.dart'; @@ -136,16 +135,11 @@ class SettingsController extends State { name: result.path, ); } else { - final result = await AppLock.of(context).pauseWhile( - FilePicker.platform.pickFiles( - type: FileType.image, - withData: true, - ), - ); - final pickedFile = result?.files.firstOrNull; + final result = await selectFiles(context, extensions: imageExtensions); + final pickedFile = result.firstOrNull; if (pickedFile == null) return; file = MatrixFile( - bytes: pickedFile.bytes!, + bytes: await pickedFile.readAsBytes(), name: pickedFile.name, ); } diff --git a/lib/pages/settings_emotes/settings_emotes.dart b/lib/pages/settings_emotes/settings_emotes.dart index fbef5479c..8a8eca430 100644 --- a/lib/pages/settings_emotes/settings_emotes.dart +++ b/lib/pages/settings_emotes/settings_emotes.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; @@ -13,8 +12,8 @@ import 'package:http/http.dart' hide Client; import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/client_manager.dart'; +import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; -import 'package:fluffychat/widgets/app_lock.dart'; import '../../widgets/matrix.dart'; import 'import_archive_dialog.dart'; import 'settings_emotes_view.dart'; @@ -222,16 +221,11 @@ class EmotesSettingsController extends State { void imagePickerAction( ValueNotifier controller, ) async { - final result = await AppLock.of(context).pauseWhile( - FilePicker.platform.pickFiles( - type: FileType.image, - withData: true, - ), - ); - final pickedFile = result?.files.firstOrNull; + final result = await selectFiles(context, extensions: imageExtensions); + final pickedFile = result.firstOrNull; if (pickedFile == null) return; var file = MatrixImageFile( - bytes: pickedFile.bytes!, + bytes: await pickedFile.readAsBytes(), name: pickedFile.name, ); try { @@ -282,21 +276,17 @@ class EmotesSettingsController extends State { final result = await showFutureLoadingDialog( context: context, future: () async { - final result = await AppLock.of(context).pauseWhile( - FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: [ - 'zip', - // TODO: add further encoders - ], - // TODO: migrate to stream, currently brrrr because of `archive_io`. - withData: true, - ), + final result = await selectFiles( + context, + extensions: [ + 'zip', + // TODO: add further encoders + ], ); - if (result == null) return null; + if (result.isEmpty) return null; - final buffer = InputStream(result.files.single.bytes); + final buffer = InputStream(await result.first.readAsBytes()); final archive = ZipDecoder().decodeBuffer(buffer); diff --git a/lib/pages/settings_style/settings_style.dart b/lib/pages/settings_style/settings_style.dart index 58f8e2c1b..62ba84bf1 100644 --- a/lib/pages/settings_style/settings_style.dart +++ b/lib/pages/settings_style/settings_style.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/utils/account_config.dart'; -import 'package:fluffychat/widgets/app_lock.dart'; +import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/widgets/theme_builder.dart'; import '../../widgets/matrix.dart'; import 'settings_style_view.dart'; @@ -26,20 +25,15 @@ class SettingsStyleController extends State { void setWallpaper() async { final client = Matrix.of(context).client; - final picked = await AppLock.of(context).pauseWhile( - FilePicker.platform.pickFiles( - type: FileType.image, - withData: true, - ), - ); - final pickedFile = picked?.files.firstOrNull; + final picked = await selectFiles(context, extensions: imageExtensions); + final pickedFile = picked.firstOrNull; if (pickedFile == null) return; await showFutureLoadingDialog( context: context, future: () async { final url = await client.uploadContent( - pickedFile.bytes!, + await pickedFile.readAsBytes(), filename: pickedFile.name, ); await client.updateApplicationAccountConfig( diff --git a/lib/utils/file_selector.dart b/lib/utils/file_selector.dart new file mode 100644 index 000000000..3bdcd82f9 --- /dev/null +++ b/lib/utils/file_selector.dart @@ -0,0 +1,57 @@ +import 'package:flutter/widgets.dart'; + +import 'package:file_picker/file_picker.dart'; +import 'package:file_selector/file_selector.dart'; + +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/app_lock.dart'; + +Future> selectFiles( + BuildContext context, { + String? title, + List? extensions, + bool allowMultiple = false, +}) async { + if (!PlatformInfos.isLinux) { + final result = await AppLock.of(context).pauseWhile( + FilePicker.platform.pickFiles( + compressionQuality: 0, + allowMultiple: allowMultiple, + allowedExtensions: extensions, + ), + ); + return result?.xFiles ?? []; + } + + if (allowMultiple) { + return await AppLock.of(context).pauseWhile( + openFiles( + confirmButtonText: title, + acceptedTypeGroups: [ + if (extensions != null) XTypeGroup(extensions: extensions), + ], + ), + ); + } + final file = await AppLock.of(context).pauseWhile( + openFile( + confirmButtonText: title, + acceptedTypeGroups: [ + if (extensions != null) XTypeGroup(extensions: extensions), + ], + ), + ); + if (file == null) return []; + return [file]; +} + +const imageExtensions = [ + 'png', + 'PNG', + 'jpg', + 'JPG', + 'jpeg', + 'JPEG', + 'webp', + 'WebP', +]; diff --git a/pubspec.lock b/pubspec.lock index af057910e..81920a2ed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -382,6 +382,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.2" + file_selector: + dependency: "direct main" + description: + name: file_selector + sha256: "5019692b593455127794d5718304ff1ae15447dea286cdda9f0db2a796a1b828" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + file_selector_android: + dependency: transitive + description: + name: file_selector_android + sha256: b8c9717a0177ca6fa035554b82cd6c83b838ddc66b7704eb6df0f77f027ecc90 + url: "https://pub.dev" + source: hosted + version: "0.5.1+7" + file_selector_ios: + dependency: transitive + description: + name: file_selector_ios + sha256: "38ebf91ecbcfa89a9639a0854ccaed8ab370c75678938eebca7d34184296f0bb" + url: "https://pub.dev" + source: hosted + version: "0.5.3" file_selector_linux: dependency: transitive description: @@ -406,6 +430,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.2" + file_selector_web: + dependency: transitive + description: + name: file_selector_web + sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7 + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" file_selector_windows: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f555d4a3e..90730e4ae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: emojis: ^0.9.9 #fcm_shared_isolate: ^0.1.0 file_picker: ^8.1.2 + file_selector: ^1.0.3 flutter: sdk: flutter flutter_app_badger: ^1.5.0 diff --git a/scripts/enable-android-google-services.patch b/scripts/enable-android-google-services.patch index ec7139117..aca4f69b8 100644 --- a/scripts/enable-android-google-services.patch +++ b/scripts/enable-android-google-services.patch @@ -132,5 +132,5 @@ index 69c80d6e..efd32d89 100644 - #fcm_shared_isolate: ^0.1.0 + fcm_shared_isolate: ^0.1.0 file_picker: ^8.1.2 + file_selector: ^1.0.3 flutter: - sdk: flutter From fe06f2efb3a101a2c5d8a33587c85deb4f3d170c Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 4 Oct 2024 10:04:09 +0200 Subject: [PATCH 050/236] chore: Follow up pick files with file selector --- lib/pages/chat/chat.dart | 2 +- lib/pages/chat_details/chat_details.dart | 2 +- lib/pages/new_group/new_group.dart | 2 +- lib/pages/new_space/new_space.dart | 2 +- lib/pages/settings/settings.dart | 5 ++- .../settings_emotes/settings_emotes.dart | 10 +++--- lib/pages/settings_style/settings_style.dart | 5 ++- lib/utils/file_selector.dart | 33 +++++++++++-------- 8 files changed, 36 insertions(+), 25 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 4e45f01c7..69fd67062 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -508,7 +508,7 @@ class ChatController extends State final files = await selectFiles( context, allowMultiple: true, - extensions: imageExtensions, + type: FileSelectorType.images, ); if (files.isEmpty) return; diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 3b1ea8da3..526d1261e 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -167,7 +167,7 @@ class ChatDetailsController extends State { final picked = await selectFiles( context, allowMultiple: false, - extensions: imageExtensions, + type: FileSelectorType.images, ); final pickedFile = picked.firstOrNull; if (pickedFile == null) return; diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index 393e88372..31beb6779 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -37,7 +37,7 @@ class NewGroupController extends State { void selectPhoto() async { final photo = await selectFiles( context, - extensions: imageExtensions, + type: FileSelectorType.images, allowMultiple: false, ); final bytes = await photo.singleOrNull?.readAsBytes(); diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index 0190f15ce..1f484cda8 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -34,7 +34,7 @@ class NewSpaceController extends State { void selectPhoto() async { final photo = await selectFiles( context, - extensions: imageExtensions, + type: FileSelectorType.images, ); final bytes = await photo.firstOrNull?.readAsBytes(); setState(() { diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 455de997b..1d930910a 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -135,7 +135,10 @@ class SettingsController extends State { name: result.path, ); } else { - final result = await selectFiles(context, extensions: imageExtensions); + final result = await selectFiles( + context, + type: FileSelectorType.images, + ); final pickedFile = result.firstOrNull; if (pickedFile == null) return; file = MatrixFile( diff --git a/lib/pages/settings_emotes/settings_emotes.dart b/lib/pages/settings_emotes/settings_emotes.dart index 8a8eca430..59dade669 100644 --- a/lib/pages/settings_emotes/settings_emotes.dart +++ b/lib/pages/settings_emotes/settings_emotes.dart @@ -221,7 +221,10 @@ class EmotesSettingsController extends State { void imagePickerAction( ValueNotifier controller, ) async { - final result = await selectFiles(context, extensions: imageExtensions); + final result = await selectFiles( + context, + type: FileSelectorType.images, + ); final pickedFile = result.firstOrNull; if (pickedFile == null) return; var file = MatrixImageFile( @@ -278,10 +281,7 @@ class EmotesSettingsController extends State { future: () async { final result = await selectFiles( context, - extensions: [ - 'zip', - // TODO: add further encoders - ], + type: FileSelectorType.zip, ); if (result.isEmpty) return null; diff --git a/lib/pages/settings_style/settings_style.dart b/lib/pages/settings_style/settings_style.dart index 62ba84bf1..e63cb3d16 100644 --- a/lib/pages/settings_style/settings_style.dart +++ b/lib/pages/settings_style/settings_style.dart @@ -25,7 +25,10 @@ class SettingsStyleController extends State { void setWallpaper() async { final client = Matrix.of(context).client; - final picked = await selectFiles(context, extensions: imageExtensions); + final picked = await selectFiles( + context, + type: FileSelectorType.images, + ); final pickedFile = picked.firstOrNull; if (pickedFile == null) return; diff --git a/lib/utils/file_selector.dart b/lib/utils/file_selector.dart index 3bdcd82f9..27209f6d8 100644 --- a/lib/utils/file_selector.dart +++ b/lib/utils/file_selector.dart @@ -9,7 +9,7 @@ import 'package:fluffychat/widgets/app_lock.dart'; Future> selectFiles( BuildContext context, { String? title, - List? extensions, + FileSelectorType type = FileSelectorType.any, bool allowMultiple = false, }) async { if (!PlatformInfos.isLinux) { @@ -17,7 +17,8 @@ Future> selectFiles( FilePicker.platform.pickFiles( compressionQuality: 0, allowMultiple: allowMultiple, - allowedExtensions: extensions, + type: type.filePickerType, + allowedExtensions: type.extensions?.toList(), ), ); return result?.xFiles ?? []; @@ -28,7 +29,8 @@ Future> selectFiles( openFiles( confirmButtonText: title, acceptedTypeGroups: [ - if (extensions != null) XTypeGroup(extensions: extensions), + if (type != FileSelectorType.any) + XTypeGroup(extensions: type.extensions?.toList()), ], ), ); @@ -37,7 +39,8 @@ Future> selectFiles( openFile( confirmButtonText: title, acceptedTypeGroups: [ - if (extensions != null) XTypeGroup(extensions: extensions), + if (type != FileSelectorType.any) + XTypeGroup(extensions: type.extensions?.toList()), ], ), ); @@ -45,13 +48,15 @@ Future> selectFiles( return [file]; } -const imageExtensions = [ - 'png', - 'PNG', - 'jpg', - 'JPG', - 'jpeg', - 'JPEG', - 'webp', - 'WebP', -]; +enum FileSelectorType { + any(null, FileType.any), + images( + {'png', 'PNG', 'jpg', 'JPG', 'jpeg', 'JPEG', 'webp', 'WebP'}, + FileType.image, + ), + zip({'zip', 'ZIP'}, FileType.custom); + + const FileSelectorType(this.extensions, this.filePickerType); + final Set? extensions; + final FileType filePickerType; +} From cc928c64bccf807ab7d727ad2b8c80ce3618d511 Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 4 Oct 2024 15:09:01 +0200 Subject: [PATCH 051/236] chore: Follow up file selector --- lib/utils/file_selector.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/utils/file_selector.dart b/lib/utils/file_selector.dart index 27209f6d8..fdbd0c0cb 100644 --- a/lib/utils/file_selector.dart +++ b/lib/utils/file_selector.dart @@ -18,7 +18,9 @@ Future> selectFiles( compressionQuality: 0, allowMultiple: allowMultiple, type: type.filePickerType, - allowedExtensions: type.extensions?.toList(), + allowedExtensions: type.filePickerType == FileType.custom + ? type.extensions?.toList() + : null, ), ); return result?.xFiles ?? []; From 811434e6426e39fa4a5df15ac7759eeaa30f6254 Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 4 Oct 2024 15:09:16 +0200 Subject: [PATCH 052/236] build: migrate from deprecated appdelegate makro --- ios/Runner/AppDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 9413c6971..7f38bb254 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, From 2c2e6d094115da30341d51e535b282cdf7a93920 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Fri, 4 Oct 2024 17:53:56 +0200 Subject: [PATCH 053/236] chore: Follow up file selector --- lib/utils/file_selector.dart | 48 ++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/lib/utils/file_selector.dart b/lib/utils/file_selector.dart index fdbd0c0cb..f528f2cfd 100644 --- a/lib/utils/file_selector.dart +++ b/lib/utils/file_selector.dart @@ -18,9 +18,7 @@ Future> selectFiles( compressionQuality: 0, allowMultiple: allowMultiple, type: type.filePickerType, - allowedExtensions: type.filePickerType == FileType.custom - ? type.extensions?.toList() - : null, + allowedExtensions: type.extensions, ), ); return result?.xFiles ?? []; @@ -30,20 +28,14 @@ Future> selectFiles( return await AppLock.of(context).pauseWhile( openFiles( confirmButtonText: title, - acceptedTypeGroups: [ - if (type != FileSelectorType.any) - XTypeGroup(extensions: type.extensions?.toList()), - ], + acceptedTypeGroups: type.groups, ), ); } final file = await AppLock.of(context).pauseWhile( openFile( confirmButtonText: title, - acceptedTypeGroups: [ - if (type != FileSelectorType.any) - XTypeGroup(extensions: type.extensions?.toList()), - ], + acceptedTypeGroups: type.groups, ), ); if (file == null) return []; @@ -51,14 +43,38 @@ Future> selectFiles( } enum FileSelectorType { - any(null, FileType.any), + any([], FileType.any, null), images( - {'png', 'PNG', 'jpg', 'JPG', 'jpeg', 'JPEG', 'webp', 'WebP'}, + [ + XTypeGroup( + label: 'JPG', + extensions: ['jpg', 'JPG', 'jpeg', 'JPEG'], + ), + XTypeGroup( + label: 'PNGs', + extensions: ['png', 'PNG'], + ), + XTypeGroup( + label: 'WEBP', + extensions: ['WebP', 'WEBP'], + ), + ], FileType.image, + null, ), - zip({'zip', 'ZIP'}, FileType.custom); + zip( + [ + XTypeGroup( + label: 'ZIP', + extensions: ['zip', 'ZIP'], + ), + ], + FileType.custom, + ['zip', 'ZIP'], + ); - const FileSelectorType(this.extensions, this.filePickerType); - final Set? extensions; + const FileSelectorType(this.groups, this.filePickerType, this.extensions); + final List groups; final FileType filePickerType; + final List? extensions; } From 8cedd2a45a4676c202b85ad4c471c83539e67989 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 6 Oct 2024 08:25:39 +0200 Subject: [PATCH 054/236] build: Update go router --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 81920a2ed..60472d7b8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -857,10 +857,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459" + sha256: "6f1b756f6e863259a99135ff3c95026c3cdca17d10ebef2bba2261a25ddc8bbc" url: "https://pub.dev" source: hosted - version: "14.2.7" + version: "14.3.0" gradient_borders: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 90730e4ae..f511c8615 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,7 +53,7 @@ dependencies: flutter_webrtc: ^0.11.7 future_loading_dialog: ^0.3.0 geolocator: ^7.6.2 - go_router: ^14.0.1 + go_router: ^14.3.0 handy_window: ^0.4.0 hive: ^2.2.3 hive_flutter: ^1.1.0 From 0d4b7d67cc53e353868b7711af62d5f80ab06c8c Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 6 Oct 2024 08:35:37 +0200 Subject: [PATCH 055/236] refactor: Use non nullable localizations builder and lazy load on web --- l10n.yaml | 2 + lib/pages/archive/archive.dart | 8 +- lib/pages/archive/archive_view.dart | 6 +- lib/pages/bootstrap/bootstrap_dialog.dart | 66 ++++++------- lib/pages/chat/add_widget_tile.dart | 6 +- lib/pages/chat/add_widget_tile_view.dart | 16 ++-- lib/pages/chat/chat.dart | 83 ++++++++-------- lib/pages/chat/chat_app_bar_title.dart | 8 +- lib/pages/chat/chat_emoji_picker.dart | 6 +- lib/pages/chat/chat_input_row.dart | 30 +++--- lib/pages/chat/chat_view.dart | 30 +++--- lib/pages/chat/encryption_button.dart | 4 +- lib/pages/chat/event_info_dialog.dart | 16 ++-- lib/pages/chat/events/cute_events.dart | 6 +- lib/pages/chat/events/message.dart | 2 +- lib/pages/chat/events/message_content.dart | 18 ++-- lib/pages/chat/events/reply_content.dart | 2 +- lib/pages/chat/events/state_message.dart | 2 +- .../events/verification_request_content.dart | 6 +- lib/pages/chat/events/video_player.dart | 4 +- lib/pages/chat/input_bar.dart | 2 +- lib/pages/chat/pinned_events.dart | 10 +- lib/pages/chat/recording_dialog.dart | 10 +- lib/pages/chat/reply_display.dart | 4 +- lib/pages/chat/send_file_dialog.dart | 22 ++--- lib/pages/chat/send_location_dialog.dart | 14 +-- lib/pages/chat/sticker_picker_dialog.dart | 6 +- .../chat_access_settings_controller.dart | 20 ++-- .../chat_access_settings_page.dart | 24 ++--- lib/pages/chat_details/chat_details.dart | 28 +++--- lib/pages/chat_details/chat_details_view.dart | 39 ++++---- .../chat_details/participant_list_item.dart | 12 +-- .../chat_encryption_settings.dart | 28 +++--- .../chat_encryption_settings_view.dart | 18 ++-- lib/pages/chat_list/chat_list.dart | 95 +++++++++---------- lib/pages/chat_list/chat_list_body.dart | 20 ++-- lib/pages/chat_list/chat_list_header.dart | 8 +- lib/pages/chat_list/chat_list_item.dart | 22 ++--- lib/pages/chat_list/chat_list_view.dart | 10 +- .../chat_list/client_chooser_button.dart | 26 ++--- lib/pages/chat_list/space_view.dart | 56 +++++------ lib/pages/chat_members/chat_members_view.dart | 10 +- .../chat_permissions_settings.dart | 2 +- .../chat_permissions_settings_view.dart | 12 +-- .../permission_list_tile.dart | 42 ++++---- .../chat_search/chat_search_files_tab.dart | 10 +- .../chat_search/chat_search_images_tab.dart | 8 +- .../chat_search/chat_search_message_tab.dart | 10 +- lib/pages/chat_search/chat_search_view.dart | 17 ++-- .../device_settings/device_settings.dart | 22 ++--- .../device_settings/device_settings_view.dart | 8 +- .../user_device_list_item.dart | 18 ++-- lib/pages/dialer/dialer.dart | 10 +- .../homeserver_picker/homeserver_picker.dart | 4 +- .../homeserver_picker_view.dart | 22 ++--- lib/pages/image_viewer/image_viewer_view.dart | 8 +- .../invitation_selection.dart | 12 +-- .../invitation_selection_view.dart | 20 ++-- .../key_verification_dialog.dart | 42 ++++---- lib/pages/login/login.dart | 37 ++++---- lib/pages/login/login_view.dart | 10 +- lib/pages/new_group/new_group_view.dart | 12 +-- .../new_private_chat/new_private_chat.dart | 4 +- .../new_private_chat_view.dart | 18 ++-- .../new_private_chat/qr_scanner_modal.dart | 4 +- lib/pages/new_space/new_space.dart | 2 +- lib/pages/new_space/new_space_view.dart | 10 +- lib/pages/settings/settings.dart | 28 +++--- lib/pages/settings/settings_view.dart | 26 ++--- lib/pages/settings_3pid/settings_3pid.dart | 20 ++-- .../settings_3pid/settings_3pid_view.dart | 10 +- .../settings_chat/settings_chat_view.dart | 34 +++---- .../import_archive_dialog.dart | 18 ++-- .../settings_emotes/settings_emotes.dart | 20 ++-- .../settings_emotes/settings_emotes_view.dart | 16 ++-- .../settings_ignore_list.dart | 2 +- .../settings_ignore_list_view.dart | 10 +- .../settings_multiple_emotes_view.dart | 2 +- .../settings_notifications.dart | 16 ++-- .../settings_notifications_view.dart | 10 +- .../settings_password/settings_password.dart | 8 +- .../settings_password_view.dart | 12 +-- .../settings_security/settings_security.dart | 30 +++--- .../settings_security_view.dart | 30 +++--- .../settings_style/settings_style_view.dart | 28 +++--- .../user_bottom_sheet/user_bottom_sheet.dart | 56 +++++------ .../user_bottom_sheet_view.dart | 38 ++++---- lib/utils/client_manager.dart | 4 +- lib/utils/date_time_extension.dart | 8 +- lib/utils/error_reporter.dart | 8 +- lib/utils/fluffy_share.dart | 4 +- lib/utils/init_with_restore.dart | 2 +- lib/utils/localized_exception_extension.dart | 20 ++-- .../builder.dart | 2 +- .../cipher.dart | 2 +- .../matrix_file_extension.dart | 4 +- lib/utils/platform_infos.dart | 2 +- lib/utils/push_helper.dart | 2 +- lib/utils/room_status_extension.dart | 12 +-- lib/utils/show_update_snackbar.dart | 4 +- lib/utils/uia_request_manager.dart | 4 +- lib/utils/url_launcher.dart | 10 +- lib/utils/voip/callkeep_manager.dart | 12 +-- lib/widgets/chat_settings_popup_menu.dart | 20 ++-- lib/widgets/connection_status_header.dart | 4 +- lib/widgets/layouts/login_scaffold.dart | 4 +- .../local_notifications_extension.dart | 8 +- lib/widgets/lock_screen.dart | 6 +- lib/widgets/matrix.dart | 19 ++-- lib/widgets/permission_slider_dialog.dart | 6 +- lib/widgets/public_room_bottom_sheet.dart | 12 +-- 111 files changed, 879 insertions(+), 883 deletions(-) diff --git a/l10n.yaml b/l10n.yaml index a714613ee..95a4c4eac 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -3,3 +3,5 @@ template-arb-file: intl_en.arb output-localization-file: l10n.dart output-class: L10n preferred-supported-locales: ["en"] +use-deferred-loading: true +nullable-getter: false \ No newline at end of file diff --git a/lib/pages/archive/archive.dart b/lib/pages/archive/archive.dart index a3c35c347..d9550cd15 100644 --- a/lib/pages/archive/archive.dart +++ b/lib/pages/archive/archive.dart @@ -42,10 +42,10 @@ class ArchiveController extends State { if (await showOkCancelAlertDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.cancel, - message: L10n.of(context)!.clearArchive, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).yes, + cancelLabel: L10n.of(context).cancel, + message: L10n.of(context).clearArchive, ) != OkCancelResult.ok) { return; diff --git a/lib/pages/archive/archive_view.dart b/lib/pages/archive/archive_view.dart index dc73addb9..8cfdb20c3 100644 --- a/lib/pages/archive/archive_view.dart +++ b/lib/pages/archive/archive_view.dart @@ -20,14 +20,14 @@ class ArchiveView extends StatelessWidget { builder: (BuildContext context, snapshot) => Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), - title: Text(L10n.of(context)!.archive), + title: Text(L10n.of(context).archive), actions: [ if (snapshot.data?.isNotEmpty ?? false) Padding( padding: const EdgeInsets.all(8.0), child: TextButton.icon( onPressed: controller.forgetAllAction, - label: Text(L10n.of(context)!.clearArchive), + label: Text(L10n.of(context).clearArchive), icon: const Icon(Icons.cleaning_services_outlined), ), ), @@ -40,7 +40,7 @@ class ArchiveView extends StatelessWidget { if (snapshot.hasError) { return Center( child: Text( - L10n.of(context)!.oopsSomethingWentWrong, + L10n.of(context).oopsSomethingWentWrong, textAlign: TextAlign.center, ), ); diff --git a/lib/pages/bootstrap/bootstrap_dialog.dart b/lib/pages/bootstrap/bootstrap_dialog.dart index b816d2fc1..322f73e18 100644 --- a/lib/pages/bootstrap/bootstrap_dialog.dart +++ b/lib/pages/bootstrap/bootstrap_dialog.dart @@ -61,12 +61,12 @@ class BootstrapDialogState extends State { String _getSecureStorageLocalizedName() { if (PlatformInfos.isAndroid) { - return L10n.of(context)!.storeInAndroidKeystore; + return L10n.of(context).storeInAndroidKeystore; } if (PlatformInfos.isIOS || PlatformInfos.isMacOS) { - return L10n.of(context)!.storeInAppleKeyChain; + return L10n.of(context).storeInAppleKeyChain; } - return L10n.of(context)!.storeSecurlyOnThisDevice; + return L10n.of(context).storeSecurlyOnThisDevice; } @override @@ -92,12 +92,12 @@ class BootstrapDialogState extends State { _wipe ??= widget.wipe; final buttons = []; Widget body = const CircularProgressIndicator.adaptive(); - titleText = L10n.of(context)!.loadingPleaseWait; + titleText = L10n.of(context).loadingPleaseWait; if (bootstrap.newSsssKey?.recoveryKey != null && _recoveryKeyStored == false) { final key = bootstrap.newSsssKey!.recoveryKey; - titleText = L10n.of(context)!.recoveryKey; + titleText = L10n.of(context).recoveryKey; return Scaffold( appBar: AppBar( centerTitle: true, @@ -105,7 +105,7 @@ class BootstrapDialogState extends State { icon: const Icon(Icons.close), onPressed: Navigator.of(context).pop, ), - title: Text(L10n.of(context)!.recoveryKey), + title: Text(L10n.of(context).recoveryKey), ), body: Center( child: ConstrainedBox( @@ -123,7 +123,7 @@ class BootstrapDialogState extends State { color: theme.colorScheme.primary, ), ), - subtitle: Text(L10n.of(context)!.chatBackupDescription), + subtitle: Text(L10n.of(context).chatBackupDescription), ), const Divider( height: 32, @@ -153,7 +153,7 @@ class BootstrapDialogState extends State { }, title: Text(_getSecureStorageLocalizedName()), subtitle: - Text(L10n.of(context)!.storeInSecureStorageDescription), + Text(L10n.of(context).storeInSecureStorageDescription), ), const SizedBox(height: 16), CheckboxListTile.adaptive( @@ -164,13 +164,13 @@ class BootstrapDialogState extends State { FluffyShare.share(key!, context); setState(() => _recoveryKeyCopied = true); }, - title: Text(L10n.of(context)!.copyToClipboard), - subtitle: Text(L10n.of(context)!.saveKeyManuallyDescription), + title: Text(L10n.of(context).copyToClipboard), + subtitle: Text(L10n.of(context).saveKeyManuallyDescription), ), const SizedBox(height: 16), ElevatedButton.icon( icon: const Icon(Icons.check_outlined), - label: Text(L10n.of(context)!.next), + label: Text(L10n.of(context).next), onPressed: (_recoveryKeyCopied || _storeInSecureStorage == true) ? () { @@ -227,7 +227,7 @@ class BootstrapDialogState extends State { icon: const Icon(Icons.close), onPressed: Navigator.of(context).pop, ), - title: Text(L10n.of(context)!.chatBackup), + title: Text(L10n.of(context).chatBackup), ), body: Center( child: ConstrainedBox( @@ -245,7 +245,7 @@ class BootstrapDialogState extends State { color: theme.colorScheme.primary, ), subtitle: Text( - L10n.of(context)!.pleaseEnterRecoveryKeyDescription, + L10n.of(context).pleaseEnterRecoveryKeyDescription, ), ), const Divider(height: 32), @@ -265,7 +265,7 @@ class BootstrapDialogState extends State { fontFamily: theme.textTheme.bodyLarge?.fontFamily, ), prefixIcon: const Icon(Icons.key_outlined), - labelText: L10n.of(context)!.recoveryKey, + labelText: L10n.of(context).recoveryKey, hintText: 'Es** **** **** ****', errorText: _recoveryKeyInputError, errorMaxLines: 2, @@ -280,7 +280,7 @@ class BootstrapDialogState extends State { icon: _recoveryKeyInputLoading ? const CircularProgressIndicator.adaptive() : const Icon(Icons.lock_open_outlined), - label: Text(L10n.of(context)!.unlockOldMessages), + label: Text(L10n.of(context).unlockOldMessages), onPressed: _recoveryKeyInputLoading ? null : () async { @@ -323,7 +323,7 @@ class BootstrapDialogState extends State { } on FormatException catch (_) { setState( () => _recoveryKeyInputError = - L10n.of(context)!.wrongRecoveryKey, + L10n.of(context).wrongRecoveryKey, ); } catch (e, s) { ErrorReporter( @@ -347,7 +347,7 @@ class BootstrapDialogState extends State { const Expanded(child: Divider()), Padding( padding: const EdgeInsets.all(12.0), - child: Text(L10n.of(context)!.or), + child: Text(L10n.of(context).or), ), const Expanded(child: Divider()), ], @@ -355,17 +355,17 @@ class BootstrapDialogState extends State { const SizedBox(height: 16), ElevatedButton.icon( icon: const Icon(Icons.cast_connected_outlined), - label: Text(L10n.of(context)!.transferFromAnotherDevice), + label: Text(L10n.of(context).transferFromAnotherDevice), onPressed: _recoveryKeyInputLoading ? null : () async { final consent = await showOkCancelAlertDialog( context: context, - title: L10n.of(context)!.verifyOtherDevice, - message: L10n.of(context)! + title: L10n.of(context).verifyOtherDevice, + message: L10n.of(context) .verifyOtherDeviceDescription, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, fullyCapitalizedForMaterial: false, ); if (consent != OkCancelResult.ok) return; @@ -391,7 +391,7 @@ class BootstrapDialogState extends State { foregroundColor: theme.colorScheme.onErrorContainer, ), icon: const Icon(Icons.delete_outlined), - label: Text(L10n.of(context)!.recoveryKeyLost), + label: Text(L10n.of(context).recoveryKeyLost), onPressed: _recoveryKeyInputLoading ? null : () async { @@ -399,10 +399,10 @@ class BootstrapDialogState extends State { await showOkCancelAlertDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.recoveryKeyLost, - message: L10n.of(context)!.wipeChatBackup, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).recoveryKeyLost, + message: L10n.of(context).wipeChatBackup, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, isDestructiveAction: true, )) { setState(() => _createBootstrap(true)); @@ -440,18 +440,18 @@ class BootstrapDialogState extends State { ); break; case BootstrapState.error: - titleText = L10n.of(context)!.oopsSomethingWentWrong; + titleText = L10n.of(context).oopsSomethingWentWrong; body = const Icon(Icons.error_outline, color: Colors.red, size: 80); buttons.add( OutlinedButton( onPressed: () => Navigator.of(context, rootNavigator: false).pop(false), - child: Text(L10n.of(context)!.close), + child: Text(L10n.of(context).close), ), ); break; case BootstrapState.done: - titleText = L10n.of(context)!.everythingReady; + titleText = L10n.of(context).everythingReady; body = Column( mainAxisSize: MainAxisSize.min, children: [ @@ -462,7 +462,7 @@ class BootstrapDialogState extends State { ), const SizedBox(height: 16), Text( - L10n.of(context)!.yourChatBackupHasBeenSetUp, + L10n.of(context).yourChatBackupHasBeenSetUp, style: const TextStyle(fontSize: 20), ), const SizedBox(height: 16), @@ -472,7 +472,7 @@ class BootstrapDialogState extends State { OutlinedButton( onPressed: () => Navigator.of(context, rootNavigator: false).pop(false), - child: Text(L10n.of(context)!.close), + child: Text(L10n.of(context).close), ), ); break; @@ -487,7 +487,7 @@ class BootstrapDialogState extends State { Navigator.of(context, rootNavigator: false).pop(true), ), ), - title: Text(titleText ?? L10n.of(context)!.loadingPleaseWait), + title: Text(titleText ?? L10n.of(context).loadingPleaseWait), ), body: Center( child: Column( diff --git a/lib/pages/chat/add_widget_tile.dart b/lib/pages/chat/add_widget_tile.dart index 11e3cce5d..066ce3d65 100644 --- a/lib/pages/chat/add_widget_tile.dart +++ b/lib/pages/chat/add_widget_tile.dart @@ -43,14 +43,14 @@ class AddWidgetTileState extends State { if (name.length < 3) { setState(() { - nameError = L10n.of(context)!.widgetNameError; + nameError = L10n.of(context).widgetNameError; }); return; } if (uri == null || uri.scheme != 'https') { setState(() { - urlError = L10n.of(context)!.widgetUrlError; + urlError = L10n.of(context).widgetUrlError; }); return; } @@ -75,7 +75,7 @@ class AddWidgetTileState extends State { Navigator.of(context).pop(); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.errorAddingWidget)), + SnackBar(content: Text(L10n.of(context).errorAddingWidget)), ); } } diff --git a/lib/pages/chat/add_widget_tile_view.dart b/lib/pages/chat/add_widget_tile_view.dart index 18153faba..ea7bb9721 100644 --- a/lib/pages/chat/add_widget_tile_view.dart +++ b/lib/pages/chat/add_widget_tile_view.dart @@ -13,7 +13,7 @@ class AddWidgetTileView extends StatelessWidget { @override Widget build(BuildContext context) { return ExpansionTile( - title: Text(L10n.of(context)!.addWidget), + title: Text(L10n.of(context).addWidget), leading: const Icon(Icons.add), initiallyExpanded: controller.initiallyExpanded, children: [ @@ -21,10 +21,10 @@ class AddWidgetTileView extends StatelessWidget { groupValue: controller.widgetType, padding: const EdgeInsets.all(8), children: { - 'm.etherpad': Text(L10n.of(context)!.widgetEtherpad), - 'm.jitsi': Text(L10n.of(context)!.widgetJitsi), - 'm.video': Text(L10n.of(context)!.widgetVideo), - 'm.custom': Text(L10n.of(context)!.widgetCustom), + 'm.etherpad': Text(L10n.of(context).widgetEtherpad), + 'm.jitsi': Text(L10n.of(context).widgetJitsi), + 'm.video': Text(L10n.of(context).widgetVideo), + 'm.custom': Text(L10n.of(context).widgetCustom), }.map( (key, value) => MapEntry( key, @@ -43,7 +43,7 @@ class AddWidgetTileView extends StatelessWidget { autofocus: true, decoration: InputDecoration( prefixIcon: const Icon(Icons.label), - label: Text(L10n.of(context)!.widgetName), + label: Text(L10n.of(context).widgetName), errorText: controller.nameError, ), ), @@ -54,7 +54,7 @@ class AddWidgetTileView extends StatelessWidget { controller: controller.urlController, decoration: InputDecoration( prefixIcon: const Icon(Icons.add_link), - label: Text(L10n.of(context)!.link), + label: Text(L10n.of(context).link), errorText: controller.urlError, ), ), @@ -63,7 +63,7 @@ class AddWidgetTileView extends StatelessWidget { children: [ TextButton( onPressed: controller.addWidget, - child: Text(L10n.of(context)!.addWidget), + child: Text(L10n.of(context).addWidget), ), ], ), diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 69fd67062..d872a02f9 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -56,12 +56,11 @@ class ChatPage extends StatelessWidget { final room = Matrix.of(context).client.getRoomById(roomId); if (room == null) { return Scaffold( - appBar: AppBar(title: Text(L10n.of(context)!.oopsSomethingWentWrong)), + appBar: AppBar(title: Text(L10n.of(context).oopsSomethingWentWrong)), body: Center( child: Padding( padding: const EdgeInsets.all(16), - child: - Text(L10n.of(context)!.youAreNoLongerParticipatingInThisChat), + child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat), ), ), ); @@ -446,7 +445,7 @@ class ChatController extends State final commandMatch = RegExp(r'^\/(\w+)').firstMatch(sendController.text); if (commandMatch != null && !sendingClient.commands.keys.contains(commandMatch[1]!.toLowerCase())) { - final l10n = L10n.of(context)!; + final l10n = L10n.of(context); final dialogResult = await showOkCancelAlertDialog( context: context, title: l10n.commandInvalid, @@ -564,9 +563,9 @@ class ChatController extends State if (info.version.sdkInt < 19) { showOkAlertDialog( context: context, - title: L10n.of(context)!.unsupportedAndroidVersion, - message: L10n.of(context)!.unsupportedAndroidVersionLong, - okLabel: L10n.of(context)!.close, + title: L10n.of(context).unsupportedAndroidVersion, + message: L10n.of(context).unsupportedAndroidVersionLong, + okLabel: L10n.of(context).close, ); return; } @@ -646,12 +645,12 @@ class ChatController extends State if (selectedEvents.length == 1) { return selectedEvents.first .getDisplayEvent(timeline!) - .calcLocalizedBodyFallback(MatrixLocals(L10n.of(context)!)); + .calcLocalizedBodyFallback(MatrixLocals(L10n.of(context))); } for (final event in selectedEvents) { if (copyString.isNotEmpty) copyString += '\n\n'; copyString += event.getDisplayEvent(timeline!).calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), withSenderNamePrefix: true, ); } @@ -670,32 +669,32 @@ class ChatController extends State final event = selectedEvents.single; final score = await showConfirmationDialog( context: context, - title: L10n.of(context)!.reportMessage, - message: L10n.of(context)!.howOffensiveIsThisContent, - cancelLabel: L10n.of(context)!.cancel, - okLabel: L10n.of(context)!.ok, + title: L10n.of(context).reportMessage, + message: L10n.of(context).howOffensiveIsThisContent, + cancelLabel: L10n.of(context).cancel, + okLabel: L10n.of(context).ok, actions: [ AlertDialogAction( key: -100, - label: L10n.of(context)!.extremeOffensive, + label: L10n.of(context).extremeOffensive, ), AlertDialogAction( key: -50, - label: L10n.of(context)!.offensive, + label: L10n.of(context).offensive, ), AlertDialogAction( key: 0, - label: L10n.of(context)!.inoffensive, + label: L10n.of(context).inoffensive, ), ], ); if (score == null) return; final reason = await showTextInputDialog( context: context, - title: L10n.of(context)!.whyDoYouWantToReportThis, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - textFields: [DialogTextField(hintText: L10n.of(context)!.reason)], + title: L10n.of(context).whyDoYouWantToReportThis, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + textFields: [DialogTextField(hintText: L10n.of(context).reason)], ); if (reason == null || reason.single.isEmpty) return; final result = await showFutureLoadingDialog( @@ -713,7 +712,7 @@ class ChatController extends State selectedEvents.clear(); }); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)), + SnackBar(content: Text(L10n.of(context).contentHasBeenReported)), ); } @@ -740,16 +739,16 @@ class ChatController extends State final reasonInput = selectedEvents.any((event) => event.status.isSent) ? await showTextInputDialog( context: context, - title: L10n.of(context)!.redactMessage, - message: L10n.of(context)!.redactMessageDescription, + title: L10n.of(context).redactMessage, + message: L10n.of(context).redactMessageDescription, isDestructiveAction: true, textFields: [ DialogTextField( - hintText: L10n.of(context)!.optionalRedactReason, + hintText: L10n.of(context).optionalRedactReason, ), ], - okLabel: L10n.of(context)!.remove, - cancelLabel: L10n.of(context)!.cancel, + okLabel: L10n.of(context).remove, + cancelLabel: L10n.of(context).cancel, ) : []; if (reasonInput == null) return; @@ -1015,7 +1014,7 @@ class ChatController extends State editEvent = selectedEvents.first; sendController.text = editEvent!.getDisplayEvent(timeline!).calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), withSenderNamePrefix: false, hideReply: true, ); @@ -1028,13 +1027,13 @@ class ChatController extends State if (OkCancelResult.ok != await showOkCancelAlertDialog( context: context, - title: L10n.of(context)!.goToTheNewRoom, + title: L10n.of(context).goToTheNewRoom, message: room .getState(EventTypes.RoomTombstone)! .parsedTombstoneContent .body, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, )) { return; } @@ -1117,10 +1116,10 @@ class ChatController extends State unpinEvent(String eventId) async { final response = await showOkCancelAlertDialog( context: context, - title: L10n.of(context)!.unpin, - message: L10n.of(context)!.confirmEventUnpin, - okLabel: L10n.of(context)!.unpin, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).unpin, + message: L10n.of(context).confirmEventUnpin, + okLabel: L10n.of(context).unpin, + cancelLabel: L10n.of(context).cancel, ); if (response == OkCancelResult.ok) { final events = room.pinnedEventIds @@ -1214,26 +1213,26 @@ class ChatController extends State Navigator.pop(context); showOkAlertDialog( context: context, - title: L10n.of(context)!.unsupportedAndroidVersion, - message: L10n.of(context)!.unsupportedAndroidVersionLong, - okLabel: L10n.of(context)!.close, + title: L10n.of(context).unsupportedAndroidVersion, + message: L10n.of(context).unsupportedAndroidVersionLong, + okLabel: L10n.of(context).close, ); } }); } final callType = await showModalActionSheet( context: context, - title: L10n.of(context)!.warning, - message: L10n.of(context)!.videoCallsBetaWarning, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).warning, + message: L10n.of(context).videoCallsBetaWarning, + cancelLabel: L10n.of(context).cancel, actions: [ SheetAction( - label: L10n.of(context)!.voiceCall, + label: L10n.of(context).voiceCall, icon: Icons.phone_outlined, key: CallType.kVoice, ), SheetAction( - label: L10n.of(context)!.videoCall, + label: L10n.of(context).videoCall, icon: Icons.video_call_outlined, key: CallType.kVideo, ), diff --git a/lib/pages/chat/chat_app_bar_title.dart b/lib/pages/chat/chat_app_bar_title.dart index 5d3f34457..09ead63ce 100644 --- a/lib/pages/chat/chat_app_bar_title.dart +++ b/lib/pages/chat/chat_app_bar_title.dart @@ -36,7 +36,7 @@ class ChatAppBarTitle extends StatelessWidget { child: Avatar( mxContent: room.avatar, name: room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), ), size: 32, ), @@ -47,7 +47,7 @@ class ChatAppBarTitle extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), + room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( @@ -63,13 +63,13 @@ class ChatAppBarTitle extends StatelessWidget { final style = Theme.of(context).textTheme.bodySmall; if (presence?.currentlyActive == true) { return Text( - L10n.of(context)!.currentlyActive, + L10n.of(context).currentlyActive, style: style, ); } if (lastActiveTimestamp != null) { return Text( - L10n.of(context)!.lastActiveAgo( + L10n.of(context).lastActiveAgo( lastActiveTimestamp.localizedTimeShort(context), ), style: style, diff --git a/lib/pages/chat/chat_emoji_picker.dart b/lib/pages/chat/chat_emoji_picker.dart index 1ef7e2363..0c5633545 100644 --- a/lib/pages/chat/chat_emoji_picker.dart +++ b/lib/pages/chat/chat_emoji_picker.dart @@ -30,8 +30,8 @@ class ChatEmojiPicker extends StatelessWidget { children: [ TabBar( tabs: [ - Tab(text: L10n.of(context)!.emojis), - Tab(text: L10n.of(context)!.stickers), + Tab(text: L10n.of(context).emojis), + Tab(text: L10n.of(context).stickers), ], ), Expanded( @@ -97,7 +97,7 @@ class NoRecent extends StatelessWidget { @override Widget build(BuildContext context) { return Text( - L10n.of(context)!.emoteKeyboardNoRecents, + L10n.of(context).emoteKeyboardNoRecents, style: Theme.of(context).textTheme.bodyLarge, ); } diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 3ad750376..b22d905af 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -44,7 +44,7 @@ class ChatInputRow extends StatelessWidget { child: Row( children: [ const Icon(Icons.delete), - Text(L10n.of(context)!.delete), + Text(L10n.of(context).delete), ], ), ), @@ -57,7 +57,7 @@ class ChatInputRow extends StatelessWidget { child: Row( children: [ const Icon(Icons.keyboard_arrow_left_outlined), - Text(L10n.of(context)!.forward), + Text(L10n.of(context).forward), ], ), ), @@ -73,7 +73,7 @@ class ChatInputRow extends StatelessWidget { onPressed: controller.replyAction, child: Row( children: [ - Text(L10n.of(context)!.reply), + Text(L10n.of(context).reply), const Icon(Icons.keyboard_arrow_right), ], ), @@ -85,7 +85,7 @@ class ChatInputRow extends StatelessWidget { onPressed: controller.sendAgainAction, child: Row( children: [ - Text(L10n.of(context)!.tryToSendAgain), + Text(L10n.of(context).tryToSendAgain), const SizedBox(width: 4), const Icon(Icons.send_outlined, size: 16), ], @@ -103,7 +103,7 @@ class ChatInputRow extends StatelessWidget { }, onKeysPressed: () => controller.onAddPopupMenuButtonSelected('file'), - helpLabel: L10n.of(context)!.sendFile, + helpLabel: L10n.of(context).sendFile, child: AnimatedContainer( duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, @@ -125,7 +125,7 @@ class ChatInputRow extends StatelessWidget { foregroundColor: Colors.white, child: Icon(Icons.attachment_outlined), ), - title: Text(L10n.of(context)!.sendFile), + title: Text(L10n.of(context).sendFile), contentPadding: const EdgeInsets.all(0), ), ), @@ -137,7 +137,7 @@ class ChatInputRow extends StatelessWidget { foregroundColor: Colors.white, child: Icon(Icons.image_outlined), ), - title: Text(L10n.of(context)!.sendImage), + title: Text(L10n.of(context).sendImage), contentPadding: const EdgeInsets.all(0), ), ), @@ -150,7 +150,7 @@ class ChatInputRow extends StatelessWidget { foregroundColor: Colors.white, child: Icon(Icons.camera_alt_outlined), ), - title: Text(L10n.of(context)!.openCamera), + title: Text(L10n.of(context).openCamera), contentPadding: const EdgeInsets.all(0), ), ), @@ -163,7 +163,7 @@ class ChatInputRow extends StatelessWidget { foregroundColor: Colors.white, child: Icon(Icons.videocam_outlined), ), - title: Text(L10n.of(context)!.openVideoCamera), + title: Text(L10n.of(context).openVideoCamera), contentPadding: const EdgeInsets.all(0), ), ), @@ -176,7 +176,7 @@ class ChatInputRow extends StatelessWidget { foregroundColor: Colors.white, child: Icon(Icons.gps_fixed_outlined), ), - title: Text(L10n.of(context)!.shareLocation), + title: Text(L10n.of(context).shareLocation), contentPadding: const EdgeInsets.all(0), ), ), @@ -194,9 +194,9 @@ class ChatInputRow extends StatelessWidget { LogicalKeyboardKey.keyE, }, onKeysPressed: controller.emojiPickerAction, - helpLabel: L10n.of(context)!.emojis, + helpLabel: L10n.of(context).emojis, child: IconButton( - tooltip: L10n.of(context)!.emojis, + tooltip: L10n.of(context).emojis, icon: PageTransitionSwitcher( transitionBuilder: ( Widget child, @@ -255,7 +255,7 @@ class ChatInputRow extends StatelessWidget { bottom: 6.0, top: 3.0, ), - hintText: L10n.of(context)!.writeAMessage, + hintText: L10n.of(context).writeAMessage, hintMaxLines: 1, border: InputBorder.none, enabledBorder: InputBorder.none, @@ -272,7 +272,7 @@ class ChatInputRow extends StatelessWidget { child: PlatformInfos.platformCanRecord && controller.sendController.text.isEmpty ? FloatingActionButton.small( - tooltip: L10n.of(context)!.voiceMessage, + tooltip: L10n.of(context).voiceMessage, onPressed: controller.voiceMessageAction, elevation: 0, heroTag: null, @@ -284,7 +284,7 @@ class ChatInputRow extends StatelessWidget { child: const Icon(Icons.mic_none_outlined), ) : FloatingActionButton.small( - tooltip: L10n.of(context)!.send, + tooltip: L10n.of(context).send, onPressed: controller.send, elevation: 0, heroTag: null, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index f22ef46bb..38484baa6 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -38,12 +38,12 @@ class ChatView extends StatelessWidget { if (controller.canEditSelectedEvents) IconButton( icon: const Icon(Icons.edit_outlined), - tooltip: L10n.of(context)!.edit, + tooltip: L10n.of(context).edit, onPressed: controller.editSelectedEventAction, ), IconButton( icon: const Icon(Icons.copy_outlined), - tooltip: L10n.of(context)!.copy, + tooltip: L10n.of(context).copy, onPressed: controller.copyEventsAction, ), if (controller.canSaveSelectedEvent) @@ -51,7 +51,7 @@ class ChatView extends StatelessWidget { Builder( builder: (context) => IconButton( icon: Icon(Icons.adaptive.share), - tooltip: L10n.of(context)!.share, + tooltip: L10n.of(context).share, onPressed: () => controller.saveSelectedEvent(context), ), ), @@ -59,12 +59,12 @@ class ChatView extends StatelessWidget { IconButton( icon: const Icon(Icons.push_pin_outlined), onPressed: controller.pinEvent, - tooltip: L10n.of(context)!.pinMessage, + tooltip: L10n.of(context).pinMessage, ), if (controller.canRedactSelectedEvents) IconButton( icon: const Icon(Icons.delete_outlined), - tooltip: L10n.of(context)!.redactMessage, + tooltip: L10n.of(context).redactMessage, onPressed: controller.redactEventsAction, ), if (controller.selectedEvents.length == 1) @@ -88,7 +88,7 @@ class ChatView extends StatelessWidget { children: [ const Icon(Icons.info_outlined), const SizedBox(width: 12), - Text(L10n.of(context)!.messageInfo), + Text(L10n.of(context).messageInfo), ], ), ), @@ -103,7 +103,7 @@ class ChatView extends StatelessWidget { color: Colors.red, ), const SizedBox(width: 12), - Text(L10n.of(context)!.reportMessage), + Text(L10n.of(context).reportMessage), ], ), ), @@ -117,7 +117,7 @@ class ChatView extends StatelessWidget { IconButton( onPressed: controller.onPhoneButtonTap, icon: const Icon(Icons.call_outlined), - tooltip: L10n.of(context)!.placeCall, + tooltip: L10n.of(context).placeCall, ), EncryptionButton(controller.room), ChatSettingsPopupMenu(controller.room, true), @@ -180,7 +180,7 @@ class ChatView extends StatelessWidget { ? IconButton( icon: const Icon(Icons.close), onPressed: controller.clearSelectedEvents, - tooltip: L10n.of(context)!.close, + tooltip: L10n.of(context).close, color: theme.colorScheme.primary, ) : StreamBuilder( @@ -213,7 +213,7 @@ class ChatView extends StatelessWidget { ), trailing: TextButton( onPressed: controller.goToNewRoomAction, - child: Text(L10n.of(context)!.goToTheNewRoom), + child: Text(L10n.of(context).goToTheNewRoom), ), ), if (scrollUpBannerEventId != null) @@ -221,13 +221,13 @@ class ChatView extends StatelessWidget { leading: IconButton( color: theme.colorScheme.onSurfaceVariant, icon: const Icon(Icons.close), - tooltip: L10n.of(context)!.close, + tooltip: L10n.of(context).close, onPressed: () { controller.discardScrollUpBannerEventId(); controller.setReadMarker(); }, ), - title: L10n.of(context)!.jumpToLastReadMessage, + title: L10n.of(context).jumpToLastReadMessage, trailing: TextButton( onPressed: () { controller.scrollToEventId( @@ -235,7 +235,7 @@ class ChatView extends StatelessWidget { ); controller.discardScrollUpBannerEventId(); }, - child: Text(L10n.of(context)!.jump), + child: Text(L10n.of(context).jump), ), ), ], @@ -333,7 +333,7 @@ class ChatView extends StatelessWidget { ), onPressed: controller.leaveChat, label: Text( - L10n.of(context)!.leave, + L10n.of(context).leave, ), ), TextButton.icon( @@ -347,7 +347,7 @@ class ChatView extends StatelessWidget { ), onPressed: controller.recreateChat, label: Text( - L10n.of(context)!.reopenChat, + L10n.of(context).reopenChat, ), ), ], diff --git a/lib/pages/chat/encryption_button.dart b/lib/pages/chat/encryption_button.dart index c68a1c976..9eae07677 100644 --- a/lib/pages/chat/encryption_button.dart +++ b/lib/pages/chat/encryption_button.dart @@ -25,8 +25,8 @@ class EncryptionButton extends StatelessWidget { : Future.value(EncryptionHealthState.allVerified), builder: (BuildContext context, snapshot) => IconButton( tooltip: room.encrypted - ? L10n.of(context)!.encrypted - : L10n.of(context)!.encryptionNotEnabled, + ? L10n.of(context).encrypted + : L10n.of(context).encryptionNotEnabled, icon: Icon( room.encrypted ? Icons.lock_outlined : Icons.lock_open_outlined, size: 20, diff --git a/lib/pages/chat/event_info_dialog.dart b/lib/pages/chat/event_info_dialog.dart index fd2dcc68f..dca325dfd 100644 --- a/lib/pages/chat/event_info_dialog.dart +++ b/lib/pages/chat/event_info_dialog.dart @@ -14,7 +14,7 @@ extension EventInfoDialogExtension on Event { void showInfoDialog(BuildContext context) => showAdaptiveBottomSheet( context: context, builder: (context) => - EventInfoDialog(l10n: L10n.of(context)!, event: this), + EventInfoDialog(l10n: L10n.of(context), event: this), ); } @@ -40,11 +40,11 @@ class EventInfoDialog extends StatelessWidget { final originalSource = event.originalSource; return Scaffold( appBar: AppBar( - title: Text(L10n.of(context)!.messageInfo), + title: Text(L10n.of(context).messageInfo), leading: IconButton( icon: const Icon(Icons.arrow_downward_outlined), onPressed: Navigator.of(context, rootNavigator: false).pop, - tooltip: L10n.of(context)!.close, + tooltip: L10n.of(context).close, ), ), body: ListView( @@ -56,20 +56,20 @@ class EventInfoDialog extends StatelessWidget { client: event.room.client, presenceUserId: event.senderId, ), - title: Text(L10n.of(context)!.sender), + title: Text(L10n.of(context).sender), subtitle: Text( '${event.senderFromMemoryOrFallback.calcDisplayname()} [${event.senderId}]', ), ), ListTile( - title: Text('${L10n.of(context)!.time}:'), + title: Text('${L10n.of(context).time}:'), subtitle: Text(event.originServerTs.localizedTime(context)), ), ListTile( - title: Text('${L10n.of(context)!.status}:'), + title: Text('${L10n.of(context).status}:'), subtitle: Text(event.status.name), ), - ListTile(title: Text('${L10n.of(context)!.sourceCode}:')), + ListTile(title: Text('${L10n.of(context).sourceCode}:')), Padding( padding: const EdgeInsets.all(12.0), child: Material( @@ -88,7 +88,7 @@ class EventInfoDialog extends StatelessWidget { ), ), if (originalSource != null) ...[ - ListTile(title: Text('${L10n.of(context)!.encrypted}:')), + ListTile(title: Text('${L10n.of(context).encrypted}:')), Padding( padding: const EdgeInsets.all(12.0), child: Material( diff --git a/lib/pages/chat/events/cute_events.dart b/lib/pages/chat/events/cute_events.dart index 6a9424818..4f073a283 100644 --- a/lib/pages/chat/events/cute_events.dart +++ b/lib/pages/chat/events/cute_events.dart @@ -72,19 +72,19 @@ class _CuteContentState extends State { generateLabel(User? user) { switch (widget.event.content['cute_type']) { case 'googly_eyes': - return L10n.of(context)?.googlyEyesContent( + return L10n.of(context).googlyEyesContent( user?.displayName ?? widget.event.senderFromMemoryOrFallback.displayName ?? '', ); case 'cuddle': - return L10n.of(context)?.cuddleContent( + return L10n.of(context).cuddleContent( user?.displayName ?? widget.event.senderFromMemoryOrFallback.displayName ?? '', ); case 'hug': - return L10n.of(context)?.hugContent( + return L10n.of(context).hugContent( user?.displayName ?? widget.event.senderFromMemoryOrFallback.displayName ?? '', diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 08bf9e306..d1a627b55 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -484,7 +484,7 @@ class Message extends StatelessWidget { horizontal: 8, ), child: Text( - L10n.of(context)!.readUpToHere, + L10n.of(context).readUpToHere, style: TextStyle(color: theme.colorScheme.primary), ), ), diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 415e37428..48bc8c583 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -38,7 +38,7 @@ class MessageContent extends StatelessWidget { }); void _verifyOrRequestKey(BuildContext context) async { - final l10n = L10n.of(context)!; + final l10n = L10n.of(context); if (event.content['can_request_session'] != true) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -184,7 +184,7 @@ class MessageContent extends StatelessWidget { textColor: buttonTextColor, onPressed: () => _verifyOrRequestKey(context), icon: '🔒', - label: L10n.of(context)!.encrypted, + label: L10n.of(context).encrypted, fontSize: fontSize, ); case MessageTypes.Location: @@ -213,7 +213,7 @@ class MessageContent extends StatelessWidget { onPressed: UrlLauncher(context, geoUri.toString()).launchUrl, label: Text( - L10n.of(context)!.openInMaps, + L10n.of(context).openInMaps, style: TextStyle(color: textColor), ), ), @@ -233,11 +233,11 @@ class MessageContent extends StatelessWidget { event.redactedBecause?.content.tryGet('reason'); final redactedBy = snapshot.data?.calcDisplayname() ?? event.redactedBecause?.senderId.localpart ?? - L10n.of(context)!.user; + L10n.of(context).user; return _ButtonContent( label: reason == null - ? L10n.of(context)!.redactedBy(redactedBy) - : L10n.of(context)!.redactedByBecause( + ? L10n.of(context).redactedBy(redactedBy) + : L10n.of(context).redactedByBecause( redactedBy, reason, ), @@ -254,7 +254,7 @@ class MessageContent extends StatelessWidget { event.numberEmotes <= 10; return Linkify( text: event.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), hideReply: true, ), style: TextStyle( @@ -277,7 +277,7 @@ class MessageContent extends StatelessWidget { future: event.fetchSenderUser(), builder: (context, snapshot) { return _ButtonContent( - label: L10n.of(context)!.startedACall( + label: L10n.of(context).startedACall( snapshot.data?.calcDisplayname() ?? event.senderFromMemoryOrFallback.calcDisplayname(), ), @@ -293,7 +293,7 @@ class MessageContent extends StatelessWidget { future: event.fetchSenderUser(), builder: (context, snapshot) { return _ButtonContent( - label: L10n.of(context)!.userSentUnknownEvent( + label: L10n.of(context).userSentUnknownEvent( snapshot.data?.calcDisplayname() ?? event.senderFromMemoryOrFallback.calcDisplayname(), event.type, diff --git a/lib/pages/chat/events/reply_content.dart b/lib/pages/chat/events/reply_content.dart index 02e986f81..a3f18efee 100644 --- a/lib/pages/chat/events/reply_content.dart +++ b/lib/pages/chat/events/reply_content.dart @@ -72,7 +72,7 @@ class ReplyContent extends StatelessWidget { ), Text( displayEvent.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), withSenderNamePrefix: false, hideReply: true, ), diff --git a/lib/pages/chat/events/state_message.dart b/lib/pages/chat/events/state_message.dart index e38f75815..ab135a5c1 100644 --- a/lib/pages/chat/events/state_message.dart +++ b/lib/pages/chat/events/state_message.dart @@ -21,7 +21,7 @@ class StateMessage extends StatelessWidget { padding: const EdgeInsets.all(8), child: Text( event.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), ), textAlign: TextAlign.center, style: TextStyle( diff --git a/lib/pages/chat/events/verification_request_content.dart b/lib/pages/chat/events/verification_request_content.dart index a09f007f7..9b085114f 100644 --- a/lib/pages/chat/events/verification_request_content.dart +++ b/lib/pages/chat/events/verification_request_content.dart @@ -58,10 +58,10 @@ class VerificationRequestContent extends StatelessWidget { canceled ? 'Error ${cancel.first.content.tryGet('code')}: ${cancel.first.content.tryGet('reason')}' : (fullyDone - ? L10n.of(context)!.verifySuccess + ? L10n.of(context).verifySuccess : (started - ? L10n.of(context)!.loadingPleaseWait - : L10n.of(context)!.newVerificationRequest)), + ? L10n.of(context).loadingPleaseWait + : L10n.of(context).newVerificationRequest)), ), ], ), diff --git a/lib/pages/chat/events/video_player.dart b/lib/pages/chat/events/video_player.dart index a23ab7bb0..3f2b3d0cc 100644 --- a/lib/pages/chat/events/video_player.dart +++ b/lib/pages/chat/events/video_player.dart @@ -137,8 +137,8 @@ class EventVideoPlayerState extends State { ) : const Icon(Icons.play_circle_outlined), tooltip: _isDownloading - ? L10n.of(context)!.loadingPleaseWait - : L10n.of(context)!.videoWithSize( + ? L10n.of(context).loadingPleaseWait + : L10n.of(context).videoWithSize( widget.event.sizeString ?? '?MB', ), onPressed: _isDownloading ? null : _downloadAction, diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 1e1f3f40e..1d34c7055 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -226,7 +226,7 @@ class InputBar extends StatelessWidget { const padding = EdgeInsets.all(4.0); if (suggestion['type'] == 'command') { final command = suggestion['name']!; - final hint = commandHint(L10n.of(context)!, command); + final hint = commandHint(L10n.of(context), command); return Tooltip( message: hint, waitDuration: const Duration(days: 1), // don't show on hover diff --git a/lib/pages/chat/pinned_events.dart b/lib/pages/chat/pinned_events.dart index 1baa488c5..cae6bd2c9 100644 --- a/lib/pages/chat/pinned_events.dart +++ b/lib/pages/chat/pinned_events.dart @@ -32,13 +32,13 @@ class PinnedEvents extends StatelessWidget { ? events.single?.eventId : await showConfirmationDialog( context: context, - title: L10n.of(context)!.pinMessage, + title: L10n.of(context).pinMessage, actions: events .map( (event) => AlertDialogAction( key: event?.eventId ?? '', label: event?.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), withSenderNamePrefix: true, hideReply: true, ) ?? @@ -67,17 +67,17 @@ class PinnedEvents extends StatelessWidget { final event = snapshot.data; return ChatAppBarListTile( title: event?.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), withSenderNamePrefix: true, hideReply: true, ) ?? - L10n.of(context)!.loadingPleaseWait, + L10n.of(context).loadingPleaseWait, leading: IconButton( splashRadius: 18, iconSize: 18, color: theme.colorScheme.onSurfaceVariant, icon: const Icon(Icons.push_pin), - tooltip: L10n.of(context)!.unpin, + tooltip: L10n.of(context).unpin, onPressed: controller.room.canSendEvent(EventTypes.RoomPinnedEvents) ? () => controller.unpinEvent(event!.eventId) : null, diff --git a/lib/pages/chat/recording_dialog.dart b/lib/pages/chat/recording_dialog.dart index 937b0045d..68e86fa11 100644 --- a/lib/pages/chat/recording_dialog.dart +++ b/lib/pages/chat/recording_dialog.dart @@ -137,7 +137,7 @@ class RecordingDialogState extends State { final time = '${_duration.inMinutes.toString().padLeft(2, '0')}:${(_duration.inSeconds % 60).toString().padLeft(2, '0')}'; final content = error - ? Text(L10n.of(context)!.oopsSomethingWentWrong) + ? Text(L10n.of(context).oopsSomethingWentWrong) : Row( children: [ Container( @@ -185,7 +185,7 @@ class RecordingDialogState extends State { CupertinoDialogAction( onPressed: () => Navigator.of(context, rootNavigator: false).pop(), child: Text( - L10n.of(context)!.cancel.toUpperCase(), + L10n.of(context).cancel.toUpperCase(), style: TextStyle( color: theme.textTheme.bodyMedium?.color?.withAlpha(150), ), @@ -194,7 +194,7 @@ class RecordingDialogState extends State { if (error != true) CupertinoDialogAction( onPressed: _stopAndSend, - child: Text(L10n.of(context)!.send.toUpperCase()), + child: Text(L10n.of(context).send.toUpperCase()), ), ], ); @@ -205,7 +205,7 @@ class RecordingDialogState extends State { TextButton( onPressed: () => Navigator.of(context, rootNavigator: false).pop(), child: Text( - L10n.of(context)!.cancel.toUpperCase(), + L10n.of(context).cancel.toUpperCase(), style: TextStyle( color: theme.textTheme.bodyMedium?.color?.withAlpha(150), ), @@ -217,7 +217,7 @@ class RecordingDialogState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text(L10n.of(context)!.send.toUpperCase()), + Text(L10n.of(context).send.toUpperCase()), const SizedBox(width: 4), const Icon(Icons.send_outlined, size: 15), ], diff --git a/lib/pages/chat/reply_display.dart b/lib/pages/chat/reply_display.dart index 8de51d0f3..ec607e1ac 100644 --- a/lib/pages/chat/reply_display.dart +++ b/lib/pages/chat/reply_display.dart @@ -29,7 +29,7 @@ class ReplyDisplay extends StatelessWidget { child: Row( children: [ IconButton( - tooltip: L10n.of(context)!.close, + tooltip: L10n.of(context).close, icon: const Icon(Icons.close), onPressed: controller.cancelReplyEventAction, ), @@ -71,7 +71,7 @@ class _EditContent extends StatelessWidget { Container(width: 15.0), Text( event.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), withSenderNamePrefix: false, hideReply: true, ), diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index 5092d5566..121c215d6 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -40,7 +40,7 @@ class SendFileDialogState extends State { Future _send() async { final scaffoldMessenger = ScaffoldMessenger.of(widget.outerContext); - final l10n = L10n.of(context)!; + final l10n = L10n.of(context); try { scaffoldMessenger.showLoadingSnackBar(l10n.prepareSendingAttachment); @@ -146,7 +146,7 @@ class SendFileDialogState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); - var sendStr = L10n.of(context)!.sendFile; + var sendStr = L10n.of(context).sendFile; final uniqueMimeType = widget.files .map((file) => file.mimeType ?? lookupMimeType(file.path)) .toSet() @@ -154,21 +154,21 @@ class SendFileDialogState extends State { final fileName = widget.files.length == 1 ? widget.files.single.name - : L10n.of(context)!.countFiles(widget.files.length.toString()); + : L10n.of(context).countFiles(widget.files.length.toString()); if (uniqueMimeType?.startsWith('image') ?? false) { - sendStr = L10n.of(context)!.sendImage; + sendStr = L10n.of(context).sendImage; } else if (uniqueMimeType?.startsWith('audio') ?? false) { - sendStr = L10n.of(context)!.sendAudio; + sendStr = L10n.of(context).sendAudio; } else if (uniqueMimeType?.startsWith('video') ?? false) { - sendStr = L10n.of(context)!.sendVideo; + sendStr = L10n.of(context).sendVideo; } return FutureBuilder( future: _calcCombinedFileSize(), builder: (context, snapshot) { final sizeString = - snapshot.data ?? L10n.of(context)!.calculatingFileSize; + snapshot.data ?? L10n.of(context).calculatingFileSize; Widget contentWidget; if (uniqueMimeType?.startsWith('image') ?? false) { @@ -210,7 +210,7 @@ class SendFileDialogState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - L10n.of(context)!.sendOriginal, + L10n.of(context).sendOriginal, style: const TextStyle(fontWeight: FontWeight.bold), ), Text(sizeString), @@ -270,7 +270,7 @@ class SendFileDialogState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - L10n.of(context)!.sendOriginal, + L10n.of(context).sendOriginal, style: const TextStyle(fontWeight: FontWeight.bold), ), @@ -293,11 +293,11 @@ class SendFileDialogState extends State { // just close the dialog Navigator.of(context, rootNavigator: false).pop(); }, - child: Text(L10n.of(context)!.cancel), + child: Text(L10n.of(context).cancel), ), TextButton( onPressed: _send, - child: Text(L10n.of(context)!.send), + child: Text(L10n.of(context).send), ), ], ); diff --git a/lib/pages/chat/send_location_dialog.dart b/lib/pages/chat/send_location_dialog.dart index b2a99004e..71d13b2c1 100644 --- a/lib/pages/chat/send_location_dialog.dart +++ b/lib/pages/chat/send_location_dialog.dart @@ -93,12 +93,12 @@ class SendLocationDialogState extends State { longitude: position!.longitude, ); } else if (disabled) { - contentWidget = Text(L10n.of(context)!.locationDisabledNotice); + contentWidget = Text(L10n.of(context).locationDisabledNotice); } else if (denied) { - contentWidget = Text(L10n.of(context)!.locationPermissionDeniedNotice); + contentWidget = Text(L10n.of(context).locationPermissionDeniedNotice); } else if (error != null) { contentWidget = - Text(L10n.of(context)!.errorObtainingLocation(error.toString())); + Text(L10n.of(context).errorObtainingLocation(error.toString())); } else { contentWidget = Row( mainAxisSize: MainAxisSize.min, @@ -106,22 +106,22 @@ class SendLocationDialogState extends State { children: [ const CupertinoActivityIndicator(), const SizedBox(width: 12), - Text(L10n.of(context)!.obtainingLocation), + Text(L10n.of(context).obtainingLocation), ], ); } return AlertDialog.adaptive( - title: Text(L10n.of(context)!.shareLocation), + title: Text(L10n.of(context).shareLocation), content: contentWidget, actions: [ TextButton( onPressed: Navigator.of(context, rootNavigator: false).pop, - child: Text(L10n.of(context)!.cancel), + child: Text(L10n.of(context).cancel), ), if (position != null) TextButton( onPressed: isSending ? null : sendAction, - child: Text(L10n.of(context)!.send), + child: Text(L10n.of(context).send), ), ], ); diff --git a/lib/pages/chat/sticker_picker_dialog.dart b/lib/pages/chat/sticker_picker_dialog.dart index 1517b8a40..eaa066413 100644 --- a/lib/pages/chat/sticker_picker_dialog.dart +++ b/lib/pages/chat/sticker_picker_dialog.dart @@ -117,7 +117,7 @@ class StickerPickerDialogState extends State { child: TextField( autofocus: false, decoration: InputDecoration( - hintText: L10n.of(context)!.search, + hintText: L10n.of(context).search, prefixIcon: const Icon(Icons.search_outlined), contentPadding: EdgeInsets.zero, ), @@ -131,7 +131,7 @@ class StickerPickerDialogState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text(L10n.of(context)!.noEmotesFound), + Text(L10n.of(context).noEmotesFound), const SizedBox(height: 12), OutlinedButton.icon( onPressed: () => UrlLauncher( @@ -139,7 +139,7 @@ class StickerPickerDialogState extends State { 'https://matrix.to/#/#fluffychat-stickers:janian.de', ).launchUrl(), icon: const Icon(Icons.explore_outlined), - label: Text(L10n.of(context)!.discover), + label: Text(L10n.of(context).discover), ), ], ), diff --git a/lib/pages/chat_access_settings/chat_access_settings_controller.dart b/lib/pages/chat_access_settings/chat_access_settings_controller.dart index fd7624790..11fd0fbc6 100644 --- a/lib/pages/chat_access_settings/chat_access_settings_controller.dart +++ b/lib/pages/chat_access_settings/chat_access_settings_controller.dart @@ -151,7 +151,7 @@ class ChatAccessSettingsController extends State { if (capabilities == null) return; final newVersion = await showConfirmationDialog( context: context, - title: L10n.of(context)!.replaceRoomWithNewerVersion, + title: L10n.of(context).replaceRoomWithNewerVersion, actions: capabilities.mRoomVersions!.available.entries .where((r) => r.key != roomVersion) .map( @@ -168,10 +168,10 @@ class ChatAccessSettingsController extends State { await showOkCancelAlertDialog( useRootNavigator: false, context: context, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.cancel, - title: L10n.of(context)!.areYouSure, - message: L10n.of(context)!.roomUpgradeDescription, + okLabel: L10n.of(context).yes, + cancelLabel: L10n.of(context).cancel, + title: L10n.of(context).areYouSure, + message: L10n.of(context).roomUpgradeDescription, isDestructiveAction: true, )) { return; @@ -190,12 +190,12 @@ class ChatAccessSettingsController extends State { final input = await showTextInputDialog( context: context, - title: L10n.of(context)!.editRoomAliases, + title: L10n.of(context).editRoomAliases, textFields: [ DialogTextField( prefixText: '#', suffixText: domain, - hintText: L10n.of(context)!.alias, + hintText: L10n.of(context).alias, ), ], ); @@ -214,10 +214,10 @@ class ChatAccessSettingsController extends State { final canonicalAliasConsent = await showOkCancelAlertDialog( context: context, - title: L10n.of(context)!.setAsCanonicalAlias, + title: L10n.of(context).setAsCanonicalAlias, message: alias, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.no, + okLabel: L10n.of(context).yes, + cancelLabel: L10n.of(context).no, ); final altAliases = room diff --git a/lib/pages/chat_access_settings/chat_access_settings_page.dart b/lib/pages/chat_access_settings/chat_access_settings_page.dart index a4a6538a6..34ee6c559 100644 --- a/lib/pages/chat_access_settings/chat_access_settings_page.dart +++ b/lib/pages/chat_access_settings/chat_access_settings_page.dart @@ -20,7 +20,7 @@ class ChatAccessSettingsPageView extends StatelessWidget { return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), - title: Text(L10n.of(context)!.accessAndVisibility), + title: Text(L10n.of(context).accessAndVisibility), ), body: MaxWidthBody( child: StreamBuilder( @@ -38,7 +38,7 @@ class ChatAccessSettingsPageView extends StatelessWidget { children: [ ListTile( title: Text( - L10n.of(context)!.visibilityOfTheChatHistory, + L10n.of(context).visibilityOfTheChatHistory, style: TextStyle( color: theme.colorScheme.secondary, fontWeight: FontWeight.bold, @@ -49,7 +49,7 @@ class ChatAccessSettingsPageView extends StatelessWidget { RadioListTile.adaptive( title: Text( historyVisibility - .getLocalizedString(MatrixLocals(L10n.of(context)!)), + .getLocalizedString(MatrixLocals(L10n.of(context))), ), value: historyVisibility, groupValue: room.historyVisibility, @@ -61,7 +61,7 @@ class ChatAccessSettingsPageView extends StatelessWidget { Divider(color: theme.dividerColor), ListTile( title: Text( - L10n.of(context)!.whoIsAllowedToJoinThisGroup, + L10n.of(context).whoIsAllowedToJoinThisGroup, style: TextStyle( color: theme.colorScheme.secondary, fontWeight: FontWeight.bold, @@ -72,7 +72,7 @@ class ChatAccessSettingsPageView extends StatelessWidget { if (joinRule != JoinRules.private) RadioListTile.adaptive( title: Text( - joinRule.localizedString(L10n.of(context)!), + joinRule.localizedString(L10n.of(context)), ), value: joinRule, groupValue: room.joinRules, @@ -86,7 +86,7 @@ class ChatAccessSettingsPageView extends StatelessWidget { .contains(room.joinRules)) ...[ ListTile( title: Text( - L10n.of(context)!.areGuestsAllowedToJoin, + L10n.of(context).areGuestsAllowedToJoin, style: TextStyle( color: theme.colorScheme.secondary, fontWeight: FontWeight.bold, @@ -97,7 +97,7 @@ class ChatAccessSettingsPageView extends StatelessWidget { RadioListTile.adaptive( title: Text( guestAccess.getLocalizedString( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), ), ), value: guestAccess, @@ -110,7 +110,7 @@ class ChatAccessSettingsPageView extends StatelessWidget { Divider(color: theme.dividerColor), ListTile( title: Text( - L10n.of(context)!.publicChatAddresses, + L10n.of(context).publicChatAddresses, style: TextStyle( color: theme.colorScheme.secondary, fontWeight: FontWeight.bold, @@ -118,7 +118,7 @@ class ChatAccessSettingsPageView extends StatelessWidget { ), trailing: IconButton( icon: const Icon(Icons.add_outlined), - tooltip: L10n.of(context)!.createNewAddress, + tooltip: L10n.of(context).createNewAddress, onPressed: controller.addAlias, ), ), @@ -171,7 +171,7 @@ class ChatAccessSettingsPageView extends StatelessWidget { builder: (context, snapshot) => SwitchListTile.adaptive( value: snapshot.data == Visibility.public, title: Text( - L10n.of(context)!.chatCanBeDiscoveredViaSearchOnServer( + L10n.of(context).chatCanBeDiscoveredViaSearchOnServer( room.client.userID!.domain!, ), ), @@ -180,7 +180,7 @@ class ChatAccessSettingsPageView extends StatelessWidget { ), ], ListTile( - title: Text(L10n.of(context)!.globalChatId), + title: Text(L10n.of(context).globalChatId), subtitle: SelectableText(room.id), trailing: IconButton( icon: const Icon(Icons.copy_outlined), @@ -188,7 +188,7 @@ class ChatAccessSettingsPageView extends StatelessWidget { ), ), ListTile( - title: Text(L10n.of(context)!.roomVersion), + title: Text(L10n.of(context).roomVersion), subtitle: SelectableText( room .getState(EventTypes.RoomCreate)! diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 526d1261e..4159249d7 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -43,14 +43,14 @@ class ChatDetailsController extends State { final room = Matrix.of(context).client.getRoomById(roomId!)!; final input = await showTextInputDialog( context: context, - title: L10n.of(context)!.changeTheNameOfTheGroup, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).changeTheNameOfTheGroup, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, textFields: [ DialogTextField( initialText: room.getLocalizedDisplayname( MatrixLocals( - L10n.of(context)!, + L10n.of(context), ), ), ), @@ -63,7 +63,7 @@ class ChatDetailsController extends State { ); if (success.error == null) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.displaynameHasBeenChanged)), + SnackBar(content: Text(L10n.of(context).displaynameHasBeenChanged)), ); } } @@ -72,12 +72,12 @@ class ChatDetailsController extends State { final room = Matrix.of(context).client.getRoomById(roomId!)!; final input = await showTextInputDialog( context: context, - title: L10n.of(context)!.setChatDescription, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).setChatDescription, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, textFields: [ DialogTextField( - hintText: L10n.of(context)!.noChatDescriptionYet, + hintText: L10n.of(context).noChatDescriptionYet, initialText: room.topic, minLines: 4, maxLines: 8, @@ -92,7 +92,7 @@ class ChatDetailsController extends State { if (success.error == null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(L10n.of(context)!.chatDescriptionHasBeenChanged), + content: Text(L10n.of(context).chatDescriptionHasBeenChanged), ), ); } @@ -118,19 +118,19 @@ class ChatDetailsController extends State { if (PlatformInfos.isMobile) SheetAction( key: AvatarAction.camera, - label: L10n.of(context)!.openCamera, + label: L10n.of(context).openCamera, isDefaultAction: true, icon: Icons.camera_alt_outlined, ), SheetAction( key: AvatarAction.file, - label: L10n.of(context)!.openGallery, + label: L10n.of(context).openGallery, icon: Icons.photo_outlined, ), if (room?.avatar != null) SheetAction( key: AvatarAction.remove, - label: L10n.of(context)!.delete, + label: L10n.of(context).delete, isDestructiveAction: true, icon: Icons.delete_outlined, ), @@ -139,7 +139,7 @@ class ChatDetailsController extends State { ? actions.single.key : await showModalActionSheet( context: context, - title: L10n.of(context)!.editRoomAvatar, + title: L10n.of(context).editRoomAvatar, actions: actions, ); if (action == null) return; diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index c55e3fe0f..87baae98f 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -29,10 +29,10 @@ class ChatDetailsView extends StatelessWidget { if (room == null) { return Scaffold( appBar: AppBar( - title: Text(L10n.of(context)!.oopsSomethingWentWrong), + title: Text(L10n.of(context).oopsSomethingWentWrong), ), body: Center( - child: Text(L10n.of(context)!.youAreNoLongerParticipatingInThisChat), + child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat), ), ); } @@ -49,7 +49,7 @@ class ChatDetailsView extends StatelessWidget { final canRequestMoreMembers = members.length < actualMembersCount; final iconColor = theme.textTheme.bodyLarge!.color; final displayname = room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), ); return Scaffold( appBar: AppBar( @@ -59,7 +59,7 @@ class ChatDetailsView extends StatelessWidget { actions: [ if (room.canonicalAlias.isNotEmpty) IconButton( - tooltip: L10n.of(context)!.share, + tooltip: L10n.of(context).share, icon: Icon(Icons.adaptive.share_outlined), onPressed: () => FluffyShare.share( AppConfig.inviteLinkPrefix + room.canonicalAlias, @@ -69,7 +69,7 @@ class ChatDetailsView extends StatelessWidget { if (controller.widget.embeddedCloseButton == null) ChatSettingsPopupMenu(room, false), ], - title: Text(L10n.of(context)!.chatDetails), + title: Text(L10n.of(context).chatDetails), backgroundColor: theme.appBarTheme.backgroundColor, ), body: MaxWidthBody( @@ -150,7 +150,7 @@ class ChatDetailsView extends StatelessWidget { ), label: Text( room.isDirectChat - ? L10n.of(context)!.directChat + ? L10n.of(context).directChat : displayname, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -172,7 +172,7 @@ class ChatDetailsView extends StatelessWidget { theme.colorScheme.secondary, ), label: Text( - L10n.of(context)!.countParticipants( + L10n.of(context).countParticipants( actualMembersCount, ), maxLines: 1, @@ -189,7 +189,7 @@ class ChatDetailsView extends StatelessWidget { if (!room.canChangeStateEvent(EventTypes.RoomTopic)) ListTile( title: Text( - L10n.of(context)!.chatDescription, + L10n.of(context).chatDescription, style: TextStyle( color: theme.colorScheme.secondary, fontWeight: FontWeight.bold, @@ -201,7 +201,7 @@ class ChatDetailsView extends StatelessWidget { padding: const EdgeInsets.all(16.0), child: TextButton.icon( onPressed: controller.setTopicAction, - label: Text(L10n.of(context)!.setChatDescription), + label: Text(L10n.of(context).setChatDescription), icon: const Icon(Icons.edit_outlined), style: TextButton.styleFrom( backgroundColor: @@ -217,7 +217,7 @@ class ChatDetailsView extends StatelessWidget { ), child: SelectableLinkify( text: room.topic.isEmpty - ? L10n.of(context)!.noChatDescriptionYet + ? L10n.of(context).noChatDescriptionYet : room.topic, options: const LinkifyOptions(humanize: false), linkStyle: const TextStyle( @@ -247,9 +247,8 @@ class ChatDetailsView extends StatelessWidget { Icons.insert_emoticon_outlined, ), ), - title: - Text(L10n.of(context)!.customEmojisAndStickers), - subtitle: Text(L10n.of(context)!.setCustomEmotes), + title: Text(L10n.of(context).customEmojisAndStickers), + subtitle: Text(L10n.of(context).setCustomEmotes), onTap: controller.goToEmoteSettings, trailing: const Icon(Icons.chevron_right_outlined), ), @@ -261,10 +260,10 @@ class ChatDetailsView extends StatelessWidget { child: const Icon(Icons.shield_outlined), ), title: Text( - L10n.of(context)!.accessAndVisibility, + L10n.of(context).accessAndVisibility, ), subtitle: Text( - L10n.of(context)!.accessAndVisibilityDescription, + L10n.of(context).accessAndVisibilityDescription, ), onTap: () => context .push('/rooms/${room.id}/details/access'), @@ -272,9 +271,9 @@ class ChatDetailsView extends StatelessWidget { ), if (!room.isDirectChat) ListTile( - title: Text(L10n.of(context)!.chatPermissions), + title: Text(L10n.of(context).chatPermissions), subtitle: Text( - L10n.of(context)!.whoCanPerformWhichAction, + L10n.of(context).whoCanPerformWhichAction, ), leading: CircleAvatar( backgroundColor: theme.scaffoldBackgroundColor, @@ -290,7 +289,7 @@ class ChatDetailsView extends StatelessWidget { Divider(color: theme.dividerColor), ListTile( title: Text( - L10n.of(context)!.countParticipants( + L10n.of(context).countParticipants( actualMembersCount.toString(), ), style: TextStyle( @@ -301,7 +300,7 @@ class ChatDetailsView extends StatelessWidget { ), if (!room.isDirectChat && room.canInvite) ListTile( - title: Text(L10n.of(context)!.inviteContact), + title: Text(L10n.of(context).inviteContact), leading: CircleAvatar( backgroundColor: theme.colorScheme.primaryContainer, @@ -319,7 +318,7 @@ class ChatDetailsView extends StatelessWidget { ? ParticipantListItem(members[i - 1]) : ListTile( title: Text( - L10n.of(context)!.loadCountMoreParticipants( + L10n.of(context).loadCountMoreParticipants( (actualMembersCount - members.length).toString(), ), ), diff --git a/lib/pages/chat_details/participant_list_item.dart b/lib/pages/chat_details/participant_list_item.dart index ddacbb7c1..183b3e4fc 100644 --- a/lib/pages/chat_details/participant_list_item.dart +++ b/lib/pages/chat_details/participant_list_item.dart @@ -17,17 +17,17 @@ class ParticipantListItem extends StatelessWidget { final theme = Theme.of(context); final membershipBatch = switch (user.membership) { - Membership.ban => L10n.of(context)!.banned, - Membership.invite => L10n.of(context)!.invited, + Membership.ban => L10n.of(context).banned, + Membership.invite => L10n.of(context).invited, Membership.join => null, - Membership.knock => L10n.of(context)!.knocking, - Membership.leave => L10n.of(context)!.leftTheChat, + Membership.knock => L10n.of(context).knocking, + Membership.leave => L10n.of(context).leftTheChat, }; final permissionBatch = user.powerLevel == 100 - ? L10n.of(context)!.admin + ? L10n.of(context).admin : user.powerLevel >= 50 - ? L10n.of(context)!.moderator + ? L10n.of(context).moderator : ''; return Opacity( diff --git a/lib/pages/chat_encryption_settings/chat_encryption_settings.dart b/lib/pages/chat_encryption_settings/chat_encryption_settings.dart index 559aa2063..c92ae61f7 100644 --- a/lib/pages/chat_encryption_settings/chat_encryption_settings.dart +++ b/lib/pages/chat_encryption_settings/chat_encryption_settings.dart @@ -34,33 +34,33 @@ class ChatEncryptionSettingsController extends State { if (room.encrypted) { showOkAlertDialog( context: context, - title: L10n.of(context)!.sorryThatsNotPossible, - message: L10n.of(context)!.disableEncryptionWarning, + title: L10n.of(context).sorryThatsNotPossible, + message: L10n.of(context).disableEncryptionWarning, ); return; } if (room.joinRules == JoinRules.public) { showOkAlertDialog( context: context, - title: L10n.of(context)!.sorryThatsNotPossible, - message: L10n.of(context)!.noEncryptionForPublicRooms, + title: L10n.of(context).sorryThatsNotPossible, + message: L10n.of(context).noEncryptionForPublicRooms, ); return; } if (!room.canChangeStateEvent(EventTypes.Encryption)) { showOkAlertDialog( context: context, - title: L10n.of(context)!.sorryThatsNotPossible, - message: L10n.of(context)!.noPermission, + title: L10n.of(context).sorryThatsNotPossible, + message: L10n.of(context).noPermission, ); return; } final consent = await showOkCancelAlertDialog( context: context, - title: L10n.of(context)!.areYouSure, - message: L10n.of(context)!.enableEncryptionWarning, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).areYouSure, + message: L10n.of(context).enableEncryptionWarning, + okLabel: L10n.of(context).yes, + cancelLabel: L10n.of(context).cancel, ); if (consent != OkCancelResult.ok) return; await showFutureLoadingDialog( @@ -72,10 +72,10 @@ class ChatEncryptionSettingsController extends State { void startVerification() async { final consent = await showOkCancelAlertDialog( context: context, - title: L10n.of(context)!.verifyOtherUser, - message: L10n.of(context)!.verifyOtherUserDescription, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).verifyOtherUser, + message: L10n.of(context).verifyOtherUserDescription, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, fullyCapitalizedForMaterial: false, ); if (consent != OkCancelResult.ok) return; diff --git a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart index 2a17a5a33..2c39a942e 100644 --- a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart +++ b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart @@ -31,11 +31,11 @@ class ChatEncryptionSettingsView extends StatelessWidget { icon: const Icon(Icons.close_outlined), onPressed: () => context.go('/rooms/${controller.roomId!}'), ), - title: Text(L10n.of(context)!.encryption), + title: Text(L10n.of(context).encryption), actions: [ TextButton( onPressed: () => launchUrlString(AppConfig.encryptionTutorial), - child: Text(L10n.of(context)!.help), + child: Text(L10n.of(context).help), ), ], ), @@ -49,7 +49,7 @@ class ChatEncryptionSettingsView extends StatelessWidget { backgroundColor: theme.colorScheme.primaryContainer, child: const Icon(Icons.lock_outlined), ), - title: Text(L10n.of(context)!.encryptThisChat), + title: Text(L10n.of(context).encryptThisChat), value: room.encrypted, onChanged: controller.enableEncryption, ), @@ -67,7 +67,7 @@ class ChatEncryptionSettingsView extends StatelessWidget { child: ElevatedButton.icon( onPressed: controller.startVerification, icon: const Icon(Icons.verified_outlined), - label: Text(L10n.of(context)!.verifyStart), + label: Text(L10n.of(context).verifyStart), ), ), ), @@ -75,7 +75,7 @@ class ChatEncryptionSettingsView extends StatelessWidget { const SizedBox(height: 16), ListTile( title: Text( - L10n.of(context)!.deviceKeys, + L10n.of(context).deviceKeys, style: const TextStyle( fontWeight: FontWeight.bold, ), @@ -91,7 +91,7 @@ class ChatEncryptionSettingsView extends StatelessWidget { if (snapshot.hasError) { return Center( child: Text( - '${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}', + '${L10n.of(context).oopsSomethingWentWrong}: ${snapshot.error}', ), ); } @@ -133,7 +133,7 @@ class ChatEncryptionSettingsView extends StatelessWidget { const SizedBox(width: 4), Text( deviceKeys[i].deviceId ?? - L10n.of(context)!.unknownDevice, + L10n.of(context).unknownDevice, ), const SizedBox(width: 4), Flexible( @@ -167,7 +167,7 @@ class ChatEncryptionSettingsView extends StatelessWidget { ), subtitle: Text( deviceKeys[i].ed25519Key?.beautified ?? - L10n.of(context)!.unknownEncryptionAlgorithm, + L10n.of(context).unknownEncryptionAlgorithm, style: TextStyle( fontFamily: 'RobotoMono', color: theme.colorScheme.secondary, @@ -183,7 +183,7 @@ class ChatEncryptionSettingsView extends StatelessWidget { padding: const EdgeInsets.all(16.0), child: Center( child: Text( - L10n.of(context)!.encryptionNotEnabled, + L10n.of(context).encryptionNotEnabled, style: const TextStyle( fontStyle: FontStyle.italic, ), diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 581a62b3a..9fe479728 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -62,15 +62,15 @@ extension LocalizedActiveFilter on ActiveFilter { String toLocalizedString(BuildContext context) { switch (this) { case ActiveFilter.allChats: - return L10n.of(context)!.all; + return L10n.of(context).all; case ActiveFilter.messages: - return L10n.of(context)!.messages; + return L10n.of(context).messages; case ActiveFilter.unread: - return L10n.of(context)!.unread; + return L10n.of(context).unread; case ActiveFilter.groups: - return L10n.of(context)!.groups; + return L10n.of(context).groups; case ActiveFilter.spaces: - return L10n.of(context)!.spaces; + return L10n.of(context).spaces; } } } @@ -128,25 +128,25 @@ class ChatListController extends State final inviteAction = await showModalActionSheet( context: context, message: room.isDirectChat - ? L10n.of(context)!.invitePrivateChat - : L10n.of(context)!.inviteGroupChat, - title: room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), + ? L10n.of(context).invitePrivateChat + : L10n.of(context).inviteGroupChat, + title: room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), actions: [ SheetAction( key: InviteActions.accept, - label: L10n.of(context)!.accept, + label: L10n.of(context).accept, icon: Icons.check_outlined, isDefaultAction: true, ), SheetAction( key: InviteActions.decline, - label: L10n.of(context)!.decline, + label: L10n.of(context).decline, icon: Icons.close_outlined, isDestructiveAction: true, ), SheetAction( key: InviteActions.block, - label: L10n.of(context)!.block, + label: L10n.of(context).block, icon: Icons.block_outlined, isDestructiveAction: true, ), @@ -181,7 +181,7 @@ class ChatListController extends State if (room.membership == Membership.ban) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(L10n.of(context)!.youHaveBeenBannedFromThisChat), + content: Text(L10n.of(context).youHaveBeenBannedFromThisChat), ), ); return; @@ -221,12 +221,12 @@ class ChatListController extends State } else { final consent = await showOkCancelAlertDialog( context: context, - title: L10n.of(context)!.forward, - message: L10n.of(context)!.forwardMessageTo( - room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), + title: L10n.of(context).forward, + message: L10n.of(context).forwardMessageTo( + room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), ), - okLabel: L10n.of(context)!.forward, - cancelLabel: L10n.of(context)!.cancel, + okLabel: L10n.of(context).forward, + cancelLabel: L10n.of(context).cancel, ); if (consent == OkCancelResult.cancel) { Matrix.of(context).shareContent = null; @@ -276,10 +276,10 @@ class ChatListController extends State void setServer() async { final newServer = await showTextInputDialog( useRootNavigator: false, - title: L10n.of(context)!.changeTheHomeserver, + title: L10n.of(context).changeTheHomeserver, context: context, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, textFields: [ DialogTextField( prefixText: 'https://', @@ -289,7 +289,7 @@ class ChatListController extends State autocorrect: false, validator: (server) => server?.contains('.') == true ? null - : L10n.of(context)!.invalidServerName, + : L10n.of(context).invalidServerName, ), ], ); @@ -569,7 +569,7 @@ class ChatListController extends State ); final displayname = - room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)); + room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))); final spacesWithPowerLevels = room.client.rooms .where( @@ -618,8 +618,7 @@ class ChatListController extends State const SizedBox(width: 12), Expanded( child: Text( - L10n.of(context)! - .goToSpace(space.getLocalizedDisplayname()), + L10n.of(context).goToSpace(space.getLocalizedDisplayname()), ), ), ], @@ -638,8 +637,8 @@ class ChatListController extends State const SizedBox(width: 12), Text( room.pushRuleState == PushRuleState.notify - ? L10n.of(context)!.muteChat - : L10n.of(context)!.unmuteChat, + ? L10n.of(context).muteChat + : L10n.of(context).unmuteChat, ), ], ), @@ -657,8 +656,8 @@ class ChatListController extends State const SizedBox(width: 12), Text( room.markedUnread - ? L10n.of(context)!.markAsRead - : L10n.of(context)!.markAsUnread, + ? L10n.of(context).markAsRead + : L10n.of(context).markAsUnread, ), ], ), @@ -672,8 +671,8 @@ class ChatListController extends State const SizedBox(width: 12), Text( room.isFavourite - ? L10n.of(context)!.unpin - : L10n.of(context)!.pin, + ? L10n.of(context).unpin + : L10n.of(context).pin, ), ], ), @@ -686,7 +685,7 @@ class ChatListController extends State children: [ const Icon(Icons.group_work_outlined), const SizedBox(width: 12), - Text(L10n.of(context)!.addToSpace), + Text(L10n.of(context).addToSpace), ], ), ), @@ -697,7 +696,7 @@ class ChatListController extends State children: [ const Icon(Icons.delete_outlined), const SizedBox(width: 12), - Text(L10n.of(context)!.leave), + Text(L10n.of(context).leave), ], ), ), @@ -740,10 +739,10 @@ class ChatListController extends State final confirmed = await showOkCancelAlertDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.leave, - cancelLabel: L10n.of(context)!.no, - message: L10n.of(context)!.archiveRoomDescription, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).leave, + cancelLabel: L10n.of(context).no, + message: L10n.of(context).archiveRoomDescription, isDestructiveAction: true, ); if (confirmed == OkCancelResult.cancel) return; @@ -755,13 +754,13 @@ class ChatListController extends State case ChatContextAction.addToSpace: final space = await showConfirmationDialog( context: context, - title: L10n.of(context)!.space, + title: L10n.of(context).space, actions: spacesWithPowerLevels .map( (space) => AlertDialogAction( key: space, label: space - .getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), + .getLocalizedDisplayname(MatrixLocals(L10n.of(context))), ), ) .toList(), @@ -776,7 +775,7 @@ class ChatListController extends State void dismissStatusList() async { final result = await showOkCancelAlertDialog( - title: L10n.of(context)!.hidePresences, + title: L10n.of(context).hidePresences, context: context, ); if (result == OkCancelResult.ok) { @@ -792,13 +791,13 @@ class ChatListController extends State final input = await showTextInputDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.setStatus, - message: L10n.of(context)!.leaveEmptyToClearStatus, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).setStatus, + message: L10n.of(context).leaveEmptyToClearStatus, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, textFields: [ DialogTextField( - hintText: L10n.of(context)!.statusExampleMessage, + hintText: L10n.of(context).statusExampleMessage, maxLines: 6, minLines: 1, maxLength: 255, @@ -880,22 +879,22 @@ class ChatListController extends State } void editBundlesForAccount(String? userId, String? activeBundle) async { - final l10n = L10n.of(context)!; + final l10n = L10n.of(context); final client = Matrix.of(context) .widget .clients[Matrix.of(context).getClientIndexByMatrixId(userId!)]; final action = await showConfirmationDialog( context: context, - title: L10n.of(context)!.editBundlesForAccount, + title: L10n.of(context).editBundlesForAccount, actions: [ AlertDialogAction( key: EditBundleAction.addToBundle, - label: L10n.of(context)!.addToBundle, + label: L10n.of(context).addToBundle, ), if (activeBundle != client.userID) AlertDialogAction( key: EditBundleAction.removeFromBundle, - label: L10n.of(context)!.removeFromBundle, + label: L10n.of(context).removeFromBundle, ), ], ); diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index bf23e841e..7060bd3f6 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -83,17 +83,17 @@ class ChatListViewBody extends StatelessWidget { [ if (controller.isSearchMode) ...[ SearchTitle( - title: L10n.of(context)!.publicRooms, + title: L10n.of(context).publicRooms, icon: const Icon(Icons.explore_outlined), ), PublicRoomsHorizontalList(publicRooms: publicRooms), SearchTitle( - title: L10n.of(context)!.publicSpaces, + title: L10n.of(context).publicSpaces, icon: const Icon(Icons.workspaces_outlined), ), PublicRoomsHorizontalList(publicRooms: publicSpaces), SearchTitle( - title: L10n.of(context)!.users, + title: L10n.of(context).users, icon: const Icon(Icons.group_outlined), ), AnimatedContainer( @@ -115,7 +115,7 @@ class ChatListViewBody extends StatelessWidget { userSearchResult.results[i].displayName ?? userSearchResult .results[i].userId.localpart ?? - L10n.of(context)!.unknownDevice, + L10n.of(context).unknownDevice, avatar: userSearchResult.results[i].avatarUrl, onPressed: () => showAdaptiveBottomSheet( context: context, @@ -146,8 +146,8 @@ class ChatListViewBody extends StatelessWidget { color: theme.colorScheme.surface, child: ListTile( leading: const Icon(Icons.vpn_key), - title: Text(L10n.of(context)!.dehydrateTor), - subtitle: Text(L10n.of(context)!.dehydrateTorLong), + title: Text(L10n.of(context).dehydrateTor), + subtitle: Text(L10n.of(context).dehydrateTorLong), trailing: const Icon(Icons.chevron_right_outlined), onTap: controller.dehydrate, ), @@ -231,7 +231,7 @@ class ChatListViewBody extends StatelessWidget { ), if (controller.isSearchMode) SearchTitle( - title: L10n.of(context)!.chats, + title: L10n.of(context).chats, icon: const Icon(Icons.forum_outlined), ), if (client.prevBatch != null && @@ -267,8 +267,8 @@ class ChatListViewBody extends StatelessWidget { padding: const EdgeInsets.all(16.0), child: Text( client.rooms.isEmpty - ? L10n.of(context)!.noChatsFoundHere - : L10n.of(context)!.noMoreChatsFound, + ? L10n.of(context).noChatsFoundHere + : L10n.of(context).noMoreChatsFound, textAlign: TextAlign.center, style: TextStyle( fontSize: 18, @@ -343,7 +343,7 @@ class PublicRoomsHorizontalList extends StatelessWidget { itemBuilder: (context, i) => _SearchItem( title: publicRooms[i].name ?? publicRooms[i].canonicalAlias?.localpart ?? - L10n.of(context)!.group, + L10n.of(context).group, avatar: publicRooms[i].avatarUrl, onPressed: () => showAdaptiveBottomSheet( context: context, diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index f42999042..5d878be9a 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -35,14 +35,14 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { leading: selectMode == SelectMode.normal ? null : IconButton( - tooltip: L10n.of(context)!.cancel, + tooltip: L10n.of(context).cancel, icon: const Icon(Icons.close_outlined), onPressed: controller.cancelAction, color: theme.colorScheme.primary, ), title: selectMode == SelectMode.share ? Text( - L10n.of(context)!.share, + L10n.of(context).share, key: const ValueKey(SelectMode.share), ) : TextField( @@ -61,7 +61,7 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { borderRadius: BorderRadius.circular(99), ), contentPadding: EdgeInsets.zero, - hintText: L10n.of(context)!.searchChatsRooms, + hintText: L10n.of(context).searchChatsRooms, hintStyle: TextStyle( color: theme.colorScheme.onPrimaryContainer, fontWeight: FontWeight.normal, @@ -69,7 +69,7 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { floatingLabelBehavior: FloatingLabelBehavior.never, prefixIcon: controller.isSearchMode ? IconButton( - tooltip: L10n.of(context)!.cancel, + tooltip: L10n.of(context).cancel, icon: const Icon(Icons.close_outlined), onPressed: controller.cancelSearch, color: theme.colorScheme.onPrimaryContainer, diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index dacc6bec0..4e6ba56b7 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -47,10 +47,10 @@ class ChatListItem extends StatelessWidget { final confirmed = await showOkCancelAlertDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.no, - message: L10n.of(context)!.archiveRoomDescription, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).yes, + cancelLabel: L10n.of(context).no, + message: L10n.of(context).archiveRoomDescription, ); if (confirmed == OkCancelResult.cancel) return; await showFutureLoadingDialog( @@ -81,7 +81,7 @@ class ChatListItem extends StatelessWidget { final backgroundColor = activeChat ? theme.colorScheme.secondaryContainer : null; final displayname = room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), ); final filter = this.filter; if (filter != null && !displayname.toLowerCase().contains(filter)) { @@ -272,7 +272,7 @@ class ChatListItem extends StatelessWidget { Expanded( child: room.isSpace && room.membership == Membership.join ? Text( - L10n.of(context)!.countChatsAndCountParticipants( + L10n.of(context).countChatsAndCountParticipants( room.spaceChildren.length.toString(), (room.summary.mJoinedMemberCount ?? 1).toString(), ), @@ -292,7 +292,7 @@ class ChatListItem extends StatelessWidget { ), future: needLastEventSender ? lastEvent.calcLocalizedBody( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), hideReply: true, hideEdit: true, plaintextBody: true, @@ -304,7 +304,7 @@ class ChatListItem extends StatelessWidget { : null, initialData: lastEvent?.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), hideReply: true, hideEdit: true, plaintextBody: true, @@ -316,10 +316,10 @@ class ChatListItem extends StatelessWidget { builder: (context, snapshot) => Text( room.membership == Membership.invite ? isDirectChat - ? L10n.of(context)!.invitePrivateChat - : L10n.of(context)!.inviteGroupChat + ? L10n.of(context).invitePrivateChat + : L10n.of(context).inviteGroupChat : snapshot.data ?? - L10n.of(context)!.emptyChat, + L10n.of(context).emptyChat, softWrap: false, maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 71c559d70..674aa9406 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -85,7 +85,7 @@ class ChatListView extends StatelessWidget { onTap: controller.clearActiveSpace, icon: const Icon(Icons.forum_outlined), selectedIcon: const Icon(Icons.forum), - toolTip: L10n.of(context)!.chats, + toolTip: L10n.of(context).chats, unreadBadgeFilter: (room) => true, ); } @@ -95,13 +95,13 @@ class ChatListView extends StatelessWidget { isSelected: false, onTap: () => context.go('/rooms/newspace'), icon: const Icon(Icons.add), - toolTip: L10n.of(context)!.createNewSpace, + toolTip: L10n.of(context).createNewSpace, ); } final space = rootSpaces[i]; final displayname = rootSpaces[i].getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), ); final spaceChildrenIds = space.spaceChildren.map((c) => c.roomId).toSet(); @@ -144,7 +144,7 @@ class ChatListView extends StatelessWidget { LogicalKeyboardKey.keyN, }, onKeysPressed: () => context.go('/rooms/newprivatechat'), - helpLabel: L10n.of(context)!.newChat, + helpLabel: L10n.of(context).newChat, child: selectMode == SelectMode.normal && !controller.isSearchMode && controller.activeSpaceId == null @@ -153,7 +153,7 @@ class ChatListView extends StatelessWidget { context.go('/rooms/newprivatechat'), icon: const Icon(Icons.add_outlined), label: Text( - L10n.of(context)!.chat, + L10n.of(context).chat, overflow: TextOverflow.fade, ), ) diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index c004bd167..921ef6b83 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -34,7 +34,7 @@ class ClientChooserButton extends StatelessWidget { children: [ const Icon(Icons.group_add_outlined), const SizedBox(width: 18), - Text(L10n.of(context)!.createGroup), + Text(L10n.of(context).createGroup), ], ), ), @@ -44,7 +44,7 @@ class ClientChooserButton extends StatelessWidget { children: [ const Icon(Icons.workspaces_outlined), const SizedBox(width: 18), - Text(L10n.of(context)!.createNewSpace), + Text(L10n.of(context).createNewSpace), ], ), ), @@ -54,7 +54,7 @@ class ClientChooserButton extends StatelessWidget { children: [ const Icon(Icons.edit_outlined), const SizedBox(width: 18), - Text(L10n.of(context)!.setStatus), + Text(L10n.of(context).setStatus), ], ), ), @@ -64,7 +64,7 @@ class ClientChooserButton extends StatelessWidget { children: [ Icon(Icons.adaptive.share_outlined), const SizedBox(width: 18), - Text(L10n.of(context)!.inviteContact), + Text(L10n.of(context).inviteContact), ], ), ), @@ -86,7 +86,7 @@ class ClientChooserButton extends StatelessWidget { children: [ const Icon(Icons.settings_outlined), const SizedBox(width: 18), - Text(L10n.of(context)!.settings), + Text(L10n.of(context).settings), ], ), ), @@ -156,7 +156,7 @@ class ClientChooserButton extends StatelessWidget { children: [ const Icon(Icons.person_add_outlined), const SizedBox(width: 18), - Text(L10n.of(context)!.addAccount), + Text(L10n.of(context).addAccount), ], ), ), @@ -178,7 +178,7 @@ class ClientChooserButton extends StatelessWidget { clientCount, (index) => KeyBoardShortcuts( keysToPress: _buildKeyboardShortcut(index + 1), - helpLabel: L10n.of(context)!.switchToAccount(index + 1), + helpLabel: L10n.of(context).switchToAccount(index + 1), onKeysPressed: () => _handleKeyboardShortcut( matrix, index, @@ -192,7 +192,7 @@ class ClientChooserButton extends StatelessWidget { LogicalKeyboardKey.controlLeft, LogicalKeyboardKey.tab, }, - helpLabel: L10n.of(context)!.nextAccount, + helpLabel: L10n.of(context).nextAccount, onKeysPressed: () => _nextAccount(matrix, context), child: const SizedBox.shrink(), ), @@ -202,7 +202,7 @@ class ClientChooserButton extends StatelessWidget { LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.tab, }, - helpLabel: L10n.of(context)!.previousAccount, + helpLabel: L10n.of(context).previousAccount, onKeysPressed: () => _previousAccount(matrix, context), child: const SizedBox.shrink(), ), @@ -249,10 +249,10 @@ class ClientChooserButton extends StatelessWidget { case SettingsAction.addAccount: final consent = await showOkCancelAlertDialog( context: context, - title: L10n.of(context)!.addAccount, - message: L10n.of(context)!.enableMultiAccounts, - okLabel: L10n.of(context)!.next, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).addAccount, + message: L10n.of(context).enableMultiAccounts, + okLabel: L10n.of(context).next, + cancelLabel: L10n.of(context).cancel, ); if (consent != OkCancelResult.ok) return; context.go('/rooms/settings/addaccount'); diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 69d9c9696..a1936c02c 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -132,10 +132,10 @@ class _SpaceViewState extends State { final confirmed = await showOkCancelAlertDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - message: L10n.of(context)!.archiveRoomDescription, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + message: L10n.of(context).archiveRoomDescription, ); if (!mounted) return; if (confirmed != OkCancelResult.ok) return; @@ -153,15 +153,15 @@ class _SpaceViewState extends State { void _addChatOrSubspace() async { final roomType = await showConfirmationDialog( context: context, - title: L10n.of(context)!.addChatOrSubSpace, + title: L10n.of(context).addChatOrSubSpace, actions: [ AlertDialogAction( key: AddRoomType.subspace, - label: L10n.of(context)!.createNewSpace, + label: L10n.of(context).createNewSpace, ), AlertDialogAction( key: AddRoomType.chat, - label: L10n.of(context)!.createGroup, + label: L10n.of(context).createGroup, ), ], ); @@ -170,32 +170,32 @@ class _SpaceViewState extends State { final names = await showTextInputDialog( context: context, title: roomType == AddRoomType.subspace - ? L10n.of(context)!.createNewSpace - : L10n.of(context)!.createGroup, + ? L10n.of(context).createNewSpace + : L10n.of(context).createGroup, textFields: [ DialogTextField( hintText: roomType == AddRoomType.subspace - ? L10n.of(context)!.spaceName - : L10n.of(context)!.groupName, + ? L10n.of(context).spaceName + : L10n.of(context).groupName, minLines: 1, maxLines: 1, maxLength: 64, validator: (text) { if (text == null || text.isEmpty) { - return L10n.of(context)!.pleaseChoose; + return L10n.of(context).pleaseChoose; } return null; }, ), DialogTextField( - hintText: L10n.of(context)!.chatDescription, + hintText: L10n.of(context).chatDescription, minLines: 4, maxLines: 8, maxLength: 255, ), ], - okLabel: L10n.of(context)!.create, - cancelLabel: L10n.of(context)!.cancel, + okLabel: L10n.of(context).create, + cancelLabel: L10n.of(context).cancel, ); if (names == null) return; final client = Matrix.of(context).client; @@ -245,7 +245,7 @@ class _SpaceViewState extends State { final room = Matrix.of(context).client.getRoomById(widget.spaceId); final displayname = - room?.getLocalizedDisplayname() ?? L10n.of(context)!.nothingFound; + room?.getLocalizedDisplayname() ?? L10n.of(context).nothingFound; return Scaffold( appBar: AppBar( leading: Center( @@ -269,7 +269,7 @@ class _SpaceViewState extends State { subtitle: room == null ? null : Text( - L10n.of(context)!.countChatsAndCountParticipants( + L10n.of(context).countChatsAndCountParticipants( room.spaceChildren.length, room.summary.mJoinedMemberCount ?? 1, ), @@ -288,7 +288,7 @@ class _SpaceViewState extends State { children: [ const Icon(Icons.settings_outlined), const SizedBox(width: 12), - Text(L10n.of(context)!.settings), + Text(L10n.of(context).settings), ], ), ), @@ -299,7 +299,7 @@ class _SpaceViewState extends State { children: [ const Icon(Icons.person_add_outlined), const SizedBox(width: 12), - Text(L10n.of(context)!.invite), + Text(L10n.of(context).invite), ], ), ), @@ -310,7 +310,7 @@ class _SpaceViewState extends State { children: [ const Icon(Icons.delete_outlined), const SizedBox(width: 12), - Text(L10n.of(context)!.leave), + Text(L10n.of(context).leave), ], ), ), @@ -368,7 +368,7 @@ class _SpaceViewState extends State { borderRadius: BorderRadius.circular(99), ), contentPadding: EdgeInsets.zero, - hintText: L10n.of(context)!.search, + hintText: L10n.of(context).search, hintStyle: TextStyle( color: theme.colorScheme.onPrimaryContainer, fontWeight: FontWeight.normal, @@ -453,7 +453,7 @@ class _SpaceViewState extends State { child: Icon(Icons.add_outlined), ), title: Text( - L10n.of(context)!.addChatOrSubSpace, + L10n.of(context).addChatOrSubSpace, style: const TextStyle(fontSize: 14), ), ), @@ -461,7 +461,7 @@ class _SpaceViewState extends State { ), ], SearchTitle( - title: L10n.of(context)!.joinedChats, + title: L10n.of(context).joinedChats, icon: const Icon(Icons.chat_outlined), ), ], @@ -486,7 +486,7 @@ class _SpaceViewState extends State { itemBuilder: (context, i) { if (i == 0) { return SearchTitle( - title: L10n.of(context)!.discover, + title: L10n.of(context).discover, icon: const Icon(Icons.explore_outlined), ); } @@ -497,7 +497,7 @@ class _SpaceViewState extends State { padding: const EdgeInsets.all(12.0), child: Center( child: Text( - L10n.of(context)!.noMoreChatsFound, + L10n.of(context).noMoreChatsFound, style: const TextStyle(fontSize: 13), ), ), @@ -516,14 +516,14 @@ class _SpaceViewState extends State { AppConfig.borderRadius, ), ) - : Text(L10n.of(context)!.loadMore), + : Text(L10n.of(context).loadMore), ), ); } final item = _discoveredChildren[i]; final displayname = item.name ?? item.canonicalAlias ?? - L10n.of(context)!.emptyChat; + L10n.of(context).emptyChat; if (!displayname.toLowerCase().contains(filter)) { return const SizedBox.shrink(); } @@ -564,7 +564,7 @@ class _SpaceViewState extends State { ), subtitle: Text( item.topic ?? - L10n.of(context)!.countParticipants( + L10n.of(context).countParticipants( item.numJoinedMembers, ), maxLines: 1, diff --git a/lib/pages/chat_members/chat_members_view.dart b/lib/pages/chat_members/chat_members_view.dart index daa681a52..be53f99c7 100644 --- a/lib/pages/chat_members/chat_members_view.dart +++ b/lib/pages/chat_members/chat_members_view.dart @@ -20,10 +20,10 @@ class ChatMembersView extends StatelessWidget { if (room == null) { return Scaffold( appBar: AppBar( - title: Text(L10n.of(context)!.oopsSomethingWentWrong), + title: Text(L10n.of(context).oopsSomethingWentWrong), ), body: Center( - child: Text(L10n.of(context)!.youAreNoLongerParticipatingInThisChat), + child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat), ), ); } @@ -40,7 +40,7 @@ class ChatMembersView extends StatelessWidget { appBar: AppBar( leading: const Center(child: BackButton()), title: Text( - L10n.of(context)!.countParticipants(roomCount), + L10n.of(context).countParticipants(roomCount), ), actions: [ if (room.canInvite) @@ -68,7 +68,7 @@ class ChatMembersView extends StatelessWidget { OutlinedButton.icon( onPressed: controller.refreshMembers, icon: const Icon(Icons.refresh_outlined), - label: Text(L10n.of(context)!.tryAgain), + label: Text(L10n.of(context).tryAgain), ), ], ), @@ -102,7 +102,7 @@ class ChatMembersView extends StatelessWidget { fontWeight: FontWeight.normal, ), prefixIcon: const Icon(Icons.search_outlined), - hintText: L10n.of(context)!.search, + hintText: L10n.of(context).search, ), ), ) diff --git a/lib/pages/chat_permissions_settings/chat_permissions_settings.dart b/lib/pages/chat_permissions_settings/chat_permissions_settings.dart index 6ea94c105..2211a8fac 100644 --- a/lib/pages/chat_permissions_settings/chat_permissions_settings.dart +++ b/lib/pages/chat_permissions_settings/chat_permissions_settings.dart @@ -31,7 +31,7 @@ class ChatPermissionsSettingsController extends State { final room = Matrix.of(context).client.getRoomById(roomId!)!; if (!room.canSendEvent(EventTypes.RoomPowerLevels)) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.noPermission)), + SnackBar(content: Text(L10n.of(context).noPermission)), ); return; } diff --git a/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart b/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart index 458e1c503..b29fa9eed 100644 --- a/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart +++ b/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart @@ -20,7 +20,7 @@ class ChatPermissionsSettingsView extends StatelessWidget { return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), - title: Text(L10n.of(context)!.chatPermissions), + title: Text(L10n.of(context).chatPermissions), ), body: MaxWidthBody( child: StreamBuilder( @@ -31,7 +31,7 @@ class ChatPermissionsSettingsView extends StatelessWidget { ? null : Matrix.of(context).client.getRoomById(roomId); if (room == null) { - return Center(child: Text(L10n.of(context)!.noRoomsFound)); + return Center(child: Text(L10n.of(context).noRoomsFound)); } final powerLevelsContent = Map.from( room.getState(EventTypes.RoomPowerLevels)?.content ?? {}, @@ -46,13 +46,13 @@ class ChatPermissionsSettingsView extends StatelessWidget { ListTile( leading: const Icon(Icons.info_outlined), subtitle: Text( - L10n.of(context)!.chatPermissionsDescription, + L10n.of(context).chatPermissionsDescription, ), ), Divider(color: theme.dividerColor), ListTile( title: Text( - L10n.of(context)!.chatPermissions, + L10n.of(context).chatPermissions, style: TextStyle( color: theme.colorScheme.primary, fontWeight: FontWeight.bold, @@ -77,7 +77,7 @@ class ChatPermissionsSettingsView extends StatelessWidget { Divider(color: theme.dividerColor), ListTile( title: Text( - L10n.of(context)!.notifications, + L10n.of(context).notifications, style: TextStyle( color: theme.colorScheme.primary, fontWeight: FontWeight.bold, @@ -112,7 +112,7 @@ class ChatPermissionsSettingsView extends StatelessWidget { Divider(color: theme.dividerColor), ListTile( title: Text( - L10n.of(context)!.configureChat, + L10n.of(context).configureChat, style: TextStyle( color: theme.colorScheme.primary, fontWeight: FontWeight.bold, diff --git a/lib/pages/chat_permissions_settings/permission_list_tile.dart b/lib/pages/chat_permissions_settings/permission_list_tile.dart index 0ca516965..bb5e03dcc 100644 --- a/lib/pages/chat_permissions_settings/permission_list_tile.dart +++ b/lib/pages/chat_permissions_settings/permission_list_tile.dart @@ -25,45 +25,45 @@ class PermissionsListTile extends StatelessWidget { if (category == null) { switch (permissionKey) { case 'users_default': - return L10n.of(context)!.defaultPermissionLevel; + return L10n.of(context).defaultPermissionLevel; case 'events_default': - return L10n.of(context)!.sendMessages; + return L10n.of(context).sendMessages; case 'state_default': - return L10n.of(context)!.changeGeneralChatSettings; + return L10n.of(context).changeGeneralChatSettings; case 'ban': - return L10n.of(context)!.banFromChat; + return L10n.of(context).banFromChat; case 'kick': - return L10n.of(context)!.kickFromChat; + return L10n.of(context).kickFromChat; case 'redact': - return L10n.of(context)!.deleteMessage; + return L10n.of(context).deleteMessage; case 'invite': - return L10n.of(context)!.inviteOtherUsers; + return L10n.of(context).inviteOtherUsers; } } else if (category == 'notifications') { switch (permissionKey) { case 'rooms': - return L10n.of(context)!.sendRoomNotifications; + return L10n.of(context).sendRoomNotifications; } } else if (category == 'events') { switch (permissionKey) { case EventTypes.RoomName: - return L10n.of(context)!.changeTheNameOfTheGroup; + return L10n.of(context).changeTheNameOfTheGroup; case EventTypes.RoomTopic: - return L10n.of(context)!.changeTheDescriptionOfTheGroup; + return L10n.of(context).changeTheDescriptionOfTheGroup; case EventTypes.RoomPowerLevels: - return L10n.of(context)!.changeTheChatPermissions; + return L10n.of(context).changeTheChatPermissions; case EventTypes.HistoryVisibility: - return L10n.of(context)!.changeTheVisibilityOfChatHistory; + return L10n.of(context).changeTheVisibilityOfChatHistory; case EventTypes.RoomCanonicalAlias: - return L10n.of(context)!.changeTheCanonicalRoomAlias; + return L10n.of(context).changeTheCanonicalRoomAlias; case EventTypes.RoomAvatar: - return L10n.of(context)!.editRoomAvatar; + return L10n.of(context).editRoomAvatar; case EventTypes.RoomTombstone: - return L10n.of(context)!.replaceRoomWithNewerVersion; + return L10n.of(context).replaceRoomWithNewerVersion; case EventTypes.Encryption: - return L10n.of(context)!.enableEncryption; + return L10n.of(context).enableEncryption; case 'm.room.server_acl': - return L10n.of(context)!.editBlockedServers; + return L10n.of(context).editBlockedServers; } } return permissionKey; @@ -96,13 +96,13 @@ class PermissionsListTile extends StatelessWidget { DropdownMenuItem( value: permission < 50 ? permission : 0, child: Text( - L10n.of(context)!.userLevel(permission < 50 ? permission : 0), + L10n.of(context).userLevel(permission < 50 ? permission : 0), ), ), DropdownMenuItem( value: permission < 100 && permission >= 50 ? permission : 50, child: Text( - L10n.of(context)!.moderatorLevel( + L10n.of(context).moderatorLevel( permission < 100 && permission >= 50 ? permission : 50, ), ), @@ -110,13 +110,13 @@ class PermissionsListTile extends StatelessWidget { DropdownMenuItem( value: permission >= 100 ? permission : 100, child: Text( - L10n.of(context)! + L10n.of(context) .adminLevel(permission >= 100 ? permission : 100), ), ), DropdownMenuItem( value: null, - child: Text(L10n.of(context)!.custom), + child: Text(L10n.of(context).custom), ), ], ), diff --git a/lib/pages/chat_search/chat_search_files_tab.dart b/lib/pages/chat_search/chat_search_files_tab.dart index e981efb19..59a2f26e2 100644 --- a/lib/pages/chat_search/chat_search_files_tab.dart +++ b/lib/pages/chat_search/chat_search_files_tab.dart @@ -37,9 +37,9 @@ class ChatSearchFilesTab extends StatelessWidget { const CircularProgressIndicator.adaptive(strokeWidth: 2), const SizedBox(height: 8), Text( - L10n.of(context)!.searchIn( + L10n.of(context).searchIn( room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), ), ), ), @@ -53,7 +53,7 @@ class ChatSearchFilesTab extends StatelessWidget { children: [ const Icon(Icons.file_present_outlined, size: 64), const SizedBox(height: 8), - Text(L10n.of(context)!.nothingFound), + Text(L10n.of(context).nothingFound), ], ); } @@ -93,7 +93,7 @@ class ChatSearchFilesTab extends StatelessWidget { icon: const Icon( Icons.arrow_downward_outlined, ), - label: Text(L10n.of(context)!.searchMore), + label: Text(L10n.of(context).searchMore), ), ), ); @@ -101,7 +101,7 @@ class ChatSearchFilesTab extends StatelessWidget { final event = events[i]; final filename = event.content.tryGet('filename') ?? event.content.tryGet('body') ?? - L10n.of(context)!.unknownEvent('File'); + L10n.of(context).unknownEvent('File'); final filetype = (filename.contains('.') ? filename.split('.').last.toUpperCase() : event.content diff --git a/lib/pages/chat_search/chat_search_images_tab.dart b/lib/pages/chat_search/chat_search_images_tab.dart index 90cdc839b..a930ed02d 100644 --- a/lib/pages/chat_search/chat_search_images_tab.dart +++ b/lib/pages/chat_search/chat_search_images_tab.dart @@ -37,9 +37,9 @@ class ChatSearchImagesTab extends StatelessWidget { const CircularProgressIndicator.adaptive(strokeWidth: 2), const SizedBox(height: 8), Text( - L10n.of(context)!.searchIn( + L10n.of(context).searchIn( room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), ), ), ), @@ -52,7 +52,7 @@ class ChatSearchImagesTab extends StatelessWidget { children: [ const Icon(Icons.photo_outlined, size: 64), const SizedBox(height: 8), - Text(L10n.of(context)!.nothingFound), + Text(L10n.of(context).nothingFound), ], ); } @@ -102,7 +102,7 @@ class ChatSearchImagesTab extends StatelessWidget { icon: const Icon( Icons.arrow_downward_outlined, ), - label: Text(L10n.of(context)!.searchMore), + label: Text(L10n.of(context).searchMore), ), ), ); diff --git a/lib/pages/chat_search/chat_search_message_tab.dart b/lib/pages/chat_search/chat_search_message_tab.dart index a2d34fa9e..7c7024e8d 100644 --- a/lib/pages/chat_search/chat_search_message_tab.dart +++ b/lib/pages/chat_search/chat_search_message_tab.dart @@ -41,9 +41,9 @@ class ChatSearchMessageTab extends StatelessWidget { const Icon(Icons.search_outlined, size: 64), const SizedBox(height: 8), Text( - L10n.of(context)!.searchIn( + L10n.of(context).searchIn( room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), ), ), ), @@ -90,7 +90,7 @@ class ChatSearchMessageTab extends StatelessWidget { icon: const Icon( Icons.arrow_downward_outlined, ), - label: Text(L10n.of(context)!.searchMore), + label: Text(L10n.of(context).searchMore), ), ), ); @@ -98,7 +98,7 @@ class ChatSearchMessageTab extends StatelessWidget { final event = events[i]; final sender = event.senderFromMemoryOrFallback; final displayname = sender.calcDisplayname( - i18n: MatrixLocals(L10n.of(context)!), + i18n: MatrixLocals(L10n.of(context)), ); return _MessageSearchResultListTile( sender: sender, @@ -164,7 +164,7 @@ class _MessageSearchResultListTile extends StatelessWidget { plaintextBody: true, removeMarkdown: true, MatrixLocals( - L10n.of(context)!, + L10n.of(context), ), ) .trim(), diff --git a/lib/pages/chat_search/chat_search_view.dart b/lib/pages/chat_search/chat_search_view.dart index e29fa1d01..1f458ca98 100644 --- a/lib/pages/chat_search/chat_search_view.dart +++ b/lib/pages/chat_search/chat_search_view.dart @@ -20,12 +20,11 @@ class ChatSearchView extends StatelessWidget { final room = controller.room; if (room == null) { return Scaffold( - appBar: AppBar(title: Text(L10n.of(context)!.oopsSomethingWentWrong)), + appBar: AppBar(title: Text(L10n.of(context).oopsSomethingWentWrong)), body: Center( child: Padding( padding: const EdgeInsets.all(16), - child: - Text(L10n.of(context)!.youAreNoLongerParticipatingInThisChat), + child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat), ), ), ); @@ -38,8 +37,8 @@ class ChatSearchView extends StatelessWidget { leading: const Center(child: BackButton()), titleSpacing: 0, title: Text( - L10n.of(context)!.searchIn( - room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), + L10n.of(context).searchIn( + room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), ), ), ), @@ -59,7 +58,7 @@ class ChatSearchView extends StatelessWidget { autofocus: true, enabled: controller.tabController.index == 0, decoration: InputDecoration( - hintText: L10n.of(context)!.search, + hintText: L10n.of(context).search, suffixIcon: const Icon(Icons.search_outlined), filled: true, fillColor: theme.colorScheme.secondaryContainer, @@ -77,9 +76,9 @@ class ChatSearchView extends StatelessWidget { TabBar( controller: controller.tabController, tabs: [ - Tab(child: Text(L10n.of(context)!.messages)), - Tab(child: Text(L10n.of(context)!.gallery)), - Tab(child: Text(L10n.of(context)!.files)), + Tab(child: Text(L10n.of(context).messages)), + Tab(child: Text(L10n.of(context).gallery)), + Tab(child: Text(L10n.of(context).files)), ], ), Expanded( diff --git a/lib/pages/device_settings/device_settings.dart b/lib/pages/device_settings/device_settings.dart index d2eb96a42..daff3ad28 100644 --- a/lib/pages/device_settings/device_settings.dart +++ b/lib/pages/device_settings/device_settings.dart @@ -35,10 +35,10 @@ class DevicesSettingsController extends State { void removeDevicesAction(List devices) async { if (await showOkCancelAlertDialog( context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.cancel, - message: L10n.of(context)!.removeDevicesDescription, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).yes, + cancelLabel: L10n.of(context).cancel, + message: L10n.of(context).removeDevicesDescription, ) == OkCancelResult.cancel) return; final matrix = Matrix.of(context); @@ -70,9 +70,9 @@ class DevicesSettingsController extends State { void renameDeviceAction(Device device) async { final displayName = await showTextInputDialog( context: context, - title: L10n.of(context)!.changeDeviceName, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).changeDeviceName, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, textFields: [ DialogTextField( hintText: device.displayName, @@ -94,10 +94,10 @@ class DevicesSettingsController extends State { void verifyDeviceAction(Device device) async { final consent = await showOkCancelAlertDialog( context: context, - title: L10n.of(context)!.verifyOtherDevice, - message: L10n.of(context)!.verifyOtherDeviceDescription, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).verifyOtherDevice, + message: L10n.of(context).verifyOtherDeviceDescription, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, fullyCapitalizedForMaterial: false, ); if (consent != OkCancelResult.ok) return; diff --git a/lib/pages/device_settings/device_settings_view.dart b/lib/pages/device_settings/device_settings_view.dart index 20fb9e800..32a48b561 100644 --- a/lib/pages/device_settings/device_settings_view.dart +++ b/lib/pages/device_settings/device_settings_view.dart @@ -16,7 +16,7 @@ class DevicesSettingsView extends StatelessWidget { return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), - title: Text(L10n.of(context)!.devices), + title: Text(L10n.of(context).devices), ), body: MaxWidthBody( child: FutureBuilder( @@ -56,7 +56,7 @@ class DevicesSettingsView extends StatelessWidget { ), alignment: Alignment.centerLeft, child: Text( - L10n.of(context)!.thisDevice, + L10n.of(context).thisDevice, style: TextStyle( fontWeight: FontWeight.bold, color: theme.colorScheme.primary, @@ -84,7 +84,7 @@ class DevicesSettingsView extends StatelessWidget { child: TextButton.icon( label: Text( controller.errorDeletingDevices ?? - L10n.of(context)!.removeAllOtherDevices, + L10n.of(context).removeAllOtherDevices, ), style: TextButton.styleFrom( foregroundColor: @@ -109,7 +109,7 @@ class DevicesSettingsView extends StatelessWidget { Center( child: Padding( padding: const EdgeInsets.all(16.0), - child: Text(L10n.of(context)!.noOtherDevicesFound), + child: Text(L10n.of(context).noOtherDevicesFound), ), ), ], diff --git a/lib/pages/device_settings/user_device_list_item.dart b/lib/pages/device_settings/user_device_list_item.dart index cf3acd6c9..96be6c787 100644 --- a/lib/pages/device_settings/user_device_list_item.dart +++ b/lib/pages/device_settings/user_device_list_item.dart @@ -55,30 +55,30 @@ class UserDeviceListItem extends StatelessWidget { actions: [ SheetAction( key: UserDeviceListItemAction.rename, - label: L10n.of(context)!.changeDeviceName, + label: L10n.of(context).changeDeviceName, ), if (!isOwnDevice && keys != null) ...{ SheetAction( key: UserDeviceListItemAction.verify, - label: L10n.of(context)!.verifyStart, + label: L10n.of(context).verifyStart, ), if (!keys.blocked) SheetAction( key: UserDeviceListItemAction.block, - label: L10n.of(context)!.blockDevice, + label: L10n.of(context).blockDevice, isDestructiveAction: true, ), if (keys.blocked) SheetAction( key: UserDeviceListItemAction.unblock, - label: L10n.of(context)!.unblockDevice, + label: L10n.of(context).unblockDevice, isDestructiveAction: true, ), }, if (!isOwnDevice) SheetAction( key: UserDeviceListItemAction.remove, - label: L10n.of(context)!.delete, + label: L10n.of(context).delete, isDestructiveAction: true, ), ], @@ -119,7 +119,7 @@ class UserDeviceListItem extends StatelessWidget { overflow: TextOverflow.ellipsis, ), subtitle: Text( - L10n.of(context)!.lastActiveAgo( + L10n.of(context).lastActiveAgo( DateTime.fromMillisecondsSinceEpoch(userDevice.lastSeenTs ?? 0) .localizedTimeShort(context), ), @@ -129,10 +129,10 @@ class UserDeviceListItem extends StatelessWidget { ? null : Text( keys.blocked - ? L10n.of(context)!.blocked + ? L10n.of(context).blocked : keys.verified - ? L10n.of(context)!.verified - : L10n.of(context)!.unverified, + ? L10n.of(context).verified + : L10n.of(context).unverified, style: TextStyle( color: keys.blocked ? Colors.red diff --git a/lib/pages/dialer/dialer.dart b/lib/pages/dialer/dialer.dart index 84e6fe923..edcc6868c 100644 --- a/lib/pages/dialer/dialer.dart +++ b/lib/pages/dialer/dialer.dart @@ -134,7 +134,7 @@ class MyCallingPage extends State { Room? get room => call.room; String get displayName => call.room.getLocalizedDisplayname( - MatrixLocals(L10n.of(widget.context)!), + MatrixLocals(L10n.of(widget.context)), ); String get callId => widget.callId; @@ -296,14 +296,14 @@ class MyCallingPage extends State { channelId: 'notification_channel_id', channelName: 'Foreground Notification', channelDescription: - L10n.of(widget.context)!.foregroundServiceRunning, + L10n.of(widget.context).foregroundServiceRunning, ), iosNotificationOptions: const IOSNotificationOptions(), foregroundTaskOptions: const ForegroundTaskOptions(), ); FlutterForegroundTask.startService( - notificationTitle: L10n.of(widget.context)!.screenSharingTitle, - notificationText: L10n.of(widget.context)!.screenSharingDetail, + notificationTitle: L10n.of(widget.context).screenSharingTitle, + notificationText: L10n.of(widget.context).screenSharingDetail, ); } else { FlutterForegroundTask.stopService(); @@ -457,7 +457,7 @@ class MyCallingPage extends State { var title = ''; if (call.localHold) { title = '${call.room.getLocalizedDisplayname( - MatrixLocals(L10n.of(widget.context)!), + MatrixLocals(L10n.of(widget.context)), )} held the call.'; } else if (call.remoteOnHold) { title = 'You held the call.'; diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index d5fe0700a..81b33aa00 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -49,8 +49,8 @@ class HomeserverPickerController extends State { (e, s) async { await showOkAlertDialog( context: context, - title: L10n.of(context)!.indexedDbErrorTitle, - message: L10n.of(context)!.indexedDbErrorLong, + title: L10n.of(context).indexedDbErrorTitle, + message: L10n.of(context).indexedDbErrorLong, ); _checkTorBrowser(); }, diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index ff89bf4e1..71ca58fd3 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -24,7 +24,7 @@ class HomeserverPickerView extends StatelessWidget { appBar: controller.widget.addMultiAccount ? AppBar( centerTitle: true, - title: Text(L10n.of(context)!.addAccount), + title: Text(L10n.of(context).addAccount), ) : null, body: Column( @@ -46,8 +46,8 @@ class HomeserverPickerView extends StatelessWidget { color: theme.colorScheme.surface, child: ListTile( leading: const Icon(Icons.vpn_key), - title: Text(L10n.of(context)!.hydrateTor), - subtitle: Text(L10n.of(context)!.hydrateTorLong), + title: Text(L10n.of(context).hydrateTor), + subtitle: Text(L10n.of(context).hydrateTorLong), trailing: const Icon(Icons.chevron_right_outlined), onTap: controller.restoreBackup, ), @@ -95,16 +95,16 @@ class HomeserverPickerView extends StatelessWidget { borderRadius: BorderRadius.circular(AppConfig.borderRadius), ), hintText: AppConfig.defaultHomeserver, - labelText: L10n.of(context)!.homeserver, + labelText: L10n.of(context).homeserver, errorText: controller.error, suffixIcon: IconButton( onPressed: () { showDialog( context: context, builder: (context) => AlertDialog.adaptive( - title: Text(L10n.of(context)!.whatIsAHomeserver), + title: Text(L10n.of(context).whatIsAHomeserver), content: Linkify( - text: L10n.of(context)!.homeserverDescription, + text: L10n.of(context).homeserverDescription, ), actions: [ TextButton( @@ -112,12 +112,12 @@ class HomeserverPickerView extends StatelessWidget { Uri.https('servers.joinmatrix.org'), ), child: Text( - L10n.of(context)!.discoverHomeservers, + L10n.of(context).discoverHomeservers, ), ), TextButton( onPressed: Navigator.of(context).pop, - child: Text(L10n.of(context)!.close), + child: Text(L10n.of(context).close), ), ], ), @@ -144,7 +144,7 @@ class HomeserverPickerView extends StatelessWidget { onPressed: controller.isLoggingIn || controller.isLoading ? null : controller.restoreBackup, - child: Text(L10n.of(context)!.hydrate), + child: Text(L10n.of(context).hydrate), ), if (controller.supportsPasswordLogin && controller.supportsSso) TextButton( @@ -155,7 +155,7 @@ class HomeserverPickerView extends StatelessWidget { onPressed: controller.isLoggingIn || controller.isLoading ? null : controller.login, - child: Text(L10n.of(context)!.loginWithMatrixId), + child: Text(L10n.of(context).loginWithMatrixId), ), const SizedBox(height: 8.0), if (controller.supportsPasswordLogin || controller.supportsSso) @@ -169,7 +169,7 @@ class HomeserverPickerView extends StatelessWidget { : controller.supportsSso ? controller.ssoLoginAction : controller.login, - child: Text(L10n.of(context)!.next), + child: Text(L10n.of(context).next), ), ], ), diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index fd04183e4..4c29cbc87 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -25,7 +25,7 @@ class ImageViewerView extends StatelessWidget { icon: const Icon(Icons.close), onPressed: Navigator.of(context).pop, color: Colors.white, - tooltip: L10n.of(context)!.close, + tooltip: L10n.of(context).close, ), backgroundColor: Colors.transparent, actions: [ @@ -36,7 +36,7 @@ class ImageViewerView extends StatelessWidget { icon: const Icon(Icons.reply_outlined), onPressed: controller.forwardAction, color: Colors.white, - tooltip: L10n.of(context)!.share, + tooltip: L10n.of(context).share, ), const SizedBox(width: 8), IconButton( @@ -46,7 +46,7 @@ class ImageViewerView extends StatelessWidget { icon: const Icon(Icons.download_outlined), onPressed: () => controller.saveFileAction(context), color: Colors.white, - tooltip: L10n.of(context)!.downloadFile, + tooltip: L10n.of(context).downloadFile, ), const SizedBox(width: 8), if (PlatformInfos.isMobile) @@ -59,7 +59,7 @@ class ImageViewerView extends StatelessWidget { backgroundColor: Colors.black.withOpacity(0.5), ), onPressed: () => controller.shareFileAction(context), - tooltip: L10n.of(context)!.share, + tooltip: L10n.of(context).share, color: Colors.white, icon: Icon(Icons.adaptive.share_outlined), ), diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 15078735d..102234333 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.dart @@ -57,15 +57,15 @@ class InvitationSelectionController extends State { if (OkCancelResult.ok != await showOkCancelAlertDialog( context: context, - title: L10n.of(context)!.inviteContact, - message: L10n.of(context)!.inviteContactToGroupQuestion( + title: L10n.of(context).inviteContact, + message: L10n.of(context).inviteContactToGroupQuestion( displayname, room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), ), ), - okLabel: L10n.of(context)!.invite, - cancelLabel: L10n.of(context)!.cancel, + okLabel: L10n.of(context).invite, + cancelLabel: L10n.of(context).cancel, )) { return; } @@ -76,7 +76,7 @@ class InvitationSelectionController extends State { if (success.error == null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(L10n.of(context)!.contactHasBeenInvitedToTheGroup), + content: Text(L10n.of(context).contactHasBeenInvitedToTheGroup), ), ); } diff --git a/lib/pages/invitation_selection/invitation_selection_view.dart b/lib/pages/invitation_selection/invitation_selection_view.dart index 6ea03d8ed..84fe2ff7b 100644 --- a/lib/pages/invitation_selection/invitation_selection_view.dart +++ b/lib/pages/invitation_selection/invitation_selection_view.dart @@ -20,21 +20,21 @@ class InvitationSelectionView extends StatelessWidget { if (room == null) { return Scaffold( appBar: AppBar( - title: Text(L10n.of(context)!.oopsSomethingWentWrong), + title: Text(L10n.of(context).oopsSomethingWentWrong), ), body: Center( - child: Text(L10n.of(context)!.youAreNoLongerParticipatingInThisChat), + child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat), ), ); } - final groupName = room.name.isEmpty ? L10n.of(context)!.group : room.name; + final groupName = room.name.isEmpty ? L10n.of(context).group : room.name; final theme = Theme.of(context); return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), titleSpacing: 0, - title: Text(L10n.of(context)!.inviteContact), + title: Text(L10n.of(context).inviteContact), ), body: MaxWidthBody( innerPadding: const EdgeInsets.symmetric(vertical: 8), @@ -55,7 +55,7 @@ class InvitationSelectionView extends StatelessWidget { color: theme.colorScheme.onPrimaryContainer, fontWeight: FontWeight.normal, ), - hintText: L10n.of(context)!.inviteContactToGroup(groupName), + hintText: L10n.of(context).inviteContactToGroup(groupName), prefixIcon: controller.loading ? const Padding( padding: EdgeInsets.symmetric( @@ -91,7 +91,7 @@ class InvitationSelectionView extends StatelessWidget { displayname: controller .foundProfiles[i].displayName ?? controller.foundProfiles[i].userId.localpart ?? - L10n.of(context)!.user, + L10n.of(context).user, userId: controller.foundProfiles[i].userId, isMember: participants .contains(controller.foundProfiles[i].userId), @@ -100,7 +100,7 @@ class InvitationSelectionView extends StatelessWidget { controller.foundProfiles[i].userId, controller.foundProfiles[i].displayName ?? controller.foundProfiles[i].userId.localpart ?? - L10n.of(context)!.user, + L10n.of(context).user, ), ), ) @@ -124,7 +124,7 @@ class InvitationSelectionView extends StatelessWidget { avatarUrl: contacts[i].avatarUrl, displayname: contacts[i].displayName ?? contacts[i].id.localpart ?? - L10n.of(context)!.user, + L10n.of(context).user, userId: contacts[i].id, isMember: participants.contains(contacts[i].id), onTap: () => controller.inviteAction( @@ -132,7 +132,7 @@ class InvitationSelectionView extends StatelessWidget { contacts[i].id, contacts[i].displayName ?? contacts[i].id.localpart ?? - L10n.of(context)!.user, + L10n.of(context).user, ), ), ); @@ -189,7 +189,7 @@ class _InviteContactListTile extends StatelessWidget { ), onTap: isMember ? null : onTap, trailing: isMember - ? Text(L10n.of(context)!.participant) + ? Text(L10n.of(context).participant) : const Icon(Icons.person_add_outlined), ), ); diff --git a/lib/pages/key_verification/key_verification_dialog.dart b/lib/pages/key_verification/key_verification_dialog.dart index b4e5a91ae..0bbee5ed1 100644 --- a/lib/pages/key_verification/key_verification_dialog.dart +++ b/lib/pages/key_verification/key_verification_dialog.dart @@ -87,7 +87,7 @@ class KeyVerificationPageState extends State { await showOkAlertDialog( useRootNavigator: false, context: context, - message: L10n.of(context)!.incorrectPassphraseOrKey, + message: L10n.of(context).incorrectPassphraseOrKey, ); } } @@ -106,7 +106,7 @@ class KeyVerificationPageState extends State { } final displayName = user?.calcDisplayname() ?? widget.request.userId.localpart!; - var title = Text(L10n.of(context)!.verifyTitle); + var title = Text(L10n.of(context).verifyTitle); Widget body; final buttons = []; @@ -124,7 +124,7 @@ class KeyVerificationPageState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - L10n.of(context)!.askSSSSSign, + L10n.of(context).askSSSSSign, style: const TextStyle(fontSize: 20), ), Container(height: 10), @@ -140,7 +140,7 @@ class KeyVerificationPageState extends State { maxLines: 1, obscureText: true, decoration: InputDecoration( - hintText: L10n.of(context)!.passphraseOrKey, + hintText: L10n.of(context).passphraseOrKey, prefixStyle: TextStyle(color: theme.colorScheme.primary), suffixStyle: TextStyle(color: theme.colorScheme.primary), border: const OutlineInputBorder(), @@ -152,7 +152,7 @@ class KeyVerificationPageState extends State { buttons.add( TextButton( child: Text( - L10n.of(context)!.submit, + L10n.of(context).submit, ), onPressed: () => checkInput(textEditingController.text), ), @@ -160,14 +160,14 @@ class KeyVerificationPageState extends State { buttons.add( TextButton( child: Text( - L10n.of(context)!.skip, + L10n.of(context).skip, ), onPressed: () => widget.request.openSSSS(skip: true), ), ); break; case KeyVerificationState.askAccept: - title = Text(L10n.of(context)!.newVerificationRequest); + title = Text(L10n.of(context).newVerificationRequest); body = Column( mainAxisSize: MainAxisSize.min, children: [ @@ -179,7 +179,7 @@ class KeyVerificationPageState extends State { ), const SizedBox(height: 16), Text( - L10n.of(context)!.askVerificationRequest(displayName), + L10n.of(context).askVerificationRequest(displayName), ), ], ); @@ -187,7 +187,7 @@ class KeyVerificationPageState extends State { TextButton.icon( icon: const Icon(Icons.close), style: TextButton.styleFrom(foregroundColor: Colors.red), - label: Text(L10n.of(context)!.reject), + label: Text(L10n.of(context).reject), onPressed: () => widget.request .rejectVerification() .then((_) => Navigator.of(context, rootNavigator: false).pop()), @@ -196,7 +196,7 @@ class KeyVerificationPageState extends State { buttons.add( TextButton.icon( icon: const Icon(Icons.check), - label: Text(L10n.of(context)!.accept), + label: Text(L10n.of(context).accept), onPressed: () => widget.request.acceptVerification(), ), ); @@ -222,7 +222,7 @@ class KeyVerificationPageState extends State { ), const SizedBox(height: 16), Text( - L10n.of(context)!.waitingPartnerAcceptRequest, + L10n.of(context).waitingPartnerAcceptRequest, textAlign: TextAlign.center, ), ], @@ -231,7 +231,7 @@ class KeyVerificationPageState extends State { buttons.add( TextButton.icon( icon: const Icon(Icons.close), - label: Text(L10n.of(context)!.cancel), + label: Text(L10n.of(context).cancel), onPressed: () => widget.request.cancel(), ), ); @@ -244,7 +244,7 @@ class KeyVerificationPageState extends State { if (widget.request.sasTypes.contains('emoji')) { title = Text( - L10n.of(context)!.compareEmojiMatch, + L10n.of(context).compareEmojiMatch, maxLines: 1, style: const TextStyle(fontSize: 16), ); @@ -254,7 +254,7 @@ class KeyVerificationPageState extends State { .toList(), ); } else { - title = Text(L10n.of(context)!.compareNumbersMatch); + title = Text(L10n.of(context).compareNumbersMatch); final numbers = widget.request.sasNumbers; final numbstr = '${numbers[0]}-${numbers[1]}-${numbers[2]}'; compareWidget = @@ -275,22 +275,22 @@ class KeyVerificationPageState extends State { style: TextButton.styleFrom( foregroundColor: Colors.red, ), - label: Text(L10n.of(context)!.theyDontMatch), + label: Text(L10n.of(context).theyDontMatch), onPressed: () => widget.request.rejectSas(), ), ); buttons.add( TextButton.icon( icon: const Icon(Icons.check_outlined), - label: Text(L10n.of(context)!.theyMatch), + label: Text(L10n.of(context).theyMatch), onPressed: () => widget.request.acceptSas(), ), ); break; case KeyVerificationState.waitingSas: final acceptText = widget.request.sasTypes.contains('emoji') - ? L10n.of(context)!.waitingPartnerEmoji - : L10n.of(context)!.waitingPartnerNumbers; + ? L10n.of(context).waitingPartnerEmoji + : L10n.of(context).waitingPartnerNumbers; body = Column( mainAxisSize: MainAxisSize.min, children: [ @@ -314,7 +314,7 @@ class KeyVerificationPageState extends State { ), const SizedBox(height: 10), Text( - L10n.of(context)!.verifySuccess, + L10n.of(context).verifySuccess, textAlign: TextAlign.center, ), ], @@ -322,7 +322,7 @@ class KeyVerificationPageState extends State { buttons.add( TextButton( child: Text( - L10n.of(context)!.close, + L10n.of(context).close, ), onPressed: () => Navigator.of(context, rootNavigator: false).pop(), ), @@ -345,7 +345,7 @@ class KeyVerificationPageState extends State { buttons.add( TextButton( child: Text( - L10n.of(context)!.close, + L10n.of(context).close, ), onPressed: () => Navigator.of(context, rootNavigator: false).pop(), ), diff --git a/lib/pages/login/login.dart b/lib/pages/login/login.dart index c62e2100b..d47d2cc09 100644 --- a/lib/pages/login/login.dart +++ b/lib/pages/login/login.dart @@ -33,12 +33,12 @@ class LoginController extends State { void login() async { final matrix = Matrix.of(context); if (usernameController.text.isEmpty) { - setState(() => usernameError = L10n.of(context)!.pleaseEnterYourUsername); + setState(() => usernameError = L10n.of(context).pleaseEnterYourUsername); } else { setState(() => usernameError = null); } if (passwordController.text.isEmpty) { - setState(() => passwordError = L10n.of(context)!.pleaseEnterYourPassword); + setState(() => passwordError = L10n.of(context).pleaseEnterYourPassword); } else { setState(() => passwordError = null); } @@ -128,10 +128,9 @@ class LoginController extends State { final dialogResult = await showOkCancelAlertDialog( context: context, useRootNavigator: false, - message: - L10n.of(context)!.noMatrixServer(newDomain, oldHomeserver!), - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + message: L10n.of(context).noMatrixServer(newDomain, oldHomeserver!), + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, ); if (dialogResult == OkCancelResult.ok) { if (mounted) setState(() => usernameError = null); @@ -159,16 +158,16 @@ class LoginController extends State { final input = await showTextInputDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.passwordForgotten, - message: L10n.of(context)!.enterAnEmailAddress, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).passwordForgotten, + message: L10n.of(context).enterAnEmailAddress, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, fullyCapitalizedForMaterial: false, textFields: [ DialogTextField( initialText: usernameController.text.isEmail ? usernameController.text : '', - hintText: L10n.of(context)!.enterAnEmailAddress, + hintText: L10n.of(context).enterAnEmailAddress, keyboardType: TextInputType.emailAddress, ), ], @@ -188,10 +187,10 @@ class LoginController extends State { final password = await showTextInputDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.passwordForgotten, - message: L10n.of(context)!.chooseAStrongPassword, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).passwordForgotten, + message: L10n.of(context).chooseAStrongPassword, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, fullyCapitalizedForMaterial: false, textFields: [ const DialogTextField( @@ -206,9 +205,9 @@ class LoginController extends State { final ok = await showOkAlertDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.weSentYouAnEmail, - message: L10n.of(context)!.pleaseClickOnLink, - okLabel: L10n.of(context)!.iHaveClickedOnLink, + title: L10n.of(context).weSentYouAnEmail, + message: L10n.of(context).pleaseClickOnLink, + okLabel: L10n.of(context).iHaveClickedOnLink, fullyCapitalizedForMaterial: false, ); if (ok != OkCancelResult.ok) return; @@ -233,7 +232,7 @@ class LoginController extends State { ); if (success.error == null) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.passwordHasBeenChanged)), + SnackBar(content: Text(L10n.of(context).passwordHasBeenChanged)), ); usernameController.text = input.single; passwordController.text = password.single; diff --git a/lib/pages/login/login_view.dart b/lib/pages/login/login_view.dart index a8ec3f502..7a7dbc953 100644 --- a/lib/pages/login/login_view.dart +++ b/lib/pages/login/login_view.dart @@ -20,7 +20,7 @@ class LoginView extends StatelessWidget { .homeserver .toString() .replaceFirst('https://', ''); - final title = L10n.of(context)!.logInTo(homeserver); + final title = L10n.of(context).logInTo(homeserver); final titleParts = title.split(homeserver); return LoginScaffold( @@ -68,7 +68,7 @@ class LoginView extends StatelessWidget { errorText: controller.usernameError, errorStyle: const TextStyle(color: Colors.orange), hintText: '@username:localpart', - labelText: L10n.of(context)!.emailOrUsername, + labelText: L10n.of(context).emailOrUsername, ), ), ), @@ -98,7 +98,7 @@ class LoginView extends StatelessWidget { ), ), hintText: '******', - labelText: L10n.of(context)!.password, + labelText: L10n.of(context).password, ), ), ), @@ -113,7 +113,7 @@ class LoginView extends StatelessWidget { onPressed: controller.loading ? null : controller.login, child: controller.loading ? const LinearProgressIndicator() - : Text(L10n.of(context)!.login), + : Text(L10n.of(context).login), ), ), const SizedBox(height: 16), @@ -126,7 +126,7 @@ class LoginView extends StatelessWidget { style: TextButton.styleFrom( foregroundColor: theme.colorScheme.error, ), - child: Text(L10n.of(context)!.passwordForgotten), + child: Text(L10n.of(context).passwordForgotten), ), ), const SizedBox(height: 16), diff --git a/lib/pages/new_group/new_group_view.dart b/lib/pages/new_group/new_group_view.dart index 54c9a1286..932fd2958 100644 --- a/lib/pages/new_group/new_group_view.dart +++ b/lib/pages/new_group/new_group_view.dart @@ -26,7 +26,7 @@ class NewGroupView extends StatelessWidget { onPressed: controller.loading ? null : Navigator.of(context).pop, ), ), - title: Text(L10n.of(context)!.createGroup), + title: Text(L10n.of(context).createGroup), ), body: MaxWidthBody( child: Column( @@ -61,7 +61,7 @@ class NewGroupView extends StatelessWidget { readOnly: controller.loading, decoration: InputDecoration( prefixIcon: const Icon(Icons.people_outlined), - labelText: L10n.of(context)!.groupName, + labelText: L10n.of(context).groupName, ), ), ), @@ -69,7 +69,7 @@ class NewGroupView extends StatelessWidget { SwitchListTile.adaptive( contentPadding: const EdgeInsets.symmetric(horizontal: 32), secondary: const Icon(Icons.public_outlined), - title: Text(L10n.of(context)!.groupIsPublic), + title: Text(L10n.of(context).groupIsPublic), value: controller.publicGroup, onChanged: controller.loading ? null : controller.setPublicGroup, ), @@ -80,7 +80,7 @@ class NewGroupView extends StatelessWidget { contentPadding: const EdgeInsets.symmetric(horizontal: 32), secondary: const Icon(Icons.search_outlined), - title: Text(L10n.of(context)!.groupCanBeFoundViaSearch), + title: Text(L10n.of(context).groupCanBeFoundViaSearch), value: controller.groupCanBeFound, onChanged: controller.loading ? null @@ -95,7 +95,7 @@ class NewGroupView extends StatelessWidget { color: theme.colorScheme.onSurface, ), title: Text( - L10n.of(context)!.enableEncryption, + L10n.of(context).enableEncryption, style: TextStyle( color: theme.colorScheme.onSurface, ), @@ -112,7 +112,7 @@ class NewGroupView extends StatelessWidget { controller.loading ? null : controller.submitAction, child: controller.loading ? const LinearProgressIndicator() - : Text(L10n.of(context)!.createGroupAndInviteUsers), + : Text(L10n.of(context).createGroupAndInviteUsers), ), ), ), diff --git a/lib/pages/new_private_chat/new_private_chat.dart b/lib/pages/new_private_chat/new_private_chat.dart index c1d8c649a..7cb177afc 100644 --- a/lib/pages/new_private_chat/new_private_chat.dart +++ b/lib/pages/new_private_chat/new_private_chat.dart @@ -74,7 +74,7 @@ class NewPrivateChatController extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - L10n.of(context)!.unsupportedAndroidVersionLong, + L10n.of(context).unsupportedAndroidVersionLong, ), ), ); @@ -94,7 +94,7 @@ class NewPrivateChatController extends State { ClipboardData(text: Matrix.of(context).client.userID!), ); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.copiedToClipboard)), + SnackBar(content: Text(L10n.of(context).copiedToClipboard)), ); } diff --git a/lib/pages/new_private_chat/new_private_chat_view.dart b/lib/pages/new_private_chat/new_private_chat_view.dart index f46e8a4c8..db18c61de 100644 --- a/lib/pages/new_private_chat/new_private_chat_view.dart +++ b/lib/pages/new_private_chat/new_private_chat_view.dart @@ -29,13 +29,13 @@ class NewPrivateChatView extends StatelessWidget { appBar: AppBar( scrolledUnderElevation: 0, leading: const Center(child: BackButton()), - title: Text(L10n.of(context)!.newChat), + title: Text(L10n.of(context).newChat), backgroundColor: theme.scaffoldBackgroundColor, actions: [ TextButton( onPressed: UrlLauncher(context, AppConfig.startChatTutorial).launchUrl, - child: Text(L10n.of(context)!.help), + child: Text(L10n.of(context).help), ), ], ), @@ -53,7 +53,7 @@ class NewPrivateChatView extends StatelessWidget { controller: controller.controller, onChanged: controller.searchUsers, decoration: InputDecoration( - hintText: L10n.of(context)!.searchForUsers, + hintText: L10n.of(context).searchForUsers, filled: true, fillColor: theme.colorScheme.secondaryContainer, border: OutlineInputBorder( @@ -110,7 +110,7 @@ class NewPrivateChatView extends StatelessWidget { TextSpan( children: [ TextSpan( - text: L10n.of(context)!.yourGlobalUserIdIs, + text: L10n.of(context).yourGlobalUserIdIs, ), TextSpan( text: Matrix.of(context).client.userID, @@ -133,7 +133,7 @@ class NewPrivateChatView extends StatelessWidget { foregroundColor: theme.colorScheme.onSecondaryContainer, child: Icon(Icons.adaptive.share_outlined), ), - title: Text(L10n.of(context)!.shareInviteLink), + title: Text(L10n.of(context).shareInviteLink), onTap: controller.inviteAction, ), ListTile( @@ -142,7 +142,7 @@ class NewPrivateChatView extends StatelessWidget { foregroundColor: theme.colorScheme.onTertiaryContainer, child: const Icon(Icons.group_add_outlined), ), - title: Text(L10n.of(context)!.createGroup), + title: Text(L10n.of(context).createGroup), onTap: () => context.go('/rooms/newgroup'), ), if (PlatformInfos.isMobile) @@ -152,7 +152,7 @@ class NewPrivateChatView extends StatelessWidget { foregroundColor: theme.colorScheme.onPrimaryContainer, child: const Icon(Icons.qr_code_scanner_outlined), ), - title: Text(L10n.of(context)!.scanQrCode), + title: Text(L10n.of(context).scanQrCode), onTap: controller.openScannerAction, ), Center( @@ -207,7 +207,7 @@ class NewPrivateChatView extends StatelessWidget { OutlinedButton.icon( onPressed: controller.searchUsers, icon: const Icon(Icons.refresh_outlined), - label: Text(L10n.of(context)!.tryAgain), + label: Text(L10n.of(context).tryAgain), ), ], ); @@ -225,7 +225,7 @@ class NewPrivateChatView extends StatelessWidget { Padding( padding: const EdgeInsets.all(16.0), child: Text( - L10n.of(context)!.noUsersFoundWithQuery( + L10n.of(context).noUsersFoundWithQuery( controller.controller.text, ), style: TextStyle( diff --git a/lib/pages/new_private_chat/qr_scanner_modal.dart b/lib/pages/new_private_chat/qr_scanner_modal.dart index fbc1d5e4b..f0c571ddd 100644 --- a/lib/pages/new_private_chat/qr_scanner_modal.dart +++ b/lib/pages/new_private_chat/qr_scanner_modal.dart @@ -35,9 +35,9 @@ class QrScannerModalState extends State { leading: IconButton( icon: const Icon(Icons.close_outlined), onPressed: Navigator.of(context).pop, - tooltip: L10n.of(context)!.close, + tooltip: L10n.of(context).close, ), - title: Text(L10n.of(context)!.scanQrCode), + title: Text(L10n.of(context).scanQrCode), ), body: Stack( children: [ diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index 1f484cda8..12bb4cc17 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -52,7 +52,7 @@ class NewSpaceController extends State { }); if (nameController.text.isEmpty) { setState(() { - nameError = L10n.of(context)!.pleaseChoose; + nameError = L10n.of(context).pleaseChoose; }); return; } diff --git a/lib/pages/new_space/new_space_view.dart b/lib/pages/new_space/new_space_view.dart index ed60d670d..0f5813952 100644 --- a/lib/pages/new_space/new_space_view.dart +++ b/lib/pages/new_space/new_space_view.dart @@ -16,7 +16,7 @@ class NewSpaceView extends StatelessWidget { final avatar = controller.avatar; return Scaffold( appBar: AppBar( - title: Text(L10n.of(context)!.createNewSpace), + title: Text(L10n.of(context).createNewSpace), ), body: MaxWidthBody( child: Column( @@ -51,7 +51,7 @@ class NewSpaceView extends StatelessWidget { readOnly: controller.loading, decoration: InputDecoration( prefixIcon: const Icon(Icons.people_outlined), - labelText: L10n.of(context)!.spaceName, + labelText: L10n.of(context).spaceName, errorText: controller.nameError, ), ), @@ -59,7 +59,7 @@ class NewSpaceView extends StatelessWidget { const SizedBox(height: 16), SwitchListTile.adaptive( contentPadding: const EdgeInsets.symmetric(horizontal: 32), - title: Text(L10n.of(context)!.spaceIsPublic), + title: Text(L10n.of(context).spaceIsPublic), value: controller.publicGroup, onChanged: controller.setPublicGroup, ), @@ -69,7 +69,7 @@ class NewSpaceView extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 16.0), child: Icon(Icons.info_outlined), ), - subtitle: Text(L10n.of(context)!.newSpaceDescription), + subtitle: Text(L10n.of(context).newSpaceDescription), ), Padding( padding: const EdgeInsets.all(16.0), @@ -80,7 +80,7 @@ class NewSpaceView extends StatelessWidget { controller.loading ? null : controller.submitAction, child: controller.loading ? const LinearProgressIndicator() - : Text(L10n.of(context)!.createNewSpace), + : Text(L10n.of(context).createNewSpace), ), ), ), diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 1d930910a..ca3e29285 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -36,9 +36,9 @@ class SettingsController extends State { final input = await showTextInputDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.editDisplayname, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).editDisplayname, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, textFields: [ DialogTextField( initialText: profile?.displayName ?? @@ -63,11 +63,11 @@ class SettingsController extends State { if (await showOkCancelAlertDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.areYouSureYouWantToLogout, - message: L10n.of(context)!.noBackupWarning, + title: L10n.of(context).areYouSureYouWantToLogout, + message: L10n.of(context).noBackupWarning, isDestructiveAction: noBackup, - okLabel: L10n.of(context)!.logout, - cancelLabel: L10n.of(context)!.cancel, + okLabel: L10n.of(context).logout, + cancelLabel: L10n.of(context).cancel, ) == OkCancelResult.cancel) { return; @@ -85,19 +85,19 @@ class SettingsController extends State { if (PlatformInfos.isMobile) SheetAction( key: AvatarAction.camera, - label: L10n.of(context)!.openCamera, + label: L10n.of(context).openCamera, isDefaultAction: true, icon: Icons.camera_alt_outlined, ), SheetAction( key: AvatarAction.file, - label: L10n.of(context)!.openGallery, + label: L10n.of(context).openGallery, icon: Icons.photo_outlined, ), if (profile?.avatarUrl != null) SheetAction( key: AvatarAction.remove, - label: L10n.of(context)!.removeYourAvatar, + label: L10n.of(context).removeYourAvatar, isDestructiveAction: true, icon: Icons.delete_outlined, ), @@ -106,7 +106,7 @@ class SettingsController extends State { ? actions.single.key : await showModalActionSheet( context: context, - title: L10n.of(context)!.changeYourAvatar, + title: L10n.of(context).changeYourAvatar, actions: actions, ); if (action == null) return; @@ -189,9 +189,9 @@ class SettingsController extends State { if (showChatBackupBanner != true) { showOkAlertDialog( context: context, - title: L10n.of(context)!.chatBackup, - message: L10n.of(context)!.onlineKeyBackupEnabled, - okLabel: L10n.of(context)!.close, + title: L10n.of(context).chatBackup, + message: L10n.of(context).onlineKeyBackupEnabled, + okLabel: L10n.of(context).close, ); return; } diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index 70133433b..f6fbcbed2 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -28,7 +28,7 @@ class SettingsView extends StatelessWidget { onPressed: () => context.go('/rooms'), ), ), - title: Text(L10n.of(context)!.settings), + title: Text(L10n.of(context).settings), ), body: ListTileTheme( iconColor: theme.colorScheme.onSurface, @@ -40,7 +40,7 @@ class SettingsView extends StatelessWidget { builder: (context, snapshot) { final profile = snapshot.data; final mxid = - Matrix.of(context).client.userID ?? L10n.of(context)!.user; + Matrix.of(context).client.userID ?? L10n.of(context).user; final displayname = profile?.displayName ?? mxid.localpart ?? mxid; return Row( @@ -118,7 +118,7 @@ class SettingsView extends StatelessWidget { if (showChatBackupBanner == null) ListTile( leading: const Icon(Icons.backup_outlined), - title: Text(L10n.of(context)!.chatBackup), + title: Text(L10n.of(context).chatBackup), trailing: const CircularProgressIndicator.adaptive(), ) else @@ -126,7 +126,7 @@ class SettingsView extends StatelessWidget { controlAffinity: ListTileControlAffinity.trailing, value: controller.showChatBackupBanner == false, secondary: const Icon(Icons.backup_outlined), - title: Text(L10n.of(context)!.chatBackup), + title: Text(L10n.of(context).chatBackup), onChanged: controller.firstRunBootstrapAction, ), Divider( @@ -134,49 +134,49 @@ class SettingsView extends StatelessWidget { ), ListTile( leading: const Icon(Icons.format_paint_outlined), - title: Text(L10n.of(context)!.changeTheme), + title: Text(L10n.of(context).changeTheme), onTap: () => context.go('/rooms/settings/style'), ), ListTile( leading: const Icon(Icons.notifications_outlined), - title: Text(L10n.of(context)!.notifications), + title: Text(L10n.of(context).notifications), onTap: () => context.go('/rooms/settings/notifications'), ), ListTile( leading: const Icon(Icons.devices_outlined), - title: Text(L10n.of(context)!.devices), + title: Text(L10n.of(context).devices), onTap: () => context.go('/rooms/settings/devices'), ), ListTile( leading: const Icon(Icons.forum_outlined), - title: Text(L10n.of(context)!.chat), + title: Text(L10n.of(context).chat), onTap: () => context.go('/rooms/settings/chat'), ), ListTile( leading: const Icon(Icons.shield_outlined), - title: Text(L10n.of(context)!.security), + title: Text(L10n.of(context).security), onTap: () => context.go('/rooms/settings/security'), ), Divider(color: theme.dividerColor), ListTile( leading: const Icon(Icons.help_outline_outlined), - title: Text(L10n.of(context)!.help), + title: Text(L10n.of(context).help), onTap: () => launchUrlString(AppConfig.supportUrl), ), ListTile( leading: const Icon(Icons.shield_sharp), - title: Text(L10n.of(context)!.privacy), + title: Text(L10n.of(context).privacy), onTap: () => launchUrlString(AppConfig.privacyUrl), ), ListTile( leading: const Icon(Icons.info_outline_rounded), - title: Text(L10n.of(context)!.about), + title: Text(L10n.of(context).about), onTap: () => PlatformInfos.showDialog(context), ), Divider(color: theme.dividerColor), ListTile( leading: const Icon(Icons.logout_outlined), - title: Text(L10n.of(context)!.logout), + title: Text(L10n.of(context).logout), onTap: controller.logoutAction, ), ], diff --git a/lib/pages/settings_3pid/settings_3pid.dart b/lib/pages/settings_3pid/settings_3pid.dart index d46cac2a9..76dbc50bf 100644 --- a/lib/pages/settings_3pid/settings_3pid.dart +++ b/lib/pages/settings_3pid/settings_3pid.dart @@ -22,12 +22,12 @@ class Settings3PidController extends State { final input = await showTextInputDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.enterAnEmailAddress, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).enterAnEmailAddress, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, textFields: [ DialogTextField( - hintText: L10n.of(context)!.enterAnEmailAddress, + hintText: L10n.of(context).enterAnEmailAddress, keyboardType: TextInputType.emailAddress, ), ], @@ -46,9 +46,9 @@ class Settings3PidController extends State { final ok = await showOkAlertDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.weSentYouAnEmail, - message: L10n.of(context)!.pleaseClickOnLink, - okLabel: L10n.of(context)!.iHaveClickedOnLink, + title: L10n.of(context).weSentYouAnEmail, + message: L10n.of(context).pleaseClickOnLink, + okLabel: L10n.of(context).iHaveClickedOnLink, ); if (ok != OkCancelResult.ok) return; final success = await showFutureLoadingDialog( @@ -71,9 +71,9 @@ class Settings3PidController extends State { if (await showOkCancelAlertDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).yes, + cancelLabel: L10n.of(context).cancel, ) != OkCancelResult.ok) { return; diff --git a/lib/pages/settings_3pid/settings_3pid_view.dart b/lib/pages/settings_3pid/settings_3pid_view.dart index af5e1f757..86b67a1b3 100644 --- a/lib/pages/settings_3pid/settings_3pid_view.dart +++ b/lib/pages/settings_3pid/settings_3pid_view.dart @@ -20,12 +20,12 @@ class Settings3PidView extends StatelessWidget { return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), - title: Text(L10n.of(context)!.passwordRecovery), + title: Text(L10n.of(context).passwordRecovery), actions: [ IconButton( icon: const Icon(Icons.add_outlined), onPressed: controller.add3PidAction, - tooltip: L10n.of(context)!.addEmail, + tooltip: L10n.of(context).addEmail, ), ], ), @@ -66,8 +66,8 @@ class Settings3PidView extends StatelessWidget { ), title: Text( identifier.isEmpty - ? L10n.of(context)!.noPasswordRecoveryDescription - : L10n.of(context)! + ? L10n.of(context).noPasswordRecoveryDescription + : L10n.of(context) .withTheseAddressesRecoveryDescription, ), ), @@ -83,7 +83,7 @@ class Settings3PidView extends StatelessWidget { ), title: Text(identifier[i].address), trailing: IconButton( - tooltip: L10n.of(context)!.delete, + tooltip: L10n.of(context).delete, icon: const Icon(Icons.delete_forever_outlined), color: Colors.red, onPressed: () => controller.delete3Pid(identifier[i]), diff --git a/lib/pages/settings_chat/settings_chat_view.dart b/lib/pages/settings_chat/settings_chat_view.dart index e23829d87..7efa65a1e 100644 --- a/lib/pages/settings_chat/settings_chat_view.dart +++ b/lib/pages/settings_chat/settings_chat_view.dart @@ -21,54 +21,54 @@ class SettingsChatView extends StatelessWidget { final theme = Theme.of(context); return Scaffold( - appBar: AppBar(title: Text(L10n.of(context)!.chat)), + appBar: AppBar(title: Text(L10n.of(context).chat)), body: ListTileTheme( iconColor: theme.textTheme.bodyLarge!.color, child: MaxWidthBody( child: Column( children: [ SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.formattedMessages, - subtitle: L10n.of(context)!.formattedMessagesDescription, + title: L10n.of(context).formattedMessages, + subtitle: L10n.of(context).formattedMessagesDescription, onChanged: (b) => AppConfig.renderHtml = b, storeKey: SettingKeys.renderHtml, defaultValue: AppConfig.renderHtml, ), SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.hideMemberChangesInPublicChats, - subtitle: L10n.of(context)!.hideMemberChangesInPublicChatsBody, + title: L10n.of(context).hideMemberChangesInPublicChats, + subtitle: L10n.of(context).hideMemberChangesInPublicChatsBody, onChanged: (b) => AppConfig.hideUnimportantStateEvents = b, storeKey: SettingKeys.hideUnimportantStateEvents, defaultValue: AppConfig.hideUnimportantStateEvents, ), SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.hideRedactedMessages, - subtitle: L10n.of(context)!.hideRedactedMessagesBody, + title: L10n.of(context).hideRedactedMessages, + subtitle: L10n.of(context).hideRedactedMessagesBody, onChanged: (b) => AppConfig.hideRedactedEvents = b, storeKey: SettingKeys.hideRedactedEvents, defaultValue: AppConfig.hideRedactedEvents, ), SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.hideInvalidOrUnknownMessageFormats, + title: L10n.of(context).hideInvalidOrUnknownMessageFormats, onChanged: (b) => AppConfig.hideUnknownEvents = b, storeKey: SettingKeys.hideUnknownEvents, defaultValue: AppConfig.hideUnknownEvents, ), if (PlatformInfos.isMobile) SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.autoplayImages, + title: L10n.of(context).autoplayImages, onChanged: (b) => AppConfig.autoplayImages = b, storeKey: SettingKeys.autoplayImages, defaultValue: AppConfig.autoplayImages, ), SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.sendOnEnter, + title: L10n.of(context).sendOnEnter, onChanged: (b) => AppConfig.sendOnEnter = b, storeKey: SettingKeys.sendOnEnter, defaultValue: AppConfig.sendOnEnter ?? !PlatformInfos.isMobile, ), SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.swipeRightToLeftToReply, + title: L10n.of(context).swipeRightToLeftToReply, onChanged: (b) => AppConfig.swipeRightToLeftToReply = b, storeKey: SettingKeys.swipeRightToLeftToReply, defaultValue: AppConfig.swipeRightToLeftToReply, @@ -76,7 +76,7 @@ class SettingsChatView extends StatelessWidget { Divider(color: theme.dividerColor), ListTile( title: Text( - L10n.of(context)!.customEmojisAndStickers, + L10n.of(context).customEmojisAndStickers, style: TextStyle( color: theme.colorScheme.secondary, fontWeight: FontWeight.bold, @@ -84,8 +84,8 @@ class SettingsChatView extends StatelessWidget { ), ), ListTile( - title: Text(L10n.of(context)!.customEmojisAndStickers), - subtitle: Text(L10n.of(context)!.customEmojisAndStickersBody), + title: Text(L10n.of(context).customEmojisAndStickers), + subtitle: Text(L10n.of(context).customEmojisAndStickersBody), onTap: () => context.go('/rooms/settings/chat/emotes'), trailing: const Padding( padding: EdgeInsets.all(16.0), @@ -95,7 +95,7 @@ class SettingsChatView extends StatelessWidget { Divider(color: theme.dividerColor), ListTile( title: Text( - L10n.of(context)!.calls, + L10n.of(context).calls, style: TextStyle( color: theme.colorScheme.secondary, fontWeight: FontWeight.bold, @@ -103,7 +103,7 @@ class SettingsChatView extends StatelessWidget { ), ), SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.experimentalVideoCalls, + title: L10n.of(context).experimentalVideoCalls, onChanged: (b) { AppConfig.experimentalVoip = b; Matrix.of(context).createVoipPlugin(); @@ -114,7 +114,7 @@ class SettingsChatView extends StatelessWidget { ), if (PlatformInfos.isMobile) ListTile( - title: Text(L10n.of(context)!.callingPermissions), + title: Text(L10n.of(context).callingPermissions), onTap: () => CallKeepManager().checkoutPhoneAccountSetting(context), trailing: const Padding( diff --git a/lib/pages/settings_emotes/import_archive_dialog.dart b/lib/pages/settings_emotes/import_archive_dialog.dart index 5c6361577..02cafad78 100644 --- a/lib/pages/settings_emotes/import_archive_dialog.dart +++ b/lib/pages/settings_emotes/import_archive_dialog.dart @@ -44,7 +44,7 @@ class _ImportEmoteArchiveDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(L10n.of(context)!.importEmojis), + title: Text(L10n.of(context).importEmojis), content: _loading ? Center( child: CircularProgressIndicator( @@ -73,7 +73,7 @@ class _ImportEmoteArchiveDialogState extends State { actions: [ TextButton( onPressed: _loading ? null : Navigator.of(context).pop, - child: Text(L10n.of(context)!.cancel), + child: Text(L10n.of(context).cancel), ), TextButton( onPressed: _loading @@ -81,7 +81,7 @@ class _ImportEmoteArchiveDialogState extends State { : _importMap.isNotEmpty ? _addEmotePack : null, - child: Text(L10n.of(context)!.importNow), + child: Text(L10n.of(context).importNow), ), ], ); @@ -121,10 +121,10 @@ class _ImportEmoteArchiveDialogState extends State { final result = await showOkCancelAlertDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.emoteExists, + title: L10n.of(context).emoteExists, message: imageCode, - cancelLabel: L10n.of(context)!.replace, - okLabel: L10n.of(context)!.skip, + cancelLabel: L10n.of(context).replace, + okLabel: L10n.of(context).skip, ); completer.complete(result); }); @@ -242,7 +242,7 @@ class _EmojiImportPreviewState extends State<_EmojiImportPreview> { IconButton( onPressed: widget.onRemove, icon: const Icon(Icons.remove_circle), - tooltip: L10n.of(context)!.remove, + tooltip: L10n.of(context).remove, ), ValueListenableBuilder( valueListenable: hasErrorNotifier, @@ -278,7 +278,7 @@ class _EmojiImportPreviewState extends State<_EmojiImportPreview> { minLines: 1, maxLines: 1, decoration: InputDecoration( - hintText: L10n.of(context)!.emoteShortcode, + hintText: L10n.of(context).emoteShortcode, prefixText: ': ', suffixText: ':', border: const OutlineInputBorder(), @@ -329,7 +329,7 @@ class _ImageFileError extends StatelessWidget { children: [ const Icon(Icons.error), Text( - L10n.of(context)!.notAnImage, + L10n.of(context).notAnImage, textAlign: TextAlign.center, style: theme.textTheme.labelSmall, ), diff --git a/lib/pages/settings_emotes/settings_emotes.dart b/lib/pages/settings_emotes/settings_emotes.dart index 59dade669..37a18d8fc 100644 --- a/lib/pages/settings_emotes/settings_emotes.dart +++ b/lib/pages/settings_emotes/settings_emotes.dart @@ -139,8 +139,8 @@ class EmotesSettingsController extends State { showOkAlertDialog( useRootNavigator: false, context: context, - message: L10n.of(context)!.emoteExists, - okLabel: L10n.of(context)!.ok, + message: L10n.of(context).emoteExists, + okLabel: L10n.of(context).ok, ); return; } @@ -149,8 +149,8 @@ class EmotesSettingsController extends State { showOkAlertDialog( useRootNavigator: false, context: context, - message: L10n.of(context)!.emoteInvalid, - okLabel: L10n.of(context)!.ok, + message: L10n.of(context).emoteInvalid, + okLabel: L10n.of(context).ok, ); return; } @@ -185,8 +185,8 @@ class EmotesSettingsController extends State { await showOkAlertDialog( useRootNavigator: false, context: context, - message: L10n.of(context)!.emoteWarnNeedToPick, - okLabel: L10n.of(context)!.ok, + message: L10n.of(context).emoteWarnNeedToPick, + okLabel: L10n.of(context).ok, ); return; } @@ -195,8 +195,8 @@ class EmotesSettingsController extends State { await showOkAlertDialog( useRootNavigator: false, context: context, - message: L10n.of(context)!.emoteExists, - okLabel: L10n.of(context)!.ok, + message: L10n.of(context).emoteExists, + okLabel: L10n.of(context).ok, ); return; } @@ -204,8 +204,8 @@ class EmotesSettingsController extends State { await showOkAlertDialog( useRootNavigator: false, context: context, - message: L10n.of(context)!.emoteInvalid, - okLabel: L10n.of(context)!.ok, + message: L10n.of(context).emoteInvalid, + okLabel: L10n.of(context).ok, ); return; } diff --git a/lib/pages/settings_emotes/settings_emotes_view.dart b/lib/pages/settings_emotes/settings_emotes_view.dart index 1337873a2..fb94b748c 100644 --- a/lib/pages/settings_emotes/settings_emotes_view.dart +++ b/lib/pages/settings_emotes/settings_emotes_view.dart @@ -26,7 +26,7 @@ class EmotesSettingsView extends StatelessWidget { return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), - title: Text(L10n.of(context)!.customEmojisAndStickers), + title: Text(L10n.of(context).customEmojisAndStickers), actions: [ PopupMenuButton( onSelected: (value) { @@ -43,11 +43,11 @@ class EmotesSettingsView extends StatelessWidget { itemBuilder: (context) => [ PopupMenuItem( value: PopupMenuEmojiActions.import, - child: Text(L10n.of(context)!.importFromZipFile), + child: Text(L10n.of(context).importFromZipFile), ), PopupMenuItem( value: PopupMenuEmojiActions.export, - child: Text(L10n.of(context)!.exportEmotePack), + child: Text(L10n.of(context).exportEmotePack), ), ], ), @@ -83,7 +83,7 @@ class EmotesSettingsView extends StatelessWidget { minLines: 1, maxLines: 1, decoration: InputDecoration( - hintText: L10n.of(context)!.emoteShortcode, + hintText: L10n.of(context).emoteShortcode, prefixText: ': ', suffixText: ':', prefixStyle: TextStyle( @@ -114,7 +114,7 @@ class EmotesSettingsView extends StatelessWidget { ), if (controller.room != null) SwitchListTile.adaptive( - title: Text(L10n.of(context)!.enableEmotesGlobally), + title: Text(L10n.of(context).enableEmotesGlobally), value: controller.isGloballyActive(client), onChanged: controller.setIsGloballyActive, ), @@ -125,7 +125,7 @@ class EmotesSettingsView extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(16), child: Text( - L10n.of(context)!.noEmotesFound, + L10n.of(context).noEmotesFound, style: const TextStyle(fontSize: 20), ), ), @@ -186,7 +186,7 @@ class EmotesSettingsView extends StatelessWidget { minLines: 1, maxLines: 1, decoration: InputDecoration( - hintText: L10n.of(context)!.emoteShortcode, + hintText: L10n.of(context).emoteShortcode, prefixText: ': ', suffixText: ':', prefixStyle: TextStyle( @@ -269,7 +269,7 @@ class _ImagePickerState extends State<_ImagePicker> { if (widget.controller.value == null) { return ElevatedButton( onPressed: () => widget.onPressed(widget.controller), - child: Text(L10n.of(context)!.pickImage), + child: Text(L10n.of(context).pickImage), ); } else { return _EmoteImage(widget.controller.value!.url); diff --git a/lib/pages/settings_ignore_list/settings_ignore_list.dart b/lib/pages/settings_ignore_list/settings_ignore_list.dart index 0468d267b..c2ad3404c 100644 --- a/lib/pages/settings_ignore_list/settings_ignore_list.dart +++ b/lib/pages/settings_ignore_list/settings_ignore_list.dart @@ -35,7 +35,7 @@ class SettingsIgnoreListController extends State { if (userId.isEmpty) return; if (!userId.isValidMatrixId || userId.sigil != '@') { setState(() { - errorText = L10n.of(context)!.invalidInput; + errorText = L10n.of(context).invalidInput; }); return; } diff --git a/lib/pages/settings_ignore_list/settings_ignore_list_view.dart b/lib/pages/settings_ignore_list/settings_ignore_list_view.dart index 63466ddbb..09922c308 100644 --- a/lib/pages/settings_ignore_list/settings_ignore_list_view.dart +++ b/lib/pages/settings_ignore_list/settings_ignore_list_view.dart @@ -22,7 +22,7 @@ class SettingsIgnoreListView extends StatelessWidget { return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), - title: Text(L10n.of(context)!.blockedUsers), + title: Text(L10n.of(context).blockedUsers), ), body: MaxWidthBody( withScrolling: false, @@ -43,9 +43,9 @@ class SettingsIgnoreListView extends StatelessWidget { errorText: controller.errorText, hintText: '@bad_guy:domain.abc', floatingLabelBehavior: FloatingLabelBehavior.always, - labelText: L10n.of(context)!.blockUsername, + labelText: L10n.of(context).blockUsername, suffixIcon: IconButton( - tooltip: L10n.of(context)!.block, + tooltip: L10n.of(context).block, icon: const Icon(Icons.add), onPressed: () => controller.ignoreUser(context), ), @@ -53,7 +53,7 @@ class SettingsIgnoreListView extends StatelessWidget { ), const SizedBox(height: 16), Text( - L10n.of(context)!.blockListDescription, + L10n.of(context).blockListDescription, style: const TextStyle(color: Colors.orange), ), ], @@ -89,7 +89,7 @@ class SettingsIgnoreListView extends StatelessWidget { subtitle: Text(s.data?.userId ?? client.ignoredUsers[i]), trailing: IconButton( - tooltip: L10n.of(context)!.delete, + tooltip: L10n.of(context).delete, icon: const Icon(Icons.delete_outlined), onPressed: () => showFutureLoadingDialog( context: context, diff --git a/lib/pages/settings_multiple_emotes/settings_multiple_emotes_view.dart b/lib/pages/settings_multiple_emotes/settings_multiple_emotes_view.dart index 5b910a8ba..83b1da436 100644 --- a/lib/pages/settings_multiple_emotes/settings_multiple_emotes_view.dart +++ b/lib/pages/settings_multiple_emotes/settings_multiple_emotes_view.dart @@ -18,7 +18,7 @@ class MultipleEmotesSettingsView extends StatelessWidget { return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), - title: Text(L10n.of(context)!.emotePacks), + title: Text(L10n.of(context).emotePacks), ), body: StreamBuilder( stream: room.client.onRoomState.stream diff --git a/lib/pages/settings_notifications/settings_notifications.dart b/lib/pages/settings_notifications/settings_notifications.dart index 2baf14dd0..ca47e8e28 100644 --- a/lib/pages/settings_notifications/settings_notifications.dart +++ b/lib/pages/settings_notifications/settings_notifications.dart @@ -19,37 +19,37 @@ class NotificationSettingsItem { NotificationSettingsItem( PushRuleKind.underride, '.m.rule.message', - (c) => L10n.of(c)!.allRooms, + (c) => L10n.of(c).allRooms, ), NotificationSettingsItem( PushRuleKind.underride, '.m.rule.room_one_to_one', - (c) => L10n.of(c)!.directChats, + (c) => L10n.of(c).directChats, ), NotificationSettingsItem( PushRuleKind.override, '.m.rule.contains_display_name', - (c) => L10n.of(c)!.containsDisplayName, + (c) => L10n.of(c).containsDisplayName, ), NotificationSettingsItem( PushRuleKind.content, '.m.rule.contains_user_name', - (c) => L10n.of(c)!.containsUserName, + (c) => L10n.of(c).containsUserName, ), NotificationSettingsItem( PushRuleKind.override, '.m.rule.invite_for_me', - (c) => L10n.of(c)!.inviteForMe, + (c) => L10n.of(c).inviteForMe, ), NotificationSettingsItem( PushRuleKind.override, '.m.rule.member_event', - (c) => L10n.of(c)!.memberChanges, + (c) => L10n.of(c).memberChanges, ), NotificationSettingsItem( PushRuleKind.override, '.m.rule.suppress_notices', - (c) => L10n.of(c)!.botMessages, + (c) => L10n.of(c).botMessages, ), ]; } @@ -145,7 +145,7 @@ class SettingsNotificationsController extends State { message: '${pusher.appDisplayName} (${pusher.appId})', actions: [ SheetAction( - label: L10n.of(context)!.delete, + label: L10n.of(context).delete, isDestructiveAction: true, key: true, ), diff --git a/lib/pages/settings_notifications/settings_notifications_view.dart b/lib/pages/settings_notifications/settings_notifications_view.dart index 0128b7593..ed934c539 100644 --- a/lib/pages/settings_notifications/settings_notifications_view.dart +++ b/lib/pages/settings_notifications/settings_notifications_view.dart @@ -18,7 +18,7 @@ class SettingsNotificationsView extends StatelessWidget { return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), - title: Text(L10n.of(context)!.notifications), + title: Text(L10n.of(context).notifications), ), body: MaxWidthBody( child: StreamBuilder( @@ -36,7 +36,7 @@ class SettingsNotificationsView extends StatelessWidget { SwitchListTile.adaptive( value: !Matrix.of(context).client.allPushNotificationsMuted, title: Text( - L10n.of(context)!.notificationsEnabledForThisAccount, + L10n.of(context).notificationsEnabledForThisAccount, ), onChanged: controller.isLoading ? null @@ -45,7 +45,7 @@ class SettingsNotificationsView extends StatelessWidget { Divider(color: theme.dividerColor), ListTile( title: Text( - L10n.of(context)!.notifyMeFor, + L10n.of(context).notifyMeFor, style: TextStyle( color: theme.colorScheme.secondary, fontWeight: FontWeight.bold, @@ -68,7 +68,7 @@ class SettingsNotificationsView extends StatelessWidget { Divider(color: theme.dividerColor), ListTile( title: Text( - L10n.of(context)!.devices, + L10n.of(context).devices, style: TextStyle( color: theme.colorScheme.secondary, fontWeight: FontWeight.bold, @@ -98,7 +98,7 @@ class SettingsNotificationsView extends StatelessWidget { return Center( child: Padding( padding: const EdgeInsets.only(bottom: 16.0), - child: Text(L10n.of(context)!.noOtherDevicesFound), + child: Text(L10n.of(context).noOtherDevicesFound), ), ); } diff --git a/lib/pages/settings_password/settings_password.dart b/lib/pages/settings_password/settings_password.dart index cb631d7f3..48579d395 100644 --- a/lib/pages/settings_password/settings_password.dart +++ b/lib/pages/settings_password/settings_password.dart @@ -31,20 +31,20 @@ class SettingsPasswordController extends State { }); if (oldPasswordController.text.isEmpty) { setState(() { - oldPasswordError = L10n.of(context)!.pleaseEnterYourPassword; + oldPasswordError = L10n.of(context).pleaseEnterYourPassword; }); return; } if (newPassword1Controller.text.isEmpty || newPassword1Controller.text.length < 6) { setState(() { - newPassword1Error = L10n.of(context)!.pleaseChooseAStrongPassword; + newPassword1Error = L10n.of(context).pleaseChooseAStrongPassword; }); return; } if (newPassword1Controller.text != newPassword2Controller.text) { setState(() { - newPassword2Error = L10n.of(context)!.passwordsDoNotMatch; + newPassword2Error = L10n.of(context).passwordsDoNotMatch; }); return; } @@ -60,7 +60,7 @@ class SettingsPasswordController extends State { ); scaffoldMessenger.showSnackBar( SnackBar( - content: Text(L10n.of(context)!.passwordHasBeenChanged), + content: Text(L10n.of(context).passwordHasBeenChanged), ), ); if (mounted) context.pop(); diff --git a/lib/pages/settings_password/settings_password_view.dart b/lib/pages/settings_password/settings_password_view.dart index 3e9b64f33..023559643 100644 --- a/lib/pages/settings_password/settings_password_view.dart +++ b/lib/pages/settings_password/settings_password_view.dart @@ -16,7 +16,7 @@ class SettingsPasswordView extends StatelessWidget { return Scaffold( appBar: AppBar( - title: Text(L10n.of(context)!.changePassword), + title: Text(L10n.of(context).changePassword), ), body: ListTileTheme( iconColor: theme.colorScheme.onSurface, @@ -35,7 +35,7 @@ class SettingsPasswordView extends StatelessWidget { decoration: InputDecoration( prefixIcon: const Icon(Icons.lock_outlined), hintText: '********', - labelText: L10n.of(context)!.pleaseEnterYourCurrentPassword, + labelText: L10n.of(context).pleaseEnterYourCurrentPassword, errorText: controller.oldPasswordError, ), ), @@ -48,7 +48,7 @@ class SettingsPasswordView extends StatelessWidget { decoration: InputDecoration( prefixIcon: const Icon(Icons.lock_reset_outlined), hintText: '********', - labelText: L10n.of(context)!.newPassword, + labelText: L10n.of(context).newPassword, errorText: controller.newPassword1Error, ), ), @@ -61,7 +61,7 @@ class SettingsPasswordView extends StatelessWidget { decoration: InputDecoration( prefixIcon: const Icon(Icons.repeat_outlined), hintText: '********', - labelText: L10n.of(context)!.repeatPassword, + labelText: L10n.of(context).repeatPassword, errorText: controller.newPassword2Error, ), ), @@ -73,12 +73,12 @@ class SettingsPasswordView extends StatelessWidget { controller.loading ? null : controller.changePassword, child: controller.loading ? const LinearProgressIndicator() - : Text(L10n.of(context)!.changePassword), + : Text(L10n.of(context).changePassword), ), ), const SizedBox(height: 16), TextButton( - child: Text(L10n.of(context)!.passwordRecoverySettings), + child: Text(L10n.of(context).passwordRecoverySettings), onPressed: () => context.go('/rooms/settings/security/3pid'), ), ], diff --git a/lib/pages/settings_security/settings_security.dart b/lib/pages/settings_security/settings_security.dart index a4c1c4a6d..7d08eda47 100644 --- a/lib/pages/settings_security/settings_security.dart +++ b/lib/pages/settings_security/settings_security.dart @@ -25,9 +25,9 @@ class SettingsSecurityController extends State { final newLock = await showTextInputDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.pleaseChooseAPasscode, - message: L10n.of(context)!.pleaseEnter4Digits, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).pleaseChooseAPasscode, + message: L10n.of(context).pleaseEnter4Digits, + cancelLabel: L10n.of(context).cancel, textFields: [ DialogTextField( validator: (text) { @@ -35,7 +35,7 @@ class SettingsSecurityController extends State { (text.length == 4 && int.tryParse(text)! >= 0)) { return null; } - return L10n.of(context)!.pleaseEnter4Digits; + return L10n.of(context).pleaseEnter4Digits; }, keyboardType: TextInputType.number, obscureText: true, @@ -54,10 +54,10 @@ class SettingsSecurityController extends State { if (await showOkCancelAlertDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.warning, - message: L10n.of(context)!.deactivateAccountWarning, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).warning, + message: L10n.of(context).deactivateAccountWarning, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, isDestructiveAction: true, ) == OkCancelResult.cancel) { @@ -67,17 +67,17 @@ class SettingsSecurityController extends State { final mxids = await showTextInputDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.confirmMatrixId, + title: L10n.of(context).confirmMatrixId, textFields: [ DialogTextField( validator: (text) => text == supposedMxid ? null - : L10n.of(context)!.supposedMxid(supposedMxid), + : L10n.of(context).supposedMxid(supposedMxid), ), ], isDestructiveAction: true, - okLabel: L10n.of(context)!.delete, - cancelLabel: L10n.of(context)!.cancel, + okLabel: L10n.of(context).delete, + cancelLabel: L10n.of(context).cancel, ); if (mxids == null || mxids.length != 1 || mxids.single != supposedMxid) { return; @@ -85,9 +85,9 @@ class SettingsSecurityController extends State { final input = await showTextInputDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.pleaseEnterYourPassword, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, + title: L10n.of(context).pleaseEnterYourPassword, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, isDestructiveAction: true, textFields: [ const DialogTextField( diff --git a/lib/pages/settings_security/settings_security_view.dart b/lib/pages/settings_security/settings_security_view.dart index a805a6e33..28f8908ba 100644 --- a/lib/pages/settings_security/settings_security_view.dart +++ b/lib/pages/settings_security/settings_security_view.dart @@ -21,7 +21,7 @@ class SettingsSecurityView extends StatelessWidget { final theme = Theme.of(context); return Scaffold( - appBar: AppBar(title: Text(L10n.of(context)!.security)), + appBar: AppBar(title: Text(L10n.of(context).security)), body: ListTileTheme( iconColor: theme.colorScheme.onSurface, child: MaxWidthBody( @@ -45,7 +45,7 @@ class SettingsSecurityView extends StatelessWidget { children: [ ListTile( title: Text( - L10n.of(context)!.privacy, + L10n.of(context).privacy, style: TextStyle( color: theme.colorScheme.secondary, fontWeight: FontWeight.bold, @@ -53,25 +53,25 @@ class SettingsSecurityView extends StatelessWidget { ), ), SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.sendTypingNotifications, + title: L10n.of(context).sendTypingNotifications, subtitle: - L10n.of(context)!.sendTypingNotificationsDescription, + L10n.of(context).sendTypingNotificationsDescription, onChanged: (b) => AppConfig.sendTypingNotifications = b, storeKey: SettingKeys.sendTypingNotifications, defaultValue: AppConfig.sendTypingNotifications, ), SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.sendReadReceipts, - subtitle: L10n.of(context)!.sendReadReceiptsDescription, + title: L10n.of(context).sendReadReceipts, + subtitle: L10n.of(context).sendReadReceiptsDescription, onChanged: (b) => AppConfig.sendPublicReadReceipts = b, storeKey: SettingKeys.sendPublicReadReceipts, defaultValue: AppConfig.sendPublicReadReceipts, ), ListTile( trailing: const Icon(Icons.chevron_right_outlined), - title: Text(L10n.of(context)!.blockedUsers), + title: Text(L10n.of(context).blockedUsers), subtitle: Text( - L10n.of(context)!.thereAreCountUsersBlocked( + L10n.of(context).thereAreCountUsersBlocked( Matrix.of(context).client.ignoredUsers.length, ), ), @@ -82,15 +82,15 @@ class SettingsSecurityView extends StatelessWidget { if (PlatformInfos.isMobile) ListTile( trailing: const Icon(Icons.chevron_right_outlined), - title: Text(L10n.of(context)!.appLock), - subtitle: Text(L10n.of(context)!.appLockDescription), + title: Text(L10n.of(context).appLock), + subtitle: Text(L10n.of(context).appLockDescription), onTap: controller.setAppLockAction, ), }, Divider(color: theme.dividerColor), ListTile( title: Text( - L10n.of(context)!.account, + L10n.of(context).account, style: TextStyle( color: theme.colorScheme.secondary, fontWeight: FontWeight.bold, @@ -98,7 +98,7 @@ class SettingsSecurityView extends StatelessWidget { ), ), ListTile( - title: Text(L10n.of(context)!.yourPublicKey), + title: Text(L10n.of(context).yourPublicKey), leading: const Icon(Icons.vpn_key_outlined), subtitle: SelectableText( Matrix.of(context).client.fingerprintKey.beautified, @@ -110,7 +110,7 @@ class SettingsSecurityView extends StatelessWidget { ListTile( leading: const Icon(Icons.password_outlined), trailing: const Icon(Icons.chevron_right_outlined), - title: Text(L10n.of(context)!.changePassword), + title: Text(L10n.of(context).changePassword), onTap: () => context.go('/rooms/settings/security/password'), ), @@ -118,7 +118,7 @@ class SettingsSecurityView extends StatelessWidget { iconColor: Colors.orange, leading: const Icon(Icons.delete_sweep_outlined), title: Text( - L10n.of(context)!.dehydrate, + L10n.of(context).dehydrate, style: const TextStyle(color: Colors.orange), ), onTap: controller.dehydrateAction, @@ -128,7 +128,7 @@ class SettingsSecurityView extends StatelessWidget { iconColor: Colors.red, leading: const Icon(Icons.delete_outlined), title: Text( - L10n.of(context)!.deleteAccount, + L10n.of(context).deleteAccount, style: const TextStyle(color: Colors.red), ), onTap: controller.deleteAccountAction, diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index fd0a4c4f9..d40315697 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -27,7 +27,7 @@ class SettingsStyleView extends StatelessWidget { return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), - title: Text(L10n.of(context)!.changeTheme), + title: Text(L10n.of(context).changeTheme), ), backgroundColor: theme.colorScheme.surface, body: MaxWidthBody( @@ -35,7 +35,7 @@ class SettingsStyleView extends StatelessWidget { children: [ ListTile( title: Text( - L10n.of(context)!.setColorTheme, + L10n.of(context).setColorTheme, style: TextStyle( color: theme.colorScheme.secondary, fontWeight: FontWeight.bold, @@ -95,7 +95,7 @@ class SettingsStyleView extends StatelessWidget { ), ), Text( - L10n.of(context)!.systemTheme, + L10n.of(context).systemTheme, textAlign: TextAlign.center, style: TextStyle( color: theme @@ -140,7 +140,7 @@ class SettingsStyleView extends StatelessWidget { ), ListTile( title: Text( - L10n.of(context)!.setTheme, + L10n.of(context).setTheme, style: TextStyle( color: theme.colorScheme.secondary, fontWeight: FontWeight.bold, @@ -150,19 +150,19 @@ class SettingsStyleView extends StatelessWidget { RadioListTile( groupValue: controller.currentTheme, value: ThemeMode.system, - title: Text(L10n.of(context)!.systemTheme), + title: Text(L10n.of(context).systemTheme), onChanged: controller.switchTheme, ), RadioListTile( groupValue: controller.currentTheme, value: ThemeMode.light, - title: Text(L10n.of(context)!.lightTheme), + title: Text(L10n.of(context).lightTheme), onChanged: controller.switchTheme, ), RadioListTile( groupValue: controller.currentTheme, value: ThemeMode.dark, - title: Text(L10n.of(context)!.darkTheme), + title: Text(L10n.of(context).darkTheme), onChanged: controller.switchTheme, ), Divider( @@ -170,7 +170,7 @@ class SettingsStyleView extends StatelessWidget { ), ListTile( title: Text( - L10n.of(context)!.overview, + L10n.of(context).overview, style: TextStyle( color: theme.colorScheme.secondary, fontWeight: FontWeight.bold, @@ -178,13 +178,13 @@ class SettingsStyleView extends StatelessWidget { ), ), SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.presencesToggle, + title: L10n.of(context).presencesToggle, onChanged: (b) => AppConfig.showPresences = b, storeKey: SettingKeys.showPresences, defaultValue: AppConfig.showPresences, ), SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.separateChatTypes, + title: L10n.of(context).separateChatTypes, onChanged: (b) => AppConfig.separateChatTypes = b, storeKey: SettingKeys.separateChatTypes, defaultValue: AppConfig.separateChatTypes, @@ -194,7 +194,7 @@ class SettingsStyleView extends StatelessWidget { ), ListTile( title: Text( - L10n.of(context)!.messagesStyle, + L10n.of(context).messagesStyle, style: TextStyle( color: theme.colorScheme.secondary, fontWeight: FontWeight.bold, @@ -270,7 +270,7 @@ class SettingsStyleView extends StatelessWidget { ), ), ListTile( - title: Text(L10n.of(context)!.wallpaper), + title: Text(L10n.of(context).wallpaper), leading: const Icon(Icons.photo_outlined), trailing: accountConfig.wallpaperUrl == null ? null @@ -286,7 +286,7 @@ class SettingsStyleView extends StatelessWidget { curve: FluffyThemes.animationCurve, child: accountConfig.wallpaperUrl != null ? SwitchListTile.adaptive( - title: Text(L10n.of(context)!.transparent), + title: Text(L10n.of(context).transparent), secondary: const Icon(Icons.blur_linear_outlined), value: !wallpaperOpacityIsDefault, onChanged: (_) => @@ -301,7 +301,7 @@ class SettingsStyleView extends StatelessWidget { }, ), ListTile( - title: Text(L10n.of(context)!.fontSize), + title: Text(L10n.of(context).fontSize), trailing: Text('× ${AppConfig.fontSizeFactor}'), ), Slider.adaptive( diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart index 45be5c975..0a2cc267a 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart @@ -98,22 +98,22 @@ class UserBottomSheetController extends State { final score = await showConfirmationDialog( context: context, - title: L10n.of(context)!.reportUser, - message: L10n.of(context)!.howOffensiveIsThisContent, - cancelLabel: L10n.of(context)!.cancel, - okLabel: L10n.of(context)!.ok, + title: L10n.of(context).reportUser, + message: L10n.of(context).howOffensiveIsThisContent, + cancelLabel: L10n.of(context).cancel, + okLabel: L10n.of(context).ok, actions: [ AlertDialogAction( key: -100, - label: L10n.of(context)!.extremeOffensive, + label: L10n.of(context).extremeOffensive, ), AlertDialogAction( key: -50, - label: L10n.of(context)!.offensive, + label: L10n.of(context).offensive, ), AlertDialogAction( key: 0, - label: L10n.of(context)!.inoffensive, + label: L10n.of(context).inoffensive, ), ], ); @@ -121,10 +121,10 @@ class UserBottomSheetController extends State { final reason = await showTextInputDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.whyDoYouWantToReportThis, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - textFields: [DialogTextField(hintText: L10n.of(context)!.reason)], + title: L10n.of(context).whyDoYouWantToReportThis, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + textFields: [DialogTextField(hintText: L10n.of(context).reason)], ); if (reason == null || reason.single.isEmpty) return; @@ -139,7 +139,7 @@ class UserBottomSheetController extends State { ); if (result.error != null) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)), + SnackBar(content: Text(L10n.of(context).contentHasBeenReported)), ); break; case UserBottomSheetAction.mention: @@ -152,10 +152,10 @@ class UserBottomSheetController extends State { if (await showOkCancelAlertDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.no, - message: L10n.of(context)!.banUserDescription, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).yes, + cancelLabel: L10n.of(context).no, + message: L10n.of(context).banUserDescription, ) == OkCancelResult.ok) { await showFutureLoadingDialog( @@ -170,10 +170,10 @@ class UserBottomSheetController extends State { if (await showOkCancelAlertDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.no, - message: L10n.of(context)!.unbanUserDescription, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).yes, + cancelLabel: L10n.of(context).no, + message: L10n.of(context).unbanUserDescription, ) == OkCancelResult.ok) { await showFutureLoadingDialog( @@ -188,10 +188,10 @@ class UserBottomSheetController extends State { if (await showOkCancelAlertDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.no, - message: L10n.of(context)!.kickUserDescription, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).yes, + cancelLabel: L10n.of(context).no, + message: L10n.of(context).kickUserDescription, ) == OkCancelResult.ok) { await showFutureLoadingDialog( @@ -300,10 +300,10 @@ class UserBottomSheetController extends State { final consent = await showOkCancelAlertDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.no, - message: L10n.of(context)!.makeAdminDescription, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).yes, + cancelLabel: L10n.of(context).no, + message: L10n.of(context).makeAdminDescription, ); if (consent != OkCancelResult.ok) return; } diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart index 16426f871..e44ca47fc 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart @@ -79,7 +79,7 @@ class UserBottomSheetView extends StatelessWidget { title: Padding( padding: const EdgeInsets.only(bottom: 12.0), child: Text( - L10n.of(context)! + L10n.of(context) .userWouldLikeToChangeTheChat(displayname), ), ), @@ -92,7 +92,7 @@ class UserBottomSheetView extends StatelessWidget { ), onPressed: controller.knockAccept, icon: const Icon(Icons.check_outlined), - label: Text(L10n.of(context)!.accept), + label: Text(L10n.of(context).accept), ), const SizedBox(width: 12), TextButton.icon( @@ -104,7 +104,7 @@ class UserBottomSheetView extends StatelessWidget { ), onPressed: controller.knockDecline, icon: const Icon(Icons.cancel_outlined), - label: Text(L10n.of(context)!.decline), + label: Text(L10n.of(context).decline), ), ], ), @@ -181,13 +181,13 @@ class UserBottomSheetView extends StatelessWidget { const SizedBox(width: 12), if (presence.currentlyActive == true) Text( - L10n.of(context)!.currentlyActive, + L10n.of(context).currentlyActive, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall, ) else if (lastActiveTimestamp != null) Text( - L10n.of(context)!.lastActiveAgo( + L10n.of(context).lastActiveAgo( lastActiveTimestamp .localizedTimeShort(context), ), @@ -238,7 +238,7 @@ class UserBottomSheetView extends StatelessWidget { UserBottomSheetAction.message, ), icon: const Icon(Icons.chat_outlined), - label: Text(L10n.of(context)!.startConversation), + label: Text(L10n.of(context).startConversation), ) : TextField( controller: controller.sendController, @@ -250,7 +250,7 @@ class UserBottomSheetView extends StatelessWidget { decoration: InputDecoration( errorText: controller.sendError ?.toLocalizedString(context), - hintText: L10n.of(context)!.sendMessages, + hintText: L10n.of(context).sendMessages, suffix: controller.isSending ? const SizedBox( width: 16, @@ -272,7 +272,7 @@ class UserBottomSheetView extends StatelessWidget { if (controller.widget.onMention != null) ListTile( leading: const Icon(Icons.alternate_email_outlined), - title: Text(L10n.of(context)!.mention), + title: Text(L10n.of(context).mention), onTap: () => controller .participantAction(UserBottomSheetAction.mention), ), @@ -280,7 +280,7 @@ class UserBottomSheetView extends StatelessWidget { Divider(color: theme.dividerColor), ListTile( title: Text( - '${L10n.of(context)!.userRole} (${user.powerLevel})', + '${L10n.of(context).userRole} (${user.powerLevel})', ), leading: const Icon(Icons.person_outlined), trailing: Material( @@ -304,19 +304,19 @@ class UserBottomSheetView extends StatelessWidget { items: [ DropdownMenuItem( value: 0, - child: Text(L10n.of(context)!.user), + child: Text(L10n.of(context).user), ), DropdownMenuItem( value: 50, - child: Text(L10n.of(context)!.moderator), + child: Text(L10n.of(context).moderator), ), DropdownMenuItem( value: 100, - child: Text(L10n.of(context)!.admin), + child: Text(L10n.of(context).admin), ), DropdownMenuItem( value: null, - child: Text(L10n.of(context)!.custom), + child: Text(L10n.of(context).custom), ), ], ), @@ -328,7 +328,7 @@ class UserBottomSheetView extends StatelessWidget { ListTile( textColor: theme.colorScheme.error, iconColor: theme.colorScheme.error, - title: Text(L10n.of(context)!.kickFromChat), + title: Text(L10n.of(context).kickFromChat), leading: const Icon(Icons.exit_to_app_outlined), onTap: () => controller .participantAction(UserBottomSheetAction.kick), @@ -339,7 +339,7 @@ class UserBottomSheetView extends StatelessWidget { ListTile( textColor: theme.colorScheme.onErrorContainer, iconColor: theme.colorScheme.onErrorContainer, - title: Text(L10n.of(context)!.banFromChat), + title: Text(L10n.of(context).banFromChat), leading: const Icon(Icons.warning_sharp), onTap: () => controller.participantAction(UserBottomSheetAction.ban), @@ -348,7 +348,7 @@ class UserBottomSheetView extends StatelessWidget { user.canBan && user.membership == Membership.ban) ListTile( - title: Text(L10n.of(context)!.unbanFromChat), + title: Text(L10n.of(context).unbanFromChat), leading: const Icon(Icons.warning_outlined), onTap: () => controller .participantAction(UserBottomSheetAction.unban), @@ -357,7 +357,7 @@ class UserBottomSheetView extends StatelessWidget { ListTile( textColor: theme.colorScheme.onErrorContainer, iconColor: theme.colorScheme.onErrorContainer, - title: Text(L10n.of(context)!.reportUser), + title: Text(L10n.of(context).reportUser), leading: const Icon(Icons.gavel_outlined), onTap: () => controller .participantAction(UserBottomSheetAction.report), @@ -369,7 +369,7 @@ class UserBottomSheetView extends StatelessWidget { color: Colors.orange, ), subtitle: Text( - L10n.of(context)!.profileNotFound, + L10n.of(context).profileNotFound, style: const TextStyle(color: Colors.orange), ), ), @@ -379,7 +379,7 @@ class UserBottomSheetView extends StatelessWidget { textColor: theme.colorScheme.onErrorContainer, iconColor: theme.colorScheme.onErrorContainer, leading: const Icon(Icons.block_outlined), - title: Text(L10n.of(context)!.block), + title: Text(L10n.of(context).block), onTap: () => controller .participantAction(UserBottomSheetAction.ignore), ), diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 3a14e4e79..d6bc69669 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -48,8 +48,8 @@ abstract class ClientManager { await Future.wait( clients.map( (client) => client.initWithRestore( - onMigration: () { - final l10n = lookupL10n(PlatformDispatcher.instance.locale); + onMigration: () async { + final l10n = await lookupL10n(PlatformDispatcher.instance.locale); sendInitNotification( l10n.databaseMigrationTitle, l10n.databaseMigrationBody, diff --git a/lib/utils/date_time_extension.dart b/lib/utils/date_time_extension.dart index 0346dd289..8043805f0 100644 --- a/lib/utils/date_time_extension.dart +++ b/lib/utils/date_time_extension.dart @@ -35,9 +35,9 @@ extension DateTimeExtension on DateTime { /// Returns a simple time String. String localizedTimeOfDay(BuildContext context) => - L10n.of(context)!.alwaysUse24HourFormat == 'true' - ? DateFormat('HH:mm', L10n.of(context)!.localeName).format(this) - : DateFormat('h:mm a', L10n.of(context)!.localeName).format(this); + L10n.of(context).alwaysUse24HourFormat == 'true' + ? DateFormat('HH:mm', L10n.of(context).localeName).format(this) + : DateFormat('h:mm a', L10n.of(context).localeName).format(this); /// Returns [localizedTimeOfDay()] if the ChatTime is today, the name of the week /// day if the ChatTime is this week and a date string else. @@ -77,7 +77,7 @@ extension DateTimeExtension on DateTime { final sameDay = sameYear && now.month == month && now.day == day; if (sameDay) return localizedTimeOfDay(context); - return L10n.of(context)!.dateAndTimeOfDay( + return L10n.of(context).dateAndTimeOfDay( localizedTimeShort(context), localizedTimeOfDay(context), ); diff --git a/lib/utils/error_reporter.dart b/lib/utils/error_reporter.dart index f9254d351..28e7b374a 100644 --- a/lib/utils/error_reporter.dart +++ b/lib/utils/error_reporter.dart @@ -21,7 +21,7 @@ class ErrorReporter { await showAdaptiveDialog( context: context, builder: (context) => AlertDialog.adaptive( - title: Text(L10n.of(context)!.reportErrorDescription), + title: Text(L10n.of(context).reportErrorDescription), content: SizedBox( height: 256, width: 256, @@ -36,13 +36,13 @@ class ErrorReporter { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: Text(L10n.of(context)!.close), + child: Text(L10n.of(context).close), ), TextButton( onPressed: () => Clipboard.setData( ClipboardData(text: text), ), - child: Text(L10n.of(context)!.copy), + child: Text(L10n.of(context).copy), ), TextButton( onPressed: () => launchUrl( @@ -56,7 +56,7 @@ class ErrorReporter { ), mode: LaunchMode.externalApplication, ), - child: Text(L10n.of(context)!.report), + child: Text(L10n.of(context).report), ), ], ), diff --git a/lib/utils/fluffy_share.dart b/lib/utils/fluffy_share.dart index fbe533c54..4fd7967c5 100644 --- a/lib/utils/fluffy_share.dart +++ b/lib/utils/fluffy_share.dart @@ -25,7 +25,7 @@ abstract class FluffyShare { ClipboardData(text: text), ); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.copiedToClipboard)), + SnackBar(content: Text(L10n.of(context).copiedToClipboard)), ); return; } @@ -34,7 +34,7 @@ abstract class FluffyShare { final client = Matrix.of(context).client; final ownProfile = await client.fetchOwnProfile(); await FluffyShare.share( - L10n.of(context)!.inviteText( + L10n.of(context).inviteText( ownProfile.displayName ?? client.userID!, 'https://matrix.to/#/${client.userID}?client=im.fluffychat', ), diff --git a/lib/utils/init_with_restore.dart b/lib/utils/init_with_restore.dart index c99e2ba08..f979adbe8 100644 --- a/lib/utils/init_with_restore.dart +++ b/lib/utils/init_with_restore.dart @@ -101,7 +101,7 @@ extension InitWithRestoreExtension on Client { } } catch (e, s) { Logs().wtf('Client init failed!', e, s); - final l10n = lookupL10n(PlatformDispatcher.instance.locale); + final l10n = await lookupL10n(PlatformDispatcher.instance.locale); final sessionBackupString = await storage?.read(key: storageKey); if (sessionBackupString == null) { ClientManager.sendInitNotification( diff --git a/lib/utils/localized_exception_extension.dart b/lib/utils/localized_exception_extension.dart index c501bcbcb..8c1c3a53a 100644 --- a/lib/utils/localized_exception_extension.dart +++ b/lib/utils/localized_exception_extension.dart @@ -30,7 +30,7 @@ extension LocalizedExceptionExtension on Object { ]) { if (this is FileTooBigMatrixException) { final exception = this as FileTooBigMatrixException; - return L10n.of(context)!.fileIsTooBigForServer( + return L10n.of(context).fileIsTooBigForServer( _formatFileSize(exception.maxFileSize), ); } @@ -38,17 +38,17 @@ extension LocalizedExceptionExtension on Object { switch ((this as MatrixException).error) { case MatrixError.M_FORBIDDEN: if (exceptionContext == ExceptionContext.changePassword) { - return L10n.of(context)!.passwordIsWrong; + return L10n.of(context).passwordIsWrong; } - return L10n.of(context)!.noPermission; + return L10n.of(context).noPermission; case MatrixError.M_LIMIT_EXCEEDED: - return L10n.of(context)!.tooManyRequestsWarning; + return L10n.of(context).tooManyRequestsWarning; default: return (this as MatrixException).errorMessage; } } if (this is InvalidPassphraseException) { - return L10n.of(context)!.wrongRecoveryKey; + return L10n.of(context).wrongRecoveryKey; } if (this is BadServerVersionsException) { final serverVersions = (this as BadServerVersionsException) @@ -61,7 +61,7 @@ extension LocalizedExceptionExtension on Object { .toString() .replaceAll('{', '"') .replaceAll('}', '"'); - return L10n.of(context)!.badServerVersionsException( + return L10n.of(context).badServerVersionsException( serverVersions, supportedVersions, serverVersions, @@ -79,7 +79,7 @@ extension LocalizedExceptionExtension on Object { .toString() .replaceAll('{', '"') .replaceAll('}', '"'); - return L10n.of(context)!.badServerLoginTypesException( + return L10n.of(context).badServerLoginTypesException( serverVersions, supportedVersions, supportedVersions, @@ -89,16 +89,16 @@ extension LocalizedExceptionExtension on Object { this is SocketException || this is SyncConnectionException || this is ClientException) { - return L10n.of(context)!.noConnectionToTheServer; + return L10n.of(context).noConnectionToTheServer; } if (this is FormatException && exceptionContext == ExceptionContext.checkHomeserver) { - return L10n.of(context)!.doesNotSeemToBeAValidHomeserver; + return L10n.of(context).doesNotSeemToBeAValidHomeserver; } if (this is String) return toString(); if (this is UiaException) return toString(); Logs().w('Something went wrong: ', this); - return L10n.of(context)!.oopsSomethingWentWrong; + return L10n.of(context).oopsSomethingWentWrong; } } diff --git a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart index 1add432d2..f149091df 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart @@ -43,7 +43,7 @@ Future flutterMatrixSdkDatabaseBuilder(Client client) async { try { // Send error notification: - final l10n = lookupL10n(PlatformDispatcher.instance.locale); + final l10n = await lookupL10n(PlatformDispatcher.instance.locale); ClientManager.sendInitNotification( l10n.initAppError, l10n.databaseBuildErrorBody( diff --git a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart index a1903018d..021cd4636 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart @@ -57,7 +57,7 @@ void _sendNoEncryptionWarning(Object exception) async { if (isStored == true) return; - final l10n = lookupL10n(PlatformDispatcher.instance.locale); + final l10n = await lookupL10n(PlatformDispatcher.instance.locale); ClientManager.sendInitNotification( l10n.noDatabaseEncryption, exception.toString(), diff --git a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart index 8ffa9201f..e9dd9e21f 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart @@ -20,7 +20,7 @@ extension MatrixFileExtension on MatrixFile { } final downloadPath = await FilePicker.platform.saveFile( - dialogTitle: L10n.of(context)!.saveFile, + dialogTitle: L10n.of(context).saveFile, fileName: name, type: filePickerFileType, bytes: bytes, @@ -38,7 +38,7 @@ extension MatrixFileExtension on MatrixFile { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - L10n.of(context)!.fileHasBeenSavedAt(downloadPath), + L10n.of(context).fileHasBeenSavedAt(downloadPath), ), ), ); diff --git a/lib/utils/platform_infos.dart b/lib/utils/platform_infos.dart index d43176d7a..d36d8ae97 100644 --- a/lib/utils/platform_infos.dart +++ b/lib/utils/platform_infos.dart @@ -52,7 +52,7 @@ abstract class PlatformInfos { TextButton.icon( onPressed: () => launchUrlString(AppConfig.sourceCodeUrl), icon: const Icon(Icons.source_outlined), - label: Text(L10n.of(context)!.sourceCode), + label: Text(L10n.of(context).sourceCode), ), TextButton.icon( onPressed: () => launchUrlString(AppConfig.emojiFontUrl), diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart index ff6c5934e..05f0e90c7 100644 --- a/lib/utils/push_helper.dart +++ b/lib/utils/push_helper.dart @@ -35,7 +35,7 @@ Future pushHelper( } catch (e, s) { Logs().v('Push Helper has crashed!', e, s); - l10n ??= lookupL10n(const Locale('en')); + l10n ??= await lookupL10n(const Locale('en')); flutterLocalNotificationsPlugin.show( notification.roomId?.hashCode ?? 0, l10n.newMessageInFluffyChat, diff --git a/lib/utils/room_status_extension.dart b/lib/utils/room_status_extension.dart index 02c70b088..4407d8b6a 100644 --- a/lib/utils/room_status_extension.dart +++ b/lib/utils/room_status_extension.dart @@ -12,24 +12,24 @@ extension RoomStatusExtension on Room { typingUsers.removeWhere((User u) => u.id == client.userID); if (AppConfig.hideTypingUsernames) { - typingText = L10n.of(context)!.isTyping; + typingText = L10n.of(context).isTyping; if (typingUsers.first.id != directChatMatrixID) { typingText = - L10n.of(context)!.numUsersTyping(typingUsers.length.toString()); + L10n.of(context).numUsersTyping(typingUsers.length.toString()); } } else if (typingUsers.length == 1) { - typingText = L10n.of(context)!.isTyping; + typingText = L10n.of(context).isTyping; if (typingUsers.first.id != directChatMatrixID) { typingText = - L10n.of(context)!.userIsTyping(typingUsers.first.calcDisplayname()); + L10n.of(context).userIsTyping(typingUsers.first.calcDisplayname()); } } else if (typingUsers.length == 2) { - typingText = L10n.of(context)!.userAndUserAreTyping( + typingText = L10n.of(context).userAndUserAreTyping( typingUsers.first.calcDisplayname(), typingUsers[1].calcDisplayname(), ); } else if (typingUsers.length > 2) { - typingText = L10n.of(context)!.userAndOthersAreTyping( + typingText = L10n.of(context).userAndOthersAreTyping( typingUsers.first.calcDisplayname(), (typingUsers.length - 1).toString(), ); diff --git a/lib/utils/show_update_snackbar.dart b/lib/utils/show_update_snackbar.dart index 38334a508..1c4ee8972 100644 --- a/lib/utils/show_update_snackbar.dart +++ b/lib/utils/show_update_snackbar.dart @@ -35,13 +35,13 @@ abstract class UpdateNotifier { ), Expanded( child: Text( - L10n.of(context)!.updateInstalled(currentVersion), + L10n.of(context).updateInstalled(currentVersion), ), ), ], ), action: SnackBarAction( - label: L10n.of(context)!.changelog, + label: L10n.of(context).changelog, onPressed: () => launchUrlString(AppConfig.changelogUrl), ), ), diff --git a/lib/utils/uia_request_manager.dart b/lib/utils/uia_request_manager.dart index 04c8b4844..440dfda37 100644 --- a/lib/utils/uia_request_manager.dart +++ b/lib/utils/uia_request_manager.dart @@ -9,7 +9,7 @@ import 'package:fluffychat/widgets/matrix.dart'; extension UiaRequestManager on MatrixState { Future uiaRequestHandler(UiaRequest uiaRequest) async { - final l10n = L10n.of(context)!; + final l10n = L10n.of(context); try { if (uiaRequest.state != UiaRequestState.waitForUser || uiaRequest.nextStages.isEmpty) { @@ -49,7 +49,7 @@ extension UiaRequestManager on MatrixState { case AuthenticationTypes.emailIdentity: if (currentThreepidCreds == null) { return uiaRequest.cancel( - UiaException(L10n.of(context)!.serverRequiresEmail), + UiaException(L10n.of(context).serverRequiresEmail), ); } final auth = AuthenticationThreePidCreds( diff --git a/lib/utils/url_launcher.dart b/lib/utils/url_launcher.dart index 53de9b138..a5c006998 100644 --- a/lib/utils/url_launcher.dart +++ b/lib/utils/url_launcher.dart @@ -39,7 +39,7 @@ class UrlLauncher { if (uri == null) { // we can't open this thing ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.cantOpenUri(url!))), + SnackBar(content: Text(L10n.of(context).cantOpenUri(url!))), ); return; } @@ -49,10 +49,10 @@ class UrlLauncher { // that the user can see the actual url before opening the browser. final consent = await showOkCancelAlertDialog( context: context, - title: L10n.of(context)!.openLinkInBrowser, + title: L10n.of(context).openLinkInBrowser, message: url, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.cancel, + okLabel: L10n.of(context).yes, + cancelLabel: L10n.of(context).cancel, ); if (consent != OkCancelResult.ok) return; } @@ -93,7 +93,7 @@ class UrlLauncher { } if (uri.host.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.cantOpenUri(url!))), + SnackBar(content: Text(L10n.of(context).cantOpenUri(url!))), ); return; } diff --git a/lib/utils/voip/callkeep_manager.dart b/lib/utils/voip/callkeep_manager.dart index a3db3eba1..a66252414 100644 --- a/lib/utils/voip/callkeep_manager.dart +++ b/lib/utils/voip/callkeep_manager.dart @@ -226,14 +226,14 @@ class CallKeepManager { barrierDismissible: true, useRootNavigator: false, builder: (_) => AlertDialog( - title: Text(L10n.of(context)!.callingPermissions), + title: Text(L10n.of(context).callingPermissions), content: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( onTap: () => openCallingAccountsPage(context), - title: Text(L10n.of(context)!.callingAccount), - subtitle: Text(L10n.of(context)!.callingAccountDetails), + title: Text(L10n.of(context).callingAccount), + subtitle: Text(L10n.of(context).callingAccountDetails), trailing: const Icon(Icons.phone), ), const Divider(), @@ -241,14 +241,14 @@ class CallKeepManager { onTap: () => FlutterForegroundTask.openSystemAlertWindowSettings( forceOpen: true, ), - title: Text(L10n.of(context)!.appearOnTop), - subtitle: Text(L10n.of(context)!.appearOnTopDetails), + title: Text(L10n.of(context).appearOnTop), + subtitle: Text(L10n.of(context).appearOnTopDetails), trailing: const Icon(Icons.file_upload_rounded), ), const Divider(), ListTile( onTap: () => openAppSettings(), - title: Text(L10n.of(context)!.otherCallingPermissions), + title: Text(L10n.of(context).otherCallingPermissions), trailing: const Icon(Icons.mic), ), ], diff --git a/lib/widgets/chat_settings_popup_menu.dart b/lib/widgets/chat_settings_popup_menu.dart index 6d9a4ce57..4139e6bbe 100644 --- a/lib/widgets/chat_settings_popup_menu.dart +++ b/lib/widgets/chat_settings_popup_menu.dart @@ -57,7 +57,7 @@ class ChatSettingsPopupMenuState extends State { LogicalKeyboardKey.controlLeft, LogicalKeyboardKey.keyI, }, - helpLabel: L10n.of(context)!.chatDetails, + helpLabel: L10n.of(context).chatDetails, onKeysPressed: _showChatDetails, child: const SizedBox.shrink(), ), @@ -68,10 +68,10 @@ class ChatSettingsPopupMenuState extends State { final confirmed = await showOkCancelAlertDialog( useRootNavigator: false, context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - message: L10n.of(context)!.archiveRoomDescription, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + message: L10n.of(context).archiveRoomDescription, ); if (confirmed == OkCancelResult.ok) { final success = await showFutureLoadingDialog( @@ -113,7 +113,7 @@ class ChatSettingsPopupMenuState extends State { children: [ const Icon(Icons.info_outline_rounded), const SizedBox(width: 12), - Text(L10n.of(context)!.chatDetails), + Text(L10n.of(context).chatDetails), ], ), ), @@ -124,7 +124,7 @@ class ChatSettingsPopupMenuState extends State { children: [ const Icon(Icons.notifications_off_outlined), const SizedBox(width: 12), - Text(L10n.of(context)!.muteChat), + Text(L10n.of(context).muteChat), ], ), ) @@ -135,7 +135,7 @@ class ChatSettingsPopupMenuState extends State { children: [ const Icon(Icons.notifications_on_outlined), const SizedBox(width: 12), - Text(L10n.of(context)!.unmuteChat), + Text(L10n.of(context).unmuteChat), ], ), ), @@ -145,7 +145,7 @@ class ChatSettingsPopupMenuState extends State { children: [ const Icon(Icons.search_outlined), const SizedBox(width: 12), - Text(L10n.of(context)!.search), + Text(L10n.of(context).search), ], ), ), @@ -155,7 +155,7 @@ class ChatSettingsPopupMenuState extends State { children: [ const Icon(Icons.delete_outlined), const SizedBox(width: 12), - Text(L10n.of(context)!.leave), + Text(L10n.of(context).leave), ], ), ), diff --git a/lib/widgets/connection_status_header.dart b/lib/widgets/connection_status_header.dart index eef2b795d..27c393759 100644 --- a/lib/widgets/connection_status_header.dart +++ b/lib/widgets/connection_status_header.dart @@ -79,7 +79,7 @@ extension on SyncStatusUpdate { String toLocalizedString(BuildContext context) { switch (status) { case SyncStatus.waitingForResponse: - return L10n.of(context)!.loadingPleaseWait; + return L10n.of(context).loadingPleaseWait; case SyncStatus.error: return ((error?.exception ?? Object()) as Object) .toLocalizedString(context); @@ -87,7 +87,7 @@ extension on SyncStatusUpdate { case SyncStatus.cleaningUp: case SyncStatus.finished: default: - return L10n.of(context)!.synchronizingPleaseWait; + return L10n.of(context).synchronizingPleaseWait; } } } diff --git a/lib/widgets/layouts/login_scaffold.dart b/lib/widgets/layouts/login_scaffold.dart index 8b9a13739..d2363ced4 100644 --- a/lib/widgets/layouts/login_scaffold.dart +++ b/lib/widgets/layouts/login_scaffold.dart @@ -128,14 +128,14 @@ class _PrivacyButtons extends StatelessWidget { TextButton( onPressed: () => PlatformInfos.showDialog(context), child: Text( - L10n.of(context)!.about, + L10n.of(context).about, style: shadowTextStyle, ), ), TextButton( onPressed: () => launchUrlString(AppConfig.privacyUrl), child: Text( - L10n.of(context)!.privacy, + L10n.of(context).privacy, style: shadowTextStyle, ), ), diff --git a/lib/widgets/local_notifications_extension.dart b/lib/widgets/local_notifications_extension.dart index 0549663f8..a59a540fa 100644 --- a/lib/widgets/local_notifications_extension.dart +++ b/lib/widgets/local_notifications_extension.dart @@ -37,9 +37,9 @@ extension LocalNotificationsExtension on MatrixState { if (room.notificationCount == 0) return; final event = Event.fromJson(eventUpdate.content, room); - final title = room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)); + final title = room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))); final body = await event.calcLocalizedBody( - MatrixLocals(L10n.of(context)!), + MatrixLocals(L10n.of(context)), withSenderNamePrefix: !room.isDirectChat || room.lastEvent?.senderId == client.userID, plaintextBody: true, @@ -91,11 +91,11 @@ extension LocalNotificationsExtension on MatrixState { actions: [ NotificationAction( DesktopNotificationActions.openChat.name, - L10n.of(context)!.openChat, + L10n.of(context).openChat, ), NotificationAction( DesktopNotificationActions.seen.name, - L10n.of(context)!.markAsRead, + L10n.of(context).markAsRead, ), ], hints: [ diff --git a/lib/widgets/lock_screen.dart b/lib/widgets/lock_screen.dart index b756a3954..d8d3d3c63 100644 --- a/lib/widgets/lock_screen.dart +++ b/lib/widgets/lock_screen.dart @@ -30,7 +30,7 @@ class _LockScreenState extends State { final enteredPin = int.tryParse(text); if (enteredPin == null || text.length != 4) { setState(() { - _errorText = L10n.of(context)!.invalidInput; + _errorText = L10n.of(context).invalidInput; }); _textEditingController.clear(); return; @@ -46,7 +46,7 @@ class _LockScreenState extends State { } setState(() { - _errorText = L10n.of(context)!.wrongPinEntered(_coolDownSeconds); + _errorText = L10n.of(context).wrongPinEntered(_coolDownSeconds); _inputBlocked = true; }); Future.delayed(Duration(seconds: _coolDownSeconds)).then((_) { @@ -63,7 +63,7 @@ class _LockScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(L10n.of(context)!.pleaseEnterYourPin), + title: Text(L10n.of(context).pleaseEnterYourPin), centerTitle: true, ), extendBodyBehindAppBar: true, diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index aa3cc2c4c..c425e0688 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -239,8 +239,8 @@ class MatrixState extends State with WidgetsBindingObserver { void initLoadingDialog() { WidgetsBinding.instance.addPostFrameCallback((_) { - LoadingDialog.defaultTitle = L10n.of(context)!.loadingPleaseWait; - LoadingDialog.defaultBackLabel = L10n.of(context)!.close; + LoadingDialog.defaultTitle = L10n.of(context).loadingPleaseWait; + LoadingDialog.defaultBackLabel = L10n.of(context).close; LoadingDialog.defaultOnError = (e) => (e as Object?)!.toLocalizedString(context); }); @@ -312,7 +312,7 @@ class MatrixState extends State with WidgetsBindingObserver { context, ).showSnackBar( SnackBar( - content: Text(L10n.of(context)!.oneClientLoggedOut), + content: Text(L10n.of(context).oneClientLoggedOut), ), ); @@ -371,13 +371,12 @@ class MatrixState extends State with WidgetsBindingObserver { context: FluffyChatApp .router.routerDelegate.navigatorKey.currentContext ?? context, - title: L10n.of(context)!.pushNotificationsNotAvailable, + title: L10n.of(context).pushNotificationsNotAvailable, message: errorMsg, fullyCapitalizedForMaterial: false, - okLabel: link == null - ? L10n.of(context)!.ok - : L10n.of(context)!.learnMore, - cancelLabel: L10n.of(context)!.doNotShowAgain, + okLabel: + link == null ? L10n.of(context).ok : L10n.of(context).learnMore, + cancelLabel: L10n.of(context).doNotShowAgain, ); if (result == OkCancelResult.ok && link != null) { launchUrlString( @@ -495,8 +494,8 @@ class MatrixState extends State with WidgetsBindingObserver { final response = await showOkCancelAlertDialog( context: context, isDestructiveAction: true, - title: L10n.of(context)!.dehydrate, - message: L10n.of(context)!.dehydrateWarning, + title: L10n.of(context).dehydrate, + message: L10n.of(context).dehydrateWarning, ); if (response != OkCancelResult.ok) { return; diff --git a/lib/widgets/permission_slider_dialog.dart b/lib/widgets/permission_slider_dialog.dart index 1e883e123..e568717aa 100644 --- a/lib/widgets/permission_slider_dialog.dart +++ b/lib/widgets/permission_slider_dialog.dart @@ -10,7 +10,7 @@ Future showPermissionChooser( }) async { final customLevel = await showTextInputDialog( context: context, - title: L10n.of(context)!.setPermissionsLevel, + title: L10n.of(context).setPermissionsLevel, textFields: [ DialogTextField( initialText: currentLevel.toString(), @@ -18,11 +18,11 @@ Future showPermissionChooser( autocorrect: false, validator: (text) { if (text == null) { - return L10n.of(context)!.pleaseEnterANumber; + return L10n.of(context).pleaseEnterANumber; } final level = int.tryParse(text); if (level == null) { - return L10n.of(context)!.pleaseEnterANumber; + return L10n.of(context).pleaseEnterANumber; } return null; }, diff --git a/lib/widgets/public_room_bottom_sheet.dart b/lib/widgets/public_room_bottom_sheet.dart index 991739ac6..9acecb3f0 100644 --- a/lib/widgets/public_room_bottom_sheet.dart +++ b/lib/widgets/public_room_bottom_sheet.dart @@ -76,7 +76,7 @@ class PublicRoomBottomSheet extends StatelessWidget { ), ); if (!query.chunk.any(_testRoom)) { - throw (L10n.of(outerContext)!.noRoomsFound); + throw (L10n.of(outerContext).noRoomsFound); } return query.chunk.firstWhere(_testRoom); } @@ -95,7 +95,7 @@ class PublicRoomBottomSheet extends StatelessWidget { leading: IconButton( icon: const Icon(Icons.arrow_downward_outlined), onPressed: Navigator.of(context, rootNavigator: false).pop, - tooltip: L10n.of(context)!.close, + tooltip: L10n.of(context).close, ), actions: [ Padding( @@ -170,7 +170,7 @@ class PublicRoomBottomSheet extends StatelessWidget { foregroundColor: theme.colorScheme.onSurface, ), label: Text( - L10n.of(context)!.countParticipants( + L10n.of(context).countParticipants( profile?.numJoinedMembers ?? 0, ), maxLines: 1, @@ -192,10 +192,10 @@ class PublicRoomBottomSheet extends StatelessWidget { .client .getRoomById(chunk!.roomId) == null - ? L10n.of(context)!.knock + ? L10n.of(context).knock : chunk?.roomType == 'm.space' - ? L10n.of(context)!.joinSpace - : L10n.of(context)!.joinRoom, + ? L10n.of(context).joinSpace + : L10n.of(context).joinRoom, ), icon: const Icon(Icons.navigate_next), ), From 1a3774fbcf1b1fd840e18356ba624db0cff23e6b Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 6 Oct 2024 09:34:51 +0200 Subject: [PATCH 056/236] build: Update secure storage on linux --- pubspec.lock | 6 +++--- pubspec.yaml | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 60472d7b8..32e53bafa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -678,13 +678,13 @@ packages: source: hosted version: "9.2.2" flutter_secure_storage_linux: - dependency: "direct overridden" + dependency: transitive description: name: flutter_secure_storage_linux - sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3" + sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.2.1" flutter_secure_storage_macos: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f511c8615..85dfb2d33 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,7 +45,7 @@ dependencies: flutter_olm: 1.3.2 # Keep in sync with scripts/prepare-web.sh ! 1.4.0 does currently not build on Android flutter_openssl_crypto: ^0.3.0 flutter_ringtone_player: ^4.0.0+2 - flutter_secure_storage: ^9.0.0 + flutter_secure_storage: ^9.2.2 flutter_shortcuts: git: https://github.com/krille-chan/flutter_shortcuts.git flutter_typeahead: ^5.2.0 @@ -154,8 +154,6 @@ msix_config: install_certificate: false dependency_overrides: - # Until https://github.com/mogol/flutter_secure_storage/issues/616 is fixed - flutter_secure_storage_linux: 1.1.3 geolocator_android: hosted: https://hanntech-gmbh.gitlab.io/free2pass/flutter-geolocator-floss version: ^1.0.1 From 1540464c52759cafc8812833d4aded7e1238f543 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 6 Oct 2024 09:36:07 +0200 Subject: [PATCH 057/236] build: Update dependencies minor versions --- pubspec.lock | 104 +++++++++++++++++++++++++-------------------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 32e53bafa..51f11e686 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -18,10 +18,10 @@ packages: dependency: "direct main" description: name: adaptive_dialog - sha256: "165018c7520eb42ea8184b87601f7489c100c87abce15cd06e0a036c60f954e6" + sha256: b02055729c225c369f90fdbc9564452e183cb919f83a971f512199084474530f url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1+2" analyzer: dependency: transitive description: @@ -394,34 +394,34 @@ packages: dependency: transitive description: name: file_selector_android - sha256: b8c9717a0177ca6fa035554b82cd6c83b838ddc66b7704eb6df0f77f027ecc90 + sha256: "00aafa9ae05a8663d0b4f17abd2a02316911ca0f46f9b9dacb9578b324d99590" url: "https://pub.dev" source: hosted - version: "0.5.1+7" + version: "0.5.1+9" file_selector_ios: dependency: transitive description: name: file_selector_ios - sha256: "38ebf91ecbcfa89a9639a0854ccaed8ab370c75678938eebca7d34184296f0bb" + sha256: "94b98ad950b8d40d96fee8fa88640c2e4bd8afcdd4817993bd04e20310f45420" url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.5.3+1" file_selector_linux: dependency: transitive description: name: file_selector_linux - sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + sha256: "712ce7fab537ba532c8febdb1a8f167b32441e74acd68c3ccb2e36dcb52c4ab2" url: "https://pub.dev" source: hosted - version: "0.9.2+1" + version: "0.9.3" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" url: "https://pub.dev" source: hosted - version: "0.9.4" + version: "0.9.4+2" file_selector_platform_interface: dependency: transitive description: @@ -442,10 +442,10 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" url: "https://pub.dev" source: hosted - version: "0.9.3+2" + version: "0.9.3+3" fixnum: dependency: transitive description: @@ -657,10 +657,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" + sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.0.23" flutter_ringtone_player: dependency: "direct main" description: @@ -961,10 +961,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: c0a6763d50b354793d0192afd0a12560b823147d3ded7c6b77daf658fa05cc85 + sha256: d3e5e00fdfeca8fd4ffb3227001264d449cc8950414c2ff70b0e06b9c628e643 url: "https://pub.dev" source: hosted - version: "0.8.12+13" + version: "0.8.12+15" image_picker_for_web: dependency: transitive description: @@ -1070,10 +1070,10 @@ packages: dependency: "direct main" description: name: just_audio - sha256: d8e8aaf417d33e345299c17f6457f72bd4ba0c549dc34607abb5183a354edc4d + sha256: b41646a8241688f1d99c2e69c4da2bb26aa4b3a99795f6ff205c2a165e033fda url: "https://pub.dev" source: hosted - version: "0.9.40" + version: "0.9.41" just_audio_platform_interface: dependency: transitive description: @@ -1086,10 +1086,10 @@ packages: dependency: transitive description: name: just_audio_web - sha256: b163878529d9b028c53a6972fcd58cae2405bcd11cbfcea620b6fb9f151429d6 + sha256: "9a98035b8b24b40749507687520ec5ab404e291d2b0937823ff45d92cb18d448" url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "0.4.13" keyboard_shortcuts: dependency: "direct main" description: @@ -1383,10 +1383,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + sha256: f7544c346a0742aee1450f9e5c0f5269d7c602b9c95fdbcd9fb8f5b1df13b1cc url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "2.2.11" path_provider_foundation: dependency: transitive description: @@ -1767,18 +1767,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_linux: dependency: transitive description: @@ -1892,26 +1892,26 @@ packages: dependency: transitive description: name: sqflite - sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + sha256: ff5a2436ef8ebdfda748fbfe957f9981524cb5ff11e7bafa8c42771840e8a788 url: "https://pub.dev" source: hosted - version: "2.3.3+1" + version: "2.3.3+2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "4058172e418eb7e7f2058dcb7657d451a8fc264afa0dea4dbd0f304a57131611" + sha256: "2d8e607db72e9cb7748c9c6e739e2c9618320a5517de693d5a24609c4671b1a4" url: "https://pub.dev" source: hosted - version: "2.5.4+3" + version: "2.5.4+4" sqflite_common_ffi: dependency: "direct main" description: name: sqflite_common_ffi - sha256: "4d6137c29e930d6e4a8ff373989dd9de7bac12e3bc87bce950f6e844e8ad3bb5" + sha256: a6057d4c87e9260ba1ec436ebac24760a110589b9c0a859e128842eb69a7ef04 url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.3+1" sqlcipher_flutter_libs: dependency: "direct main" description: @@ -1980,10 +1980,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "51b08572b9f091f8c3eb4d9d4be253f196ff0075d5ec9b10a884026d5b55d7bc" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.3.0+2" + version: "3.3.0+3" tar: dependency: transitive description: @@ -2172,10 +2172,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab + sha256: "8fc3bae0b68c02c47c5c86fa8bfa74471d42687b0eded01b78de87872db745e2" url: "https://pub.dev" source: hosted - version: "6.3.10" + version: "6.3.12" url_launcher_ios: dependency: transitive description: @@ -2196,10 +2196,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_platform_interface: dependency: transitive description: @@ -2228,10 +2228,10 @@ packages: dependency: transitive description: name: uuid - sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.5.1" vector_graphics: dependency: transitive description: @@ -2276,34 +2276,34 @@ packages: dependency: "direct main" description: name: video_player - sha256: e30df0d226c4ef82e2c150ebf6834b3522cf3f654d8e2f9419d376cdc071425d + sha256: "4a8c3492d734f7c39c2588a3206707a05ee80cef52e8c7f3b2078d430c84bc17" url: "https://pub.dev" source: hosted - version: "2.9.1" + version: "2.9.2" video_player_android: dependency: transitive description: name: video_player_android - sha256: "38d8fe136c427abdce68b5e8c3c08ea29d7a794b453c7a51b12ecfad4aad9437" + sha256: ae5287ca367e206eb74d7b3dc1ce0b8912ab9a3fc0597b6a101a0a5239f229d3 url: "https://pub.dev" source: hosted - version: "2.7.3" + version: "2.7.9" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c + sha256: cd5ab8a8bc0eab65ab0cea40304097edc46da574c8c1ecdee96f28cd8ef3792f url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.2" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" + sha256: "229d7642ccd9f3dc4aba169609dd6b5f3f443bb4cc15b82f7785fcada5af9bbb" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.2.3" video_player_web: dependency: transitive description: @@ -2356,10 +2356,10 @@ packages: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" web_socket: dependency: transitive description: @@ -2436,10 +2436,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: From ee6f19088c3b643595f41d64b8547910a1463f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Bed=C3=A1=C5=88?= Date: Sat, 5 Oct 2024 17:28:36 +0000 Subject: [PATCH 058/236] Translated using Weblate (Czech) Currently translated at 80.4% (539 of 670 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/cs/ --- assets/l10n/intl_cs.arb | 64 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/assets/l10n/intl_cs.arb b/assets/l10n/intl_cs.arb index 73ed1592a..6fa9911ad 100644 --- a/assets/l10n/intl_cs.arb +++ b/assets/l10n/intl_cs.arb @@ -559,7 +559,7 @@ "type": "text", "placeholders": {} }, - "defaultPermissionLevel": "Výchozí úroveň oprávnění", + "defaultPermissionLevel": "Výchozí úroveň oprávnění nových uživatelů", "@defaultPermissionLevel": { "type": "text", "placeholders": {} @@ -1085,7 +1085,7 @@ "type": "text", "placeholders": {} }, - "noGoogleServicesWarning": "Zdá se, že v telefonu nemáte žádné služby Google. To je dobré rozhodnutí pro vaše soukromí! Chcete-li dostávat push oznámení ve FluffyChat, doporučujeme použít: https://microg.org/ nebo https://unifiedpush.org/.", + "noGoogleServicesWarning": "Zdá se, že služba Firebase Cloud Messaging není ve vašem zařízení k dispozici. Chcete-li i nadále přijímat push oznámení, doporučujeme nainstalovat ntfy. Pomocí ntfy nebo jiného poskytovatele Unified Push můžete přijímat oznámení push zabezpečeným způsobem přenosu dat. Aplikaci ntfy si můžete stáhnout z obchodu PlayStore nebo z webu F-Droid.", "@noGoogleServicesWarning": { "type": "text", "placeholders": {} @@ -2379,5 +2379,63 @@ } }, "noMoreChatsFound": "Žádné další konverzace nalezeny...", - "@noMoreChatsFound": {} + "@noMoreChatsFound": {}, + "hideRedactedMessages": "Skrýt upravené zprávy", + "@hideRedactedMessages": {}, + "hideRedactedMessagesBody": "Pokud někdo zprávu zrediguje, nebude tato zpráva v chatu již viditelná.", + "@hideRedactedMessagesBody": {}, + "hideInvalidOrUnknownMessageFormats": "Skrytí nesprávných nebo neznámých formátů zpráv", + "@hideInvalidOrUnknownMessageFormats": {}, + "blockUsername": "Ignorovat uživatelské jméno", + "@blockUsername": {}, + "hideMemberChangesInPublicChats": "Skrýt změny členů ve veřejných chatech", + "@hideMemberChangesInPublicChats": {}, + "hideMemberChangesInPublicChatsBody": "Nezobrazovat na časové ose chatu, pokud se někdo připojí nebo opustí veřejný chat, aby se zlepšila čitelnost.", + "@hideMemberChangesInPublicChatsBody": {}, + "overview": "Přehled", + "@overview": {}, + "notifyMeFor": "Upozorněte mě na", + "@notifyMeFor": {}, + "passwordRecoverySettings": "Nastavení obnovení hesla", + "@passwordRecoverySettings": {}, + "presenceStyle": "Dostupnost:", + "@presenceStyle": { + "type": "text", + "placeholders": {} + }, + "block": "Blokovat", + "@block": {}, + "indexedDbErrorLong": "Ukládání zpráv bohužel není ve výchozím nastavení v soukromém režimu povoleno.\nNavštivte prosím\n - about:config\n - nastavte dom.indexedDB.privateBrowsing.enabled na true\nV opačném případě nebude možné FluffyChat spustit.", + "@indexedDbErrorLong": {}, + "youInvitedToBy": "📩 Prostřednictvím odkazu jste byli pozváni na:\n{alias}", + "@youInvitedToBy": { + "placeholders": { + "alias": {} + } + }, + "indexedDbErrorTitle": "Problémy privátního prostředí", + "@indexedDbErrorTitle": {}, + "blockListDescription": "Můžete blokovat uživatele, kteří vás obtěžují. Od uživatelů na vašem osobním seznamu blokovaných uživatelů nebudete moci přijímat žádné zprávy ani pozvánky do místnosti.", + "@blockListDescription": {}, + "blockedUsers": "Zablokování uživatelé", + "@blockedUsers": {}, + "alwaysUse24HourFormat": "Vypnuto", + "@alwaysUse24HourFormat": { + "description": "Set to true to always display time of day in 24 hour format." + }, + "noChatsFoundHere": "Nejsou zde žádné chaty. Začněte nový chat s někým, použitím níže uvedeného tlačítka. ⤵️", + "@noChatsFoundHere": {}, + "joinedChats": "Připojené chaty", + "@joinedChats": {}, + "unread": "Nepřečtené", + "@unread": {}, + "space": "Prostor", + "@space": {}, + "spaces": "Prostory", + "@spaces": {}, + "presencesToggle": "Zobrazení stavových zpráv od jiných uživatelů", + "@presencesToggle": { + "type": "text", + "placeholders": {} + } } From e72ba7cf6d0f15bf4bb810792b707e0485e8b456 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Mon, 7 Oct 2024 23:48:03 +0000 Subject: [PATCH 059/236] Translated using Weblate (Ukrainian) Currently translated at 100.0% (670 of 670 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/ --- assets/l10n/intl_uk.arb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/assets/l10n/intl_uk.arb b/assets/l10n/intl_uk.arb index bca3c8815..64d8220bb 100644 --- a/assets/l10n/intl_uk.arb +++ b/assets/l10n/intl_uk.arb @@ -2806,15 +2806,15 @@ "@doesNotSeemToBeAValidHomeserver": {}, "calculatingFileSize": "Обчислення розміру файлу...", "@calculatingFileSize": {}, - "prepareSendingAttachment": "Підготовка до відправлення вкладення...", + "prepareSendingAttachment": "Підготовка до надсилання вкладення...", "@prepareSendingAttachment": {}, - "sendingAttachment": "Відправлення вкладення...", + "sendingAttachment": "Надсилання вкладення...", "@sendingAttachment": {}, "generatingVideoThumbnail": "Генерування мініатюри відео...", "@generatingVideoThumbnail": {}, - "compressVideo": "Стискання відео...", + "compressVideo": "Стиснення відео...", "@compressVideo": {}, - "sendingAttachmentCountOfCount": "Відправлення вкладення {index} з {length}...", + "sendingAttachmentCountOfCount": "Надсилання вкладення {index} з {length}...", "@sendingAttachmentCountOfCount": { "type": "integer", "placeholders": { @@ -2822,7 +2822,7 @@ "length": {} } }, - "serverLimitReached": "Досягнуто ліміт сервера! Очікування {seconds} секунд...", + "serverLimitReached": "Досягнуто ліміту сервера! Очікування {seconds} секунд...", "@serverLimitReached": { "type": "integer", "placeholders": { From 4afb5eb466d7a025dd29a4c9aea5f8d0c76e4715 Mon Sep 17 00:00:00 2001 From: Krille Date: Wed, 9 Oct 2024 09:21:40 +0200 Subject: [PATCH 060/236] build: Update dart olm to 2.0.4 --- pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index 51f11e686..4535447b1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1306,7 +1306,7 @@ packages: sha256: "37948a6576949256f3ee1d0063d5b408634ff7e452b9a5c2f6410f9d7ced1c20" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" opus_caf_converter_dart: dependency: "direct main" description: From a2cc1bc5b1627837642ceda93a42298ab2ba86a1 Mon Sep 17 00:00:00 2001 From: Krille Date: Wed, 9 Oct 2024 09:52:43 +0200 Subject: [PATCH 061/236] build: Update olm sha in pubspec.lock --- pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index 4535447b1..9620ac4a0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1303,7 +1303,7 @@ packages: dependency: transitive description: name: olm - sha256: "37948a6576949256f3ee1d0063d5b408634ff7e452b9a5c2f6410f9d7ced1c20" + sha256: "3306bf534ceb914fd148b3b4a3d603fb5e067b2e6da8304025b47c24cfdf6b46" url: "https://pub.dev" source: hosted version: "2.0.4" From 694bc488fd6b86c44804d9948a9672cf0438b67d Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 11 Oct 2024 13:13:47 +0200 Subject: [PATCH 062/236] chore: Improve read marker design --- lib/pages/chat/events/message.dart | 29 ++++++++++++++++++----------- lib/utils/string_color.dart | 2 +- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index d1a627b55..11ffad745 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -249,7 +249,8 @@ class Message extends StatelessWidget { return Text( displayname, style: TextStyle( - fontSize: 12, + fontSize: 11, + fontWeight: FontWeight.bold, color: (theme.brightness == Brightness.light ? displayname.color @@ -469,27 +470,33 @@ class Message extends StatelessWidget { Row( children: [ Expanded( - child: Divider(color: theme.colorScheme.primary), + child: Divider(color: theme.colorScheme.secondary), ), Container( - decoration: BoxDecoration( - border: Border.all( - color: theme.colorScheme.primary, - ), - color: theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(4), + margin: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 16.0, ), - margin: const EdgeInsets.all(8.0), padding: const EdgeInsets.symmetric( horizontal: 8, ), child: Text( L10n.of(context).readUpToHere, - style: TextStyle(color: theme.colorScheme.primary), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12 * AppConfig.fontSizeFactor, + color: theme.colorScheme.secondary, + shadows: [ + Shadow( + color: theme.colorScheme.surface, + blurRadius: 3, + ), + ], + ), ), ), Expanded( - child: Divider(color: theme.colorScheme.primary), + child: Divider(color: theme.colorScheme.secondary), ), ], ), diff --git a/lib/utils/string_color.dart b/lib/utils/string_color.dart index b3c251b3a..d854d1527 100644 --- a/lib/utils/string_color.dart +++ b/lib/utils/string_color.dart @@ -14,7 +14,7 @@ extension StringColor on String { Color get color { _colorCache[this] ??= {}; - return _colorCache[this]![0.35] ??= _getColorLight(0.35); + return _colorCache[this]![0.3] ??= _getColorLight(0.3); } Color get darkColor { From 19c9a72478c4561aa5c1eb9901f654603f70d31d Mon Sep 17 00:00:00 2001 From: Linerly Date: Fri, 11 Oct 2024 15:40:48 +0000 Subject: [PATCH 063/236] Translated using Weblate (Indonesian) Currently translated at 100.0% (670 of 670 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/id/ --- assets/l10n/intl_id.arb | 122 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 3 deletions(-) diff --git a/assets/l10n/intl_id.arb b/assets/l10n/intl_id.arb index aa4879c95..f6d2c5919 100644 --- a/assets/l10n/intl_id.arb +++ b/assets/l10n/intl_id.arb @@ -1701,7 +1701,7 @@ "type": "text", "placeholders": {} }, - "defaultPermissionLevel": "Level izin default", + "defaultPermissionLevel": "Level izin bawaan untuk pengguna baru", "@defaultPermissionLevel": { "type": "text", "placeholders": {} @@ -2270,7 +2270,7 @@ "@noBackupWarning": {}, "noOtherDevicesFound": "Tidak ada perangkat lain yang ditemukan", "@noOtherDevicesFound": {}, - "fileIsTooBigForServer": "Server melaporkan bahwa file terlalu besar untuk dikirim.", + "fileIsTooBigForServer": "Tidak dapat mengirim! Server hanya mendukung lampiran sampai dengan {max}.", "@fileIsTooBigForServer": {}, "fileHasBeenSavedAt": "Berkas telah disimpan di {path}", "@fileHasBeenSavedAt": { @@ -2711,5 +2711,121 @@ "alwaysUse24HourFormat": "tidak", "@alwaysUse24HourFormat": { "description": "Set to true to always display time of day in 24 hour format." - } + }, + "sendCanceled": "Pengiriman dibatalkan", + "@sendCanceled": {}, + "noChatsFoundHere": "Belum ada chat di sini. Mulai chat baru dengan seseorang menggunakan tombol di bawah. ⤵️", + "@noChatsFoundHere": {}, + "invitedBy": "📩 Diundang oleh {user}", + "@invitedBy": { + "placeholders": { + "user": {} + } + }, + "markAsUnread": "Tandai sebagai belum dibaca", + "@markAsUnread": {}, + "goToSpace": "Pergi ke space: {space}", + "@goToSpace": { + "type": "text", + "space": {} + }, + "changeTheChatPermissions": "Ubah perizinan chat", + "@changeTheChatPermissions": {}, + "changeTheCanonicalRoomAlias": "Ubah alamat chat publik utama", + "@changeTheCanonicalRoomAlias": {}, + "changeTheVisibilityOfChatHistory": "Ubah keterlihatan riwayat chat", + "@changeTheVisibilityOfChatHistory": {}, + "changeTheDescriptionOfTheGroup": "Ubah deskripsi chat", + "@changeTheDescriptionOfTheGroup": {}, + "sendingAttachment": "Mengirim lampiran...", + "@sendingAttachment": {}, + "compressVideo": "Mengompres video...", + "@compressVideo": {}, + "calculatingFileSize": "Menghitung ukuran berkas...", + "@calculatingFileSize": {}, + "prepareSendingAttachment": "Menyiapkan pengiriman lampiran...", + "@prepareSendingAttachment": {}, + "sendingAttachmentCountOfCount": "Mengirim lampiran {index} dari {length}...", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + }, + "userLevel": "{level} - Pengguna", + "@userLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "adminLevel": "{level} - Admin", + "@adminLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "moderatorLevel": "{level} - Moderator", + "@moderatorLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "changeGeneralChatSettings": "Ubah pengaturan chat umum", + "@changeGeneralChatSettings": {}, + "discoverHomeservers": "Jelajahi homeserver", + "@discoverHomeservers": {}, + "loginWithMatrixId": "Masuk dengan ID Matrix", + "@loginWithMatrixId": {}, + "doesNotSeemToBeAValidHomeserver": "Sepertinya bukan homeserver yang kompatibel. URL salah?", + "@doesNotSeemToBeAValidHomeserver": {}, + "countChatsAndCountParticipants": "{chats} chat dan {participants} anggota", + "@countChatsAndCountParticipants": { + "type": "text", + "placeholders": { + "chats": {}, + "participants": {} + } + }, + "unread": "Tidak dibaca", + "@unread": {}, + "space": "Space", + "@space": {}, + "spaces": "Space", + "@spaces": {}, + "joinedChats": "Bergabung chat", + "@joinedChats": {}, + "noMoreChatsFound": "Tidak ada chat lagi yang ditemukan...", + "@noMoreChatsFound": {}, + "generatingVideoThumbnail": "Membuat gambar kecil video...", + "@generatingVideoThumbnail": {}, + "changelog": "Catatan perubahan", + "@changelog": {}, + "whatIsAHomeserver": "Apa itu homeserver?", + "@whatIsAHomeserver": {}, + "sendRoomNotifications": "Kirim notifikasi @room", + "@sendRoomNotifications": {}, + "updateInstalled": "🎉 Pembaruan {version} terpasang!", + "@updateInstalled": { + "type": "text", + "placeholders": { + "version": {} + } + }, + "inviteOtherUsers": "Undang pengguna lain ke chat ini", + "@inviteOtherUsers": {}, + "serverLimitReached": "Batasan server tercapai! Menunggu {seconds} detik...", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + }, + "chatPermissionsDescription": "Tentukan tingkat kekuasaan yang diperlukan untuk tindakan tertentu dalam chat ini. Tingkat kekuasaan 0, 50 dan 100 biasanya mewakili pengguna, moderator dan admin, tetapi gradasi apa pun dimungkinkan.", + "@chatPermissionsDescription": {}, + "homeserverDescription": "Semua data Anda disimpan di dalam server, seperti halnya penyedia email. Anda dapat memilih homeserver mana yang ingin Anda gunakan, sementara Anda masih dapat berkomunikasi dengan semua orang. Pelajari lebih lanjut di https://matrix.org.", + "@homeserverDescription": {} } From 453b3383858fcf97b8a0119143b19f929dbb996c Mon Sep 17 00:00:00 2001 From: Christian Date: Sat, 12 Oct 2024 19:12:17 +0000 Subject: [PATCH 064/236] Translated using Weblate (German) Currently translated at 100.0% (670 of 670 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/ --- assets/l10n/intl_de.arb | 48 ++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/assets/l10n/intl_de.arb b/assets/l10n/intl_de.arb index 021f77046..6ff9ddf39 100644 --- a/assets/l10n/intl_de.arb +++ b/assets/l10n/intl_de.arb @@ -2184,7 +2184,7 @@ "@hideUnimportantStateEvents": {}, "doNotShowAgain": "Nicht mehr anzeigen", "@doNotShowAgain": {}, - "appearOnTopDetails": "Ermöglicht, dass die App oben angezeigt wird (nicht erforderlich, wenn Sie Fluffychat bereits als Anrufkonto eingerichtet haben)", + "appearOnTopDetails": "Ermöglicht, dass die App oben angezeigt wird (nicht erforderlich, wenn du Fluffychat bereits als Anrufkonto eingerichtet haben)", "@appearOnTopDetails": {}, "noKeyForThisMessage": "Dies kann passieren, wenn die Nachricht gesendet wurde, bevor du dich auf diesem Gerät bei deinem Konto angemeldet hast.\n\nEs ist auch möglich, dass der Absender dein Gerät blockiert hat oder etwas mit der Internetverbindung schief gelaufen ist.\n\nKannst du die Nachricht in einer anderen Sitzung lesen? Dann kannst du die Nachricht davon übertragen! Gehe zu den Einstellungen > Geräte und vergewissere dich, dass sich deine Geräte gegenseitig verifiziert haben. Wenn du den Raum das nächste Mal öffnest und beide Sitzungen im Vordergrund sind, werden die Schlüssel automatisch übertragen.\n\nDu möchtest die Schlüssel beim Abmelden oder Gerätewechsel nicht verlieren? Stelle sicher, dass du das Chat-Backup in den Einstellungen aktiviert hast.", "@noKeyForThisMessage": {}, @@ -2267,7 +2267,7 @@ }, "commandHint_googly": "Glupschaugen senden", "@commandHint_googly": {}, - "disableEncryptionWarning": "Aus Sicherheitsgründen können Sie die Verschlüsselung in einem Chat nicht deaktivieren, wo sie zuvor aktiviert wurde.", + "disableEncryptionWarning": "Aus Sicherheitsgründen kannst du die Verschlüsselung in einem Chat nicht deaktivieren, wo sie zuvor aktiviert wurde.", "@disableEncryptionWarning": {}, "reopenChat": "Chat wieder eröffnen", "@reopenChat": {}, @@ -2587,7 +2587,7 @@ "sender": {} } }, - "verifyOtherDeviceDescription": "Wenn Sie ein anderes Gerät verifizieren, können diese Geräteschlüssel austauschen, was Ihre Sicherheit insgesamt erhöht. 💪 Wenn Sie eine Verifizierung starten, erscheint ein Pop-up in der App auf beiden Geräten. Dort sehen Sie dann eine Reihe von Emojis oder Zahlen, die Sie miteinander vergleichen müssen. Am besten halten Sie beide Geräte bereit, bevor Sie die Verifizierung starten. 🤳", + "verifyOtherDeviceDescription": "Wenn du ein anderes Gerät verifizieren, können diese Geräteschlüssel austauschen, was die Sicherheit insgesamt erhöht. 💪\n\nSobald du eine Verifizierung starten, erscheint ein Pop-up in der App auf beiden Geräten. Dort siehst du dann eine Reihe von Emojis oder Zahlen, die du miteinander vergleichen musst.\n\nAm besten hältst du beide Geräte bereit, bevor du die Verifizierung startest. 🤳", "@verifyOtherDeviceDescription": {}, "presenceStyle": "Statusmeldungen:", "@presenceStyle": { @@ -2756,7 +2756,7 @@ "@changeTheChatPermissions": {}, "changeTheVisibilityOfChatHistory": "Wechsele die Sichtbarkeit der Chat-Historie", "@changeTheVisibilityOfChatHistory": {}, - "chatPermissionsDescription": "Definieren Sie, welche Befugnisstufe für bestimmte Aktionen in diesem Chat erforderlich ist. Die Befugnisstufen 0, 50 und 100 stehen üblicherweise für Benutzer, Moderatoren und Admins, aber jede Abstufung ist möglich.", + "chatPermissionsDescription": "Einstellen, welches Level für bestimmte Aktionen in diesem Chat erforderlich ist. Die Level 0, 50 und 100 stehen üblicherweise für Benutzer, Moderatoren und Admins, aber jede Abstufung ist möglich.", "@chatPermissionsDescription": {}, "invitedBy": "📩 Eingeladen von {user}", "@invitedBy": { @@ -2775,9 +2775,9 @@ "@inviteOtherUsers": {}, "changeTheCanonicalRoomAlias": "Ändern der Hauptadresse für den öffentlichen Chat", "@changeTheCanonicalRoomAlias": {}, - "sendRoomNotifications": "Senden Sie eine @room-Benachrichtigung", + "sendRoomNotifications": "Sende eine @room-Benachrichtigung", "@sendRoomNotifications": {}, - "changeTheDescriptionOfTheGroup": "Ändern Sie die Beschreibung des Chats", + "changeTheDescriptionOfTheGroup": "Chat-Beschreibung ändern", "@changeTheDescriptionOfTheGroup": {}, "updateInstalled": "🎉 Update {version} installiert!", "@updateInstalled": { @@ -2790,7 +2790,7 @@ "@changelog": {}, "sendCanceled": "Senden abgebrochen", "@sendCanceled": {}, - "noChatsFoundHere": "Hier wurden noch keine Chats gefunden. Starten Sie einen neuen Chat mit jemandem, indem Sie die Schaltfläche unten verwenden. ⤵️", + "noChatsFoundHere": "Hier wurden noch keine Chats gefunden. Starte einen neuen Chat mit jemandem, indem du die Schaltfläche unten verwenden. ⤵️", "@noChatsFoundHere": {}, "whatIsAHomeserver": "Was ist ein Homeserver?", "@whatIsAHomeserver": {}, @@ -2801,5 +2801,37 @@ "discoverHomeservers": "Server suchen", "@discoverHomeservers": {}, "homeserverDescription": "Alle deine Daten werden auf dem Homeserver gespeichert, so wie bei einem Email Anbieter. Du kannst aussuchen, welchen Homeserver du benutzen willst und kannst trotzdem mit allen kommunizieren. Erfahre mehr auf https:/matrix.org.", - "@homeserverDescription": {} + "@homeserverDescription": {}, + "sendingAttachment": "Anhang wird gesendet ...", + "@sendingAttachment": {}, + "generatingVideoThumbnail": "Generiere Video-Vorschaubild ...", + "@generatingVideoThumbnail": {}, + "serverLimitReached": "Server-Limit erreicht! Warte {seconds} Sekunden ...", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + }, + "calculatingFileSize": "Dateigröße wird berechnet ...", + "@calculatingFileSize": {}, + "prepareSendingAttachment": "Anhang zum Senden vorbereiten ...", + "@prepareSendingAttachment": {}, + "compressVideo": "Video wird komprimiert ...", + "@compressVideo": {}, + "sendingAttachmentCountOfCount": "Sende Anhang {index} von {length} ...", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + }, + "fileIsTooBigForServer": "Kann nicht gesendet werden! Der Server unterstützt nur Anhänge bis höchstens {max}.", + "@fileIsTooBigForServer": { + "type": "text", + "placeholders": { + "max": {} + } + } } From d8acd92023dc38b59fbd121c820ef267a0514a1e Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sat, 12 Oct 2024 21:50:15 +0200 Subject: [PATCH 065/236] feat: Display warning banner on unverified devices --- assets/l10n/intl_en.arb | 4 ++- lib/config/themes.dart | 3 ++ lib/pages/chat_list/chat_list.dart | 28 +++++++++++++++++++ .../device_settings/device_settings.dart | 22 +++++++++++++++ .../device_settings/device_settings_view.dart | 13 +++++++++ lib/widgets/fluffy_chat_app.dart | 5 +++- 6 files changed, 73 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index b25e1977d..40ba16c50 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2786,5 +2786,7 @@ "placeholders": { "seconds": {} } - } + }, + "oneOfYourDevicesIsNotVerified": "One of your devices is not verified", + "noticeChatBackupDeviceVerification": "Note: When you connect all your devices to the chat backup, they are automatically verified." } diff --git a/lib/config/themes.dart b/lib/config/themes.dart index 4fcff7b75..3c06c06ee 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -130,6 +130,9 @@ abstract class FluffyThemes { borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), ), ), + snackBarTheme: const SnackBarThemeData( + behavior: SnackBarBehavior.floating, + ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: colorScheme.secondaryContainer, diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 9fe479728..35447e716 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -820,6 +820,7 @@ class ChatListController extends State bool waitForFirstSync = false; Future _waitForFirstSync() async { + final router = GoRouter.of(context); final client = Matrix.of(context).client; await client.roomsLoading; await client.accountDataLoading; @@ -840,6 +841,33 @@ class ChatListController extends State setState(() { waitForFirstSync = true; }); + + if (client.userDeviceKeys[client.userID!]?.deviceKeys.values + .any((device) => !device.verified && !device.blocked) ?? + false) { + late final ScaffoldFeatureController controller; + final theme = Theme.of(context); + controller = ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 15), + backgroundColor: theme.colorScheme.errorContainer, + content: Text( + L10n.of(context).oneOfYourDevicesIsNotVerified, + style: TextStyle( + color: theme.colorScheme.onErrorContainer, + ), + ), + action: SnackBarAction( + onPressed: () { + controller.close(); + router.go('/rooms/settings/devices'); + }, + textColor: theme.colorScheme.onErrorContainer, + label: L10n.of(context).settings, + ), + ), + ); + } } void cancelAction() { diff --git a/lib/pages/device_settings/device_settings.dart b/lib/pages/device_settings/device_settings.dart index daff3ad28..bb5293e0e 100644 --- a/lib/pages/device_settings/device_settings.dart +++ b/lib/pages/device_settings/device_settings.dart @@ -32,6 +32,28 @@ class DevicesSettingsController extends State { bool loadingDeletingDevices = false; String? errorDeletingDevices; + bool? chatBackupEnabled; + + @override + void initState() { + _checkChatBackup(); + super.initState(); + } + + void _checkChatBackup() async { + final client = Matrix.of(context).client; + if (client.encryption?.keyManager.enabled == true) { + if (await client.encryption?.keyManager.isCached() == false || + await client.encryption?.crossSigning.isCached() == false || + client.isUnknownSession && !mounted) { + setState(() { + chatBackupEnabled = false; + }); + return; + } + } + } + void removeDevicesAction(List devices) async { if (await showOkCancelAlertDialog( context: context, diff --git a/lib/pages/device_settings/device_settings_view.dart b/lib/pages/device_settings/device_settings_view.dart index 32a48b561..88d5c0476 100644 --- a/lib/pages/device_settings/device_settings_view.dart +++ b/lib/pages/device_settings/device_settings_view.dart @@ -48,6 +48,19 @@ class DevicesSettingsView extends StatelessWidget { return Column( mainAxisSize: MainAxisSize.min, children: [ + if (controller.chatBackupEnabled == false) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: ListTile( + leading: const CircleAvatar( + child: Icon(Icons.info_outlined), + ), + subtitle: Text( + L10n.of(context) + .noticeChatBackupDeviceVerification, + ), + ), + ), if (controller.thisDevice != null) ...[ Container( padding: const EdgeInsets.symmetric( diff --git a/lib/widgets/fluffy_chat_app.dart b/lib/widgets/fluffy_chat_app.dart index dfab1a6c3..661fb3362 100644 --- a/lib/widgets/fluffy_chat_app.dart +++ b/lib/widgets/fluffy_chat_app.dart @@ -34,7 +34,10 @@ class FluffyChatApp extends StatelessWidget { // Router must be outside of build method so that hot reload does not reset // the current path. - static final GoRouter router = GoRouter(routes: AppRoutes.routes); + static final GoRouter router = GoRouter( + routes: AppRoutes.routes, + debugLogDiagnostics: true, + ); @override Widget build(BuildContext context) { From d14698ee942660551388f6308ce5706a1e8d3704 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 13 Oct 2024 09:06:34 +0200 Subject: [PATCH 066/236] refactor: Improve delete device UX flow --- .../device_settings/device_settings.dart | 24 +++++-------------- .../device_settings/device_settings_view.dart | 17 ++++--------- lib/utils/uia_request_manager.dart | 10 +++++--- 3 files changed, 18 insertions(+), 33 deletions(-) diff --git a/lib/pages/device_settings/device_settings.dart b/lib/pages/device_settings/device_settings.dart index bb5293e0e..4beb2c649 100644 --- a/lib/pages/device_settings/device_settings.dart +++ b/lib/pages/device_settings/device_settings.dart @@ -9,7 +9,6 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/device_settings/device_settings_view.dart'; import 'package:fluffychat/pages/key_verification/key_verification_dialog.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; import '../../widgets/matrix.dart'; class DevicesSettings extends StatefulWidget { @@ -29,9 +28,6 @@ class DevicesSettingsController extends State { void reload() => setState(() => devices = null); - bool loadingDeletingDevices = false; - String? errorDeletingDevices; - bool? chatBackupEnabled; @override @@ -69,24 +65,16 @@ class DevicesSettingsController extends State { deviceIds.add(userDevice.deviceId); } - try { - setState(() { - loadingDeletingDevices = true; - errorDeletingDevices = null; - }); - await matrix.client.uiaRequestBackground( + await showFutureLoadingDialog( + context: context, + future: () => matrix.client.uiaRequestBackground( (auth) => matrix.client.deleteDevices( deviceIds, auth: auth, ), - ); - reload(); - } catch (e, s) { - Logs().w('Error while deleting devices', e, s); - setState(() => errorDeletingDevices = e.toLocalizedString(context)); - } finally { - setState(() => loadingDeletingDevices = false); - } + ), + ); + reload(); } void renameDeviceAction(Device device) async { diff --git a/lib/pages/device_settings/device_settings_view.dart b/lib/pages/device_settings/device_settings_view.dart index 88d5c0476..01880512c 100644 --- a/lib/pages/device_settings/device_settings_view.dart +++ b/lib/pages/device_settings/device_settings_view.dart @@ -96,8 +96,7 @@ class DevicesSettingsView extends StatelessWidget { width: double.infinity, child: TextButton.icon( label: Text( - controller.errorDeletingDevices ?? - L10n.of(context).removeAllOtherDevices, + L10n.of(context).removeAllOtherDevices, ), style: TextButton.styleFrom( foregroundColor: @@ -105,16 +104,10 @@ class DevicesSettingsView extends StatelessWidget { backgroundColor: theme.colorScheme.errorContainer, ), - icon: controller.loadingDeletingDevices - ? const CircularProgressIndicator.adaptive( - strokeWidth: 2, - ) - : const Icon(Icons.delete_outline), - onPressed: controller.loadingDeletingDevices - ? null - : () => controller.removeDevicesAction( - controller.notThisDevice, - ), + icon: const Icon(Icons.delete_outline), + onPressed: () => controller.removeDevicesAction( + controller.notThisDevice, + ), ), ), ) diff --git a/lib/utils/uia_request_manager.dart b/lib/utils/uia_request_manager.dart index 440dfda37..414e5e647 100644 --- a/lib/utils/uia_request_manager.dart +++ b/lib/utils/uia_request_manager.dart @@ -5,11 +5,15 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:fluffychat/widgets/fluffy_chat_app.dart'; import 'package:fluffychat/widgets/matrix.dart'; extension UiaRequestManager on MatrixState { Future uiaRequestHandler(UiaRequest uiaRequest) async { final l10n = L10n.of(context); + final navigatorContext = + FluffyChatApp.router.routerDelegate.navigatorKey.currentContext ?? + context; try { if (uiaRequest.state != UiaRequestState.waitForUser || uiaRequest.nextStages.isEmpty) { @@ -22,7 +26,7 @@ extension UiaRequestManager on MatrixState { case AuthenticationTypes.password: final input = cachedPassword ?? (await showTextInputDialog( - context: context, + context: navigatorContext, title: l10n.pleaseEnterYourPassword, okLabel: l10n.ok, cancelLabel: l10n.cancel, @@ -63,7 +67,7 @@ extension UiaRequestManager on MatrixState { if (OkCancelResult.ok == await showOkCancelAlertDialog( useRootNavigator: false, - context: context, + context: navigatorContext, title: l10n.weSentYouAnEmail, message: l10n.pleaseClickOnLink, okLabel: l10n.iHaveClickedOnLink, @@ -88,7 +92,7 @@ extension UiaRequestManager on MatrixState { await showOkCancelAlertDialog( useRootNavigator: false, message: l10n.pleaseFollowInstructionsOnWeb, - context: context, + context: navigatorContext, okLabel: l10n.next, cancelLabel: l10n.cancel, )) { From 05d5577a7b0c7648944e1bfc0637f00b82d4a998 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 13 Oct 2024 09:14:47 +0200 Subject: [PATCH 067/236] chore: Nicer batch for power level roles --- lib/pages/chat_details/participant_list_item.dart | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/pages/chat_details/participant_list_item.dart b/lib/pages/chat_details/participant_list_item.dart index 183b3e4fc..02ff6df6f 100644 --- a/lib/pages/chat_details/participant_list_item.dart +++ b/lib/pages/chat_details/participant_list_item.dart @@ -51,22 +51,18 @@ class ParticipantListItem extends StatelessWidget { if (permissionBatch.isNotEmpty) Container( padding: const EdgeInsets.symmetric( - horizontal: 4, + horizontal: 8, vertical: 2, ), - margin: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: theme.colorScheme.primary, - ), + borderRadius: BorderRadius.circular(16), ), child: Text( permissionBatch, style: TextStyle( - fontSize: 14, - color: theme.colorScheme.primary, + fontSize: 12, + color: theme.colorScheme.onPrimaryContainer, ), ), ), From 831adceecaa16b4ef2e978a6bcedaf6d59dbb6be Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 13 Oct 2024 09:52:05 +0200 Subject: [PATCH 068/236] refactor: Better future loading dialog without flickering --- lib/config/themes.dart | 2 +- lib/pages/archive/archive.dart | 2 +- lib/pages/bootstrap/bootstrap_dialog.dart | 2 +- lib/pages/chat/chat.dart | 2 +- lib/pages/chat/chat_view.dart | 2 +- lib/pages/chat/events/message_reactions.dart | 2 +- lib/pages/chat/pinned_events.dart | 2 +- lib/pages/chat/send_location_dialog.dart | 2 +- .../chat_access_settings_controller.dart | 2 +- lib/pages/chat_details/chat_details.dart | 2 +- .../chat_encryption_settings.dart | 2 +- lib/pages/chat_list/chat_list.dart | 2 +- lib/pages/chat_list/chat_list_item.dart | 2 +- lib/pages/chat_list/space_view.dart | 2 +- .../chat_permissions_settings.dart | 2 +- .../device_settings/device_settings.dart | 2 +- .../invitation_selection.dart | 2 +- .../key_verification_dialog.dart | 2 +- lib/pages/login/login.dart | 2 +- lib/pages/settings/settings.dart | 2 +- lib/pages/settings_3pid/settings_3pid.dart | 2 +- .../settings_emotes/settings_emotes.dart | 2 +- .../settings_ignore_list.dart | 2 +- .../settings_ignore_list_view.dart | 2 +- .../settings_notifications.dart | 2 +- .../settings_security/settings_security.dart | 2 +- lib/pages/settings_style/settings_style.dart | 3 +- .../user_bottom_sheet/user_bottom_sheet.dart | 2 +- .../event_extension.dart | 2 +- .../matrix_file_extension.dart | 2 +- lib/utils/url_launcher.dart | 2 +- lib/widgets/chat_settings_popup_menu.dart | 2 +- lib/widgets/future_loading_dialog.dart | 141 ++++++++++++++++++ lib/widgets/matrix.dart | 13 +- lib/widgets/public_room_bottom_sheet.dart | 2 +- pubspec.lock | 8 - pubspec.yaml | 1 - 37 files changed, 175 insertions(+), 55 deletions(-) create mode 100644 lib/widgets/future_loading_dialog.dart diff --git a/lib/config/themes.dart b/lib/config/themes.dart index 3c06c06ee..2dcfe9dbb 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -127,7 +127,7 @@ abstract class FluffyThemes { ), dialogTheme: DialogTheme( shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), + borderRadius: BorderRadius.circular(AppConfig.borderRadius), ), ), snackBarTheme: const SnackBarThemeData( diff --git a/lib/pages/archive/archive.dart b/lib/pages/archive/archive.dart index d9550cd15..64693d7e3 100644 --- a/lib/pages/archive/archive.dart +++ b/lib/pages/archive/archive.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/archive/archive_view.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; class Archive extends StatefulWidget { diff --git a/lib/pages/bootstrap/bootstrap_dialog.dart b/lib/pages/bootstrap/bootstrap_dialog.dart index 322f73e18..89659ec7c 100644 --- a/lib/pages/bootstrap/bootstrap_dialog.dart +++ b/lib/pages/bootstrap/bootstrap_dialog.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/matrix.dart'; @@ -12,6 +11,7 @@ import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../utils/adaptive_bottom_sheet.dart'; import '../key_verification/key_verification_dialog.dart'; diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index d872a02f9..9d36f135e 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -11,7 +11,6 @@ import 'package:desktop_drop/desktop_drop.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; @@ -33,6 +32,7 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/account_bundles.dart'; import '../../utils/localized_exception_extension.dart'; diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 38484baa6..221153d27 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:badges/badges.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; @@ -18,6 +17,7 @@ import 'package:fluffychat/pages/chat/reply_display.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; import 'package:fluffychat/widgets/connection_status_header.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:fluffychat/widgets/unread_rooms_badge.dart'; diff --git a/lib/pages/chat/events/message_reactions.dart b/lib/pages/chat/events/message_reactions.dart index a4b2addbc..656f650c6 100644 --- a/lib/pages/chat/events/message_reactions.dart +++ b/lib/pages/chat/events/message_reactions.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart' show IterableExtension; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; diff --git a/lib/pages/chat/pinned_events.dart b/lib/pages/chat/pinned_events.dart index cae6bd2c9..e56b34d48 100644 --- a/lib/pages/chat/pinned_events.dart +++ b/lib/pages/chat/pinned_events.dart @@ -4,12 +4,12 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; class PinnedEvents extends StatelessWidget { final ChatController controller; diff --git a/lib/pages/chat/send_location_dialog.dart b/lib/pages/chat/send_location_dialog.dart index 71d13b2c1..bb5c9155b 100644 --- a/lib/pages/chat/send_location_dialog.dart +++ b/lib/pages/chat/send_location_dialog.dart @@ -4,11 +4,11 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:geolocator/geolocator.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat/events/map_bubble.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; class SendLocationDialog extends StatefulWidget { final Room room; diff --git a/lib/pages/chat_access_settings/chat_access_settings_controller.dart b/lib/pages/chat_access_settings/chat_access_settings_controller.dart index 11fd0fbc6..6ea313d1e 100644 --- a/lib/pages/chat_access_settings/chat_access_settings_controller.dart +++ b/lib/pages/chat_access_settings/chat_access_settings_controller.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart' hide Visibility; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat_access_settings/chat_access_settings_page.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; class ChatAccessSettings extends StatefulWidget { diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 4159249d7..0ee925e0d 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; @@ -13,6 +12,7 @@ import 'package:fluffychat/pages/settings/settings.dart'; import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; enum AliasActions { copy, delete, setCanonical } diff --git a/lib/pages/chat_encryption_settings/chat_encryption_settings.dart b/lib/pages/chat_encryption_settings/chat_encryption_settings.dart index c92ae61f7..35cabad91 100644 --- a/lib/pages/chat_encryption_settings/chat_encryption_settings.dart +++ b/lib/pages/chat_encryption_settings/chat_encryption_settings.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings_view.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../key_verification/key_verification_dialog.dart'; diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 35447e716..22b4e5186 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -9,7 +9,6 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:cross_file/cross_file.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_shortcuts/flutter_shortcuts.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; @@ -24,6 +23,7 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/show_update_snackbar.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../../utils/account_bundles.dart'; import '../../config/setting_keys.dart'; import '../../utils/matrix_sdk_extensions/matrix_file_extension.dart'; diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 4e6ba56b7..9224f5ee7 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/room_status_extension.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; import '../../config/themes.dart'; import '../../utils/date_time_extension.dart'; diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index a1936c02c..1a83fa043 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; @@ -15,6 +14,7 @@ import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/public_room_bottom_sheet.dart'; diff --git a/lib/pages/chat_permissions_settings/chat_permissions_settings.dart b/lib/pages/chat_permissions_settings/chat_permissions_settings.dart index 2211a8fac..e42917b94 100644 --- a/lib/pages/chat_permissions_settings/chat_permissions_settings.dart +++ b/lib/pages/chat_permissions_settings/chat_permissions_settings.dart @@ -3,11 +3,11 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings_view.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/permission_slider_dialog.dart'; diff --git a/lib/pages/device_settings/device_settings.dart b/lib/pages/device_settings/device_settings.dart index 4beb2c649..5d9b1ce39 100644 --- a/lib/pages/device_settings/device_settings.dart +++ b/lib/pages/device_settings/device_settings.dart @@ -3,12 +3,12 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/encryption/utils/key_verification.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/device_settings/device_settings_view.dart'; import 'package:fluffychat/pages/key_verification/key_verification_dialog.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../widgets/matrix.dart'; class DevicesSettings extends StatefulWidget { diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 102234333..7e5d2039e 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/invitation_selection/invitation_selection_view.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/localized_exception_extension.dart'; diff --git a/lib/pages/key_verification/key_verification_dialog.dart b/lib/pages/key_verification/key_verification_dialog.dart index 0bbee5ed1..2767b18a5 100644 --- a/lib/pages/key_verification/key_verification_dialog.dart +++ b/lib/pages/key_verification/key_verification_dialog.dart @@ -6,11 +6,11 @@ import 'package:flutter/services.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; class KeyVerificationDialog extends StatefulWidget { Future show(BuildContext context) => showAdaptiveDialog( diff --git a/lib/pages/login/login.dart b/lib/pages/login/login.dart index d47d2cc09..af79b7fe8 100644 --- a/lib/pages/login/login.dart +++ b/lib/pages/login/login.dart @@ -4,10 +4,10 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/platform_infos.dart'; import 'login_view.dart'; diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index ca3e29285..e1b713655 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -5,12 +5,12 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../widgets/matrix.dart'; import '../bootstrap/bootstrap_dialog.dart'; import 'settings_view.dart'; diff --git a/lib/pages/settings_3pid/settings_3pid.dart b/lib/pages/settings_3pid/settings_3pid.dart index 76dbc50bf..6b333f088 100644 --- a/lib/pages/settings_3pid/settings_3pid.dart +++ b/lib/pages/settings_3pid/settings_3pid.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'settings_3pid_view.dart'; diff --git a/lib/pages/settings_emotes/settings_emotes.dart b/lib/pages/settings_emotes/settings_emotes.dart index 37a18d8fc..6ace6a098 100644 --- a/lib/pages/settings_emotes/settings_emotes.dart +++ b/lib/pages/settings_emotes/settings_emotes.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:http/http.dart' hide Client; import 'package:matrix/matrix.dart'; @@ -14,6 +13,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../widgets/matrix.dart'; import 'import_archive_dialog.dart'; import 'settings_emotes_view.dart'; diff --git a/lib/pages/settings_ignore_list/settings_ignore_list.dart b/lib/pages/settings_ignore_list/settings_ignore_list.dart index c2ad3404c..74643af50 100644 --- a/lib/pages/settings_ignore_list/settings_ignore_list.dart +++ b/lib/pages/settings_ignore_list/settings_ignore_list.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../widgets/matrix.dart'; import 'settings_ignore_list_view.dart'; diff --git a/lib/pages/settings_ignore_list/settings_ignore_list_view.dart b/lib/pages/settings_ignore_list/settings_ignore_list_view.dart index 09922c308..71ed6ce43 100644 --- a/lib/pages/settings_ignore_list/settings_ignore_list_view.dart +++ b/lib/pages/settings_ignore_list/settings_ignore_list_view.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import '../../widgets/matrix.dart'; import 'settings_ignore_list.dart'; diff --git a/lib/pages/settings_notifications/settings_notifications.dart b/lib/pages/settings_notifications/settings_notifications.dart index ca47e8e28..1bc00edb4 100644 --- a/lib/pages/settings_notifications/settings_notifications.dart +++ b/lib/pages/settings_notifications/settings_notifications.dart @@ -3,10 +3,10 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../widgets/matrix.dart'; import 'settings_notifications_view.dart'; diff --git a/lib/pages/settings_security/settings_security.dart b/lib/pages/settings_security/settings_security.dart index 7d08eda47..6134073b2 100644 --- a/lib/pages/settings_security/settings_security.dart +++ b/lib/pages/settings_security/settings_security.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/widgets/app_lock.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../bootstrap/bootstrap_dialog.dart'; import 'settings_security_view.dart'; diff --git a/lib/pages/settings_style/settings_style.dart b/lib/pages/settings_style/settings_style.dart index e63cb3d16..121a3504c 100644 --- a/lib/pages/settings_style/settings_style.dart +++ b/lib/pages/settings_style/settings_style.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/utils/file_selector.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/theme_builder.dart'; import '../../widgets/matrix.dart'; import 'settings_style_view.dart'; diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart index 0a2cc267a..737bb96df 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/permission_slider_dialog.dart'; import '../../widgets/matrix.dart'; import 'user_bottom_sheet_view.dart'; diff --git a/lib/utils/matrix_sdk_extensions/event_extension.dart b/lib/utils/matrix_sdk_extensions/event_extension.dart index ddd5abe80..2eb5473d8 100644 --- a/lib/utils/matrix_sdk_extensions/event_extension.dart +++ b/lib/utils/matrix_sdk_extensions/event_extension.dart @@ -4,10 +4,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:async/async.dart' as async; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/size_string.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'matrix_file_extension.dart'; extension LocalizedBody on Event { diff --git a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart index e9dd9e21f..97dc1d8d9 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart @@ -4,13 +4,13 @@ import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:share_plus/share_plus.dart'; import 'package:universal_html/html.dart' as html; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/size_string.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; extension MatrixFileExtension on MatrixFile { void save(BuildContext context) async { diff --git a/lib/utils/url_launcher.dart b/lib/utils/url_launcher.dart index a5c006998..085462150 100644 --- a/lib/utils/url_launcher.dart +++ b/lib/utils/url_launcher.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:punycode/punycode.dart'; @@ -12,6 +11,7 @@ import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/public_room_bottom_sheet.dart'; import 'platform_infos.dart'; diff --git a/lib/widgets/chat_settings_popup_menu.dart b/lib/widgets/chat_settings_popup_menu.dart index 4139e6bbe..e7d6142ea 100644 --- a/lib/widgets/chat_settings_popup_menu.dart +++ b/lib/widgets/chat_settings_popup_menu.dart @@ -5,11 +5,11 @@ import 'package:flutter/services.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'matrix.dart'; enum ChatPopupMenuActions { details, mute, unmute, leave, search } diff --git a/lib/widgets/future_loading_dialog.dart b/lib/widgets/future_loading_dialog.dart new file mode 100644 index 000000000..bd042487f --- /dev/null +++ b/lib/widgets/future_loading_dialog.dart @@ -0,0 +1,141 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:async/async.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:fluffychat/utils/localized_exception_extension.dart'; + +/// Displays a loading dialog which reacts to the given [future]. The dialog +/// will be dismissed and the value will be returned when the future completes. +/// If an error occured, then [onError] will be called and this method returns +/// null. +Future> showFutureLoadingDialog({ + required BuildContext context, + required Future Function() future, + String? title, + String? backLabel, + String Function(dynamic exception)? onError, + bool barrierDismissible = false, +}) async { + final futureExec = future(); + final resultFuture = ResultFuture(futureExec); + + var i = 3; + do { + final result = resultFuture.result; + if (result != null) { + if (result.isError) break; + return result; + } + await Future.delayed(const Duration(milliseconds: 100)); + i--; + } while (i > 0); + + final result = await showAdaptiveDialog>( + context: context, + barrierDismissible: barrierDismissible, + builder: (BuildContext context) => LoadingDialog( + future: futureExec, + title: title, + backLabel: backLabel, + onError: onError, + ), + ); + return result ?? + Result.error( + Exception('FutureDialog canceled'), + StackTrace.current, + ); +} + +class LoadingDialog extends StatefulWidget { + final String? title; + final String? backLabel; + final Future future; + final String Function(dynamic exception)? onError; + + const LoadingDialog({ + super.key, + required this.future, + this.title, + this.onError, + this.backLabel, + }); + @override + LoadingDialogState createState() => LoadingDialogState(); +} + +class LoadingDialogState extends State { + Object? exception; + StackTrace? stackTrace; + + @override + void initState() { + super.initState(); + widget.future.then( + (result) => Navigator.of(context).pop>(Result.value(result)), + onError: (e, s) => setState(() { + exception = e; + stackTrace = s; + }), + ); + } + + @override + Widget build(BuildContext context) { + final exception = this.exception; + final titleLabel = exception != null + ? widget.onError?.call(exception) ?? + exception.toLocalizedString(context) + : widget.title ?? L10n.of(context).loadingPleaseWait; + + return AlertDialog.adaptive( + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 256), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (exception == null) + const CircularProgressIndicator.adaptive() + else + Icon( + Icons.error_outline_outlined, + color: Theme.of(context).colorScheme.error, + size: 48, + ), + const SizedBox(width: 20), + Expanded( + child: Text( + titleLabel, + maxLines: 2, + textAlign: TextAlign.left, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + actions: exception == null + ? null + : [ + TextButton( + onPressed: () => Navigator.of(context).pop>( + Result.error( + exception, + stackTrace, + ), + ), + child: Text(widget.backLabel ?? L10n.of(context).close), + ), + ], + ); + } +} + +extension DeprecatedApiAccessExtension on Result { + T? get result => asValue?.value; + + Object? get error => asError?.error; +} diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index c425e0688..30490a31c 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -8,7 +8,6 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:desktop_notifications/desktop_notifications.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; @@ -21,12 +20,12 @@ import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/init_with_restore.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/uia_request_manager.dart'; import 'package:fluffychat/utils/voip_plugin.dart'; import 'package:fluffychat/widgets/fluffy_chat_app.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../config/app_config.dart'; import '../config/setting_keys.dart'; import '../pages/key_verification/key_verification_dialog.dart'; @@ -234,16 +233,6 @@ class MatrixState extends State with WidgetsBindingObserver { } else { initSettings(); } - initLoadingDialog(); - } - - void initLoadingDialog() { - WidgetsBinding.instance.addPostFrameCallback((_) { - LoadingDialog.defaultTitle = L10n.of(context).loadingPleaseWait; - LoadingDialog.defaultBackLabel = L10n.of(context).close; - LoadingDialog.defaultOnError = - (e) => (e as Object?)!.toLocalizedString(context); - }); } Future initConfig() async { diff --git a/lib/widgets/public_room_bottom_sheet.dart b/lib/widgets/public_room_bottom_sheet.dart index 9acecb3f0..812f0055b 100644 --- a/lib/widgets/public_room_bottom_sheet.dart +++ b/lib/widgets/public_room_bottom_sheet.dart @@ -2,13 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; class PublicRoomBottomSheet extends StatelessWidget { diff --git a/pubspec.lock b/pubspec.lock index 9620ac4a0..91aa6c087 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -789,14 +789,6 @@ packages: description: flutter source: sdk version: "0.0.0" - future_loading_dialog: - dependency: "direct main" - description: - name: future_loading_dialog - sha256: "2718b1a308db452da32ab9bca9ad496ff92b683e217add9e92cf50520f90537e" - url: "https://pub.dev" - source: hosted - version: "0.3.0" geolocator: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 85dfb2d33..35094cfbc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,6 @@ dependencies: flutter_typeahead: ^5.2.0 flutter_web_auth_2: ^3.1.1 flutter_webrtc: ^0.11.7 - future_loading_dialog: ^0.3.0 geolocator: ^7.6.2 go_router: ^14.3.0 handy_window: ^0.4.0 From d78c45a13d31e93bd537de01bdac45797834728f Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 13 Oct 2024 10:00:56 +0200 Subject: [PATCH 069/236] chore: Follow up powerlevel role badges --- lib/pages/chat_details/participant_list_item.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/pages/chat_details/participant_list_item.dart b/lib/pages/chat_details/participant_list_item.dart index 02ff6df6f..c77407b91 100644 --- a/lib/pages/chat_details/participant_list_item.dart +++ b/lib/pages/chat_details/participant_list_item.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/config/app_config.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -51,19 +52,18 @@ class ParticipantListItem extends StatelessWidget { if (permissionBatch.isNotEmpty) Container( padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, + horizontal: 12, + vertical: 6, ), decoration: BoxDecoration( color: theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), ), child: Text( permissionBatch, - style: TextStyle( - fontSize: 12, - color: theme.colorScheme.onPrimaryContainer, - ), + style: TextStyle(color: theme.colorScheme.onPrimaryContainer), ), ), membershipBatch == null From d1e211adeed8e625672b229f4ddd8191b8598f37 Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 18 Oct 2024 15:49:46 +0200 Subject: [PATCH 070/236] refactor: Use adaptive dialog action --- lib/pages/chat/send_file_dialog.dart | 11 ++++---- lib/pages/chat/send_location_dialog.dart | 5 ++-- .../chat_details/participant_list_item.dart | 2 +- .../homeserver_picker_view.dart | 5 ++-- .../key_verification_dialog.dart | 9 +++--- lib/utils/error_reporter.dart | 7 +++-- lib/utils/size_string.dart | 15 ++++++---- lib/widgets/adaptive_dialog_action.dart | 28 +++++++++++++++++++ lib/widgets/future_loading_dialog.dart | 3 +- 9 files changed, 60 insertions(+), 25 deletions(-) create mode 100644 lib/widgets/adaptive_dialog_action.dart diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index 121c215d6..8d7fcaad0 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -14,6 +14,7 @@ import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/size_string.dart'; +import 'package:fluffychat/widgets/adaptive_dialog_action.dart'; import '../../utils/resize_video.dart'; class SendFileDialog extends StatefulWidget { @@ -288,14 +289,12 @@ class SendFileDialogState extends State { title: Text(sendStr), content: contentWidget, actions: [ - TextButton( - onPressed: () { - // just close the dialog - Navigator.of(context, rootNavigator: false).pop(); - }, + AdaptiveDialogAction( + onPressed: () => + Navigator.of(context, rootNavigator: false).pop(), child: Text(L10n.of(context).cancel), ), - TextButton( + AdaptiveDialogAction( onPressed: _send, child: Text(L10n.of(context).send), ), diff --git a/lib/pages/chat/send_location_dialog.dart b/lib/pages/chat/send_location_dialog.dart index bb5c9155b..31a311f72 100644 --- a/lib/pages/chat/send_location_dialog.dart +++ b/lib/pages/chat/send_location_dialog.dart @@ -8,6 +8,7 @@ import 'package:geolocator/geolocator.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat/events/map_bubble.dart'; +import 'package:fluffychat/widgets/adaptive_dialog_action.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; class SendLocationDialog extends StatefulWidget { @@ -114,12 +115,12 @@ class SendLocationDialogState extends State { title: Text(L10n.of(context).shareLocation), content: contentWidget, actions: [ - TextButton( + AdaptiveDialogAction( onPressed: Navigator.of(context, rootNavigator: false).pop, child: Text(L10n.of(context).cancel), ), if (position != null) - TextButton( + AdaptiveDialogAction( onPressed: isSending ? null : sendAction, child: Text(L10n.of(context).send), ), diff --git a/lib/pages/chat_details/participant_list_item.dart b/lib/pages/chat_details/participant_list_item.dart index c77407b91..b71f37310 100644 --- a/lib/pages/chat_details/participant_list_item.dart +++ b/lib/pages/chat_details/participant_list_item.dart @@ -1,9 +1,9 @@ -import 'package:fluffychat/config/app_config.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import '../../widgets/avatar.dart'; import '../user_bottom_sheet/user_bottom_sheet.dart'; diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index 71ca58fd3..4aa4d2929 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -5,6 +5,7 @@ import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/widgets/adaptive_dialog_action.dart'; import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../config/themes.dart'; @@ -107,7 +108,7 @@ class HomeserverPickerView extends StatelessWidget { text: L10n.of(context).homeserverDescription, ), actions: [ - TextButton( + AdaptiveDialogAction( onPressed: () => launchUrl( Uri.https('servers.joinmatrix.org'), ), @@ -115,7 +116,7 @@ class HomeserverPickerView extends StatelessWidget { L10n.of(context).discoverHomeservers, ), ), - TextButton( + AdaptiveDialogAction( onPressed: Navigator.of(context).pop, child: Text(L10n.of(context).close), ), diff --git a/lib/pages/key_verification/key_verification_dialog.dart b/lib/pages/key_verification/key_verification_dialog.dart index 2767b18a5..8d195149b 100644 --- a/lib/pages/key_verification/key_verification_dialog.dart +++ b/lib/pages/key_verification/key_verification_dialog.dart @@ -9,6 +9,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/widgets/adaptive_dialog_action.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; @@ -150,7 +151,7 @@ class KeyVerificationPageState extends State { ), ); buttons.add( - TextButton( + AdaptiveDialogAction( child: Text( L10n.of(context).submit, ), @@ -158,7 +159,7 @@ class KeyVerificationPageState extends State { ), ); buttons.add( - TextButton( + AdaptiveDialogAction( child: Text( L10n.of(context).skip, ), @@ -320,7 +321,7 @@ class KeyVerificationPageState extends State { ], ); buttons.add( - TextButton( + AdaptiveDialogAction( child: Text( L10n.of(context).close, ), @@ -343,7 +344,7 @@ class KeyVerificationPageState extends State { ], ); buttons.add( - TextButton( + AdaptiveDialogAction( child: Text( L10n.of(context).close, ), diff --git a/lib/utils/error_reporter.dart b/lib/utils/error_reporter.dart index 28e7b374a..72cfb83a2 100644 --- a/lib/utils/error_reporter.dart +++ b/lib/utils/error_reporter.dart @@ -8,6 +8,7 @@ import 'package:matrix/matrix.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/widgets/adaptive_dialog_action.dart'; class ErrorReporter { final BuildContext context; @@ -34,17 +35,17 @@ class ErrorReporter { ), ), actions: [ - TextButton( + AdaptiveDialogAction( onPressed: () => Navigator.of(context).pop(), child: Text(L10n.of(context).close), ), - TextButton( + AdaptiveDialogAction( onPressed: () => Clipboard.setData( ClipboardData(text: text), ), child: Text(L10n.of(context).copy), ), - TextButton( + AdaptiveDialogAction( onPressed: () => launchUrl( AppConfig.newIssueUrl.resolveUri( Uri( diff --git a/lib/utils/size_string.dart b/lib/utils/size_string.dart index 930a97f9a..315033076 100644 --- a/lib/utils/size_string.dart +++ b/lib/utils/size_string.dart @@ -1,18 +1,21 @@ extension SizeString on num { String get sizeString { var size = toDouble(); - if (size < 1000000) { + if (size < 1000) { + return '${size.round()} Bytes'; + } + if (size < 1000 * 1000) { size = size / 1000; size = (size * 10).round() / 10; return '${size.toString()} KB'; - } else if (size < 1000000000) { + } + if (size < 1000 * 1000 * 1000) { size = size / 1000000; size = (size * 10).round() / 10; return '${size.toString()} MB'; - } else { - size = size / 1000000000; - size = (size * 10).round() / 10; - return '${size.toString()} GB'; } + size = size / 1000 * 1000 * 1000 * 1000; + size = (size * 10).round() / 10; + return '${size.toString()} GB'; } } diff --git a/lib/widgets/adaptive_dialog_action.dart b/lib/widgets/adaptive_dialog_action.dart new file mode 100644 index 000000000..3df21c33b --- /dev/null +++ b/lib/widgets/adaptive_dialog_action.dart @@ -0,0 +1,28 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class AdaptiveDialogAction extends StatelessWidget { + final VoidCallback? onPressed; + final Widget child; + + const AdaptiveDialogAction({ + super.key, + required this.onPressed, + required this.child, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + switch (theme.platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + return TextButton(onPressed: onPressed, child: child); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + return CupertinoDialogAction(onPressed: onPressed, child: child); + } + } +} diff --git a/lib/widgets/future_loading_dialog.dart b/lib/widgets/future_loading_dialog.dart index bd042487f..3df0cf8c0 100644 --- a/lib/widgets/future_loading_dialog.dart +++ b/lib/widgets/future_loading_dialog.dart @@ -6,6 +6,7 @@ import 'package:async/async.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/widgets/adaptive_dialog_action.dart'; /// Displays a loading dialog which reacts to the given [future]. The dialog /// will be dismissed and the value will be returned when the future completes. @@ -120,7 +121,7 @@ class LoadingDialogState extends State { actions: exception == null ? null : [ - TextButton( + AdaptiveDialogAction( onPressed: () => Navigator.of(context).pop>( Result.error( exception, From 0301c83f4045d494a786d1fc42838a0a74ebd2ae Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sat, 19 Oct 2024 08:15:32 +0200 Subject: [PATCH 071/236] fix: Wait for room invite before open in pushhelper --- lib/utils/background_push.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index a7ce8c6de..1ba2659a6 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -328,6 +328,11 @@ class BackgroundPush { } await client.roomsLoading; await client.accountDataLoading; + if (client.getRoomById(roomId) == null) { + await client + .waitForRoomInSync(roomId) + .timeout(const Duration(seconds: 30)); + } FluffyChatApp.router.go( client.getRoomById(roomId)?.membership == Membership.invite ? '/rooms' From 3faf6df558784a5722b40af6777e1b6ffd2e8032 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sat, 19 Oct 2024 08:58:02 +0200 Subject: [PATCH 072/236] feat: Nicer room creation UI --- lib/pages/chat/chat.dart | 9 ++--- lib/pages/chat/chat_event_list.dart | 4 +- lib/pages/chat/events/message.dart | 4 ++ .../events/room_creation_state_event.dart | 40 +++++++++++++++++++ .../filtered_timeline_extension.dart | 30 ++++++++++++++ 5 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 lib/pages/chat/events/room_creation_state_event.dart diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 9d36f135e..fce1d38a4 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -261,8 +261,7 @@ class ChatController extends State var readMarkerEventIndex = readMarkerEventId.isEmpty ? -1 : timeline!.events - .where((e) => e.isVisibleInGui || e.eventId == readMarkerEventId) - .toList() + .filterByVisibleInGui(exceptionEventId: readMarkerEventId) .indexWhere((e) => e.eventId == readMarkerEventId); // Read marker is existing but not found in first events. Try a single @@ -270,8 +269,7 @@ class ChatController extends State if (readMarkerEventId.isNotEmpty && readMarkerEventIndex == -1) { await timeline?.requestHistory(historyCount: _loadHistoryCount); readMarkerEventIndex = timeline!.events - .where((e) => e.isVisibleInGui || e.eventId == readMarkerEventId) - .toList() + .filterByVisibleInGui(exceptionEventId: readMarkerEventId) .indexWhere((e) => e.eventId == readMarkerEventId); } @@ -868,8 +866,7 @@ class ChatController extends State final eventIndex = foundEvent == null ? -1 : timeline!.events - .where((event) => event.isVisibleInGui || event.eventId == eventId) - .toList() + .filterByVisibleInGui(exceptionEventId: eventId) .indexOf(foundEvent); if (eventIndex == -1) { diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 65c920360..d4614af10 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -25,9 +25,7 @@ class ChatEventList extends StatelessWidget { Widget build(BuildContext context) { final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; - final events = controller.timeline!.events - .where((event) => event.isVisibleInGui) - .toList(); + final events = controller.timeline!.events.filterByVisibleInGui(); final animateInEventIndex = controller.animateInEventIndex; // create a map of eventId --> index to greatly improve performance of diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 11ffad745..032e8967d 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -6,6 +6,7 @@ import 'package:matrix/matrix.dart'; import 'package:swipe_to_action/swipe_to_action.dart'; import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat/events/room_creation_state_event.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/avatar.dart'; @@ -68,6 +69,9 @@ class Message extends StatelessWidget { if (event.type.startsWith('m.call.')) { return const SizedBox.shrink(); } + if (event.type == EventTypes.RoomCreate) { + return RoomCreationStateEvent(event: event); + } return StateMessage(event); } diff --git a/lib/pages/chat/events/room_creation_state_event.dart b/lib/pages/chat/events/room_creation_state_event.dart new file mode 100644 index 000000000..8960a801f --- /dev/null +++ b/lib/pages/chat/events/room_creation_state_event.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/utils/date_time_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/avatar.dart'; + +class RoomCreationStateEvent extends StatelessWidget { + final Event event; + const RoomCreationStateEvent({required this.event, super.key}); + + @override + Widget build(BuildContext context) { + final l10n = L10n.of(context); + final matrixLocals = MatrixLocals(l10n); + final theme = Theme.of(context); + final roomName = event.room.getLocalizedDisplayname(matrixLocals); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Avatar( + mxContent: event.room.avatar, + name: roomName, + size: Avatar.defaultSize * 2, + ), + Text( + roomName, + style: theme.textTheme.headlineSmall, + ), + Text( + '${event.originServerTs.localizedTime(context)} | ${l10n.countParticipants((event.room.summary.mJoinedMemberCount ?? 1) + (event.room.summary.mInvitedMemberCount ?? 0))}', + style: theme.textTheme.labelSmall, + ), + const SizedBox(height: 48), + ], + ); + } +} diff --git a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart index bd1cd8e46..cd9d223ee 100644 --- a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart +++ b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart @@ -2,6 +2,36 @@ import 'package:matrix/matrix.dart'; import '../../config/app_config.dart'; +extension VisibleInGuiExtension on List { + List filterByVisibleInGui({String? exceptionEventId}) { + final visibleEvents = + where((e) => e.isVisibleInGui || e.eventId == exceptionEventId) + .toList(); + + // Hide creation state events: + if (visibleEvents.isNotEmpty && + visibleEvents.last.type == EventTypes.RoomCreate) { + var i = visibleEvents.length - 2; + while (i > 0) { + final event = visibleEvents[i]; + if (!event.isState) break; + if (event.type == EventTypes.Encryption) { + i--; + continue; + } + if (event.type == EventTypes.RoomMember && + event.roomMemberChangeType == RoomMemberChangeType.acceptInvite) { + i--; + continue; + } + visibleEvents.removeAt(i); + i--; + } + } + return visibleEvents; + } +} + extension IsStateExtension on Event { bool get isVisibleInGui => // always filter out edit and reaction relationships From 31907f74de43c63546a02d1183d46f353817d814 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sat, 19 Oct 2024 09:16:33 +0200 Subject: [PATCH 073/236] chore: Follow up loading dialog --- lib/pages/bootstrap/bootstrap_dialog.dart | 1 + .../device_settings/device_settings.dart | 1 + lib/pages/settings_3pid/settings_3pid.dart | 1 + lib/widgets/future_loading_dialog.dart | 21 +++++++++++-------- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/pages/bootstrap/bootstrap_dialog.dart b/lib/pages/bootstrap/bootstrap_dialog.dart index 89659ec7c..19ddfb1e8 100644 --- a/lib/pages/bootstrap/bootstrap_dialog.dart +++ b/lib/pages/bootstrap/bootstrap_dialog.dart @@ -371,6 +371,7 @@ class BootstrapDialogState extends State { if (consent != OkCancelResult.ok) return; final req = await showFutureLoadingDialog( context: context, + delay: false, future: () async { await widget.client.updateUserDeviceKeys(); return widget.client diff --git a/lib/pages/device_settings/device_settings.dart b/lib/pages/device_settings/device_settings.dart index 5d9b1ce39..7d863ef72 100644 --- a/lib/pages/device_settings/device_settings.dart +++ b/lib/pages/device_settings/device_settings.dart @@ -67,6 +67,7 @@ class DevicesSettingsController extends State { await showFutureLoadingDialog( context: context, + delay: false, future: () => matrix.client.uiaRequestBackground( (auth) => matrix.client.deleteDevices( deviceIds, diff --git a/lib/pages/settings_3pid/settings_3pid.dart b/lib/pages/settings_3pid/settings_3pid.dart index 6b333f088..d73814608 100644 --- a/lib/pages/settings_3pid/settings_3pid.dart +++ b/lib/pages/settings_3pid/settings_3pid.dart @@ -53,6 +53,7 @@ class Settings3PidController extends State { if (ok != OkCancelResult.ok) return; final success = await showFutureLoadingDialog( context: context, + delay: false, future: () => Matrix.of(context).client.uiaRequestBackground( (auth) => Matrix.of(context).client.add3PID( clientSecret, diff --git a/lib/widgets/future_loading_dialog.dart b/lib/widgets/future_loading_dialog.dart index 3df0cf8c0..11b867dbf 100644 --- a/lib/widgets/future_loading_dialog.dart +++ b/lib/widgets/future_loading_dialog.dart @@ -19,20 +19,23 @@ Future> showFutureLoadingDialog({ String? backLabel, String Function(dynamic exception)? onError, bool barrierDismissible = false, + bool delay = true, }) async { final futureExec = future(); final resultFuture = ResultFuture(futureExec); - var i = 3; - do { - final result = resultFuture.result; - if (result != null) { - if (result.isError) break; - return result; + if (delay) { + var i = 3; + while (i > 0) { + final result = resultFuture.result; + if (result != null) { + if (result.isError) break; + return result; + } + await Future.delayed(const Duration(milliseconds: 100)); + i--; } - await Future.delayed(const Duration(milliseconds: 100)); - i--; - } while (i > 0); + } final result = await showAdaptiveDialog>( context: context, From 8c946815fb7ad3e83c318d53c2daa318b850f586 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 21 Oct 2024 10:55:37 +0200 Subject: [PATCH 074/236] docs: fix snapstore badge on website --- docs/index.html | 3 +-- docs/snap-store-badge.svg | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 docs/snap-store-badge.svg diff --git a/docs/index.html b/docs/index.html index b0205811c..fef1124fd 100644 --- a/docs/index.html +++ b/docs/index.html @@ -59,8 +59,7 @@ - diff --git a/docs/snap-store-badge.svg b/docs/snap-store-badge.svg new file mode 100644 index 000000000..294d8737f --- /dev/null +++ b/docs/snap-store-badge.svg @@ -0,0 +1 @@ + \ No newline at end of file From 32d868b5bdbaf41859fa29fd36e8884760279189 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 21 Oct 2024 15:23:35 +0200 Subject: [PATCH 075/236] build: Update matrix dart sdk --- lib/pages/settings_notifications/settings_notifications.dart | 1 - pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/pages/settings_notifications/settings_notifications.dart b/lib/pages/settings_notifications/settings_notifications.dart index 1bc00edb4..25d3e3038 100644 --- a/lib/pages/settings_notifications/settings_notifications.dart +++ b/lib/pages/settings_notifications/settings_notifications.dart @@ -102,7 +102,6 @@ class SettingsNotificationsController extends State { }); try { await Matrix.of(context).client.setPushRuleEnabled( - 'global', item.type, item.key, enabled, diff --git a/pubspec.lock b/pubspec.lock index 91aa6c087..43fc844e0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1231,10 +1231,10 @@ packages: dependency: "direct main" description: name: matrix - sha256: bf2127e89041441ccd873a6435ff289a8f45e5e83337db95493bb239b56ed4aa + sha256: e06783394db3a49dbcd98a45803cac7d735a2fb1e3aafef65ddf01e72e16fc83 url: "https://pub.dev" source: hosted - version: "0.33.0" + version: "0.34.0" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 35094cfbc..bdf36fce2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,7 @@ dependencies: keyboard_shortcuts: ^0.1.4 latlong2: ^0.9.1 linkify: ^5.0.0 - matrix: ^0.33.0 + matrix: ^0.34.0 mime: ^1.0.6 native_imaging: ^0.1.1 opus_caf_converter_dart: ^1.0.1 From 7d8369ab3043c31cccace793e568e73d21f0b6e5 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 21 Oct 2024 16:11:53 +0200 Subject: [PATCH 076/236] feat: Add default chat wallpaper --- assets/chat_wallpaper_dark.png | Bin 0 -> 47238 bytes assets/chat_wallpaper_light.png | Bin 0 -> 120529 bytes lib/pages/chat/chat_event_list.dart | 8 +--- lib/pages/chat/chat_view.dart | 41 +++++++++++------- lib/pages/chat/events/message.dart | 2 +- .../events/room_creation_state_event.dart | 21 ++++++++- lib/pages/chat/typing_indicators.dart | 4 +- 7 files changed, 47 insertions(+), 29 deletions(-) create mode 100644 assets/chat_wallpaper_dark.png create mode 100644 assets/chat_wallpaper_light.png diff --git a/assets/chat_wallpaper_dark.png b/assets/chat_wallpaper_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..88d1cf803675b7880db0ffd77a8dde360ca70aff GIT binary patch literal 47238 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 CC|~mc literal 0 HcmV?d00001 diff --git a/assets/chat_wallpaper_light.png b/assets/chat_wallpaper_light.png new file mode 100644 index 0000000000000000000000000000000000000000..781491c40a6a8ca1b53c32699ec4a3432fa33b80 GIT binary patch literal 120529 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

&ocLoc9A$wPy8_q%vWMD!f8oM3 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 KHfOcV!T$$A-C67a diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 50b0c5708..d4614af10 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -9,6 +9,7 @@ import 'package:fluffychat/pages/chat/events/message.dart'; import 'package:fluffychat/pages/chat/seen_by_row.dart'; import 'package:fluffychat/pages/chat/typing_indicators.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; +import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -33,6 +34,10 @@ class ChatEventList extends StatelessWidget { for (var i = 0; i < events.length; i++) { thisEventsKeyMap[events[i].eventId] = i; } + + final hasWallpaper = + controller.room.client.applicationAccountConfig.wallpaperUrl != null; + return SelectionArea( child: ListView.custom( padding: EdgeInsets.only( @@ -137,7 +142,8 @@ class ChatEventList extends StatelessWidget { i > 0 && controller.readMarkerEventId == event.eventId, nextEvent: i + 1 < events.length ? events[i + 1] : null, previousEvent: i > 0 ? events[i - 1] : null, - avatarPresenceBackgroundColor: Colors.transparent, + avatarPresenceBackgroundColor: + hasWallpaper ? Colors.transparent : null, ), ); }, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index c4240eda4..221153d27 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -260,30 +260,18 @@ class ChatView extends StatelessWidget { onDragExited: controller.onDragExited, child: Stack( children: [ - accountConfig.wallpaperUrl != null - ? Opacity( - opacity: accountConfig.wallpaperOpacity ?? 1, - child: MxcImage( - uri: accountConfig.wallpaperUrl, - fit: BoxFit.cover, - isThumbnail: true, - width: FluffyThemes.columnWidth * 4, - height: FluffyThemes.columnWidth * 4, - placeholder: (_) => Container(), - ), - ) - : ColorFiltered( - colorFilter: ColorFilter.mode( - theme.colorScheme.surfaceContainerHighest, - BlendMode.color, - ), - child: Image.asset( - 'assets/chat_wallpaper_${theme.brightness.name}.png', - fit: BoxFit.cover, - width: double.infinity, - height: double.infinity, - ), - ), + if (accountConfig.wallpaperUrl != null) + Opacity( + opacity: accountConfig.wallpaperOpacity ?? 1, + child: MxcImage( + uri: accountConfig.wallpaperUrl, + fit: BoxFit.cover, + isThumbnail: true, + width: FluffyThemes.columnWidth * 4, + height: FluffyThemes.columnWidth * 4, + placeholder: (_) => Container(), + ), + ), SafeArea( child: Column( children: [ @@ -320,7 +308,10 @@ class ChatView extends StatelessWidget { alignment: Alignment.center, child: Material( clipBehavior: Clip.hardEdge, - color: theme.colorScheme.surfaceBright, + color: theme + .colorScheme + // ignore: deprecated_member_use + .surfaceVariant, borderRadius: const BorderRadius.all( Radius.circular(24), ), diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 5d1631fec..032e8967d 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -84,7 +84,7 @@ class Message extends StatelessWidget { final ownMessage = event.senderId == client.userID; final alignment = ownMessage ? Alignment.topRight : Alignment.topLeft; // ignore: deprecated_member_use - var color = theme.colorScheme.surfaceBright; + var color = theme.colorScheme.surfaceVariant; final displayTime = event.type == EventTypes.RoomCreate || nextEvent == null || !event.originServerTs.sameEnvironment(nextEvent!.originServerTs); diff --git a/lib/pages/chat/events/room_creation_state_event.dart b/lib/pages/chat/events/room_creation_state_event.dart index 9362fd3d2..8960a801f 100644 --- a/lib/pages/chat/events/room_creation_state_event.dart +++ b/lib/pages/chat/events/room_creation_state_event.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; @@ -28,27 +27,11 @@ class RoomCreationStateEvent extends StatelessWidget { ), Text( roomName, - style: TextStyle( - fontSize: 24 * AppConfig.fontSizeFactor, - shadows: [ - Shadow( - color: theme.colorScheme.surface, - blurRadius: 3, - ), - ], - ), + style: theme.textTheme.headlineSmall, ), Text( '${event.originServerTs.localizedTime(context)} | ${l10n.countParticipants((event.room.summary.mJoinedMemberCount ?? 1) + (event.room.summary.mInvitedMemberCount ?? 0))}', - style: TextStyle( - fontSize: 12 * AppConfig.fontSizeFactor, - shadows: [ - Shadow( - color: theme.colorScheme.surface, - blurRadius: 3, - ), - ], - ), + style: theme.textTheme.labelSmall, ), const SizedBox(height: 48), ], diff --git a/lib/pages/chat/typing_indicators.dart b/lib/pages/chat/typing_indicators.dart index 13949f4aa..a58138a90 100644 --- a/lib/pages/chat/typing_indicators.dart +++ b/lib/pages/chat/typing_indicators.dart @@ -81,7 +81,9 @@ class TypingIndicators extends StatelessWidget { ), const SizedBox(width: 8), Material( - color: theme.colorScheme.surfaceBright, + color: + // ignore: deprecated_member_use + theme.colorScheme.surfaceVariant, borderRadius: const BorderRadius.all( Radius.circular(AppConfig.borderRadius), ), From 4ec70bd25cff27cc7ec9b5125415edef68f66ef3 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 21 Oct 2024 18:36:52 +0200 Subject: [PATCH 078/236] chore: Follow up new chat design --- lib/pages/chat/chat_event_list.dart | 6 +- lib/pages/chat/chat_view.dart | 219 +++++++++++++------------- lib/pages/chat/events/message.dart | 4 +- lib/pages/chat/typing_indicators.dart | 4 +- 4 files changed, 119 insertions(+), 114 deletions(-) diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index d4614af10..54fc526f5 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -34,6 +34,7 @@ class ChatEventList extends StatelessWidget { for (var i = 0; i < events.length; i++) { thisEventsKeyMap[events[i].eventId] = i; } + final theme = Theme.of(context); final hasWallpaper = controller.room.client.applicationAccountConfig.wallpaperUrl != null; @@ -142,8 +143,9 @@ class ChatEventList extends StatelessWidget { i > 0 && controller.readMarkerEventId == event.eventId, nextEvent: i + 1 < events.length ? events[i + 1] : null, previousEvent: i > 0 ? events[i - 1] : null, - avatarPresenceBackgroundColor: - hasWallpaper ? Colors.transparent : null, + avatarPresenceBackgroundColor: hasWallpaper + ? Colors.transparent + : theme.colorScheme.surfaceContainer, ), ); }, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 221153d27..a0aef8b1f 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -258,125 +258,130 @@ class ChatView extends StatelessWidget { onDragDone: controller.onDragDone, onDragEntered: controller.onDragEntered, onDragExited: controller.onDragExited, - child: Stack( - children: [ - if (accountConfig.wallpaperUrl != null) - Opacity( - opacity: accountConfig.wallpaperOpacity ?? 1, - child: MxcImage( - uri: accountConfig.wallpaperUrl, - fit: BoxFit.cover, - isThumbnail: true, - width: FluffyThemes.columnWidth * 4, - height: FluffyThemes.columnWidth * 4, - placeholder: (_) => Container(), + child: DecoratedBox( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainer, + ), + child: Stack( + children: [ + if (accountConfig.wallpaperUrl != null) + Opacity( + opacity: accountConfig.wallpaperOpacity ?? 1, + child: MxcImage( + uri: accountConfig.wallpaperUrl, + fit: BoxFit.cover, + isThumbnail: true, + width: FluffyThemes.columnWidth * 4, + height: FluffyThemes.columnWidth * 4, + placeholder: (_) => Container(), + ), ), - ), - SafeArea( - child: Column( - children: [ - Expanded( - child: GestureDetector( - onTap: controller.clearSingleSelectedEvent, - child: Builder( - builder: (context) { - if (controller.timeline == null) { - return const Center( - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), + SafeArea( + child: Column( + children: [ + Expanded( + child: GestureDetector( + onTap: controller.clearSingleSelectedEvent, + child: Builder( + builder: (context) { + if (controller.timeline == null) { + return const Center( + child: + CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ); + } + return ChatEventList( + controller: controller, ); - } - return ChatEventList( - controller: controller, - ); - }, + }, + ), ), ), - ), - if (controller.room.canSendDefaultMessages && - controller.room.membership == Membership.join) - Container( - margin: EdgeInsets.only( - bottom: bottomSheetPadding, - left: bottomSheetPadding, - right: bottomSheetPadding, - ), - constraints: const BoxConstraints( - maxWidth: FluffyThemes.columnWidth * 2.5, - ), - alignment: Alignment.center, - child: Material( - clipBehavior: Clip.hardEdge, - color: theme - .colorScheme - // ignore: deprecated_member_use - .surfaceVariant, - borderRadius: const BorderRadius.all( - Radius.circular(24), + if (controller.room.canSendDefaultMessages && + controller.room.membership == Membership.join) + Container( + margin: EdgeInsets.only( + bottom: bottomSheetPadding, + left: bottomSheetPadding, + right: bottomSheetPadding, + ), + constraints: const BoxConstraints( + maxWidth: FluffyThemes.columnWidth * 2.5, ), - child: controller.room.isAbandonedDMRoom == true - ? Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children: [ - TextButton.icon( - style: TextButton.styleFrom( - padding: const EdgeInsets.all( - 16, + alignment: Alignment.center, + child: Material( + clipBehavior: Clip.hardEdge, + color: theme.colorScheme.surfaceBright, + borderRadius: const BorderRadius.all( + Radius.circular(24), + ), + child: controller.room.isAbandonedDMRoom == + true + ? Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.all( + 16, + ), + foregroundColor: + theme.colorScheme.error, ), - foregroundColor: - theme.colorScheme.error, - ), - icon: const Icon( - Icons.archive_outlined, - ), - onPressed: controller.leaveChat, - label: Text( - L10n.of(context).leave, - ), - ), - TextButton.icon( - style: TextButton.styleFrom( - padding: const EdgeInsets.all( - 16, + icon: const Icon( + Icons.archive_outlined, + ), + onPressed: controller.leaveChat, + label: Text( + L10n.of(context).leave, ), ), - icon: const Icon( - Icons.forum_outlined, - ), - onPressed: controller.recreateChat, - label: Text( - L10n.of(context).reopenChat, + TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.all( + 16, + ), + ), + icon: const Icon( + Icons.forum_outlined, + ), + onPressed: + controller.recreateChat, + label: Text( + L10n.of(context).reopenChat, + ), ), - ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - children: [ - const ConnectionStatusHeader(), - ReactionsPicker(controller), - ReplyDisplay(controller), - ChatInputRow(controller), - ChatEmojiPicker(controller), - ], - ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ConnectionStatusHeader(), + ReactionsPicker(controller), + ReplyDisplay(controller), + ChatInputRow(controller), + ChatEmojiPicker(controller), + ], + ), + ), ), - ), - ], - ), - ), - if (controller.dragging) - Container( - color: theme.scaffoldBackgroundColor.withOpacity(0.9), - alignment: Alignment.center, - child: const Icon( - Icons.upload_outlined, - size: 100, + ], ), ), - ], + if (controller.dragging) + Container( + color: theme.scaffoldBackgroundColor.withOpacity(0.9), + alignment: Alignment.center, + child: const Icon( + Icons.upload_outlined, + size: 100, + ), + ), + ], + ), ), ), ); diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 032e8967d..2d0de96a6 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -83,8 +83,8 @@ class Message extends StatelessWidget { final client = Matrix.of(context).client; final ownMessage = event.senderId == client.userID; final alignment = ownMessage ? Alignment.topRight : Alignment.topLeft; - // ignore: deprecated_member_use - var color = theme.colorScheme.surfaceVariant; + + var color = theme.colorScheme.surfaceBright; final displayTime = event.type == EventTypes.RoomCreate || nextEvent == null || !event.originServerTs.sameEnvironment(nextEvent!.originServerTs); diff --git a/lib/pages/chat/typing_indicators.dart b/lib/pages/chat/typing_indicators.dart index a58138a90..13949f4aa 100644 --- a/lib/pages/chat/typing_indicators.dart +++ b/lib/pages/chat/typing_indicators.dart @@ -81,9 +81,7 @@ class TypingIndicators extends StatelessWidget { ), const SizedBox(width: 8), Material( - color: - // ignore: deprecated_member_use - theme.colorScheme.surfaceVariant, + color: theme.colorScheme.surfaceBright, borderRadius: const BorderRadius.all( Radius.circular(AppConfig.borderRadius), ), From 728854569ce1224396681b60ce07cc8bfc7e129f Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 21 Oct 2024 18:43:37 +0200 Subject: [PATCH 079/236] Revert "chore: Follow up new chat design" This reverts commit 4ec70bd25cff27cc7ec9b5125415edef68f66ef3. --- lib/pages/chat/chat_event_list.dart | 6 +- lib/pages/chat/chat_view.dart | 219 +++++++++++++------------- lib/pages/chat/events/message.dart | 4 +- lib/pages/chat/typing_indicators.dart | 4 +- 4 files changed, 114 insertions(+), 119 deletions(-) diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 54fc526f5..d4614af10 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -34,7 +34,6 @@ class ChatEventList extends StatelessWidget { for (var i = 0; i < events.length; i++) { thisEventsKeyMap[events[i].eventId] = i; } - final theme = Theme.of(context); final hasWallpaper = controller.room.client.applicationAccountConfig.wallpaperUrl != null; @@ -143,9 +142,8 @@ class ChatEventList extends StatelessWidget { i > 0 && controller.readMarkerEventId == event.eventId, nextEvent: i + 1 < events.length ? events[i + 1] : null, previousEvent: i > 0 ? events[i - 1] : null, - avatarPresenceBackgroundColor: hasWallpaper - ? Colors.transparent - : theme.colorScheme.surfaceContainer, + avatarPresenceBackgroundColor: + hasWallpaper ? Colors.transparent : null, ), ); }, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index a0aef8b1f..221153d27 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -258,130 +258,125 @@ class ChatView extends StatelessWidget { onDragDone: controller.onDragDone, onDragEntered: controller.onDragEntered, onDragExited: controller.onDragExited, - child: DecoratedBox( - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainer, - ), - child: Stack( - children: [ - if (accountConfig.wallpaperUrl != null) - Opacity( - opacity: accountConfig.wallpaperOpacity ?? 1, - child: MxcImage( - uri: accountConfig.wallpaperUrl, - fit: BoxFit.cover, - isThumbnail: true, - width: FluffyThemes.columnWidth * 4, - height: FluffyThemes.columnWidth * 4, - placeholder: (_) => Container(), - ), + child: Stack( + children: [ + if (accountConfig.wallpaperUrl != null) + Opacity( + opacity: accountConfig.wallpaperOpacity ?? 1, + child: MxcImage( + uri: accountConfig.wallpaperUrl, + fit: BoxFit.cover, + isThumbnail: true, + width: FluffyThemes.columnWidth * 4, + height: FluffyThemes.columnWidth * 4, + placeholder: (_) => Container(), ), - SafeArea( - child: Column( - children: [ - Expanded( - child: GestureDetector( - onTap: controller.clearSingleSelectedEvent, - child: Builder( - builder: (context) { - if (controller.timeline == null) { - return const Center( - child: - CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), - ); - } - return ChatEventList( - controller: controller, + ), + SafeArea( + child: Column( + children: [ + Expanded( + child: GestureDetector( + onTap: controller.clearSingleSelectedEvent, + child: Builder( + builder: (context) { + if (controller.timeline == null) { + return const Center( + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), ); - }, - ), + } + return ChatEventList( + controller: controller, + ); + }, ), ), - if (controller.room.canSendDefaultMessages && - controller.room.membership == Membership.join) - Container( - margin: EdgeInsets.only( - bottom: bottomSheetPadding, - left: bottomSheetPadding, - right: bottomSheetPadding, - ), - constraints: const BoxConstraints( - maxWidth: FluffyThemes.columnWidth * 2.5, + ), + if (controller.room.canSendDefaultMessages && + controller.room.membership == Membership.join) + Container( + margin: EdgeInsets.only( + bottom: bottomSheetPadding, + left: bottomSheetPadding, + right: bottomSheetPadding, + ), + constraints: const BoxConstraints( + maxWidth: FluffyThemes.columnWidth * 2.5, + ), + alignment: Alignment.center, + child: Material( + clipBehavior: Clip.hardEdge, + color: theme + .colorScheme + // ignore: deprecated_member_use + .surfaceVariant, + borderRadius: const BorderRadius.all( + Radius.circular(24), ), - alignment: Alignment.center, - child: Material( - clipBehavior: Clip.hardEdge, - color: theme.colorScheme.surfaceBright, - borderRadius: const BorderRadius.all( - Radius.circular(24), - ), - child: controller.room.isAbandonedDMRoom == - true - ? Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children: [ - TextButton.icon( - style: TextButton.styleFrom( - padding: const EdgeInsets.all( - 16, - ), - foregroundColor: - theme.colorScheme.error, - ), - icon: const Icon( - Icons.archive_outlined, - ), - onPressed: controller.leaveChat, - label: Text( - L10n.of(context).leave, + child: controller.room.isAbandonedDMRoom == true + ? Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.all( + 16, ), + foregroundColor: + theme.colorScheme.error, ), - TextButton.icon( - style: TextButton.styleFrom( - padding: const EdgeInsets.all( - 16, - ), - ), - icon: const Icon( - Icons.forum_outlined, - ), - onPressed: - controller.recreateChat, - label: Text( - L10n.of(context).reopenChat, + icon: const Icon( + Icons.archive_outlined, + ), + onPressed: controller.leaveChat, + label: Text( + L10n.of(context).leave, + ), + ), + TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.all( + 16, ), ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - children: [ - const ConnectionStatusHeader(), - ReactionsPicker(controller), - ReplyDisplay(controller), - ChatInputRow(controller), - ChatEmojiPicker(controller), - ], - ), - ), + icon: const Icon( + Icons.forum_outlined, + ), + onPressed: controller.recreateChat, + label: Text( + L10n.of(context).reopenChat, + ), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ConnectionStatusHeader(), + ReactionsPicker(controller), + ReplyDisplay(controller), + ChatInputRow(controller), + ChatEmojiPicker(controller), + ], + ), ), - ], - ), + ), + ], ), - if (controller.dragging) - Container( - color: theme.scaffoldBackgroundColor.withOpacity(0.9), - alignment: Alignment.center, - child: const Icon( - Icons.upload_outlined, - size: 100, - ), + ), + if (controller.dragging) + Container( + color: theme.scaffoldBackgroundColor.withOpacity(0.9), + alignment: Alignment.center, + child: const Icon( + Icons.upload_outlined, + size: 100, ), - ], - ), + ), + ], ), ), ); diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 2d0de96a6..032e8967d 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -83,8 +83,8 @@ class Message extends StatelessWidget { final client = Matrix.of(context).client; final ownMessage = event.senderId == client.userID; final alignment = ownMessage ? Alignment.topRight : Alignment.topLeft; - - var color = theme.colorScheme.surfaceBright; + // ignore: deprecated_member_use + var color = theme.colorScheme.surfaceVariant; final displayTime = event.type == EventTypes.RoomCreate || nextEvent == null || !event.originServerTs.sameEnvironment(nextEvent!.originServerTs); diff --git a/lib/pages/chat/typing_indicators.dart b/lib/pages/chat/typing_indicators.dart index 13949f4aa..a58138a90 100644 --- a/lib/pages/chat/typing_indicators.dart +++ b/lib/pages/chat/typing_indicators.dart @@ -81,7 +81,9 @@ class TypingIndicators extends StatelessWidget { ), const SizedBox(width: 8), Material( - color: theme.colorScheme.surfaceBright, + color: + // ignore: deprecated_member_use + theme.colorScheme.surfaceVariant, borderRadius: const BorderRadius.all( Radius.circular(AppConfig.borderRadius), ), From fcc43e3328eec79f5b087748b414d4d186bb41d3 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 21 Oct 2024 18:59:39 +0200 Subject: [PATCH 080/236] chore: Slightly update chat colors --- lib/pages/chat/chat_view.dart | 5 +---- lib/pages/chat/events/message.dart | 4 ++-- lib/pages/chat/typing_indicators.dart | 4 +--- lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart | 4 +--- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 221153d27..beabe749a 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -308,10 +308,7 @@ class ChatView extends StatelessWidget { alignment: Alignment.center, child: Material( clipBehavior: Clip.hardEdge, - color: theme - .colorScheme - // ignore: deprecated_member_use - .surfaceVariant, + color: theme.colorScheme.surfaceContainerHigh, borderRadius: const BorderRadius.all( Radius.circular(24), ), diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 032e8967d..7331552c7 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -83,8 +83,8 @@ class Message extends StatelessWidget { final client = Matrix.of(context).client; final ownMessage = event.senderId == client.userID; final alignment = ownMessage ? Alignment.topRight : Alignment.topLeft; - // ignore: deprecated_member_use - var color = theme.colorScheme.surfaceVariant; + + var color = theme.colorScheme.surfaceContainerHigh; final displayTime = event.type == EventTypes.RoomCreate || nextEvent == null || !event.originServerTs.sameEnvironment(nextEvent!.originServerTs); diff --git a/lib/pages/chat/typing_indicators.dart b/lib/pages/chat/typing_indicators.dart index a58138a90..7710ee0b6 100644 --- a/lib/pages/chat/typing_indicators.dart +++ b/lib/pages/chat/typing_indicators.dart @@ -81,9 +81,7 @@ class TypingIndicators extends StatelessWidget { ), const SizedBox(width: 8), Material( - color: - // ignore: deprecated_member_use - theme.colorScheme.surfaceVariant, + color: theme.colorScheme.surfaceContainerHigh, borderRadius: const BorderRadius.all( Radius.circular(AppConfig.borderRadius), ), diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart index e44ca47fc..ffc80b35e 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart @@ -69,9 +69,7 @@ class UserBottomSheetView extends StatelessWidget { Padding( padding: const EdgeInsets.all(12.0), child: Material( - color: - // ignore: deprecated_member_use - theme.colorScheme.surfaceVariant, + color: theme.colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(AppConfig.borderRadius), child: ListTile( From 3f9c7f3e91f199f78f9d3a64c39e97ec10e0deed Mon Sep 17 00:00:00 2001 From: Krille Date: Tue, 22 Oct 2024 09:22:48 +0200 Subject: [PATCH 081/236] feat: Swipe to archive rooms --- lib/pages/chat_list/chat_list_item.dart | 575 +++++++++++++----------- 1 file changed, 303 insertions(+), 272 deletions(-) diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 9224f5ee7..07e119d2d 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -35,29 +35,31 @@ class ChatListItem extends StatelessWidget { super.key, }); - Future archiveAction(BuildContext context) async { + Future archiveAction(BuildContext context, {bool? confirmed}) async { { if ([Membership.leave, Membership.ban].contains(room.membership)) { await showFutureLoadingDialog( context: context, future: () => room.forget(), ); - return; + return true; } - final confirmed = await showOkCancelAlertDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context).areYouSure, - okLabel: L10n.of(context).yes, - cancelLabel: L10n.of(context).no, - message: L10n.of(context).archiveRoomDescription, - ); - if (confirmed == OkCancelResult.cancel) return; + confirmed ??= (await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).leave, + cancelLabel: L10n.of(context).cancel, + message: L10n.of(context).archiveRoomDescription, + isDestructiveAction: true, + )) == + OkCancelResult.ok; + if (!confirmed) return false; await showFutureLoadingDialog( context: context, future: () => room.leave(), ); - return; + return true; } } @@ -93,294 +95,323 @@ class ChatListItem extends StatelessWidget { : room.getState(EventTypes.RoomMember, lastEvent.senderId) == null; final space = this.space; - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 1, + return Dismissible( + key: ValueKey(room.id), + confirmDismiss: (_) async => + (await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).leave, + cancelLabel: L10n.of(context).cancel, + message: L10n.of(context).archiveRoomDescription, + isDestructiveAction: true, + )) == + OkCancelResult.ok, + onDismissed: (_) => archiveAction(context, confirmed: true), + background: Material( + color: theme.colorScheme.errorContainer, + child: Icon( + Icons.archive_outlined, + color: theme.colorScheme.onErrorContainer, + ), ), - child: Material( - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - clipBehavior: Clip.hardEdge, - color: backgroundColor, - child: FutureBuilder( - future: room.loadHeroUsers(), - builder: (context, snapshot) => HoverBuilder( - builder: (context, listTileHovered) => ListTile( - visualDensity: const VisualDensity(vertical: -0.5), - contentPadding: const EdgeInsets.symmetric(horizontal: 8), - onLongPress: () => onLongPress?.call(context), - leading: HoverBuilder( - builder: (context, hovered) => AnimatedScale( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - scale: hovered ? 1.1 : 1.0, - child: SizedBox( - width: Avatar.defaultSize, - height: Avatar.defaultSize, - child: Stack( - children: [ - if (space != null) + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 1, + ), + child: Material( + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + clipBehavior: Clip.hardEdge, + color: backgroundColor, + child: FutureBuilder( + future: room.loadHeroUsers(), + builder: (context, snapshot) => HoverBuilder( + builder: (context, listTileHovered) => ListTile( + visualDensity: const VisualDensity(vertical: -0.5), + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + onLongPress: () => onLongPress?.call(context), + leading: HoverBuilder( + builder: (context, hovered) => AnimatedScale( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + scale: hovered ? 1.1 : 1.0, + child: SizedBox( + width: Avatar.defaultSize, + height: Avatar.defaultSize, + child: Stack( + children: [ + if (space != null) + Positioned( + top: 0, + left: 0, + child: Avatar( + border: BorderSide( + width: 2, + color: backgroundColor ?? + theme.colorScheme.surface, + ), + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 4, + ), + mxContent: space.avatar, + size: Avatar.defaultSize * 0.75, + name: space.getLocalizedDisplayname(), + onTap: () => onLongPress?.call(context), + ), + ), Positioned( - top: 0, - left: 0, + bottom: 0, + right: 0, child: Avatar( - border: BorderSide( - width: 2, - color: backgroundColor ?? - theme.colorScheme.surface, - ), - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 4, - ), - mxContent: space.avatar, - size: Avatar.defaultSize * 0.75, - name: space.getLocalizedDisplayname(), + border: space == null + ? null + : BorderSide( + width: 2, + color: backgroundColor ?? + theme.colorScheme.surface, + ), + borderRadius: room.isSpace + ? BorderRadius.circular( + AppConfig.borderRadius / 4, + ) + : null, + mxContent: room.avatar, + size: space != null + ? Avatar.defaultSize * 0.75 + : Avatar.defaultSize, + name: displayname, + presenceUserId: directChatMatrixId, + presenceBackgroundColor: backgroundColor, onTap: () => onLongPress?.call(context), ), ), - Positioned( - bottom: 0, - right: 0, - child: Avatar( - border: space == null - ? null - : BorderSide( - width: 2, - color: backgroundColor ?? - theme.colorScheme.surface, + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () => onLongPress?.call(context), + child: AnimatedScale( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + scale: listTileHovered ? 1.0 : 0.0, + child: Material( + color: backgroundColor, + borderRadius: BorderRadius.circular(16), + child: const Icon( + Icons.arrow_drop_down_circle_outlined, + size: 18, ), - borderRadius: room.isSpace - ? BorderRadius.circular( - AppConfig.borderRadius / 4, - ) - : null, - mxContent: room.avatar, - size: space != null - ? Avatar.defaultSize * 0.75 - : Avatar.defaultSize, - name: displayname, - presenceUserId: directChatMatrixId, - presenceBackgroundColor: backgroundColor, - onTap: () => onLongPress?.call(context), - ), - ), - Positioned( - top: 0, - right: 0, - child: GestureDetector( - onTap: () => onLongPress?.call(context), - child: AnimatedScale( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - scale: listTileHovered ? 1.0 : 0.0, - child: Material( - color: backgroundColor, - borderRadius: BorderRadius.circular(16), - child: const Icon( - Icons.arrow_drop_down_circle_outlined, - size: 18, ), ), ), ), - ), - ], + ], + ), ), ), ), - ), - title: Row( - children: [ - Expanded( - child: Text( - displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - softWrap: false, - style: unread || room.hasNewMessages - ? const TextStyle(fontWeight: FontWeight.bold) - : null, - ), - ), - if (isMuted) - const Padding( - padding: EdgeInsets.only(left: 4.0), - child: Icon( - Icons.notifications_off_outlined, - size: 16, + title: Row( + children: [ + Expanded( + child: Text( + displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + style: unread || room.hasNewMessages + ? const TextStyle(fontWeight: FontWeight.bold) + : null, ), ), - if (room.isFavourite || room.membership == Membership.invite) - Padding( - padding: EdgeInsets.only( - right: hasNotifications ? 4.0 : 0.0, + if (isMuted) + const Padding( + padding: EdgeInsets.only(left: 4.0), + child: Icon( + Icons.notifications_off_outlined, + size: 16, + ), ), - child: Icon( - Icons.push_pin, - size: 16, - color: theme.colorScheme.primary, + if (room.isFavourite || + room.membership == Membership.invite) + Padding( + padding: EdgeInsets.only( + right: hasNotifications ? 4.0 : 0.0, + ), + child: Icon( + Icons.push_pin, + size: 16, + color: theme.colorScheme.primary, + ), ), - ), - if (!room.isSpace && - lastEvent != null && - room.membership != Membership.invite) - Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Text( - lastEvent.originServerTs.localizedTimeShort(context), - style: TextStyle( - fontSize: 13, - color: unread - ? theme.colorScheme.secondary - : theme.textTheme.bodyMedium!.color, + if (!room.isSpace && + lastEvent != null && + room.membership != Membership.invite) + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Text( + lastEvent.originServerTs.localizedTimeShort(context), + style: TextStyle( + fontSize: 13, + color: unread + ? theme.colorScheme.secondary + : theme.textTheme.bodyMedium!.color, + ), ), ), - ), - if (room.isSpace) - const Icon( - Icons.arrow_circle_right_outlined, - size: 18, - ), - ], - ), - subtitle: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (typingText.isEmpty && - ownMessage && - room.lastEvent!.status.isSending) ...[ - const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ), - const SizedBox(width: 4), + if (room.isSpace) + const Icon( + Icons.arrow_circle_right_outlined, + size: 18, + ), ], - AnimatedContainer( - width: typingText.isEmpty ? 0 : 18, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - padding: const EdgeInsets.only(right: 4), - child: Icon( - Icons.edit_outlined, - color: theme.colorScheme.secondary, - size: 14, + ), + subtitle: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (typingText.isEmpty && + ownMessage && + room.lastEvent!.status.isSending) ...[ + const SizedBox( + width: 16, + height: 16, + child: + CircularProgressIndicator.adaptive(strokeWidth: 2), + ), + const SizedBox(width: 4), + ], + AnimatedContainer( + width: typingText.isEmpty ? 0 : 18, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + padding: const EdgeInsets.only(right: 4), + child: Icon( + Icons.edit_outlined, + color: theme.colorScheme.secondary, + size: 14, + ), ), - ), - Expanded( - child: room.isSpace && room.membership == Membership.join - ? Text( - L10n.of(context).countChatsAndCountParticipants( - room.spaceChildren.length.toString(), - (room.summary.mJoinedMemberCount ?? 1).toString(), - ), - ) - : typingText.isNotEmpty - ? Text( - typingText, - style: TextStyle( - color: theme.colorScheme.primary, - ), - maxLines: 1, - softWrap: false, - ) - : FutureBuilder( - key: ValueKey( - '${lastEvent?.eventId}_${lastEvent?.type}', - ), - future: needLastEventSender - ? lastEvent.calcLocalizedBody( - MatrixLocals(L10n.of(context)), - hideReply: true, - hideEdit: true, - plaintextBody: true, - removeMarkdown: true, - withSenderNamePrefix: (!isDirectChat || - directChatMatrixId != - room.lastEvent?.senderId), - ) - : null, - initialData: - lastEvent?.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)), - hideReply: true, - hideEdit: true, - plaintextBody: true, - removeMarkdown: true, - withSenderNamePrefix: (!isDirectChat || - directChatMatrixId != - room.lastEvent?.senderId), - ), - builder: (context, snapshot) => Text( - room.membership == Membership.invite - ? isDirectChat - ? L10n.of(context).invitePrivateChat - : L10n.of(context).inviteGroupChat - : snapshot.data ?? - L10n.of(context).emptyChat, - softWrap: false, - maxLines: 1, - overflow: TextOverflow.ellipsis, + Expanded( + child: room.isSpace && room.membership == Membership.join + ? Text( + L10n.of(context).countChatsAndCountParticipants( + room.spaceChildren.length.toString(), + (room.summary.mJoinedMemberCount ?? 1) + .toString(), + ), + ) + : typingText.isNotEmpty + ? Text( + typingText, style: TextStyle( - fontWeight: unread || room.hasNewMessages - ? FontWeight.bold - : null, - color: theme.colorScheme.onSurfaceVariant, - decoration: room.lastEvent?.redacted == true - ? TextDecoration.lineThrough - : null, + color: theme.colorScheme.primary, + ), + maxLines: 1, + softWrap: false, + ) + : FutureBuilder( + key: ValueKey( + '${lastEvent?.eventId}_${lastEvent?.type}', + ), + future: needLastEventSender + ? lastEvent.calcLocalizedBody( + MatrixLocals(L10n.of(context)), + hideReply: true, + hideEdit: true, + plaintextBody: true, + removeMarkdown: true, + withSenderNamePrefix: + (!isDirectChat || + directChatMatrixId != + room.lastEvent?.senderId), + ) + : null, + initialData: + lastEvent?.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)), + hideReply: true, + hideEdit: true, + plaintextBody: true, + removeMarkdown: true, + withSenderNamePrefix: (!isDirectChat || + directChatMatrixId != + room.lastEvent?.senderId), + ), + builder: (context, snapshot) => Text( + room.membership == Membership.invite + ? isDirectChat + ? L10n.of(context).invitePrivateChat + : L10n.of(context).inviteGroupChat + : snapshot.data ?? + L10n.of(context).emptyChat, + softWrap: false, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: unread || room.hasNewMessages + ? FontWeight.bold + : null, + color: theme.colorScheme.onSurfaceVariant, + decoration: + room.lastEvent?.redacted == true + ? TextDecoration.lineThrough + : null, + ), ), ), - ), - ), - const SizedBox(width: 8), - AnimatedContainer( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - padding: const EdgeInsets.symmetric(horizontal: 7), - height: unreadBubbleSize, - width: !hasNotifications && !unread && !room.hasNewMessages - ? 0 - : (unreadBubbleSize - 9) * - room.notificationCount.toString().length + - 9, - decoration: BoxDecoration( - color: room.highlightCount > 0 || - room.membership == Membership.invite - ? Colors.red - : hasNotifications || room.markedUnread - ? theme.colorScheme.primary - : theme.colorScheme.primaryContainer, - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), ), - child: Center( - child: hasNotifications - ? Text( - room.notificationCount.toString(), - style: TextStyle( - color: room.highlightCount > 0 - ? Colors.white - : hasNotifications - ? theme.colorScheme.onPrimary - : theme.colorScheme.onPrimaryContainer, - fontSize: 13, - ), - ) - : const SizedBox.shrink(), + const SizedBox(width: 8), + AnimatedContainer( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + padding: const EdgeInsets.symmetric(horizontal: 7), + height: unreadBubbleSize, + width: + !hasNotifications && !unread && !room.hasNewMessages + ? 0 + : (unreadBubbleSize - 9) * + room.notificationCount.toString().length + + 9, + decoration: BoxDecoration( + color: room.highlightCount > 0 || + room.membership == Membership.invite + ? Colors.red + : hasNotifications || room.markedUnread + ? theme.colorScheme.primary + : theme.colorScheme.primaryContainer, + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + ), + child: Center( + child: hasNotifications + ? Text( + room.notificationCount.toString(), + style: TextStyle( + color: room.highlightCount > 0 + ? Colors.white + : hasNotifications + ? theme.colorScheme.onPrimary + : theme + .colorScheme.onPrimaryContainer, + fontSize: 13, + ), + ) + : const SizedBox.shrink(), + ), ), - ), - ], + ], + ), + onTap: onTap, + trailing: onForget == null + ? null + : IconButton( + icon: const Icon(Icons.delete_outlined), + onPressed: onForget, + ), ), - onTap: onTap, - trailing: onForget == null - ? null - : IconButton( - icon: const Icon(Icons.delete_outlined), - onPressed: onForget, - ), ), ), ), From 282f45059c62971bfd9aa02a30283cbba31705b9 Mon Sep 17 00:00:00 2001 From: Krille Date: Tue, 22 Oct 2024 10:44:43 +0200 Subject: [PATCH 082/236] chore: Follow up dismiss room --- lib/pages/chat_list/chat_list_item.dart | 47 ++++++++++--------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 07e119d2d..543df5a21 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -35,31 +35,30 @@ class ChatListItem extends StatelessWidget { super.key, }); - Future archiveAction(BuildContext context, {bool? confirmed}) async { + Future archiveAction(BuildContext context) async { { if ([Membership.leave, Membership.ban].contains(room.membership)) { - await showFutureLoadingDialog( + final forgetResult = await showFutureLoadingDialog( context: context, future: () => room.forget(), ); - return true; + return forgetResult.isValue; } - confirmed ??= (await showOkCancelAlertDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context).areYouSure, - okLabel: L10n.of(context).leave, - cancelLabel: L10n.of(context).cancel, - message: L10n.of(context).archiveRoomDescription, - isDestructiveAction: true, - )) == - OkCancelResult.ok; - if (!confirmed) return false; - await showFutureLoadingDialog( + final confirmed = await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).leave, + cancelLabel: L10n.of(context).cancel, + message: L10n.of(context).archiveRoomDescription, + isDestructiveAction: true, + ); + if (confirmed != OkCancelResult.ok) return false; + final leaveResult = await showFutureLoadingDialog( context: context, future: () => room.leave(), ); - return true; + return leaveResult.isValue; } } @@ -97,18 +96,10 @@ class ChatListItem extends StatelessWidget { return Dismissible( key: ValueKey(room.id), - confirmDismiss: (_) async => - (await showOkCancelAlertDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context).areYouSure, - okLabel: L10n.of(context).leave, - cancelLabel: L10n.of(context).cancel, - message: L10n.of(context).archiveRoomDescription, - isDestructiveAction: true, - )) == - OkCancelResult.ok, - onDismissed: (_) => archiveAction(context, confirmed: true), + confirmDismiss: (_) => archiveAction(context), + onDismissed: (_) { + // Empty dismissed callback to trigger the dismiss animation + }, background: Material( color: theme.colorScheme.errorContainer, child: Icon( From c76a5fd03057dd3976d3537fe42fa3afb99a53af Mon Sep 17 00:00:00 2001 From: Krille Date: Wed, 23 Oct 2024 16:53:26 +0200 Subject: [PATCH 083/236] build: Update flutter web auth 2 package --- linux/flutter/generated_plugin_registrant.cc | 4 ++++ linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 ++ pubspec.lock | 23 ++++++++++++------- pubspec.yaml | 5 ++-- .../flutter/generated_plugin_registrant.cc | 3 +++ windows/flutter/generated_plugins.cmake | 1 + 7 files changed, 29 insertions(+), 10 deletions(-) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index b5155de25..a3aeaed6f 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -23,6 +24,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) desktop_drop_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); + g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); + desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); g_autoptr(FlPluginRegistrar) dynamic_color_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index dab6fedfc..6c1d6283d 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_drop + desktop_webview_window dynamic_color emoji_picker_flutter file_selector_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 50894732e..3a4881965 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import appkit_ui_element_colors import audio_session import desktop_drop +import desktop_webview_window import device_info_plus import dynamic_color import emoji_picker_flutter @@ -39,6 +40,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppkitUiElementColorsPlugin.register(with: registry.registrar(forPlugin: "AppkitUiElementColorsPlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) + DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 43fc844e0..883959cbd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -294,6 +294,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.3" + desktop_webview_window: + dependency: transitive + description: + name: desktop_webview_window + sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0" + url: "https://pub.dev" + source: hosted + version: "0.2.3" device_info_plus: dependency: "direct main" description: @@ -751,18 +759,18 @@ packages: dependency: "direct main" description: name: flutter_web_auth_2 - sha256: "4d3d2fd3d26bf1a26b3beafd4b4b899c0ffe10dc99af25abc58ffe24e991133c" + sha256: "8f59c9fa71b5affb322cb7103b836cd0ced89c9c50c66f82b523b7d339018dc3" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "4.0.1" flutter_web_auth_2_platform_interface: dependency: transitive description: name: flutter_web_auth_2_platform_interface - sha256: e8669e262005a8354389ba2971f0fc1c36188481234ff50d013aaf993f30f739 + sha256: "222264d4979e9372c90e441736a62d800481e4a9c860cc2c235d1d605a118a2b" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.1" flutter_web_plugins: dependency: transitive description: flutter @@ -1230,10 +1238,9 @@ packages: matrix: dependency: "direct main" description: - name: matrix - sha256: e06783394db3a49dbcd98a45803cac7d735a2fb1e3aafef65ddf01e72e16fc83 - url: "https://pub.dev" - source: hosted + path: "../matrix-dart-sdk" + relative: true + source: path version: "0.34.0" meta: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index bdf36fce2..3360655b1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,7 +49,7 @@ dependencies: flutter_shortcuts: git: https://github.com/krille-chan/flutter_shortcuts.git flutter_typeahead: ^5.2.0 - flutter_web_auth_2: ^3.1.1 + flutter_web_auth_2: ^4.0.1 flutter_webrtc: ^0.11.7 geolocator: ^7.6.2 go_router: ^14.3.0 @@ -65,7 +65,8 @@ dependencies: keyboard_shortcuts: ^0.1.4 latlong2: ^0.9.1 linkify: ^5.0.0 - matrix: ^0.34.0 + matrix: + path: ../matrix-dart-sdk/ mime: ^1.0.6 native_imaging: ^0.1.1 opus_caf_converter_dart: ^1.0.1 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 1d3cbc895..eda11f869 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -23,6 +24,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { DesktopDropPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopDropPlugin")); + DesktopWebviewWindowPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); EmojiPickerFlutterPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 1e12e9789..5e8e211e5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_drop + desktop_webview_window dynamic_color emoji_picker_flutter file_selector_windows From d088a7c1540e93014bdb60d976b769e94c904701 Mon Sep 17 00:00:00 2001 From: Krille Date: Thu, 24 Oct 2024 08:03:36 +0200 Subject: [PATCH 084/236] chore: Change same enivornment to one hour --- lib/utils/date_time_extension.dart | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/utils/date_time_extension.dart b/lib/utils/date_time_extension.dart index 8043805f0..0ed561af0 100644 --- a/lib/utils/date_time_extension.dart +++ b/lib/utils/date_time_extension.dart @@ -21,17 +21,10 @@ extension DateTimeExtension on DateTime { return millisecondsSinceEpoch <= other.millisecondsSinceEpoch; } - /// Two message events can belong to the same environment. That means that they - /// don't need to display the time they were sent because they are close - /// enaugh. - static const minutesBetweenEnvironments = 10; - /// Checks if two DateTimes are close enough to belong to the same /// environment. - bool sameEnvironment(DateTime prevTime) { - return millisecondsSinceEpoch - prevTime.millisecondsSinceEpoch < - 1000 * 60 * minutesBetweenEnvironments; - } + bool sameEnvironment(DateTime prevTime) => + prevTime.difference(this) < const Duration(hours: 1); /// Returns a simple time String. String localizedTimeOfDay(BuildContext context) => From c89c95eaab7628d6f5bbd59ad339b5ca7a471083 Mon Sep 17 00:00:00 2001 From: Krille-chan Date: Fri, 25 Oct 2024 10:26:36 +0200 Subject: [PATCH 085/236] chore: Update pubspec.yaml --- pubspec.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 3360655b1..0e1e4cc3c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,8 +65,7 @@ dependencies: keyboard_shortcuts: ^0.1.4 latlong2: ^0.9.1 linkify: ^5.0.0 - matrix: - path: ../matrix-dart-sdk/ + matrix: ^0.34.0 mime: ^1.0.6 native_imaging: ^0.1.1 opus_caf_converter_dart: ^1.0.1 From d7c2d1bdc727e8d7d59b22be94498788629ece36 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sat, 26 Oct 2024 08:56:35 +0200 Subject: [PATCH 086/236] chore: Follow up sameEnvironment calc --- lib/utils/date_time_extension.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/date_time_extension.dart b/lib/utils/date_time_extension.dart index 0ed561af0..92fbee7bf 100644 --- a/lib/utils/date_time_extension.dart +++ b/lib/utils/date_time_extension.dart @@ -24,7 +24,7 @@ extension DateTimeExtension on DateTime { /// Checks if two DateTimes are close enough to belong to the same /// environment. bool sameEnvironment(DateTime prevTime) => - prevTime.difference(this) < const Duration(hours: 1); + difference(prevTime) < const Duration(hours: 1); /// Returns a simple time String. String localizedTimeOfDay(BuildContext context) => From 3420c8a7c54cb2cf5f7ea945c06e87b3d0d3f4db Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 21 Oct 2024 12:17:42 +0200 Subject: [PATCH 087/236] build: Build on rpi for linux arm64 --- .github/workflows/integrate.yaml | 19 +++++++++++-------- pubspec.lock | 7 ++++--- snap/snapcraft.yaml | 1 + 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/integrate.yaml b/.github/workflows/integrate.yaml index d487d6b4b..e27ac7ee6 100644 --- a/.github/workflows/integrate.yaml +++ b/.github/workflows/integrate.yaml @@ -59,18 +59,21 @@ jobs: - run: flutter build web build_debug_linux: - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, self-hosted] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - run: cat .github/workflows/versions.env >> $GITHUB_ENV - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - name: Install dependencies - run: sudo apt-get update && sudo apt-get install curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 libssl-dev -y - - run: flutter pub get - - run: flutter build linux --target-platform linux-x64 + run: sudo apt-get update && sudo apt-get install git wget curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 libssl-dev libwebkit2gtk-4.1-dev -y + - name: Install Flutter + run: | + git clone --branch ${{ env.FLUTTER_VERSION }} https://github.com/flutter/flutter.git + ./flutter/bin/flutter doctor + - run: ./flutter/bin/flutter pub get + - run: ./flutter/bin/flutter build linux build_debug_ios: runs-on: macos-latest diff --git a/pubspec.lock b/pubspec.lock index 883959cbd..af7511c4d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1238,9 +1238,10 @@ packages: matrix: dependency: "direct main" description: - path: "../matrix-dart-sdk" - relative: true - source: path + name: matrix + sha256: e06783394db3a49dbcd98a45803cac7d735a2fb1e3aafef65ddf01e72e16fc83 + url: "https://pub.dev" + source: hosted version: "0.34.0" meta: dependency: transitive diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 63dfbb87d..b4e353b05 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -80,6 +80,7 @@ parts: - libsecret-1-dev - libjsoncpp-dev - libssl-dev + - libwebkit2gtk-4.1-dev slots: dbus-svc: From 617744503d87a63221f7fe331ad7b349014dab16 Mon Sep 17 00:00:00 2001 From: Krille Date: Sat, 26 Oct 2024 10:46:00 +0200 Subject: [PATCH 088/236] build: Also build for arm64 for linux releases --- .github/workflows/integrate.yaml | 6 +++--- .github/workflows/release.yaml | 25 ++++++++++++++----------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/workflows/integrate.yaml b/.github/workflows/integrate.yaml index e27ac7ee6..ce96b903c 100644 --- a/.github/workflows/integrate.yaml +++ b/.github/workflows/integrate.yaml @@ -61,8 +61,8 @@ jobs: build_debug_linux: strategy: matrix: - os: [ubuntu-latest, self-hosted] - runs-on: ${{ matrix.os }} + arch: [ x64, arm64 ] + runs-on: ${{ matrix.arch == 'arm64' && 'self-hosted' || 'ubuntu-latest'}} steps: - uses: actions/checkout@v4 - run: cat .github/workflows/versions.env >> $GITHUB_ENV @@ -73,7 +73,7 @@ jobs: git clone --branch ${{ env.FLUTTER_VERSION }} https://github.com/flutter/flutter.git ./flutter/bin/flutter doctor - run: ./flutter/bin/flutter pub get - - run: ./flutter/bin/flutter build linux + - run: ./flutter/bin/flutter build linux --target-platform linux-${{ matrix.arch }} build_debug_ios: runs-on: macos-latest diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 204d1e0e0..d471abb4f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -104,28 +104,31 @@ jobs: asset_content_type: application/vnd.android.package-archive build_linux: - runs-on: ubuntu-latest + strategy: + matrix: + arch: [ x64, arm64 ] + runs-on: ${{ matrix.arch == 'arm64' && 'self-hosted' || 'ubuntu-latest'}} steps: - uses: actions/checkout@v4 - run: cat .github/workflows/versions.env >> $GITHUB_ENV - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - name: Install dependencies - run: sudo apt-get update && sudo apt-get install curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 libssl-dev -y - - run: flutter pub get - - run: flutter build linux --release --target-platform linux-x64 + run: sudo apt-get update && sudo apt-get install curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 libssl-dev libwebkit2gtk-4.1-dev -y + - name: Install Flutter + run: | + git clone --branch ${{ env.FLUTTER_VERSION }} https://github.com/flutter/flutter.git + ./flutter/bin/flutter doctor + - run: ./flutter/bin/flutter pub get + - run: ./flutter/bin/flutter build linux --target-platform linux-${{ matrix.arch }} - name: Create archive - run: tar -czf fluffychat-linux-x64.tar.gz -C build/linux/x64/release/bundle/ . + run: tar -czf fluffychat-linux-${{ matrix.arch }}.tar.gz -C build/linux/${{ matrix.arch }}/release/bundle/ . - name: Upload to release uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.PAGES_DEPLOY_TOKEN }} with: upload_url: ${{ github.event.release.upload_url }} - asset_path: fluffychat-linux-x64.tar.gz - asset_name: fluffychat-linux-x64.tar.gz + asset_path: fluffychat-linux-${{ matrix.arch }}.tar.gz + asset_name: fluffychat-linux-${{ matrix.arch }}.tar.gz asset_content_type: application/gzip deploy_playstore: From 924bb59615fe24095153e530fa08bb0e313d6e66 Mon Sep 17 00:00:00 2001 From: Pavel Kozhukhov <89pavel3@gmail.com> Date: Fri, 18 Oct 2024 09:47:17 +0000 Subject: [PATCH 089/236] Translated using Weblate (Russian) Currently translated at 97.6% (654 of 670 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ru/ --- assets/l10n/intl_ru.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_ru.arb b/assets/l10n/intl_ru.arb index 136577c49..898622304 100644 --- a/assets/l10n/intl_ru.arb +++ b/assets/l10n/intl_ru.arb @@ -2783,5 +2783,9 @@ "doesNotSeemToBeAValidHomeserver": "Этот домашний сервер выглядит несовместимым. Нет ли в ссылке опечаток?", "@doesNotSeemToBeAValidHomeserver": {}, "noMoreChatsFound": "Больше чатов не обнаружено...", - "@noMoreChatsFound": {} + "@noMoreChatsFound": {}, + "alwaysUse24HourFormat": "нет", + "@alwaysUse24HourFormat": { + "description": "Set to true to always display time of day in 24 hour format." + } } From 4beb51d7240f5d19e268d0b667a087e7c9d22ad0 Mon Sep 17 00:00:00 2001 From: Angelo Schirinzi Date: Thu, 17 Oct 2024 09:44:06 +0000 Subject: [PATCH 090/236] Translated using Weblate (Italian) Currently translated at 89.8% (602 of 670 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/it/ --- assets/l10n/intl_it.arb | 107 +++++++++++++++++++++++++++++++++++----- 1 file changed, 95 insertions(+), 12 deletions(-) diff --git a/assets/l10n/intl_it.arb b/assets/l10n/intl_it.arb index 06ca97e8f..5b8190c92 100644 --- a/assets/l10n/intl_it.arb +++ b/assets/l10n/intl_it.arb @@ -447,7 +447,7 @@ "type": "text", "placeholders": {} }, - "defaultPermissionLevel": "Livello di autorizzazione predefinito", + "defaultPermissionLevel": "Livello di autorizzazione predefinito per i nuovi utenti", "@defaultPermissionLevel": { "type": "text", "placeholders": {} @@ -482,7 +482,7 @@ "type": "text", "placeholders": {} }, - "directChats": "Discussioni dirette", + "directChats": "Chat dirette", "@directChats": { "type": "text", "placeholders": {} @@ -951,7 +951,7 @@ "type": "text", "placeholders": {} }, - "noGoogleServicesWarning": "Sembra che tu non abbia Firebase Cloud Messaging sul tuo telefono. Per ricevere notifiche push consigliamo di installare ntfy. Con ntfy, o altro provided Unified Push, puoi ricevere le notifiche push in modo sicuro. Puoi scaricare ntfy dal PlayStore o fa F-Droid.", + "noGoogleServicesWarning": "Firebase Cloud Messaging non sembra essere disponibile sul tuo dispositivo. Per continuare a ricevere notifiche push, ti consigliamo di installare ntfy. Con ntfy o un altro provider Unified Push puoi ricevere notifiche push in modo sicuro per i dati. Puoi scaricare ntfy dal PlayStore o da F-Droid.", "@noGoogleServicesWarning": { "type": "text", "placeholders": {} @@ -1754,7 +1754,7 @@ "type": "text", "description": "Usage hint for the command /ban" }, - "sendSticker": "Invia sticker", + "sendSticker": "Invia adesivo", "@sendSticker": { "type": "text", "placeholders": {} @@ -1803,7 +1803,7 @@ }, "repeatPassword": "Ripeti password", "@repeatPassword": {}, - "autoplayImages": "Riproduci automaticamente sticker animati ed emoticon", + "autoplayImages": "Riproduci automaticamente adesivi ed emote animati", "@autoplayImages": { "type": "text", "placeholder": {} @@ -2049,7 +2049,7 @@ "provider": {} } }, - "fileIsTooBigForServer": "Il server segnala che il file è troppo grande per essere inviato.", + "fileIsTooBigForServer": "Impossibile inviare! Il server supporta solo allegati fino a {max}.", "@fileIsTooBigForServer": {}, "homeserver": "Homeserver", "@homeserver": {}, @@ -2187,7 +2187,7 @@ "type": "text", "description": "Usage hint for the command /myroomavatar" }, - "hasKnocked": "{user} ha bussato", + "hasKnocked": "🚪 {user} ha bussato", "@hasKnocked": { "placeholders": { "user": {} @@ -2437,7 +2437,7 @@ "query": {} } }, - "block": "bloccare", + "block": "Blocca", "@block": {}, "yourGlobalUserIdIs": "Il tuo ID dell'utente globale è: ", "@yourGlobalUserIdIs": {}, @@ -2453,7 +2453,7 @@ "@searchChatsRooms": {}, "databaseMigrationBody": "Attendere prego. L'operazione potrebbe richiedere un momento.", "@databaseMigrationBody": {}, - "youInvitedToBy": "📩 Sei stato invitato via collegamento in:\n{alias}", + "youInvitedToBy": "📩 Sei stato invitato tramite link in:\n{alias}", "@youInvitedToBy": { "placeholders": { "alias": {} @@ -2465,7 +2465,7 @@ "@subspace": {}, "publicSpaces": "Spazio pubblico", "@publicSpaces": {}, - "hidePresences": "Nascondi Lista Stato?", + "hidePresences": "Nascondere l'elenco degli stati?", "@hidePresences": {}, "pleaseEnterYourCurrentPassword": "Per favore inserisci la tua password attuale", "@pleaseEnterYourCurrentPassword": {}, @@ -2504,7 +2504,7 @@ "@verifyOtherDeviceDescription": {}, "discover": "Scopri", "@discover": {}, - "presencesToggle": "Mostra messaggi di stato degli altri utenti", + "presencesToggle": "Mostra i messaggi di stato di altri utenti", "@presencesToggle": { "type": "text", "placeholders": {} @@ -2518,5 +2518,88 @@ "transparent": "Trasparente", "@transparent": {}, "incomingMessages": "Messaggi in arrivo", - "@incomingMessages": {} + "@incomingMessages": {}, + "noChatsFoundHere": "Nessuna chat trovata. Inizia una nuova chat con qualcuno usando il pulsante qui sotto. ⤵️", + "@noChatsFoundHere": {}, + "joinedChats": "Chat a cui partecipi", + "@joinedChats": {}, + "unread": "Non letti", + "@unread": {}, + "space": "Spazio", + "@space": {}, + "spaces": "Spazi", + "@spaces": {}, + "notifyMeFor": "Avvisami per", + "@notifyMeFor": {}, + "invitedBy": "📩 Invitato da {user}", + "@invitedBy": { + "placeholders": { + "user": {} + } + }, + "knock": "Bussa", + "@knock": {}, + "hideInvalidOrUnknownMessageFormats": "Nascondi formati di messaggi non validi o sconosciuti", + "@hideInvalidOrUnknownMessageFormats": {}, + "overview": "Panoramica", + "@overview": {}, + "presenceStyle": "Presenza:", + "@presenceStyle": { + "type": "text", + "placeholders": {} + }, + "swipeRightToLeftToReply": "Scorri da destra a sinistra per rispondere", + "@swipeRightToLeftToReply": {}, + "globalChatId": "ID chat globale", + "@globalChatId": {}, + "hideMemberChangesInPublicChats": "Nascondi le modifiche dei membri nelle chat pubbliche", + "@hideMemberChangesInPublicChats": {}, + "hideMemberChangesInPublicChatsBody": "Per migliorare la leggibilità, non mostrare nella cronologia della chat se qualcuno si unisce o abbandona una chat pubblica.", + "@hideMemberChangesInPublicChatsBody": {}, + "userWouldLikeToChangeTheChat": "{user} vorrebbe unirsi alla chat.", + "@userWouldLikeToChangeTheChat": { + "placeholders": { + "user": {} + } + }, + "noPublicLinkHasBeenCreatedYet": "Non è stato ancora creato alcun link pubblico", + "@noPublicLinkHasBeenCreatedYet": {}, + "appLockDescription": "Blocca l'app con un codice PIN quando non è in uso", + "@appLockDescription": {}, + "noOneCanJoin": "Nessuno può unirsi", + "@noOneCanJoin": {}, + "usersMustKnock": "Gli utenti devono bussare", + "@usersMustKnock": {}, + "alwaysUse24HourFormat": "disattivato", + "@alwaysUse24HourFormat": { + "description": "Set to true to always display time of day in 24 hour format." + }, + "accessAndVisibility": "Accesso e visibilità", + "@accessAndVisibility": {}, + "accessAndVisibilityDescription": "Chi è autorizzato a partecipare a questa chat e come è possibile scoprirla.", + "@accessAndVisibilityDescription": {}, + "calls": "Chiamate", + "@calls": {}, + "customEmojisAndStickers": "Emoji e adesivi personalizzati", + "@customEmojisAndStickers": {}, + "customEmojisAndStickersBody": "Aggiungi o condividi emoji o adesivi personalizzati che possono essere utilizzati in qualsiasi chat.", + "@customEmojisAndStickersBody": {}, + "hideRedactedMessages": "Mostra i messaggi rimossi", + "@hideRedactedMessages": {}, + "hideRedactedMessagesBody": "Se qualcuno rimuove un messaggio, il messaggio non sarà più visibile nella chat.", + "@hideRedactedMessagesBody": {}, + "passwordRecoverySettings": "Impostazioni di recupero password", + "@passwordRecoverySettings": {}, + "noMoreChatsFound": "Non sono state trovate altre chat...", + "@noMoreChatsFound": {}, + "countChatsAndCountParticipants": "{chats} chat e {participants} partecipanti", + "@countChatsAndCountParticipants": { + "type": "text", + "placeholders": { + "chats": {}, + "participants": {} + } + }, + "stickers": "Adesivi", + "@stickers": {} } From b86fff0ff103b78dfeb3dac23eabaaf07b9340fb Mon Sep 17 00:00:00 2001 From: Ettore Atalan Date: Sat, 19 Oct 2024 00:37:15 +0000 Subject: [PATCH 091/236] Translated using Weblate (German) Currently translated at 99.8% (671 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/ --- assets/l10n/intl_de.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_de.arb b/assets/l10n/intl_de.arb index 6ff9ddf39..7297390d0 100644 --- a/assets/l10n/intl_de.arb +++ b/assets/l10n/intl_de.arb @@ -2833,5 +2833,7 @@ "placeholders": { "max": {} } - } + }, + "oneOfYourDevicesIsNotVerified": "Eines deiner Geräte ist nicht verifiziert", + "@oneOfYourDevicesIsNotVerified": {} } From ede84b80a9c4b4b2fd134b780623a7cb5c4a2786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sat, 19 Oct 2024 09:50:36 +0000 Subject: [PATCH 092/236] Translated using Weblate (Estonian) Currently translated at 100.0% (672 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/ --- assets/l10n/intl_et.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_et.arb b/assets/l10n/intl_et.arb index 5ea3e8942..4879b112f 100644 --- a/assets/l10n/intl_et.arb +++ b/assets/l10n/intl_et.arb @@ -2828,5 +2828,9 @@ "sendingAttachment": "Saadame manust...", "@sendingAttachment": {}, "calculatingFileSize": "Arvutame faili suurust...", - "@calculatingFileSize": {} + "@calculatingFileSize": {}, + "oneOfYourDevicesIsNotVerified": "Üks sinu seadmetest pole verifitseeritud", + "@oneOfYourDevicesIsNotVerified": {}, + "noticeChatBackupDeviceVerification": "Märkus: kui liidad kõik oma seadmed vestluste varundamisega, siis on nad sellega ka automaatselt verifitseeritud.", + "@noticeChatBackupDeviceVerification": {} } From 351fd9df5a4032c4baf9112eb616e84835bc4e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Fri, 18 Oct 2024 16:09:43 +0000 Subject: [PATCH 093/236] Translated using Weblate (Turkish) Currently translated at 100.0% (672 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/tr/ --- assets/l10n/intl_tr.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_tr.arb b/assets/l10n/intl_tr.arb index d89e4e6b0..ccafdf7a5 100644 --- a/assets/l10n/intl_tr.arb +++ b/assets/l10n/intl_tr.arb @@ -2828,5 +2828,9 @@ "generatingVideoThumbnail": "Video küçük resmi oluşturuluyor...", "@generatingVideoThumbnail": {}, "compressVideo": "Video sıkıştırılıyor...", - "@compressVideo": {} + "@compressVideo": {}, + "oneOfYourDevicesIsNotVerified": "Aygıtlarınızdan biri doğrulanmadı", + "@oneOfYourDevicesIsNotVerified": {}, + "noticeChatBackupDeviceVerification": "Not: Tüm aygıtlarınızı sohbet yedeklemesine bağladığınızda, otomatik olarak doğrulanırlar.", + "@noticeChatBackupDeviceVerification": {} } From 5b159922c7e6f3db0824b7206cd75616e785cb18 Mon Sep 17 00:00:00 2001 From: Bezruchenko Simon Date: Sat, 19 Oct 2024 10:02:57 +0000 Subject: [PATCH 094/236] Translated using Weblate (Ukrainian) Currently translated at 100.0% (672 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/ --- assets/l10n/intl_uk.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_uk.arb b/assets/l10n/intl_uk.arb index 64d8220bb..9d25e7065 100644 --- a/assets/l10n/intl_uk.arb +++ b/assets/l10n/intl_uk.arb @@ -2828,5 +2828,9 @@ "placeholders": { "seconds": {} } - } + }, + "oneOfYourDevicesIsNotVerified": "Один із ваших пристроїв не підтверджений", + "@oneOfYourDevicesIsNotVerified": {}, + "noticeChatBackupDeviceVerification": "Примітка: Коли ви підключаєте всі свої пристрої до резервної копії чату, вони автоматично підтверджуються.", + "@noticeChatBackupDeviceVerification": {} } From cab0a4250d54e6d4eb9f0e5ad24388f4cf92a4d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=8E=8B=E5=8F=AB=E6=88=91=E6=9D=A5=E5=B7=A1?= =?UTF-8?q?=E5=B1=B1?= Date: Sat, 19 Oct 2024 01:59:32 +0000 Subject: [PATCH 095/236] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (672 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/zh_Hans/ --- assets/l10n/intl_zh.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_zh.arb b/assets/l10n/intl_zh.arb index b7389dc96..cbafa5b4b 100644 --- a/assets/l10n/intl_zh.arb +++ b/assets/l10n/intl_zh.arb @@ -2828,5 +2828,9 @@ "placeholders": { "seconds": {} } - } + }, + "oneOfYourDevicesIsNotVerified": "您设备中的一台未验证", + "@oneOfYourDevicesIsNotVerified": {}, + "noticeChatBackupDeviceVerification": "注意:当你连接所有设备到聊天备份时,这些设备将被自动验证。", + "@noticeChatBackupDeviceVerification": {} } From cdb979356b38a553e492433a1fa011b03dd789ee Mon Sep 17 00:00:00 2001 From: Angelo Schirinzi Date: Fri, 18 Oct 2024 15:07:40 +0000 Subject: [PATCH 096/236] Translated using Weblate (Italian) Currently translated at 95.6% (643 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/it/ --- assets/l10n/intl_it.arb | 152 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 145 insertions(+), 7 deletions(-) diff --git a/assets/l10n/intl_it.arb b/assets/l10n/intl_it.arb index 5b8190c92..20bd56cf5 100644 --- a/assets/l10n/intl_it.arb +++ b/assets/l10n/intl_it.arb @@ -2280,7 +2280,7 @@ "@dehydrateTor": {}, "removeFromSpace": "Rimuovi dallo spazio", "@removeFromSpace": {}, - "commandHint_op": "Imposta il livello di permessi dell'utente specificato (predefinito: 50)", + "commandHint_op": "Imposta il livello di privilegi dell'utente specificato (predefinito: 50)", "@commandHint_op": { "type": "text", "description": "Usage hint for the command /op" @@ -2459,7 +2459,7 @@ "alias": {} } }, - "addChatOrSubSpace": "Aggiungi chat o sotto spazio", + "addChatOrSubSpace": "Aggiungi chat o sottospazio", "@addChatOrSubSpace": {}, "subspace": "Sottospazio", "@subspace": {}, @@ -2496,11 +2496,11 @@ }, "verifyOtherUser": "🔐 Verifica altro utente", "@verifyOtherUser": {}, - "verifyOtherUserDescription": "Se verifichi un altro utente, puoi stare certo di conoscere con chi tu stia parlando veramente. 💪\n\nQuando inizi una verifica, tu e l'altro utente vedrete un popup nell'applicazione. Lì vedrete una serie di emoji o numeri che dovrete confrontare l'uno con l'altro.\n\nIl miglior modo per farlo è di incontrarsi o iniziare una chiamata video. 👭", + "verifyOtherUserDescription": "Se verifichi un altro utente, puoi essere certo di sapere a chi stai realmente scrivendo. 💪\n\nQuando inizi una verifica, tu e l'altro utente vedrete un popup nell'app. Lì vedrai una serie di emoji o numeri che dovrai confrontare tra loro.\n\nIl modo migliore per farlo è incontrarsi o avviare una videochiamata. 👭", "@verifyOtherUserDescription": {}, "verifyOtherDevice": "🔐 Verifica altro dispositivo", "@verifyOtherDevice": {}, - "verifyOtherDeviceDescription": "Quando verifichi un altro dispositivo, quei dispositivi possono scambiarsi chiavi, aumentando la tua sicurezza generale. 💪 Quando inizia una verifica, apparirà un popup su entrambi i dispositivi. Lì vedrai una serie di emoji o numeri che dovrai confrontare l'uno con l'altro. È meglio avere entrambi i dispositivi a portata di mano prima di iniziare la verifica. 🤳", + "verifyOtherDeviceDescription": "Quando verifichi un altro dispositivo, questi dispositivi possono scambiarsi le chiavi, aumentando la tua sicurezza complessiva. 💪 Quando inizi una verifica, apparirà un popup nell'app su entrambi i dispositivi. Lì vedrai una serie di emoji o numeri che dovrai confrontare tra loro. È meglio avere entrambi i dispositivi a portata di mano prima di iniziare la verifica. 🤳", "@verifyOtherDeviceDescription": {}, "discover": "Scopri", "@discover": {}, @@ -2509,9 +2509,9 @@ "type": "text", "placeholders": {} }, - "publicLink": "Collegamento pubblico", + "publicLink": "Link pubblico", "@publicLink": {}, - "leaveEmptyToClearStatus": "Lasciare vuoto per cancellare il tuo stato.", + "leaveEmptyToClearStatus": "Lascia vuoto per cancellare il tuo stato.", "@leaveEmptyToClearStatus": {}, "decline": "Declina", "@decline": {}, @@ -2601,5 +2601,143 @@ } }, "stickers": "Adesivi", - "@stickers": {} + "@stickers": {}, + "searchMore": "Cerca di più...", + "@searchMore": {}, + "sessionLostBody": "La tua sessione è andata persa. Segnala questo errore agli sviluppatori all'indirizzo {url}. Il messaggio di errore è: {error}", + "@sessionLostBody": { + "type": "text", + "placeholders": { + "url": {}, + "error": {} + } + }, + "formattedMessagesDescription": "Visualizza contenuti di messaggi complessi, come testo in grassetto, utilizzando il markdown.", + "@formattedMessagesDescription": {}, + "canceledKeyVerification": "{sender} ha annullato la verifica della chiave", + "@canceledKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "chatPermissionsDescription": "Definisci quale livello di privilegi è necessario per determinate azioni in questa chat. I livelli di privilegi 0, 50 e 100 rappresentano solitamente utenti, moderatori e amministratori, ma qualsiasi valore intermedio è possibile.", + "@chatPermissionsDescription": {}, + "passwordsDoNotMatch": "Le password non corrispondono", + "@passwordsDoNotMatch": {}, + "initAppError": "Si è verificato un errore durante l'inizializzazione dell'app", + "@initAppError": {}, + "startedKeyVerification": "{sender} ha avviato la verifica della chiave", + "@startedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "chatCanBeDiscoveredViaSearchOnServer": "La chat può essere trovata tramite la ricerca su {server}", + "@chatCanBeDiscoveredViaSearchOnServer": { + "type": "text", + "placeholders": { + "server": {} + } + }, + "unreadChatsInApp": "{appname}: {unread} chat non lette", + "@unreadChatsInApp": { + "type": "text", + "placeholders": { + "appname": {}, + "unread": {} + } + }, + "thereAreCountUsersBlocked": "Al momento ci sono {count} utenti bloccati.", + "@thereAreCountUsersBlocked": { + "type": "text", + "count": {} + }, + "nothingFound": "Non è stato trovato nulla...", + "@nothingFound": {}, + "joinSpace": "Unisciti allo spazio", + "@joinSpace": {}, + "commandHint_ignore": "Ignora il Matrix ID fornito", + "@commandHint_ignore": {}, + "commandHint_unignore": "Ignora il Matrix ID specificato", + "@commandHint_unignore": {}, + "noDatabaseEncryption": "La crittografia del database non è supportata su questa piattaforma", + "@noDatabaseEncryption": {}, + "knocking": "Bussare", + "@knocking": {}, + "sendReadReceipts": "Invia ricevute di lettura", + "@sendReadReceipts": {}, + "knockRestricted": "Limitato al bussare", + "@knockRestricted": {}, + "restricted": "Limitato", + "@restricted": {}, + "publicChatAddresses": "Indirizzi di chat pubblici", + "@publicChatAddresses": {}, + "createNewAddress": "Crea un nuovo indirizzo", + "@createNewAddress": {}, + "userRole": "Ruolo utente", + "@userRole": {}, + "minimumPowerLevel": "{level} è il livello minimo di privilegi.", + "@minimumPowerLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "searchIn": "Cerca nella chat \"{chat}\"...", + "@searchIn": { + "type": "text", + "placeholders": { + "chat": {} + } + }, + "gallery": "Galleria", + "@gallery": {}, + "formattedMessages": "Messaggi formattati", + "@formattedMessages": {}, + "files": "File", + "@files": {}, + "restoreSessionBody": "L'app ora tenta di ripristinare la sessione dal backup. Segnala questo errore agli sviluppatori all'indirizzo {url}. Il messaggio di errore è: {error}", + "@restoreSessionBody": { + "type": "text", + "placeholders": { + "url": {}, + "error": {} + } + }, + "acceptedKeyVerification": "{sender} ha accettato la verifica della chiave", + "@acceptedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "completedKeyVerification": "{sender} ha completato la verifica della chiave", + "@completedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "isReadyForKeyVerification": "{sender} è pronto per la verifica della chiave", + "@isReadyForKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "searchForUsers": "Cerca @utenti...", + "@searchForUsers": {}, + "sendTypingNotificationsDescription": "Gli altri partecipanti alla chat possono vedere quando stai scrivendo un nuovo messaggio.", + "@sendTypingNotificationsDescription": {}, + "sendReadReceiptsDescription": "Gli altri partecipanti alla chat possono vedere quando hai letto un messaggio.", + "@sendReadReceiptsDescription": {}, + "requestedKeyVerification": "{sender} ha richiesto la verifica della chiave", + "@requestedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + } } From f74735336434deec44aa0a9659b84d265d324dbb Mon Sep 17 00:00:00 2001 From: Linerly Date: Sat, 19 Oct 2024 12:40:25 +0000 Subject: [PATCH 097/236] Translated using Weblate (Indonesian) Currently translated at 100.0% (672 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/id/ --- assets/l10n/intl_id.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_id.arb b/assets/l10n/intl_id.arb index f6d2c5919..df22e66b7 100644 --- a/assets/l10n/intl_id.arb +++ b/assets/l10n/intl_id.arb @@ -2827,5 +2827,9 @@ "chatPermissionsDescription": "Tentukan tingkat kekuasaan yang diperlukan untuk tindakan tertentu dalam chat ini. Tingkat kekuasaan 0, 50 dan 100 biasanya mewakili pengguna, moderator dan admin, tetapi gradasi apa pun dimungkinkan.", "@chatPermissionsDescription": {}, "homeserverDescription": "Semua data Anda disimpan di dalam server, seperti halnya penyedia email. Anda dapat memilih homeserver mana yang ingin Anda gunakan, sementara Anda masih dapat berkomunikasi dengan semua orang. Pelajari lebih lanjut di https://matrix.org.", - "@homeserverDescription": {} + "@homeserverDescription": {}, + "oneOfYourDevicesIsNotVerified": "Salah satu perangkat Anda tidak terverifikasi", + "@oneOfYourDevicesIsNotVerified": {}, + "noticeChatBackupDeviceVerification": "Catatan: Ketika Anda menghubungkan semua perangkat Anda ke cadangan chat, mereka akan diverifikasi secara otomatis.", + "@noticeChatBackupDeviceVerification": {} } From bdc74533d2e81f9b28508bb59c297536ebc40346 Mon Sep 17 00:00:00 2001 From: GGLVXD Date: Fri, 18 Oct 2024 17:41:29 +0000 Subject: [PATCH 098/236] Translated using Weblate (Latvian) Currently translated at 100.0% (672 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/lv/ --- assets/l10n/intl_lv.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_lv.arb b/assets/l10n/intl_lv.arb index 5bcd315ef..d1eb5731a 100644 --- a/assets/l10n/intl_lv.arb +++ b/assets/l10n/intl_lv.arb @@ -2806,5 +2806,9 @@ "prepareSendingAttachment": "Sagatavo pielikuma nosūtīšanu...", "@prepareSendingAttachment": {}, "compressVideo": "Saspiež video...", - "@compressVideo": {} + "@compressVideo": {}, + "oneOfYourDevicesIsNotVerified": "Viena no jūsu ierīcēm nav verificēta", + "@oneOfYourDevicesIsNotVerified": {}, + "noticeChatBackupDeviceVerification": "Piezīme. Kad visas ierīces pievienojat tērzēšanas dublējumam, tās tiek automātiski pārbaudītas.", + "@noticeChatBackupDeviceVerification": {} } From c21deb45f694339cedacb0cd7c0f1d86b231b4e1 Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Sun, 20 Oct 2024 17:04:26 +0000 Subject: [PATCH 099/236] Translated using Weblate (Basque) Currently translated at 100.0% (672 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/eu/ --- assets/l10n/intl_eu.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_eu.arb b/assets/l10n/intl_eu.arb index aed33ec67..3d932159f 100644 --- a/assets/l10n/intl_eu.arb +++ b/assets/l10n/intl_eu.arb @@ -2828,5 +2828,9 @@ "index": {}, "length": {} } - } + }, + "oneOfYourDevicesIsNotVerified": "Zure gailuetako bat ez dago egiaztatuta", + "@oneOfYourDevicesIsNotVerified": {}, + "noticeChatBackupDeviceVerification": "Oharra: gailu guztiak txat-babeskopiarekin konektatzen dituzunean, automatikoki egiaztatzen dira.", + "@noticeChatBackupDeviceVerification": {} } From 603c08dab8ec740a0037bd2993c6edc52b912a02 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Sat, 19 Oct 2024 23:10:29 +0000 Subject: [PATCH 100/236] Translated using Weblate (Ukrainian) Currently translated at 100.0% (672 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/ --- assets/l10n/intl_uk.arb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_uk.arb b/assets/l10n/intl_uk.arb index 9d25e7065..d5e5ae451 100644 --- a/assets/l10n/intl_uk.arb +++ b/assets/l10n/intl_uk.arb @@ -2829,8 +2829,8 @@ "seconds": {} } }, - "oneOfYourDevicesIsNotVerified": "Один із ваших пристроїв не підтверджений", + "oneOfYourDevicesIsNotVerified": "Один із ваших пристроїв не верифікований", "@oneOfYourDevicesIsNotVerified": {}, - "noticeChatBackupDeviceVerification": "Примітка: Коли ви підключаєте всі свої пристрої до резервної копії чату, вони автоматично підтверджуються.", + "noticeChatBackupDeviceVerification": "Примітка: Коли ви під'єднуєте всі свої пристрої до резервної копії бесіди, вони автоматично верифікуються.", "@noticeChatBackupDeviceVerification": {} } From e4dd7c6262be568e0dd3de79cbf0b5725ef08fc3 Mon Sep 17 00:00:00 2001 From: Rex_sa Date: Mon, 21 Oct 2024 03:30:55 +0000 Subject: [PATCH 101/236] Translated using Weblate (Arabic) Currently translated at 100.0% (672 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ar/ --- assets/l10n/intl_ar.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_ar.arb b/assets/l10n/intl_ar.arb index 52921a9de..3a72a9d36 100644 --- a/assets/l10n/intl_ar.arb +++ b/assets/l10n/intl_ar.arb @@ -2828,5 +2828,9 @@ "placeholders": { "seconds": {} } - } + }, + "oneOfYourDevicesIsNotVerified": "لم يتم التحقق من أحد أجهزتك", + "@oneOfYourDevicesIsNotVerified": {}, + "noticeChatBackupDeviceVerification": "ملاحظة: عند توصيل جميع أجهزتك بنسخة احتياطية للدردشة، يتم التحقق منها تلقائيًا.", + "@noticeChatBackupDeviceVerification": {} } From e574b9808ef45d4b8caa529c7e050c9f9a0a082a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jos=C3=A9=20m?= Date: Mon, 21 Oct 2024 03:49:45 +0000 Subject: [PATCH 102/236] Translated using Weblate (Galician) Currently translated at 100.0% (672 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/gl/ --- assets/l10n/intl_gl.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_gl.arb b/assets/l10n/intl_gl.arb index b4c41870a..d16a362ce 100644 --- a/assets/l10n/intl_gl.arb +++ b/assets/l10n/intl_gl.arb @@ -2828,5 +2828,9 @@ "index": {}, "length": {} } - } + }, + "oneOfYourDevicesIsNotVerified": "Un dos teus dispositivos non está verificado", + "@oneOfYourDevicesIsNotVerified": {}, + "noticeChatBackupDeviceVerification": "Nota: Cando conectas todos os teus dispositivos á copia de apoio da conversa quedan verificados automaticamente.", + "@noticeChatBackupDeviceVerification": {} } From dbe0e087c3a21ced1cc1faa1608bf618e1d820ae Mon Sep 17 00:00:00 2001 From: Edgars Andersons Date: Mon, 21 Oct 2024 07:44:27 +0000 Subject: [PATCH 103/236] Translated using Weblate (Latvian) Currently translated at 100.0% (672 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/lv/ --- assets/l10n/intl_lv.arb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/l10n/intl_lv.arb b/assets/l10n/intl_lv.arb index d1eb5731a..b38871034 100644 --- a/assets/l10n/intl_lv.arb +++ b/assets/l10n/intl_lv.arb @@ -643,7 +643,7 @@ "count": {} } }, - "noKeyForThisMessage": "Tas var notikt, ja ziņa tika nosūtīta, pirms pieteicies savā kontā šajā ierīcē.\n\nIr arī iespējams, ka sūtītājs noliedza Tavu ierīci vai kaut kas nogāja greizi ar interneta savienojumu.\n\nVai ziņas ir lasāmas citā sesijā? Tad Tu vari pārsūtīt ziņo no tās. Jādodas uz Iestatījumi > Ierīces un jāpārliecinās, ka ierīces viena otru ir apliecinājušas. Kad nākamreiz atvērsi istabu un abas sesijas būs priekšplānā, atslēgas tiks automātiski pārsūtītas.\n\nVai nevēlies zaudēt atslēgas, kad atsakies vai maini ierīces? Jāpārliecinās, ka iestatījumos ir iespējota tērzēšanas rezerves kopija.", + "noKeyForThisMessage": "Tā var notikt, ja ziņa tika nosūtīta, pirms pieteicies savā kontā šajā ierīcē.\n\nIr arī iespējams, ka sūtītājs noliedza Tavu ierīci vai kaut kas nogāja greizi ar interneta savienojumu.\n\nVai ziņas ir lasāmas citā sesijā? Tad Tu vari pārsūtīt ziņo no tās. Jādodas uz Iestatījumi > Ierīces un jāpārliecinās, ka ierīces viena otru ir apliecinājušas. Kad nākamreiz atvērsi istabu un abas sesijas būs priekšplānā, atslēgas tiks automātiski pārsūtītas.\n\nVai nevēlies zaudēt atslēgas, kad atsakies vai maini ierīces? Jāpārliecinās, ka iestatījumos ir iespējota tērzēšanas rezerves kopija.", "@noKeyForThisMessage": {}, "enableEncryptionWarning": "Vairs nebūs iespējams atspējot šifrēšanu. Vai tiešām to darīt?", "@enableEncryptionWarning": { @@ -2807,8 +2807,8 @@ "@prepareSendingAttachment": {}, "compressVideo": "Saspiež video...", "@compressVideo": {}, - "oneOfYourDevicesIsNotVerified": "Viena no jūsu ierīcēm nav verificēta", + "oneOfYourDevicesIsNotVerified": "Viena no ierīcēm nav apliecināta", "@oneOfYourDevicesIsNotVerified": {}, - "noticeChatBackupDeviceVerification": "Piezīme. Kad visas ierīces pievienojat tērzēšanas dublējumam, tās tiek automātiski pārbaudītas.", + "noticeChatBackupDeviceVerification": "Piezīme: kad visas ierīces tiek savienotas ar tērzēšanas rezerves kopiju, tās tiek automātiski apliecinātas.", "@noticeChatBackupDeviceVerification": {} } From 8c02e632f8a574c3d46052733fa183ad1aff59d6 Mon Sep 17 00:00:00 2001 From: Angelo Schirinzi Date: Wed, 23 Oct 2024 19:05:37 +0000 Subject: [PATCH 104/236] Translated using Weblate (Italian) Currently translated at 100.0% (672 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/it/ --- assets/l10n/intl_it.arb | 102 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/assets/l10n/intl_it.arb b/assets/l10n/intl_it.arb index 20bd56cf5..85b04ad97 100644 --- a/assets/l10n/intl_it.arb +++ b/assets/l10n/intl_it.arb @@ -1913,7 +1913,7 @@ "@widgetEtherpad": {}, "removeDevicesDescription": "Sarai disconnesso da questo dispositivo e non potrai più ricevere messaggi.", "@removeDevicesDescription": {}, - "separateChatTypes": "Separa Chat Dirette e Gruppi", + "separateChatTypes": "Separare le chat dirette e i gruppi", "@separateChatTypes": { "type": "text", "placeholders": {} @@ -2260,7 +2260,7 @@ "@noChatDescriptionYet": {}, "removeFromBundle": "Rimuovi da questo bundle", "@removeFromBundle": {}, - "confirmMatrixId": "Conferma il tuo ID Matrix per cancellare il tuo account.", + "confirmMatrixId": "Per eliminare il tuo account, conferma il tuo Matrix ID.", "@confirmMatrixId": {}, "learnMore": "Scopri di più", "@learnMore": {}, @@ -2328,7 +2328,7 @@ }, "shareInviteLink": "Condividi link d'invito", "@shareInviteLink": {}, - "commandHint_markasdm": "Segna come stanza messaggi diretti per questo ID Matrix", + "commandHint_markasdm": "Contrassegna questo Matrix ID come stanza di messaggi diretti", "@commandHint_markasdm": {}, "recoveryKeyLost": "Chiave di recupero smarrita?", "@recoveryKeyLost": {}, @@ -2361,7 +2361,7 @@ "@widgetName": {}, "errorAddingWidget": "Errore aggiungendo il widget.", "@errorAddingWidget": {}, - "commandHint_dm": "Avvia una chat diretta\nUtilizza --no-encryption per disattivare la criptazione", + "commandHint_dm": "Avvia una chat diretta\nUsa --no-encryption per disabilitare la crittografia", "@commandHint_dm": { "type": "text", "description": "Usage hint for the command /dm" @@ -2739,5 +2739,97 @@ "placeholders": { "sender": {} } - } + }, + "changeTheChatPermissions": "Cambia i permessi della chat", + "@changeTheChatPermissions": {}, + "changeTheVisibilityOfChatHistory": "Cambia la visibilità della cronologia chat", + "@changeTheVisibilityOfChatHistory": {}, + "changeTheCanonicalRoomAlias": "Cambia l'indirizzo principale della chat pubblica", + "@changeTheCanonicalRoomAlias": {}, + "sendRoomNotifications": "Invia notifiche alla @stanza", + "@sendRoomNotifications": {}, + "sendCanceled": "Invio annullato", + "@sendCanceled": {}, + "calculatingFileSize": "Calcolo della dimensione del file...", + "@calculatingFileSize": {}, + "prepareSendingAttachment": "Preparazione per l'invio dell'allegato...", + "@prepareSendingAttachment": {}, + "sendingAttachment": "Invio allegato...", + "@sendingAttachment": {}, + "compressVideo": "Compressione video...", + "@compressVideo": {}, + "generatingVideoThumbnail": "Generazione miniatura video...", + "@generatingVideoThumbnail": {}, + "sendingAttachmentCountOfCount": "Invio dell'allegato {index} di {length}...", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + }, + "serverLimitReached": "Limite server raggiunto! Attendere {seconds} secondi...", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + }, + "oneOfYourDevicesIsNotVerified": "Uno dei tuoi dispositivi non è verificato", + "@oneOfYourDevicesIsNotVerified": {}, + "noticeChatBackupDeviceVerification": "Nota: quando colleghi tutti i tuoi dispositivi al backup della chat, vengono verificati automaticamente.", + "@noticeChatBackupDeviceVerification": {}, + "moderatorLevel": "{level} - Moderatore", + "@moderatorLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "changeTheDescriptionOfTheGroup": "Cambia la descrizione della chat", + "@changeTheDescriptionOfTheGroup": {}, + "updateInstalled": "🎉 Aggiornamento {version} installato!", + "@updateInstalled": { + "type": "text", + "placeholders": { + "version": {} + } + }, + "inviteOtherUsers": "Invita altri utenti a questa chat", + "@inviteOtherUsers": {}, + "userLevel": "{level} - Utente", + "@userLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "adminLevel": "{level} - Amministratore", + "@adminLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "changeGeneralChatSettings": "Modifica le impostazioni generali della chat", + "@changeGeneralChatSettings": {}, + "loginWithMatrixId": "Accedi con il Matrix ID", + "@loginWithMatrixId": {}, + "homeserverDescription": "Tutti i tuoi dati sono archiviati sull'homeserver, proprio come un provider di posta elettronica. Puoi scegliere quale homeserver vuoi usare, mentre puoi comunque comunicare con tutti. Scopri di più su https://matrix.org.", + "@homeserverDescription": {}, + "discoverHomeservers": "Scopri gli homeserver", + "@discoverHomeservers": {}, + "whatIsAHomeserver": "Cos'è un homeserver?", + "@whatIsAHomeserver": {}, + "changelog": "Registro delle modifiche", + "@changelog": {}, + "doesNotSeemToBeAValidHomeserver": "Non sembra essere un homeserver compatibile. URL sbagliato?", + "@doesNotSeemToBeAValidHomeserver": {}, + "goToSpace": "Vai allo spazio: {space}", + "@goToSpace": { + "type": "text", + "space": {} + }, + "markAsUnread": "Contrassegna come non letto", + "@markAsUnread": {} } From 7e8716ac8098149a02e78a687e1c0b1b4c65c4e2 Mon Sep 17 00:00:00 2001 From: Christian Date: Sat, 26 Oct 2024 15:57:29 +0000 Subject: [PATCH 105/236] Translated using Weblate (German) Currently translated at 100.0% (672 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/ --- assets/l10n/intl_de.arb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_de.arb b/assets/l10n/intl_de.arb index 7297390d0..826ea2d53 100644 --- a/assets/l10n/intl_de.arb +++ b/assets/l10n/intl_de.arb @@ -1186,7 +1186,7 @@ "type": "text", "placeholders": {} }, - "participant": "Mitglieder", + "participant": "Mitglied", "@participant": { "type": "text", "placeholders": {} @@ -2835,5 +2835,7 @@ } }, "oneOfYourDevicesIsNotVerified": "Eines deiner Geräte ist nicht verifiziert", - "@oneOfYourDevicesIsNotVerified": {} + "@oneOfYourDevicesIsNotVerified": {}, + "noticeChatBackupDeviceVerification": "Hinweis: Wenn du alle deine Geräte mit dem Chat-Backup verbindest, sind sie automatisch verifiziert.", + "@noticeChatBackupDeviceVerification": {} } From 41782c497e27da9fc99b6aef3706c83712675470 Mon Sep 17 00:00:00 2001 From: Krille Date: Sat, 26 Oct 2024 18:00:24 +0200 Subject: [PATCH 106/236] chore: Nicer invite selection view --- .../invitation_selection.dart | 18 +--- .../invitation_selection_view.dart | 85 ++++++++++--------- 2 files changed, 46 insertions(+), 57 deletions(-) diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 7e5d2039e..2549c4731 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.dart @@ -2,12 +2,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/invitation_selection/invitation_selection_view.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/localized_exception_extension.dart'; @@ -54,21 +52,7 @@ class InvitationSelectionController extends State { void inviteAction(BuildContext context, String id, String displayname) async { final room = Matrix.of(context).client.getRoomById(roomId!)!; - if (OkCancelResult.ok != - await showOkCancelAlertDialog( - context: context, - title: L10n.of(context).inviteContact, - message: L10n.of(context).inviteContactToGroupQuestion( - displayname, - room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)), - ), - ), - okLabel: L10n.of(context).invite, - cancelLabel: L10n.of(context).cancel, - )) { - return; - } + final success = await showFutureLoadingDialog( context: context, future: () => room.invite(id), diff --git a/lib/pages/invitation_selection/invitation_selection_view.dart b/lib/pages/invitation_selection/invitation_selection_view.dart index 84fe2ff7b..55671c03c 100644 --- a/lib/pages/invitation_selection/invitation_selection_view.dart +++ b/lib/pages/invitation_selection/invitation_selection_view.dart @@ -4,6 +4,8 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart'; +import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; +import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -87,12 +89,7 @@ class InvitationSelectionView extends StatelessWidget { itemCount: controller.foundProfiles.length, itemBuilder: (BuildContext context, int i) => _InviteContactListTile( - avatarUrl: controller.foundProfiles[i].avatarUrl, - displayname: controller - .foundProfiles[i].displayName ?? - controller.foundProfiles[i].userId.localpart ?? - L10n.of(context).user, - userId: controller.foundProfiles[i].userId, + profile: controller.foundProfiles[i], isMember: participants .contains(controller.foundProfiles[i].userId), onTap: () => controller.inviteAction( @@ -121,11 +118,14 @@ class InvitationSelectionView extends StatelessWidget { itemCount: contacts.length, itemBuilder: (BuildContext context, int i) => _InviteContactListTile( - avatarUrl: contacts[i].avatarUrl, - displayname: contacts[i].displayName ?? - contacts[i].id.localpart ?? - L10n.of(context).user, - userId: contacts[i].id, + user: contacts[i], + profile: Profile( + avatarUrl: contacts[i].avatarUrl, + displayName: contacts[i].displayName ?? + contacts[i].id.localpart ?? + L10n.of(context).user, + userId: contacts[i].id, + ), isMember: participants.contains(contacts[i].id), onTap: () => controller.inviteAction( context, @@ -148,16 +148,14 @@ class InvitationSelectionView extends StatelessWidget { } class _InviteContactListTile extends StatelessWidget { - final String userId; - final String displayname; - final Uri? avatarUrl; + final Profile profile; + final User? user; final bool isMember; final void Function() onTap; const _InviteContactListTile({ - required this.userId, - required this.displayname, - required this.avatarUrl, + required this.profile, + this.user, required this.isMember, required this.onTap, }); @@ -165,32 +163,39 @@ class _InviteContactListTile extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l10n = L10n.of(context); - return Opacity( - opacity: isMember ? 0.5 : 1, - child: ListTile( - leading: Avatar( - mxContent: avatarUrl, - name: displayname, - presenceUserId: userId, - ), - title: Text( - displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - userId, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: theme.colorScheme.secondary, + return ListTile( + leading: Avatar( + mxContent: profile.avatarUrl, + name: profile.displayName, + presenceUserId: profile.userId, + onTap: () => showAdaptiveBottomSheet( + context: context, + builder: (c) => UserBottomSheet( + user: user, + profile: profile, + outerContext: context, ), ), - onTap: isMember ? null : onTap, - trailing: isMember - ? Text(L10n.of(context).participant) - : const Icon(Icons.person_add_outlined), + ), + title: Text( + profile.displayName ?? profile.userId.localpart ?? l10n.user, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + profile.userId, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: theme.colorScheme.secondary, + ), + ), + trailing: TextButton.icon( + onPressed: isMember ? null : onTap, + label: Text(isMember ? l10n.participant : l10n.invite), + icon: Icon(isMember ? Icons.check : Icons.add), ), ); } From a422d470da6f4f0016d251083eb14a3cfade57a7 Mon Sep 17 00:00:00 2001 From: Krille Date: Sat, 26 Oct 2024 18:05:21 +0200 Subject: [PATCH 107/236] chore: Do not request thousands of users on invite page --- lib/pages/invitation_selection/invitation_selection.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 2549c4731..8cb722b21 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.dart @@ -34,7 +34,10 @@ class InvitationSelectionController extends State { Future> getContacts(BuildContext context) async { final client = Matrix.of(context).client; final room = client.getRoomById(roomId!)!; - final participants = await room.requestParticipants(); + + final participants = (room.summary.mJoinedMemberCount ?? 0) > 100 + ? room.getParticipants() + : await room.requestParticipants(); participants.removeWhere( (u) => ![Membership.join, Membership.invite].contains(u.membership), ); From 51edfedef005c09b1bbc695e07a08e826097c581 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 18:46:16 +0000 Subject: [PATCH 108/236] build(deps): bump rexml from 3.3.6 to 3.3.9 in /ios Bumps [rexml](https://github.com/ruby/rexml) from 3.3.6 to 3.3.9. - [Release notes](https://github.com/ruby/rexml/releases) - [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md) - [Commits](https://github.com/ruby/rexml/compare/v3.3.6...v3.3.9) --- updated-dependencies: - dependency-name: rexml dependency-type: indirect ... Signed-off-by: dependabot[bot] --- ios/Gemfile.lock | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 1b22c3e97..75fbd4bff 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -156,8 +156,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.6) - strscan + rexml (3.3.9) rouge (2.0.7) ruby2_keywords (0.0.4) rubyzip (2.3.0) @@ -170,7 +169,6 @@ GEM simctl (1.6.8) CFPropertyList naturally - strscan (3.1.0) terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) From 189493e074fc3ec49fe958e332963c83b3bee4ed Mon Sep 17 00:00:00 2001 From: Krille Date: Tue, 29 Oct 2024 10:11:51 +0100 Subject: [PATCH 109/236] design: Highlight emoji only messages --- lib/pages/chat/events/message.dart | 17 +++++++++++------ lib/pages/chat/events/message_content.dart | 6 +++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 7331552c7..60c1950f0 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -122,12 +122,17 @@ class Message extends StatelessWidget { bottomRight: ownMessage && previousEventSameSender ? hardCorner : roundedCorner, ); - final noBubble = { - MessageTypes.Video, - MessageTypes.Image, - MessageTypes.Sticker, - }.contains(event.messageType) && - !event.redacted; + final noBubble = ({ + MessageTypes.Video, + MessageTypes.Image, + MessageTypes.Sticker, + }.contains(event.messageType) && + !event.redacted) || + (event.messageType == MessageTypes.Text && + event.relationshipType == null && + event.onlyEmotes && + event.numberEmotes > 0 && + event.numberEmotes <= 3); final noPadding = { MessageTypes.File, MessageTypes.Audio, diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 48bc8c583..04de5cf17 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -251,7 +251,7 @@ class MessageContent extends StatelessWidget { } final bigEmotes = event.onlyEmotes && event.numberEmotes > 0 && - event.numberEmotes <= 10; + event.numberEmotes <= 3; return Linkify( text: event.calcLocalizedBodyFallback( MatrixLocals(L10n.of(context)), @@ -259,13 +259,13 @@ class MessageContent extends StatelessWidget { ), style: TextStyle( color: textColor, - fontSize: bigEmotes ? fontSize * 3 : fontSize, + fontSize: bigEmotes ? fontSize * 6 : fontSize, decoration: event.redacted ? TextDecoration.lineThrough : null, ), options: const LinkifyOptions(humanize: false), linkStyle: TextStyle( color: textColor.withAlpha(150), - fontSize: bigEmotes ? fontSize * 3 : fontSize, + fontSize: fontSize, decoration: TextDecoration.underline, decorationColor: textColor.withAlpha(150), ), From 6c548ec3505cfd171924e4a491abc3e1f8dc75ce Mon Sep 17 00:00:00 2001 From: Krille Date: Tue, 29 Oct 2024 10:25:59 +0100 Subject: [PATCH 110/236] chore: Follow up emoji only messages --- lib/pages/chat/events/message_content.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 04de5cf17..510d438e2 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -259,7 +259,7 @@ class MessageContent extends StatelessWidget { ), style: TextStyle( color: textColor, - fontSize: bigEmotes ? fontSize * 6 : fontSize, + fontSize: bigEmotes ? fontSize * 5 : fontSize, decoration: event.redacted ? TextDecoration.lineThrough : null, ), options: const LinkifyOptions(humanize: false), From 55247258c954daeab197916359570a2642bd1858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jos=C3=A9=20m?= Date: Tue, 29 Oct 2024 05:12:29 +0000 Subject: [PATCH 111/236] Translated using Weblate (Galician) Currently translated at 100.0% (672 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/gl/ --- assets/l10n/intl_gl.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/l10n/intl_gl.arb b/assets/l10n/intl_gl.arb index d16a362ce..c56d47d47 100644 --- a/assets/l10n/intl_gl.arb +++ b/assets/l10n/intl_gl.arb @@ -2446,7 +2446,7 @@ "@blockListDescription": {}, "blockedUsers": "Usuarias bloqueadas", "@blockedUsers": {}, - "block": "Bloquer", + "block": "Bloquear", "@block": {}, "blockUsername": "Ignorar identificador", "@blockUsername": {}, From 44f984605fd98156996846f1b9eaa1236c14d722 Mon Sep 17 00:00:00 2001 From: v1s7 Date: Mon, 28 Oct 2024 17:51:35 +0000 Subject: [PATCH 112/236] Translated using Weblate (Russian) Currently translated at 99.7% (670 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ru/ --- assets/l10n/intl_ru.arb | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_ru.arb b/assets/l10n/intl_ru.arb index 898622304..af876a086 100644 --- a/assets/l10n/intl_ru.arb +++ b/assets/l10n/intl_ru.arb @@ -2215,7 +2215,7 @@ "number": {} } }, - "fileIsTooBigForServer": "Файл слишком большой.", + "fileIsTooBigForServer": "Отправка не удалась! Сервер поддерживает только вложения размером до {max}.", "@fileIsTooBigForServer": {}, "hideUnimportantStateEvents": "Скрыть необязательные события статуса", "@hideUnimportantStateEvents": {}, @@ -2787,5 +2787,35 @@ "alwaysUse24HourFormat": "нет", "@alwaysUse24HourFormat": { "description": "Set to true to always display time of day in 24 hour format." - } + }, + "compressVideo": "Сжатие видео...", + "@compressVideo": {}, + "oneOfYourDevicesIsNotVerified": "Одно из ваших устройств не подтверждено", + "@oneOfYourDevicesIsNotVerified": {}, + "chatPermissionsDescription": "Задайте уровень власти, необходимый для совершения определённых действий в этом чате. Уровни власти 0, 50 и 100 обычно означают пользователей, модераторов и администраторов соответственно, но любая градация также возможна.", + "@chatPermissionsDescription": {}, + "prepareSendingAttachment": "Подготовка к отправке вложения...", + "@prepareSendingAttachment": {}, + "sendRoomNotifications": "Упоминать @room", + "@sendRoomNotifications": {}, + "calculatingFileSize": "Вычисление размера файла...", + "@calculatingFileSize": {}, + "sendingAttachment": "Отправка вложения...", + "@sendingAttachment": {}, + "generatingVideoThumbnail": "Создание превью видео...", + "@generatingVideoThumbnail": {}, + "noticeChatBackupDeviceVerification": "Примечание: Если вы подключите все свои устройства к резервному копированию чатов, то они автоматически станут подтверждёнными.", + "@noticeChatBackupDeviceVerification": {}, + "changeTheCanonicalRoomAlias": "Изменить основной общедоступный адрес чата", + "@changeTheCanonicalRoomAlias": {}, + "loginWithMatrixId": "Войти через Matrix ID", + "@loginWithMatrixId": {}, + "whatIsAHomeserver": "Для чего нужен домашний сервер?", + "@whatIsAHomeserver": {}, + "homeserverDescription": "Все ваши данные хранятся на домашнем сервере, прямо как у вашего провайдера электронной почты. Вы можете выбрать, какому серверу вы их доверите, при этом сохраняя возможность общаться со всеми. Узнайте больше на https://matrix.org.", + "@homeserverDescription": {}, + "discoverHomeservers": "Список домашних серверов", + "@discoverHomeservers": {}, + "joinedChats": "Вступленные чаты", + "@joinedChats": {} } From 3c5855c2d1123086f91ab59c5f3dfa85cff59a7f Mon Sep 17 00:00:00 2001 From: Krille Date: Tue, 29 Oct 2024 08:07:34 +0100 Subject: [PATCH 113/236] design: New login design --- assets/l10n/intl_en.arb | 4 +- .../homeserver_picker/homeserver_picker.dart | 39 +- .../homeserver_picker_view.dart | 348 +++++++++++------- lib/pages/login/login_view.dart | 7 +- lib/widgets/layouts/login_scaffold.dart | 11 - 5 files changed, 247 insertions(+), 162 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 40ba16c50..cac5777c5 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2788,5 +2788,7 @@ } }, "oneOfYourDevicesIsNotVerified": "One of your devices is not verified", - "noticeChatBackupDeviceVerification": "Note: When you connect all your devices to the chat backup, they are automatically verified." + "noticeChatBackupDeviceVerification": "Note: When you connect all your devices to the chat backup, they are automatically verified.", + "continueText": "Continue", + "welcomeText": "Hey Hey 👋 This is FluffyChat. You can sign in to any homeserver, which is compatible with https://matrix.org. And then chat with anyone. It's a huge decentralized messaging network!" } diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index 81b33aa00..679b60206 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -11,6 +11,7 @@ import 'package:go_router/go_router.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:matrix/matrix.dart'; import 'package:universal_html/html.dart' as html; +import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; @@ -83,29 +84,30 @@ class HomeserverPickerController extends State { /// well-known information and forwards to the login page depending on the /// login type. Future checkHomeserverAction([_]) async { - homeserverController.text = + final homeserverInput = homeserverController.text.trim().toLowerCase().replaceAll(' ', '-'); - if (homeserverController.text.isEmpty) { + if (homeserverInput.isEmpty || !homeserverInput.contains('.')) { setState(() { error = loginFlows = null; isLoading = false; Matrix.of(context).getLoginClient().homeserver = null; + _lastCheckedUrl = null; }); return; } - if (_lastCheckedUrl == homeserverController.text) return; + if (_lastCheckedUrl == homeserverInput) return; - _lastCheckedUrl = homeserverController.text; + _lastCheckedUrl = homeserverInput; setState(() { error = loginFlows = null; isLoading = true; }); try { - var homeserver = Uri.parse(homeserverController.text); + var homeserver = Uri.parse(homeserverInput); if (homeserver.scheme.isEmpty) { - homeserver = Uri.https(homeserverController.text, ''); + homeserver = Uri.https(homeserverInput, ''); } final client = Matrix.of(context).getLoginClient(); final (_, _, loginFlows) = await client.checkHomeserver(homeserver); @@ -185,9 +187,15 @@ class HomeserverPickerController extends State { } } - void login() => context.push( - '${GoRouter.of(context).routeInformationProvider.value.uri.path}/login', - ); + void login() async { + if (!supportsPasswordLogin) { + homeserverController.text = AppConfig.defaultHomeserver; + await checkHomeserverAction(); + } + context.push( + '${GoRouter.of(context).routeInformationProvider.value.uri.path}/login', + ); + } @override void initState() { @@ -223,8 +231,21 @@ class HomeserverPickerController extends State { } } } + + void onMoreAction(MoreLoginActions action) { + switch (action) { + case MoreLoginActions.passwordLogin: + login(); + case MoreLoginActions.privacy: + launchUrlString(AppConfig.privacyUrl); + case MoreLoginActions.about: + PlatformInfos.showDialog(context); + } + } } +enum MoreLoginActions { passwordLogin, privacy, about } + class IdentityProvider { final String? id; final String? name; diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index 4aa4d2929..4cd02cdaa 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/widgets/adaptive_dialog_action.dart'; @@ -22,159 +23,228 @@ class HomeserverPickerView extends StatelessWidget { return LoginScaffold( enforceMobileMode: Matrix.of(context).client.isLogged(), - appBar: controller.widget.addMultiAccount - ? AppBar( - centerTitle: true, - title: Text(L10n.of(context).addAccount), - ) - : null, - body: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // display a prominent banner to import session for TOR browser - // users. This feature is just some UX sugar as TOR users are - // usually forced to logout as TOR browser is non-persistent - AnimatedContainer( - height: controller.isTorBrowser ? 64 : 0, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - child: Material( - clipBehavior: Clip.hardEdge, - borderRadius: - const BorderRadius.vertical(bottom: Radius.circular(8)), - color: theme.colorScheme.surface, - child: ListTile( - leading: const Icon(Icons.vpn_key), - title: Text(L10n.of(context).hydrateTor), - subtitle: Text(L10n.of(context).hydrateTorLong), - trailing: const Icon(Icons.chevron_right_outlined), - onTap: controller.restoreBackup, + appBar: AppBar( + centerTitle: true, + title: Text(L10n.of(context).addAccount), + actions: [ + PopupMenuButton( + onSelected: controller.onMoreAction, + itemBuilder: (_) => [ + PopupMenuItem( + value: MoreLoginActions.passwordLogin, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.login_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).loginWithMatrixId), + ], + ), ), - ), - ), - if (MediaQuery.of(context).size.height > 512) - ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height / 4, + PopupMenuItem( + value: MoreLoginActions.privacy, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.privacy_tip_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).privacy), + ], + ), ), - child: Image.asset( - 'assets/banner_transparent.png', - alignment: Alignment.center, - repeat: ImageRepeat.repeat, + PopupMenuItem( + value: MoreLoginActions.about, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.info_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).about), + ], + ), ), - ), - Padding( - padding: const EdgeInsets.all(32.0), - child: TextField( - onChanged: controller.tryCheckHomeserverActionWithCooldown, - onEditingComplete: - controller.tryCheckHomeserverActionWithoutCooldown, - onSubmitted: controller.tryCheckHomeserverActionWithoutCooldown, - onTap: controller.tryCheckHomeserverActionWithCooldown, - controller: controller.homeserverController, - autocorrect: false, - keyboardType: TextInputType.url, - decoration: InputDecoration( - prefixIcon: controller.isLoading - ? Container( - width: 16, - height: 16, - alignment: Alignment.center, - child: const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), + ], + ), + ], + ), + body: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Column( + children: [ + // display a prominent banner to import session for TOR browser + // users. This feature is just some UX sugar as TOR users are + // usually forced to logout as TOR browser is non-persistent + AnimatedContainer( + height: controller.isTorBrowser ? 64 : 0, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + child: Material( + clipBehavior: Clip.hardEdge, + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(8), ), - ) - : const Icon(Icons.search_outlined), - filled: false, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - ), - hintText: AppConfig.defaultHomeserver, - labelText: L10n.of(context).homeserver, - errorText: controller.error, - suffixIcon: IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => AlertDialog.adaptive( - title: Text(L10n.of(context).whatIsAHomeserver), - content: Linkify( - text: L10n.of(context).homeserverDescription, + color: theme.colorScheme.surface, + child: ListTile( + leading: const Icon(Icons.vpn_key), + title: Text(L10n.of(context).hydrateTor), + subtitle: Text(L10n.of(context).hydrateTorLong), + trailing: const Icon(Icons.chevron_right_outlined), + onTap: controller.restoreBackup, ), - actions: [ - AdaptiveDialogAction( - onPressed: () => launchUrl( - Uri.https('servers.joinmatrix.org'), + ), + ), + Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Hero( + tag: 'info-logo', + child: Image.asset( + './assets/banner_transparent.png', + fit: BoxFit.fitWidth, + ), + ), + ), + const SizedBox(height: 32), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: SelectableLinkify( + text: L10n.of(context).welcomeText, + style: TextStyle( + color: theme.colorScheme.onSecondaryContainer, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + linkStyle: TextStyle( + color: theme.colorScheme.secondary, + decorationColor: theme.colorScheme.secondary, + ), + onOpen: (link) => launchUrlString(link.url), + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + onChanged: + controller.tryCheckHomeserverActionWithCooldown, + onEditingComplete: controller + .tryCheckHomeserverActionWithoutCooldown, + onSubmitted: controller + .tryCheckHomeserverActionWithoutCooldown, + onTap: + controller.tryCheckHomeserverActionWithCooldown, + controller: controller.homeserverController, + autocorrect: false, + keyboardType: TextInputType.url, + decoration: InputDecoration( + prefixIcon: controller.isLoading + ? Container( + width: 16, + height: 16, + alignment: Alignment.center, + child: const SizedBox( + width: 16, + height: 16, + child: + CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ), + ) + : const Icon(Icons.search_outlined), + filled: false, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + ), + hintText: AppConfig.defaultHomeserver, + hintStyle: TextStyle( + color: theme.colorScheme.surfaceTint, + ), + labelText: 'Sign in with:', + errorText: controller.error, + errorMaxLines: 4, + suffixIcon: IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog.adaptive( + title: Text( + L10n.of(context).whatIsAHomeserver, + ), + content: Linkify( + text: L10n.of(context) + .homeserverDescription, + ), + actions: [ + AdaptiveDialogAction( + onPressed: () => launchUrl( + Uri.https('servers.joinmatrix.org'), + ), + child: Text( + L10n.of(context) + .discoverHomeservers, + ), + ), + AdaptiveDialogAction( + onPressed: Navigator.of(context).pop, + child: Text(L10n.of(context).close), + ), + ], + ), + ); + }, + icon: const Icon(Icons.info_outlined), + ), ), - child: Text( - L10n.of(context).discoverHomeservers, + ), + const SizedBox(height: 32), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, ), + onPressed: + controller.isLoggingIn || controller.isLoading + ? null + : controller.supportsSso + ? controller.ssoLoginAction + : controller.supportsPasswordLogin + ? controller.login + : null, + child: Text(L10n.of(context).continueText), ), - AdaptiveDialogAction( - onPressed: Navigator.of(context).pop, - child: Text(L10n.of(context).close), + TextButton( + style: TextButton.styleFrom( + foregroundColor: theme.colorScheme.secondary, + textStyle: theme.textTheme.labelMedium, + ), + onPressed: + controller.isLoggingIn || controller.isLoading + ? null + : controller.restoreBackup, + child: Text(L10n.of(context).hydrate), ), ], ), - ); - }, - icon: const Icon(Icons.info_outlined), + ), + ], ), ), ), - ), - if (MediaQuery.of(context).size.height > 512) const Spacer(), - ListView( - shrinkWrap: true, - padding: const EdgeInsets.symmetric( - horizontal: 32.0, - vertical: 32.0, - ), - children: [ - TextButton( - style: TextButton.styleFrom( - textStyle: theme.textTheme.labelMedium, - foregroundColor: theme.colorScheme.secondary, - ), - onPressed: controller.isLoggingIn || controller.isLoading - ? null - : controller.restoreBackup, - child: Text(L10n.of(context).hydrate), - ), - if (controller.supportsPasswordLogin && controller.supportsSso) - TextButton( - style: TextButton.styleFrom( - foregroundColor: theme.colorScheme.secondary, - textStyle: theme.textTheme.labelMedium, - ), - onPressed: controller.isLoggingIn || controller.isLoading - ? null - : controller.login, - child: Text(L10n.of(context).loginWithMatrixId), - ), - const SizedBox(height: 8.0), - if (controller.supportsPasswordLogin || controller.supportsSso) - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: theme.colorScheme.primary, - foregroundColor: theme.colorScheme.onPrimary, - ), - onPressed: controller.isLoggingIn || controller.isLoading - ? null - : controller.supportsSso - ? controller.ssoLoginAction - : controller.login, - child: Text(L10n.of(context).next), - ), - ], - ), - ], + ); + }, ), ); } diff --git a/lib/pages/login/login_view.dart b/lib/pages/login/login_view.dart index 7a7dbc953..5b04bbad0 100644 --- a/lib/pages/login/login_view.dart +++ b/lib/pages/login/login_view.dart @@ -49,7 +49,10 @@ class LoginView extends StatelessWidget { child: ListView( padding: const EdgeInsets.symmetric(horizontal: 8), children: [ - Image.asset('assets/banner_transparent.png'), + Hero( + tag: 'info-logo', + child: Image.asset('assets/banner_transparent.png'), + ), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), @@ -67,7 +70,7 @@ class LoginView extends StatelessWidget { prefixIcon: const Icon(Icons.account_box_outlined), errorText: controller.usernameError, errorStyle: const TextStyle(color: Colors.orange), - hintText: '@username:localpart', + hintText: '@username:domain', labelText: L10n.of(context).emailOrUsername, ), ), diff --git a/lib/widgets/layouts/login_scaffold.dart b/lib/widgets/layouts/login_scaffold.dart index d2363ced4..354cf4125 100644 --- a/lib/widgets/layouts/login_scaffold.dart +++ b/lib/widgets/layouts/login_scaffold.dart @@ -44,17 +44,6 @@ class LoginScaffold extends StatelessWidget { body: SafeArea(child: body), backgroundColor: isMobileMode ? null : theme.colorScheme.surface.withOpacity(0.8), - bottomNavigationBar: isMobileMode - ? Material( - elevation: 4, - shadowColor: theme.colorScheme.onSurface, - child: const SafeArea( - child: _PrivacyButtons( - mainAxisAlignment: MainAxisAlignment.center, - ), - ), - ) - : null, ); if (isMobileMode) return scaffold; return Container( From a51cc082bcee140437c39577e340ece86513390b Mon Sep 17 00:00:00 2001 From: krille-chan Date: Thu, 31 Oct 2024 17:57:11 +0100 Subject: [PATCH 114/236] chore: Improve spaces design --- lib/pages/chat_list/space_view.dart | 54 +++++++++++++---------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 1a83fa043..ad985d198 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -318,6 +318,16 @@ class _SpaceViewState extends State { ), ], ), + floatingActionButton: room?.canChangeStateEvent( + EventTypes.SpaceChild, + ) == + true + ? FloatingActionButton.extended( + onPressed: _addChatOrSubspace, + label: Text(L10n.of(context).chat), + icon: const Icon(Icons.chat_outlined), + ) + : null, body: room == null ? const Center( child: Icon( @@ -432,34 +442,6 @@ class _SpaceViewState extends State { return Column( mainAxisSize: MainAxisSize.min, children: [ - if (room.canChangeStateEvent( - EventTypes.SpaceChild, - ) && - filter.isEmpty) ...[ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 1, - ), - child: Material( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, - ), - clipBehavior: Clip.hardEdge, - child: ListTile( - onTap: _addChatOrSubspace, - leading: const CircleAvatar( - radius: Avatar.defaultSize / 2, - child: Icon(Icons.add_outlined), - ), - title: Text( - L10n.of(context).addChatOrSubSpace, - style: const TextStyle(fontSize: 14), - ), - ), - ), - ), - ], SearchTitle( title: L10n.of(context).joinedChats, icon: const Icon(Icons.chat_outlined), @@ -537,6 +519,10 @@ class _SpaceViewState extends State { BorderRadius.circular(AppConfig.borderRadius), clipBehavior: Clip.hardEdge, child: ListTile( + visualDensity: + const VisualDensity(vertical: -0.5), + contentPadding: + const EdgeInsets.symmetric(horizontal: 8), onTap: () => _joinChildRoom(item), leading: Avatar( mxContent: item.avatarUrl, @@ -556,9 +542,17 @@ class _SpaceViewState extends State { overflow: TextOverflow.ellipsis, ), ), - const SizedBox(width: 8), + Text( + item.numJoinedMembers.toString(), + style: TextStyle( + fontSize: 13, + color: theme.textTheme.bodyMedium!.color, + ), + ), + const SizedBox(width: 4), const Icon( - Icons.add_circle_outline_outlined, + Icons.people_outlined, + size: 14, ), ], ), From f143a60b61929c22b9ebb851697fde26697c9985 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Fri, 1 Nov 2024 07:36:03 +0100 Subject: [PATCH 115/236] chore: Improve spaces design --- lib/pages/chat_list/chat_list_body.dart | 1 + lib/pages/chat_list/space_view.dart | 19 ++++--------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index 7060bd3f6..8170732c2 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -35,6 +35,7 @@ class ChatListViewBody extends StatelessWidget { final activeSpace = controller.activeSpaceId; if (activeSpace != null) { return SpaceView( + key: ValueKey(activeSpace), spaceId: activeSpace, onBack: controller.clearActiveSpace, onChatTab: (room) => controller.onChatTap(room), diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index ad985d198..639bd1b90 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -324,8 +324,8 @@ class _SpaceViewState extends State { true ? FloatingActionButton.extended( onPressed: _addChatOrSubspace, - label: Text(L10n.of(context).chat), - icon: const Icon(Icons.chat_outlined), + label: Text(L10n.of(context).group), + icon: const Icon(Icons.group_add_outlined), ) : null, body: room == null @@ -436,20 +436,8 @@ class _SpaceViewState extends State { }, ), SliverList.builder( - itemCount: joinedRooms.length + 1, + itemCount: joinedRooms.length, itemBuilder: (context, i) { - if (i == 0) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - SearchTitle( - title: L10n.of(context).joinedChats, - icon: const Icon(Icons.chat_outlined), - ), - ], - ); - } - i--; final joinedRoom = joinedRooms[i]; return ChatListItem( joinedRoom, @@ -569,6 +557,7 @@ class _SpaceViewState extends State { ); }, ), + const SliverPadding(padding: EdgeInsets.only(top: 32)), ], ); }, From b9cd24eea75c2dbb3d34225e16ff2d235463d755 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Fri, 1 Nov 2024 08:14:47 +0100 Subject: [PATCH 116/236] chore: Improved UX for creating groups and spaces --- lib/config/routes.dart | 3 +- lib/pages/chat_list/chat_list.dart | 4 - lib/pages/chat_list/chat_list_item.dart | 12 +-- .../chat_list/client_chooser_button.dart | 14 --- lib/pages/new_group/new_group.dart | 86 +++++++++++---- lib/pages/new_group/new_group_view.dart | 97 +++++++++++++---- lib/pages/new_space/new_space.dart | 102 ------------------ lib/pages/new_space/new_space_view.dart | 92 ---------------- 8 files changed, 151 insertions(+), 259 deletions(-) delete mode 100644 lib/pages/new_space/new_space.dart delete mode 100644 lib/pages/new_space/new_space_view.dart diff --git a/lib/config/routes.dart b/lib/config/routes.dart index b839dd3f3..f12b27fdd 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -20,7 +20,6 @@ import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart' import 'package:fluffychat/pages/login/login.dart'; import 'package:fluffychat/pages/new_group/new_group.dart'; import 'package:fluffychat/pages/new_private_chat/new_private_chat.dart'; -import 'package:fluffychat/pages/new_space/new_space.dart'; import 'package:fluffychat/pages/settings/settings.dart'; import 'package:fluffychat/pages/settings_3pid/settings_3pid.dart'; import 'package:fluffychat/pages/settings_chat/settings_chat.dart'; @@ -163,7 +162,7 @@ abstract class AppRoutes { pageBuilder: (context, state) => defaultPageBuilder( context, state, - const NewSpace(), + const NewGroup(createGroupType: CreateGroupType.space), ), redirect: loggedOutRedirect, ), diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 22b4e5186..22868fa54 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -98,10 +98,6 @@ class ChatListController extends State StreamSubscription? _intentUriStreamSubscription; - void createNewSpace() { - context.push('/rooms/newspace'); - } - ActiveFilter activeFilter = AppConfig.separateChatTypes ? ActiveFilter.messages : ActiveFilter.allChats; diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 543df5a21..1fb0fc8a4 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -157,7 +157,12 @@ class ChatListItem extends StatelessWidget { right: 0, child: Avatar( border: space == null - ? null + ? room.isSpace + ? BorderSide( + width: 1, + color: theme.dividerColor, + ) + : null : BorderSide( width: 2, color: backgroundColor ?? @@ -251,11 +256,6 @@ class ChatListItem extends StatelessWidget { ), ), ), - if (room.isSpace) - const Icon( - Icons.arrow_circle_right_outlined, - size: 18, - ), ], ), subtitle: Row( diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 921ef6b83..041b83826 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -38,16 +38,6 @@ class ClientChooserButton extends StatelessWidget { ], ), ), - PopupMenuItem( - value: SettingsAction.newSpace, - child: Row( - children: [ - const Icon(Icons.workspaces_outlined), - const SizedBox(width: 18), - Text(L10n.of(context).createNewSpace), - ], - ), - ), PopupMenuItem( value: SettingsAction.setStatus, child: Row( @@ -260,9 +250,6 @@ class ClientChooserButton extends StatelessWidget { case SettingsAction.newGroup: context.go('/rooms/newgroup'); break; - case SettingsAction.newSpace: - controller.createNewSpace(); - break; case SettingsAction.invite: FluffyShare.shareInviteLink(context); break; @@ -352,7 +339,6 @@ class ClientChooserButton extends StatelessWidget { enum SettingsAction { addAccount, newGroup, - newSpace, setStatus, invite, settings, diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index 31beb6779..1fc59b18e 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -4,13 +4,18 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart' as sdk; +import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/new_group/new_group_view.dart'; import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/widgets/matrix.dart'; class NewGroup extends StatefulWidget { - const NewGroup({super.key}); + final CreateGroupType createGroupType; + const NewGroup({ + this.createGroupType = CreateGroupType.group, + super.key, + }); @override NewGroupController createState() => NewGroupController(); @@ -30,6 +35,14 @@ class NewGroupController extends State { bool loading = false; + CreateGroupType get createGroupType => + _createGroupType ?? widget.createGroupType; + + CreateGroupType? _createGroupType; + + void setCreateGroupType(Set b) => + setState(() => _createGroupType = b.single); + void setPublicGroup(bool b) => setState(() => publicGroup = b); void setGroupCanBeFound(bool b) => setState(() => groupCanBeFound = b); @@ -48,6 +61,52 @@ class NewGroupController extends State { }); } + Future _createGroup() async { + if (!mounted) return; + final roomId = await Matrix.of(context).client.createGroupChat( + visibility: + groupCanBeFound ? sdk.Visibility.public : sdk.Visibility.private, + preset: publicGroup + ? sdk.CreateRoomPreset.publicChat + : sdk.CreateRoomPreset.privateChat, + groupName: nameController.text.isNotEmpty ? nameController.text : null, + initialState: [ + if (avatar != null) + sdk.StateEvent( + type: sdk.EventTypes.RoomAvatar, + content: {'url': avatarUrl.toString()}, + ), + ], + ); + if (!mounted) return; + context.go('/rooms/$roomId/invite'); + } + + Future _createSpace() async { + if (!mounted) return; + final spaceId = await Matrix.of(context).client.createRoom( + preset: publicGroup + ? sdk.CreateRoomPreset.publicChat + : sdk.CreateRoomPreset.privateChat, + creationContent: {'type': RoomCreationTypes.mSpace}, + visibility: publicGroup ? sdk.Visibility.public : null, + roomAliasName: publicGroup + ? nameController.text.trim().toLowerCase().replaceAll(' ', '_') + : null, + name: nameController.text.trim(), + powerLevelContentOverride: {'events_default': 100}, + initialState: [ + if (avatar != null) + sdk.StateEvent( + type: sdk.EventTypes.RoomAvatar, + content: {'url': avatarUrl.toString()}, + ), + ], + ); + if (!mounted) return; + context.pop(spaceId); + } + void submitAction([_]) async { final client = Matrix.of(context).client; @@ -62,23 +121,12 @@ class NewGroupController extends State { if (!mounted) return; - final roomId = await client.createGroupChat( - visibility: - groupCanBeFound ? sdk.Visibility.public : sdk.Visibility.private, - preset: publicGroup - ? sdk.CreateRoomPreset.publicChat - : sdk.CreateRoomPreset.privateChat, - groupName: nameController.text.isNotEmpty ? nameController.text : null, - initialState: [ - if (avatar != null) - sdk.StateEvent( - type: sdk.EventTypes.RoomAvatar, - content: {'url': avatarUrl.toString()}, - ), - ], - ); - if (!mounted) return; - context.go('/rooms/$roomId/invite'); + switch (createGroupType) { + case CreateGroupType.group: + await _createGroup(); + case CreateGroupType.space: + await _createSpace(); + } } catch (e, s) { sdk.Logs().d('Unable to create group', e, s); setState(() { @@ -91,3 +139,5 @@ class NewGroupController extends State { @override Widget build(BuildContext context) => NewGroupView(this); } + +enum CreateGroupType { group, space } diff --git a/lib/pages/new_group/new_group_view.dart b/lib/pages/new_group/new_group_view.dart index 932fd2958..f4cfe3464 100644 --- a/lib/pages/new_group/new_group_view.dart +++ b/lib/pages/new_group/new_group_view.dart @@ -26,12 +26,33 @@ class NewGroupView extends StatelessWidget { onPressed: controller.loading ? null : Navigator.of(context).pop, ), ), - title: Text(L10n.of(context).createGroup), + title: Text( + controller.createGroupType == CreateGroupType.space + ? L10n.of(context).newSpace + : L10n.of(context).createGroup, + ), ), body: MaxWidthBody( child: Column( mainAxisSize: MainAxisSize.min, children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: SegmentedButton( + selected: {controller.createGroupType}, + onSelectionChanged: controller.setCreateGroupType, + segments: [ + ButtonSegment( + value: CreateGroupType.group, + label: Text(L10n.of(context).group), + ), + ButtonSegment( + value: CreateGroupType.space, + label: Text(L10n.of(context).space), + ), + ], + ), + ), const SizedBox(height: 16), InkWell( borderRadius: BorderRadius.circular(90), @@ -44,8 +65,8 @@ class NewGroupView extends StatelessWidget { borderRadius: BorderRadius.circular(90), child: Image.memory( avatar, - width: Avatar.defaultSize, - height: Avatar.defaultSize, + width: Avatar.defaultSize * 2, + height: Avatar.defaultSize * 2, fit: BoxFit.cover, ), ), @@ -53,7 +74,7 @@ class NewGroupView extends StatelessWidget { ), const SizedBox(height: 32), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 24.0), child: TextField( autofocus: true, controller: controller.nameController, @@ -61,7 +82,9 @@ class NewGroupView extends StatelessWidget { readOnly: controller.loading, decoration: InputDecoration( prefixIcon: const Icon(Icons.people_outlined), - labelText: L10n.of(context).groupName, + labelText: controller.createGroupType == CreateGroupType.space + ? L10n.of(context).spaceName + : L10n.of(context).groupName, ), ), ), @@ -69,12 +92,17 @@ class NewGroupView extends StatelessWidget { SwitchListTile.adaptive( contentPadding: const EdgeInsets.symmetric(horizontal: 32), secondary: const Icon(Icons.public_outlined), - title: Text(L10n.of(context).groupIsPublic), + title: Text( + controller.createGroupType == CreateGroupType.space + ? L10n.of(context).spaceIsPublic + : L10n.of(context).groupIsPublic, + ), value: controller.publicGroup, onChanged: controller.loading ? null : controller.setPublicGroup, ), AnimatedSize( duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, child: controller.publicGroup ? SwitchListTile.adaptive( contentPadding: @@ -88,20 +116,42 @@ class NewGroupView extends StatelessWidget { ) : const SizedBox.shrink(), ), - SwitchListTile.adaptive( - contentPadding: const EdgeInsets.symmetric(horizontal: 32), - secondary: Icon( - Icons.lock_outlined, - color: theme.colorScheme.onSurface, - ), - title: Text( - L10n.of(context).enableEncryption, - style: TextStyle( - color: theme.colorScheme.onSurface, - ), - ), - value: !controller.publicGroup, - onChanged: null, + AnimatedSize( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + child: controller.createGroupType == CreateGroupType.space + ? const SizedBox.shrink() + : SwitchListTile.adaptive( + contentPadding: + const EdgeInsets.symmetric(horizontal: 32), + secondary: Icon( + Icons.lock_outlined, + color: theme.colorScheme.onSurface, + ), + title: Text( + L10n.of(context).enableEncryption, + style: TextStyle( + color: theme.colorScheme.onSurface, + ), + ), + value: !controller.publicGroup, + onChanged: null, + ), + ), + AnimatedSize( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + child: controller.createGroupType == CreateGroupType.space + ? ListTile( + contentPadding: + const EdgeInsets.symmetric(horizontal: 32), + trailing: const Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: Icon(Icons.info_outlined), + ), + subtitle: Text(L10n.of(context).newSpaceDescription), + ) + : const SizedBox.shrink(), ), Padding( padding: const EdgeInsets.all(16.0), @@ -112,12 +162,17 @@ class NewGroupView extends StatelessWidget { controller.loading ? null : controller.submitAction, child: controller.loading ? const LinearProgressIndicator() - : Text(L10n.of(context).createGroupAndInviteUsers), + : Text( + controller.createGroupType == CreateGroupType.space + ? L10n.of(context).createNewSpace + : L10n.of(context).createGroupAndInviteUsers, + ), ), ), ), AnimatedSize( duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, child: error == null ? const SizedBox.shrink() : ListTile( diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart deleted file mode 100644 index 12bb4cc17..000000000 --- a/lib/pages/new_space/new_space.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; -import 'package:matrix/matrix.dart' as sdk; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/pages/new_space/new_space_view.dart'; -import 'package:fluffychat/utils/file_selector.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class NewSpace extends StatefulWidget { - const NewSpace({super.key}); - - @override - NewSpaceController createState() => NewSpaceController(); -} - -class NewSpaceController extends State { - TextEditingController nameController = TextEditingController(); - TextEditingController topicController = TextEditingController(); - bool publicGroup = false; - bool loading = false; - String? nameError; - String? topicError; - - Uint8List? avatar; - - Uri? avatarUrl; - - void selectPhoto() async { - final photo = await selectFiles( - context, - type: FileSelectorType.images, - ); - final bytes = await photo.firstOrNull?.readAsBytes(); - setState(() { - avatarUrl = null; - avatar = bytes; - }); - } - - void setPublicGroup(bool b) => setState(() => publicGroup = b); - - void submitAction([_]) async { - final client = Matrix.of(context).client; - setState(() { - nameError = topicError = null; - }); - if (nameController.text.isEmpty) { - setState(() { - nameError = L10n.of(context).pleaseChoose; - }); - return; - } - setState(() { - loading = true; - }); - try { - final avatar = this.avatar; - avatarUrl ??= avatar == null ? null : await client.uploadContent(avatar); - - final spaceId = await client.createRoom( - preset: publicGroup - ? sdk.CreateRoomPreset.publicChat - : sdk.CreateRoomPreset.privateChat, - creationContent: {'type': RoomCreationTypes.mSpace}, - visibility: publicGroup ? sdk.Visibility.public : null, - roomAliasName: publicGroup - ? nameController.text.trim().toLowerCase().replaceAll(' ', '_') - : null, - name: nameController.text.trim(), - topic: topicController.text.isEmpty ? null : topicController.text, - powerLevelContentOverride: {'events_default': 100}, - initialState: [ - if (avatar != null) - sdk.StateEvent( - type: sdk.EventTypes.RoomAvatar, - content: {'url': avatarUrl.toString()}, - ), - ], - ); - if (!mounted) return; - context.pop(spaceId); - } catch (e) { - setState(() { - topicError = e.toLocalizedString(context); - }); - } finally { - setState(() { - loading = false; - }); - } - // TODO: Go to spaces - } - - @override - Widget build(BuildContext context) => NewSpaceView(this); -} diff --git a/lib/pages/new_space/new_space_view.dart b/lib/pages/new_space/new_space_view.dart deleted file mode 100644 index 0f5813952..000000000 --- a/lib/pages/new_space/new_space_view.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/layouts/max_width_body.dart'; -import 'new_space.dart'; - -class NewSpaceView extends StatelessWidget { - final NewSpaceController controller; - - const NewSpaceView(this.controller, {super.key}); - - @override - Widget build(BuildContext context) { - final avatar = controller.avatar; - return Scaffold( - appBar: AppBar( - title: Text(L10n.of(context).createNewSpace), - ), - body: MaxWidthBody( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 16), - InkWell( - borderRadius: BorderRadius.circular(90), - onTap: controller.loading ? null : controller.selectPhoto, - child: CircleAvatar( - radius: Avatar.defaultSize, - child: avatar == null - ? const Icon(Icons.add_a_photo_outlined) - : ClipRRect( - borderRadius: BorderRadius.circular(90), - child: Image.memory( - avatar, - width: Avatar.defaultSize, - height: Avatar.defaultSize, - fit: BoxFit.cover, - ), - ), - ), - ), - const SizedBox(height: 32), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: TextField( - autofocus: true, - controller: controller.nameController, - autocorrect: false, - readOnly: controller.loading, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.people_outlined), - labelText: L10n.of(context).spaceName, - errorText: controller.nameError, - ), - ), - ), - const SizedBox(height: 16), - SwitchListTile.adaptive( - contentPadding: const EdgeInsets.symmetric(horizontal: 32), - title: Text(L10n.of(context).spaceIsPublic), - value: controller.publicGroup, - onChanged: controller.setPublicGroup, - ), - ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 32), - trailing: const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), - child: Icon(Icons.info_outlined), - ), - subtitle: Text(L10n.of(context).newSpaceDescription), - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: - controller.loading ? null : controller.submitAction, - child: controller.loading - ? const LinearProgressIndicator() - : Text(L10n.of(context).createNewSpace), - ), - ), - ), - ], - ), - ), - ); - } -} From c0efa3a330702f8d7128f37fba447ecd6b0855ef Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 31 Oct 2024 08:27:18 +0000 Subject: [PATCH 117/236] Translated using Weblate (German) Currently translated at 100.0% (672 of 672 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/ --- assets/l10n/intl_de.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/l10n/intl_de.arb b/assets/l10n/intl_de.arb index 826ea2d53..ef7803a60 100644 --- a/assets/l10n/intl_de.arb +++ b/assets/l10n/intl_de.arb @@ -2800,7 +2800,7 @@ "@loginWithMatrixId": {}, "discoverHomeservers": "Server suchen", "@discoverHomeservers": {}, - "homeserverDescription": "Alle deine Daten werden auf dem Homeserver gespeichert, so wie bei einem Email Anbieter. Du kannst aussuchen, welchen Homeserver du benutzen willst und kannst trotzdem mit allen kommunizieren. Erfahre mehr auf https:/matrix.org.", + "homeserverDescription": "Alle deine Daten werden auf einem Homeserver gespeichert, so wie bei einem E-Mail Anbieter. Du kannst aussuchen, welchen Homeserver du benutzen willst und kannst trotzdem mit allen kommunizieren. Erfahre mehr auf https://matrix.org.", "@homeserverDescription": {}, "sendingAttachment": "Anhang wird gesendet ...", "@sendingAttachment": {}, From 9ee7551ad4c8994b28864dceede156a10c329a61 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Fri, 1 Nov 2024 10:49:18 +0100 Subject: [PATCH 118/236] feat: Better wallpapers with blur and opacity sliders and improved styles page --- assets/l10n/intl_en.arb | 5 +- lib/pages/chat/chat_view.dart | 25 +- lib/pages/chat/events/message.dart | 12 - lib/pages/chat/events/state_message.dart | 8 - lib/pages/settings_style/settings_style.dart | 67 ++- .../settings_style/settings_style_view.dart | 414 ++++++++++-------- lib/utils/account_config.dart | 5 + 7 files changed, 314 insertions(+), 222 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index cac5777c5..941ca7b2e 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2790,5 +2790,8 @@ "oneOfYourDevicesIsNotVerified": "One of your devices is not verified", "noticeChatBackupDeviceVerification": "Note: When you connect all your devices to the chat backup, they are automatically verified.", "continueText": "Continue", - "welcomeText": "Hey Hey 👋 This is FluffyChat. You can sign in to any homeserver, which is compatible with https://matrix.org. And then chat with anyone. It's a huge decentralized messaging network!" + "welcomeText": "Hey Hey 👋 This is FluffyChat. You can sign in to any homeserver, which is compatible with https://matrix.org. And then chat with anyone. It's a huge decentralized messaging network!", + "blur": "Blur:", + "opacity": "Opacity:", + "setWallpaper": "Set wallpaper" } diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index beabe749a..0f7c45570 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -1,3 +1,5 @@ +import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import 'package:badges/badges.dart'; @@ -262,14 +264,21 @@ class ChatView extends StatelessWidget { children: [ if (accountConfig.wallpaperUrl != null) Opacity( - opacity: accountConfig.wallpaperOpacity ?? 1, - child: MxcImage( - uri: accountConfig.wallpaperUrl, - fit: BoxFit.cover, - isThumbnail: true, - width: FluffyThemes.columnWidth * 4, - height: FluffyThemes.columnWidth * 4, - placeholder: (_) => Container(), + opacity: accountConfig.wallpaperOpacity ?? 0.5, + child: ImageFiltered( + imageFilter: ui.ImageFilter.blur( + sigmaX: accountConfig.wallpaperBlur ?? 0.0, + sigmaY: accountConfig.wallpaperBlur ?? 0.0, + ), + child: MxcImage( + cacheKey: accountConfig.wallpaperUrl.toString(), + uri: accountConfig.wallpaperUrl, + fit: BoxFit.cover, + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + isThumbnail: false, + placeholder: (_) => Container(), + ), ), ), SafeArea( diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 60c1950f0..f72aa9b2c 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -449,12 +449,6 @@ class Message extends StatelessWidget { fontWeight: FontWeight.bold, fontSize: 12 * AppConfig.fontSizeFactor, color: theme.colorScheme.secondary, - shadows: [ - Shadow( - color: theme.colorScheme.surface, - blurRadius: 3, - ), - ], ), ), ), @@ -495,12 +489,6 @@ class Message extends StatelessWidget { fontWeight: FontWeight.bold, fontSize: 12 * AppConfig.fontSizeFactor, color: theme.colorScheme.secondary, - shadows: [ - Shadow( - color: theme.colorScheme.surface, - blurRadius: 3, - ), - ], ), ), ), diff --git a/lib/pages/chat/events/state_message.dart b/lib/pages/chat/events/state_message.dart index ab135a5c1..5fe56d1e7 100644 --- a/lib/pages/chat/events/state_message.dart +++ b/lib/pages/chat/events/state_message.dart @@ -12,8 +12,6 @@ class StateMessage extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Center( @@ -27,12 +25,6 @@ class StateMessage extends StatelessWidget { style: TextStyle( fontSize: 12 * AppConfig.fontSizeFactor, decoration: event.redacted ? TextDecoration.lineThrough : null, - shadows: [ - Shadow( - color: theme.colorScheme.surface, - blurRadius: 3, - ), - ], ), ), ), diff --git a/lib/pages/settings_style/settings_style.dart b/lib/pages/settings_style/settings_style.dart index 121a3504c..49b0b003c 100644 --- a/lib/pages/settings_style/settings_style.dart +++ b/lib/pages/settings_style/settings_style.dart @@ -45,14 +45,59 @@ class SettingsStyleController extends State { ); } - void setChatWallpaperOpacity(double opacity) { + double get wallpaperOpacity => + _wallpaperOpacity ?? + Matrix.of(context).client.applicationAccountConfig.wallpaperOpacity ?? + 0.5; + + double? _wallpaperOpacity; + + void saveWallpaperOpacity(double opacity) async { final client = Matrix.of(context).client; - showFutureLoadingDialog( + final result = await showFutureLoadingDialog( context: context, future: () => client.updateApplicationAccountConfig( ApplicationAccountConfig(wallpaperOpacity: opacity), ), ); + if (result.isValue) return; + + setState(() { + _wallpaperOpacity = client.applicationAccountConfig.wallpaperOpacity; + }); + } + + void updateWallpaperOpacity(double opacity) { + setState(() { + _wallpaperOpacity = opacity; + }); + } + + double get wallpaperBlur => + _wallpaperBlur ?? + Matrix.of(context).client.applicationAccountConfig.wallpaperBlur ?? + 0.5; + double? _wallpaperBlur; + + void saveWallpaperBlur(double blur) async { + final client = Matrix.of(context).client; + final result = await showFutureLoadingDialog( + context: context, + future: () => client.updateApplicationAccountConfig( + ApplicationAccountConfig(wallpaperBlur: blur), + ), + ); + if (result.isValue) return; + + setState(() { + _wallpaperBlur = client.applicationAccountConfig.wallpaperBlur; + }); + } + + void updateWallpaperBlur(double blur) { + setState(() { + _wallpaperBlur = blur; + }); } void deleteChatWallpaper() => showFutureLoadingDialog( @@ -60,7 +105,7 @@ class SettingsStyleController extends State { future: () => Matrix.of(context).client.setApplicationAccountConfig( const ApplicationAccountConfig( wallpaperUrl: null, - wallpaperOpacity: null, + wallpaperBlur: null, ), ), ); @@ -72,10 +117,26 @@ class SettingsStyleController extends State { null, AppConfig.chatColor, Colors.indigo, + Colors.blue, + Colors.blueAccent, + Colors.teal, + Colors.tealAccent, Colors.green, + Colors.greenAccent, + Colors.yellow, + Colors.yellowAccent, Colors.orange, + Colors.orangeAccent, + Colors.red, + Colors.redAccent, Colors.pink, + Colors.pinkAccent, + Colors.purple, + Colors.purpleAccent, Colors.blueGrey, + Colors.grey, + Colors.white, + Colors.black, ]; void switchTheme(ThemeMode? newTheme) { diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index d40315697..78ea12b4d 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -1,9 +1,14 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat/events/state_message.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; @@ -32,7 +37,36 @@ class SettingsStyleView extends StatelessWidget { backgroundColor: theme.colorScheme.surface, body: MaxWidthBody( child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: SegmentedButton( + selected: {controller.currentTheme}, + onSelectionChanged: (selected) => + controller.switchTheme(selected.single), + segments: [ + ButtonSegment( + value: ThemeMode.light, + label: Text(L10n.of(context).lightTheme), + icon: const Icon(Icons.light_mode_outlined), + ), + ButtonSegment( + value: ThemeMode.dark, + label: Text(L10n.of(context).darkTheme), + icon: const Icon(Icons.dark_mode_outlined), + ), + ButtonSegment( + value: ThemeMode.system, + label: Text(L10n.of(context).systemTheme), + icon: const Icon(Icons.auto_mode_outlined), + ), + ], + ), + ), + Divider( + color: theme.dividerColor, + ), ListTile( title: Text( L10n.of(context).setColorTheme, @@ -42,152 +76,61 @@ class SettingsStyleView extends StatelessWidget { ), ), ), - SizedBox( - height: colorPickerSize + 24, - child: ListView( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - children: SettingsStyleController.customColors - .map( - (color) => Padding( - padding: const EdgeInsets.all(12.0), + DynamicColorBuilder( + builder: (light, dark) { + final systemColor = + Theme.of(context).brightness == Brightness.light + ? light?.primary + : dark?.primary; + final colors = + List.from(SettingsStyleController.customColors); + if (systemColor == null) { + colors.remove(null); + } + return GridView.builder( + shrinkWrap: true, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 64, + ), + itemCount: colors.length, + itemBuilder: (context, i) { + final color = colors[i]; + return Padding( + padding: const EdgeInsets.all(12.0), + child: Tooltip( + message: color == null + ? L10n.of(context).systemTheme + : '#${color.value.toRadixString(16).toUpperCase()}', child: InkWell( borderRadius: BorderRadius.circular(colorPickerSize), onTap: () => controller.setChatColor(color), - child: color == null - ? Material( - elevation: 6, - shadowColor: AppConfig.colorSchemeSeed, - borderRadius: - BorderRadius.circular(colorPickerSize), - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - colorPickerSize, - ), - gradient: FluffyThemes.backgroundGradient( - context, - 255, + child: Material( + color: color ?? systemColor, + elevation: 6, + borderRadius: + BorderRadius.circular(colorPickerSize), + child: SizedBox( + width: colorPickerSize, + height: colorPickerSize, + child: controller.currentColor == color + ? Center( + child: Icon( + Icons.check, + size: 16, + color: Theme.of(context) + .colorScheme + .onPrimary, ), - ), - child: SizedBox( - height: colorPickerSize, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - ), - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (controller.currentColor == - null) - Padding( - padding: - const EdgeInsets.only( - right: 8.0, - ), - child: Icon( - Icons.check, - size: 16, - color: theme - .colorScheme.onSurface, - ), - ), - Text( - L10n.of(context).systemTheme, - textAlign: TextAlign.center, - style: TextStyle( - color: theme - .colorScheme.onSurface, - ), - ), - ], - ), - ), - ), - ), - ), - ) - : Material( - color: color, - elevation: 6, - borderRadius: - BorderRadius.circular(colorPickerSize), - child: SizedBox( - width: colorPickerSize, - height: colorPickerSize, - child: controller.currentColor == color - ? const Center( - child: Icon( - Icons.check, - size: 16, - color: Colors.white, - ), - ) - : null, - ), - ), + ) + : null, + ), + ), ), ), - ) - .toList(), - ), - ), - const SizedBox(height: 8), - Divider( - color: theme.dividerColor, - ), - ListTile( - title: Text( - L10n.of(context).setTheme, - style: TextStyle( - color: theme.colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - ), - RadioListTile( - groupValue: controller.currentTheme, - value: ThemeMode.system, - title: Text(L10n.of(context).systemTheme), - onChanged: controller.switchTheme, - ), - RadioListTile( - groupValue: controller.currentTheme, - value: ThemeMode.light, - title: Text(L10n.of(context).lightTheme), - onChanged: controller.switchTheme, - ), - RadioListTile( - groupValue: controller.currentTheme, - value: ThemeMode.dark, - title: Text(L10n.of(context).darkTheme), - onChanged: controller.switchTheme, - ), - Divider( - color: theme.dividerColor, - ), - ListTile( - title: Text( - L10n.of(context).overview, - style: TextStyle( - color: theme.colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - ), - SettingsSwitchListTile.adaptive( - title: L10n.of(context).presencesToggle, - onChanged: (b) => AppConfig.showPresences = b, - storeKey: SettingKeys.showPresences, - defaultValue: AppConfig.showPresences, - ), - SettingsSwitchListTile.adaptive( - title: L10n.of(context).separateChatTypes, - onChanged: (b) => AppConfig.separateChatTypes = b, - storeKey: SettingKeys.separateChatTypes, - defaultValue: AppConfig.separateChatTypes, + ); + }, + ); + }, ), Divider( color: theme.dividerColor, @@ -213,8 +156,6 @@ class SettingsStyleView extends StatelessWidget { ), builder: (context, snapshot) { final accountConfig = client.applicationAccountConfig; - final wallpaperOpacity = accountConfig.wallpaperOpacity ?? 1; - final wallpaperOpacityIsDefault = wallpaperOpacity == 1; return Column( mainAxisSize: MainAxisSize.min, @@ -222,56 +163,119 @@ class SettingsStyleView extends StatelessWidget { AnimatedContainer( duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, - alignment: Alignment.centerLeft, decoration: const BoxDecoration(), clipBehavior: Clip.hardEdge, child: Stack( + alignment: Alignment.center, children: [ if (accountConfig.wallpaperUrl != null) Opacity( - opacity: wallpaperOpacity, - child: MxcImage( - uri: accountConfig.wallpaperUrl, - fit: BoxFit.cover, - isThumbnail: true, - width: FluffyThemes.columnWidth * 2, - height: 156, + opacity: controller.wallpaperOpacity, + child: ImageFiltered( + imageFilter: ImageFilter.blur( + sigmaX: controller.wallpaperBlur, + sigmaY: controller.wallpaperBlur, + ), + child: MxcImage( + key: ValueKey(accountConfig.wallpaperUrl), + uri: accountConfig.wallpaperUrl, + fit: BoxFit.cover, + isThumbnail: true, + width: FluffyThemes.columnWidth * 2, + height: 212, + ), ), ), - Padding( - padding: EdgeInsets.only( - left: 12 + 12 + Avatar.defaultSize, - right: 12, - top: accountConfig.wallpaperUrl == null ? 0 : 12, - bottom: 12, - ), - child: Material( - color: theme.colorScheme.primary, - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, + Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16), + StateMessage( + Event( + eventId: 'style_dummy', + room: + Room(id: '!style_dummy', client: client), + content: {'membership': 'join'}, + type: EventTypes.RoomMember, + senderId: client.userID!, + originServerTs: DateTime.now(), + stateKey: client.userID!, + ), ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, + Padding( + padding: EdgeInsets.only( + left: 12 + 12 + Avatar.defaultSize, + right: 12, + top: accountConfig.wallpaperUrl == null + ? 0 + : 12, + bottom: 12, ), - child: Text( - 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor', - style: TextStyle( - color: theme.colorScheme.onPrimary, - fontSize: AppConfig.messageFontSize * - AppConfig.fontSizeFactor, + child: Material( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Text( + 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor', + style: TextStyle( + color: theme.colorScheme.onPrimary, + fontSize: AppConfig.messageFontSize * + AppConfig.fontSizeFactor, + ), + ), ), ), ), - ), + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.only( + right: 12, + left: 12, + top: accountConfig.wallpaperUrl == null + ? 0 + : 12, + bottom: 12, + ), + child: Material( + color: theme + .colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Text( + 'Lorem ipsum dolor sit amet', + style: TextStyle( + color: theme.colorScheme.onSurface, + fontSize: AppConfig.messageFontSize * + AppConfig.fontSizeFactor, + ), + ), + ), + ), + ), + ), + ], ), ], ), ), ListTile( - title: Text(L10n.of(context).wallpaper), - leading: const Icon(Icons.photo_outlined), + title: OutlinedButton( + onPressed: controller.setWallpaper, + child: Text(L10n.of(context).setWallpaper), + ), trailing: accountConfig.wallpaperUrl == null ? null : IconButton( @@ -279,23 +283,29 @@ class SettingsStyleView extends StatelessWidget { color: theme.colorScheme.error, onPressed: controller.deleteChatWallpaper, ), - onTap: controller.setWallpaper, - ), - AnimatedSize( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - child: accountConfig.wallpaperUrl != null - ? SwitchListTile.adaptive( - title: Text(L10n.of(context).transparent), - secondary: const Icon(Icons.blur_linear_outlined), - value: !wallpaperOpacityIsDefault, - onChanged: (_) => - controller.setChatWallpaperOpacity( - wallpaperOpacityIsDefault ? 0.4 : 1, - ), - ) - : null, ), + if (accountConfig.wallpaperUrl != null) ...[ + ListTile(title: Text(L10n.of(context).opacity)), + Slider.adaptive( + min: 0.1, + max: 1.0, + divisions: 9, + semanticFormatterCallback: (d) => d.toString(), + value: controller.wallpaperOpacity, + onChanged: controller.updateWallpaperOpacity, + onChangeEnd: controller.saveWallpaperOpacity, + ), + ListTile(title: Text(L10n.of(context).blur)), + Slider.adaptive( + min: 0.0, + max: 10.0, + divisions: 10, + semanticFormatterCallback: (d) => d.toString(), + value: controller.wallpaperBlur, + onChanged: controller.updateWallpaperBlur, + onChangeEnd: controller.saveWallpaperBlur, + ), + ], ], ); }, @@ -312,6 +322,30 @@ class SettingsStyleView extends StatelessWidget { semanticFormatterCallback: (d) => d.toString(), onChanged: controller.changeFontSizeFactor, ), + Divider( + color: theme.dividerColor, + ), + ListTile( + title: Text( + L10n.of(context).overview, + style: TextStyle( + color: theme.colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + ), + SettingsSwitchListTile.adaptive( + title: L10n.of(context).presencesToggle, + onChanged: (b) => AppConfig.showPresences = b, + storeKey: SettingKeys.showPresences, + defaultValue: AppConfig.showPresences, + ), + SettingsSwitchListTile.adaptive( + title: L10n.of(context).separateChatTypes, + onChanged: (b) => AppConfig.separateChatTypes = b, + storeKey: SettingKeys.separateChatTypes, + defaultValue: AppConfig.separateChatTypes, + ), ], ), ), diff --git a/lib/utils/account_config.dart b/lib/utils/account_config.dart index ee80e246e..a5e2dfd00 100644 --- a/lib/utils/account_config.dart +++ b/lib/utils/account_config.dart @@ -29,6 +29,7 @@ extension ApplicationAccountConfigExtension on Client { wallpaperUrl: config.wallpaperUrl ?? currentConfig.wallpaperUrl, wallpaperOpacity: config.wallpaperOpacity ?? currentConfig.wallpaperOpacity, + wallpaperBlur: config.wallpaperBlur ?? currentConfig.wallpaperBlur, ).toJson(), ); } @@ -37,10 +38,12 @@ extension ApplicationAccountConfigExtension on Client { class ApplicationAccountConfig { final Uri? wallpaperUrl; final double? wallpaperOpacity; + final double? wallpaperBlur; const ApplicationAccountConfig({ this.wallpaperUrl, this.wallpaperOpacity, + this.wallpaperBlur, }); static double _sanitizedOpacity(double? opacity) { @@ -56,10 +59,12 @@ class ApplicationAccountConfig { : null, wallpaperOpacity: _sanitizedOpacity(json.tryGet('wallpaper_opacity')), + wallpaperBlur: json.tryGet('wallpaper_blur'), ); Map toJson() => { 'wallpaper_url': wallpaperUrl?.toString(), 'wallpaper_opacity': wallpaperOpacity, + 'wallpaper_blur': wallpaperBlur, }; } From 66ea73ea18afd2ad3b631f76cc5126c4a19d3292 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Fri, 1 Nov 2024 11:59:08 +0100 Subject: [PATCH 119/236] chore: Follow up wallpaper configs --- lib/pages/chat/events/message.dart | 38 ++++++++++++--- .../events/room_creation_state_event.dart | 46 ++++++++++++------- lib/pages/chat/events/state_message.dart | 30 ++++++++---- 3 files changed, 81 insertions(+), 33 deletions(-) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index f72aa9b2c..3a326e617 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -265,6 +265,23 @@ class Message extends StatelessWidget { ? displayname.color : displayname .lightColorText), + shadows: + avatarPresenceBackgroundColor == + null + ? null + : [ + Shadow( + offset: + const Offset( + 0.0, + 0.0, + ), + blurRadius: 5, + color: theme + .colorScheme + .surface, + ), + ], ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -443,12 +460,21 @@ class Message extends StatelessWidget { child: Center( child: Padding( padding: const EdgeInsets.only(top: 4.0), - child: Text( - event.originServerTs.localizedTime(context), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12 * AppConfig.fontSizeFactor, - color: theme.colorScheme.secondary, + child: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius * 2), + color: theme.colorScheme.surface.withAlpha(128), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 2.0, + ), + child: Text( + event.originServerTs.localizedTime(context), + style: TextStyle( + fontSize: 12 * AppConfig.fontSizeFactor, + ), + ), ), ), ), diff --git a/lib/pages/chat/events/room_creation_state_event.dart b/lib/pages/chat/events/room_creation_state_event.dart index 8960a801f..398afc59f 100644 --- a/lib/pages/chat/events/room_creation_state_event.dart +++ b/lib/pages/chat/events/room_creation_state_event.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; @@ -17,24 +18,35 @@ class RoomCreationStateEvent extends StatelessWidget { final matrixLocals = MatrixLocals(l10n); final theme = Theme.of(context); final roomName = event.room.getLocalizedDisplayname(matrixLocals); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Avatar( - mxContent: event.room.avatar, - name: roomName, - size: Avatar.defaultSize * 2, + return Padding( + padding: const EdgeInsets.only(bottom: 32.0), + child: Center( + child: Material( + color: theme.colorScheme.surface.withAlpha(128), + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Avatar( + mxContent: event.room.avatar, + name: roomName, + size: Avatar.defaultSize * 2, + ), + Text( + roomName, + style: theme.textTheme.headlineSmall, + ), + Text( + '${event.originServerTs.localizedTime(context)} | ${l10n.countParticipants((event.room.summary.mJoinedMemberCount ?? 1) + (event.room.summary.mInvitedMemberCount ?? 0))}', + style: theme.textTheme.labelSmall, + ), + ], + ), + ), ), - Text( - roomName, - style: theme.textTheme.headlineSmall, - ), - Text( - '${event.originServerTs.localizedTime(context)} | ${l10n.countParticipants((event.room.summary.mJoinedMemberCount ?? 1) + (event.room.summary.mInvitedMemberCount ?? 0))}', - style: theme.textTheme.labelSmall, - ), - const SizedBox(height: 48), - ], + ), ); } } diff --git a/lib/pages/chat/events/state_message.dart b/lib/pages/chat/events/state_message.dart index 5fe56d1e7..6ef420e64 100644 --- a/lib/pages/chat/events/state_message.dart +++ b/lib/pages/chat/events/state_message.dart @@ -12,19 +12,29 @@ class StateMessage extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Center( - child: Container( - padding: const EdgeInsets.all(8), - child: Text( - event.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)), - ), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12 * AppConfig.fontSizeFactor, - decoration: event.redacted ? TextDecoration.lineThrough : null, + child: Padding( + padding: const EdgeInsets.all(4), + child: Material( + color: theme.colorScheme.surface.withAlpha(128), + borderRadius: BorderRadius.circular(AppConfig.borderRadius / 3), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: Text( + event.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)), + ), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12 * AppConfig.fontSizeFactor, + decoration: + event.redacted ? TextDecoration.lineThrough : null, + ), + ), ), ), ), From 84e2563628fcf732f2e4a2b5fa0cbd89688fefb7 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Fri, 1 Nov 2024 12:09:07 +0100 Subject: [PATCH 120/236] chore: Add max length to state messages --- lib/pages/chat/events/state_message.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pages/chat/events/state_message.dart b/lib/pages/chat/events/state_message.dart index 6ef420e64..28f1147b3 100644 --- a/lib/pages/chat/events/state_message.dart +++ b/lib/pages/chat/events/state_message.dart @@ -29,6 +29,8 @@ class StateMessage extends StatelessWidget { MatrixLocals(L10n.of(context)), ), textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12 * AppConfig.fontSizeFactor, decoration: From b0a074c35636e8ab79e598cd0768ab1798d7c8ed Mon Sep 17 00:00:00 2001 From: krille-chan Date: Fri, 1 Nov 2024 12:54:04 +0100 Subject: [PATCH 121/236] chore: Follow up wallpaper design --- lib/pages/chat/events/message.dart | 5 +++-- lib/pages/chat/events/message_content.dart | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 3a326e617..ae4f47c08 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -302,8 +302,7 @@ class Message extends StatelessWidget { child: AnimatedOpacity( opacity: animateIn ? 0 - : event.redacted || - event.messageType == + : event.messageType == MessageTypes.BadEncrypted || event.status.isSending ? 0.5 @@ -473,6 +472,8 @@ class Message extends StatelessWidget { event.originServerTs.localizedTime(context), style: TextStyle( fontSize: 12 * AppConfig.fontSizeFactor, + fontWeight: FontWeight.bold, + color: theme.colorScheme.secondary, ), ), ), diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 510d438e2..8f5d47485 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -242,7 +242,7 @@ class MessageContent extends StatelessWidget { reason, ), icon: '🗑️', - textColor: buttonTextColor, + textColor: buttonTextColor.withAlpha(128), onPressed: () => onInfoTab!(event), fontSize: fontSize, ); From be7a7fb671b6c16c2e0d2765878089eacfb407be Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sat, 2 Nov 2024 10:51:57 +0100 Subject: [PATCH 122/236] feat: Open account manage url when using MAS --- assets/l10n/intl_en.arb | 3 +- .../homeserver_picker/homeserver_picker.dart | 1 + lib/pages/settings/settings.dart | 7 + lib/pages/settings/settings_view.dart | 304 ++++++++++-------- 4 files changed, 171 insertions(+), 144 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 941ca7b2e..00325b0ca 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2793,5 +2793,6 @@ "welcomeText": "Hey Hey 👋 This is FluffyChat. You can sign in to any homeserver, which is compatible with https://matrix.org. And then chat with anyone. It's a huge decentralized messaging network!", "blur": "Blur:", "opacity": "Opacity:", - "setWallpaper": "Set wallpaper" + "setWallpaper": "Set wallpaper", + "manageAccount": "Manage account" } diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index 679b60206..d0535d77c 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -160,6 +160,7 @@ class HomeserverPickerController extends State { final result = await FlutterWebAuth2.authenticate( url: url.toString(), callbackUrlScheme: urlScheme, + options: FlutterWebAuth2Options(useWebview: !isDefaultPlatform), ); final token = Uri.parse(result).queryParameters['loginToken']; if (token?.isEmpty ?? false) return; diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index e1b713655..51929f357 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -201,6 +201,13 @@ class SettingsController extends State { checkBootstrap(); } + Future getOidcAccountManageUrl() async { + final wellKnown = await Matrix.of(context).client.getWellknown(); + return wellKnown.additionalProperties + .tryGetMap('org.matrix.msc2965.authentication') + ?.tryGet('account'); + } + @override Widget build(BuildContext context) { final client = Matrix.of(context).client; diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index f6fbcbed2..40934653f 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -32,154 +32,172 @@ class SettingsView extends StatelessWidget { ), body: ListTileTheme( iconColor: theme.colorScheme.onSurface, - child: ListView( - key: const Key('SettingsListViewContent'), - children: [ - FutureBuilder( - future: controller.profileFuture, - builder: (context, snapshot) { - final profile = snapshot.data; - final mxid = - Matrix.of(context).client.userID ?? L10n.of(context).user; - final displayname = - profile?.displayName ?? mxid.localpart ?? mxid; - return Row( - children: [ - Padding( - padding: const EdgeInsets.all(32.0), - child: Stack( - children: [ - Avatar( - mxContent: profile?.avatarUrl, - name: displayname, - size: Avatar.defaultSize * 2.5, + child: FutureBuilder( + future: controller.getOidcAccountManageUrl(), + builder: (context, snapshot) { + final accountManageUrl = snapshot.data; + return ListView( + key: const Key('SettingsListViewContent'), + children: [ + FutureBuilder( + future: controller.profileFuture, + builder: (context, snapshot) { + final profile = snapshot.data; + final mxid = Matrix.of(context).client.userID ?? + L10n.of(context).user; + final displayname = + profile?.displayName ?? mxid.localpart ?? mxid; + return Row( + children: [ + Padding( + padding: const EdgeInsets.all(32.0), + child: Stack( + children: [ + Avatar( + mxContent: profile?.avatarUrl, + name: displayname, + size: Avatar.defaultSize * 2.5, + ), + if (profile != null) + Positioned( + bottom: 0, + right: 0, + child: FloatingActionButton.small( + elevation: 2, + onPressed: controller.setAvatarAction, + heroTag: null, + child: + const Icon(Icons.camera_alt_outlined), + ), + ), + ], ), - if (profile != null) - Positioned( - bottom: 0, - right: 0, - child: FloatingActionButton.small( - elevation: 2, - onPressed: controller.setAvatarAction, - heroTag: null, - child: const Icon(Icons.camera_alt_outlined), + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextButton.icon( + onPressed: controller.setDisplaynameAction, + icon: const Icon( + Icons.edit_outlined, + size: 16, + ), + style: TextButton.styleFrom( + foregroundColor: theme.colorScheme.onSurface, + ), + label: Text( + displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 18, + ), + ), ), - ), - ], - ), - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextButton.icon( - onPressed: controller.setDisplaynameAction, - icon: const Icon( - Icons.edit_outlined, - size: 16, - ), - style: TextButton.styleFrom( - foregroundColor: theme.colorScheme.onSurface, - ), - label: Text( - displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 18, + TextButton.icon( + onPressed: () => + FluffyShare.share(mxid, context), + icon: const Icon( + Icons.copy_outlined, + size: 14, + ), + style: TextButton.styleFrom( + foregroundColor: theme.colorScheme.secondary, + ), + label: Text( + mxid, + maxLines: 1, + overflow: TextOverflow.ellipsis, + // style: const TextStyle(fontSize: 12), + ), ), - ), - ), - TextButton.icon( - onPressed: () => FluffyShare.share(mxid, context), - icon: const Icon( - Icons.copy_outlined, - size: 14, - ), - style: TextButton.styleFrom( - foregroundColor: theme.colorScheme.secondary, - ), - label: Text( - mxid, - maxLines: 1, - overflow: TextOverflow.ellipsis, - // style: const TextStyle(fontSize: 12), - ), + ], ), - ], - ), + ), + ], + ); + }, + ), + if (accountManageUrl != null) + ListTile( + leading: const Icon(Icons.account_circle_outlined), + title: Text(L10n.of(context).manageAccount), + trailing: const Icon(Icons.open_in_new_outlined), + onTap: () => launchUrlString( + accountManageUrl, + mode: LaunchMode.inAppBrowserView, ), - ], - ); - }, - ), - Divider(color: theme.dividerColor), - if (showChatBackupBanner == null) - ListTile( - leading: const Icon(Icons.backup_outlined), - title: Text(L10n.of(context).chatBackup), - trailing: const CircularProgressIndicator.adaptive(), - ) - else - SwitchListTile.adaptive( - controlAffinity: ListTileControlAffinity.trailing, - value: controller.showChatBackupBanner == false, - secondary: const Icon(Icons.backup_outlined), - title: Text(L10n.of(context).chatBackup), - onChanged: controller.firstRunBootstrapAction, - ), - Divider( - color: theme.dividerColor, - ), - ListTile( - leading: const Icon(Icons.format_paint_outlined), - title: Text(L10n.of(context).changeTheme), - onTap: () => context.go('/rooms/settings/style'), - ), - ListTile( - leading: const Icon(Icons.notifications_outlined), - title: Text(L10n.of(context).notifications), - onTap: () => context.go('/rooms/settings/notifications'), - ), - ListTile( - leading: const Icon(Icons.devices_outlined), - title: Text(L10n.of(context).devices), - onTap: () => context.go('/rooms/settings/devices'), - ), - ListTile( - leading: const Icon(Icons.forum_outlined), - title: Text(L10n.of(context).chat), - onTap: () => context.go('/rooms/settings/chat'), - ), - ListTile( - leading: const Icon(Icons.shield_outlined), - title: Text(L10n.of(context).security), - onTap: () => context.go('/rooms/settings/security'), - ), - Divider(color: theme.dividerColor), - ListTile( - leading: const Icon(Icons.help_outline_outlined), - title: Text(L10n.of(context).help), - onTap: () => launchUrlString(AppConfig.supportUrl), - ), - ListTile( - leading: const Icon(Icons.shield_sharp), - title: Text(L10n.of(context).privacy), - onTap: () => launchUrlString(AppConfig.privacyUrl), - ), - ListTile( - leading: const Icon(Icons.info_outline_rounded), - title: Text(L10n.of(context).about), - onTap: () => PlatformInfos.showDialog(context), - ), - Divider(color: theme.dividerColor), - ListTile( - leading: const Icon(Icons.logout_outlined), - title: Text(L10n.of(context).logout), - onTap: controller.logoutAction, - ), - ], + ), + Divider(color: theme.dividerColor), + if (showChatBackupBanner == null) + ListTile( + leading: const Icon(Icons.backup_outlined), + title: Text(L10n.of(context).chatBackup), + trailing: const CircularProgressIndicator.adaptive(), + ) + else + SwitchListTile.adaptive( + controlAffinity: ListTileControlAffinity.trailing, + value: controller.showChatBackupBanner == false, + secondary: const Icon(Icons.backup_outlined), + title: Text(L10n.of(context).chatBackup), + onChanged: controller.firstRunBootstrapAction, + ), + Divider( + color: theme.dividerColor, + ), + ListTile( + leading: const Icon(Icons.format_paint_outlined), + title: Text(L10n.of(context).changeTheme), + onTap: () => context.go('/rooms/settings/style'), + ), + ListTile( + leading: const Icon(Icons.notifications_outlined), + title: Text(L10n.of(context).notifications), + onTap: () => context.go('/rooms/settings/notifications'), + ), + ListTile( + leading: const Icon(Icons.devices_outlined), + title: Text(L10n.of(context).devices), + onTap: () => context.go('/rooms/settings/devices'), + ), + ListTile( + leading: const Icon(Icons.forum_outlined), + title: Text(L10n.of(context).chat), + onTap: () => context.go('/rooms/settings/chat'), + ), + ListTile( + leading: const Icon(Icons.shield_outlined), + title: Text(L10n.of(context).security), + onTap: () => context.go('/rooms/settings/security'), + ), + Divider(color: theme.dividerColor), + ListTile( + leading: const Icon(Icons.help_outline_outlined), + title: Text(L10n.of(context).help), + onTap: () => launchUrlString(AppConfig.supportUrl), + ), + ListTile( + leading: const Icon(Icons.shield_sharp), + title: Text(L10n.of(context).privacy), + onTap: () => launchUrlString(AppConfig.privacyUrl), + ), + ListTile( + leading: const Icon(Icons.info_outline_rounded), + title: Text(L10n.of(context).about), + onTap: () => PlatformInfos.showDialog(context), + ), + Divider(color: theme.dividerColor), + ListTile( + leading: const Icon(Icons.logout_outlined), + title: Text(L10n.of(context).logout), + onTap: controller.logoutAction, + ), + ], + ); + }, ), ), ); From 593acd7c3a2f655bbc846811cd63ed6507dc5933 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sat, 2 Nov 2024 11:27:22 +0100 Subject: [PATCH 123/236] chore: follow up wellknown fetch --- lib/pages/settings/settings.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 51929f357..5ab126359 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -202,7 +202,8 @@ class SettingsController extends State { } Future getOidcAccountManageUrl() async { - final wellKnown = await Matrix.of(context).client.getWellknown(); + final client = Matrix.of(context).client; + final wellKnown = client.wellKnown ?? await client.getWellknown(); return wellKnown.additionalProperties .tryGetMap('org.matrix.msc2965.authentication') ?.tryGet('account'); From ce57a34e03c68d5f026bb5ae900fcf85144b07d7 Mon Sep 17 00:00:00 2001 From: Rex_sa Date: Fri, 1 Nov 2024 20:33:36 +0000 Subject: [PATCH 124/236] Translated using Weblate (Arabic) Currently translated at 100.0% (674 of 674 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ar/ --- assets/l10n/intl_ar.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_ar.arb b/assets/l10n/intl_ar.arb index 3a72a9d36..a9c06797b 100644 --- a/assets/l10n/intl_ar.arb +++ b/assets/l10n/intl_ar.arb @@ -2832,5 +2832,9 @@ "oneOfYourDevicesIsNotVerified": "لم يتم التحقق من أحد أجهزتك", "@oneOfYourDevicesIsNotVerified": {}, "noticeChatBackupDeviceVerification": "ملاحظة: عند توصيل جميع أجهزتك بنسخة احتياطية للدردشة، يتم التحقق منها تلقائيًا.", - "@noticeChatBackupDeviceVerification": {} + "@noticeChatBackupDeviceVerification": {}, + "continueText": "استمرار", + "@continueText": {}, + "welcomeText": "مرحبًا، 👋 معك FluffyChat. يمكنك تسجيل الدخول إلى أي خادم منزلي، وهو متوافق مع https://matrix.org. ثم دردش مع أي شخص. إنها شبكة مراسلة لا مركزية ضخمة!", + "@welcomeText": {} } From 2c7bac82cb635ed1401de6ebc0cc91fe9c7cef8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Fri, 1 Nov 2024 10:10:12 +0000 Subject: [PATCH 125/236] Translated using Weblate (Estonian) Currently translated at 100.0% (674 of 674 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/ --- assets/l10n/intl_et.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_et.arb b/assets/l10n/intl_et.arb index 4879b112f..379dabe7a 100644 --- a/assets/l10n/intl_et.arb +++ b/assets/l10n/intl_et.arb @@ -2832,5 +2832,9 @@ "oneOfYourDevicesIsNotVerified": "Üks sinu seadmetest pole verifitseeritud", "@oneOfYourDevicesIsNotVerified": {}, "noticeChatBackupDeviceVerification": "Märkus: kui liidad kõik oma seadmed vestluste varundamisega, siis on nad sellega ka automaatselt verifitseeritud.", - "@noticeChatBackupDeviceVerification": {} + "@noticeChatBackupDeviceVerification": {}, + "continueText": "Jätka", + "@continueText": {}, + "welcomeText": "Tere, tere 👋 See on FluffyChat. Sa võid sisse logida igasse koduserverisse, mis ühildub https://matrix.org serveriga. Ja seejärel saad suhelda kõigiga. Tegemist on ikka väga suure detsentraliseeritud sõnumivõrguga!", + "@welcomeText": {} } From c28820174e94847a0655de617e71f92bab83a811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=8E=8B=E5=8F=AB=E6=88=91=E6=9D=A5=E5=B7=A1?= =?UTF-8?q?=E5=B1=B1?= Date: Sat, 2 Nov 2024 03:33:08 +0000 Subject: [PATCH 126/236] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (674 of 674 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/zh_Hans/ --- assets/l10n/intl_zh.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_zh.arb b/assets/l10n/intl_zh.arb index cbafa5b4b..717719b4f 100644 --- a/assets/l10n/intl_zh.arb +++ b/assets/l10n/intl_zh.arb @@ -2832,5 +2832,9 @@ "oneOfYourDevicesIsNotVerified": "您设备中的一台未验证", "@oneOfYourDevicesIsNotVerified": {}, "noticeChatBackupDeviceVerification": "注意:当你连接所有设备到聊天备份时,这些设备将被自动验证。", - "@noticeChatBackupDeviceVerification": {} + "@noticeChatBackupDeviceVerification": {}, + "welcomeText": "你好呀 👋 欢迎来到 FluffyChat。你可以登录任意兼容 https://matrix.org 的 homeserver,然后和任何人聊天。这是个巨大的去中心化消息网络!", + "@welcomeText": {}, + "continueText": "继续", + "@continueText": {} } From 5d402ebceb24e95ca983862d35745c5fe6472b5e Mon Sep 17 00:00:00 2001 From: Linerly Date: Sat, 2 Nov 2024 06:00:32 +0000 Subject: [PATCH 127/236] Translated using Weblate (Indonesian) Currently translated at 100.0% (674 of 674 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/id/ --- assets/l10n/intl_id.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_id.arb b/assets/l10n/intl_id.arb index df22e66b7..76b5a74b7 100644 --- a/assets/l10n/intl_id.arb +++ b/assets/l10n/intl_id.arb @@ -2831,5 +2831,9 @@ "oneOfYourDevicesIsNotVerified": "Salah satu perangkat Anda tidak terverifikasi", "@oneOfYourDevicesIsNotVerified": {}, "noticeChatBackupDeviceVerification": "Catatan: Ketika Anda menghubungkan semua perangkat Anda ke cadangan chat, mereka akan diverifikasi secara otomatis.", - "@noticeChatBackupDeviceVerification": {} + "@noticeChatBackupDeviceVerification": {}, + "welcomeText": "Halo 👋 Ini FluffyChat. Kamu bisa masuk ke homeserver mana pun, yang kompatibel dengan https://matrix.org. Lalu, chat dengan siapa pun. Ini merupakan jaringan perpesanan besar yang terdesentralisasi!", + "@welcomeText": {}, + "continueText": "Lanjutkan", + "@continueText": {} } From 0463aae68a3c351b7433f343a739d382d3d19c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Fri, 1 Nov 2024 10:16:01 +0000 Subject: [PATCH 128/236] Translated using Weblate (Finnish) Currently translated at 79.0% (533 of 674 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/fi/ --- assets/l10n/intl_fi.arb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/assets/l10n/intl_fi.arb b/assets/l10n/intl_fi.arb index b9123ff0f..f710a5909 100644 --- a/assets/l10n/intl_fi.arb +++ b/assets/l10n/intl_fi.arb @@ -424,7 +424,7 @@ "type": "text", "placeholders": {} }, - "defaultPermissionLevel": "Oikeuksien oletustaso", + "defaultPermissionLevel": "Uusien käyttäjien oikeuksien oletustaso", "@defaultPermissionLevel": { "type": "text", "placeholders": {} @@ -2282,9 +2282,9 @@ "@encryptThisChat": {}, "noBackupWarning": "Varoitus! Ilman avainvarmuuskopion käyttöönottoa menetät pääsyn salattuihin viesteihisi. Suosittelemme ehdottomasti avainvarmuuskopion käyttöönottoa ennen uloskirjautumista.", "@noBackupWarning": {}, - "fileIsTooBigForServer": "Palvelimen mukaan tiedosto on liian suuri lähetettäväksi.", + "fileIsTooBigForServer": "Ei voi lähettää! Palvelin tukee liitetiedostoja vain enintään {max}.", "@fileIsTooBigForServer": {}, - "reportErrorDescription": "Voi ei. Jokin meni pieleen. Yritäthän myöhemmin uudelleen. Halutessasi voit ilmoittaa ongelman kehittäjille.", + "reportErrorDescription": "😭 Voi ei. Jokin meni pieleen. Halutessasi voit ilmoittaa ongelman kehittäjille.", "@reportErrorDescription": {}, "wasDirectChatDisplayName": "Tyhjä keskustelu (oli {oldDisplayName})", "@wasDirectChatDisplayName": { @@ -2349,9 +2349,9 @@ "@redactMessageDescription": {}, "invalidInput": "Virheellinen syöte!", "@invalidInput": {}, - "addChatDescription": "Lisää keskustelulle kuvaus", + "addChatDescription": "Lisää keskustelulle kuvaus...", "@addChatDescription": {}, - "hasKnocked": "{user} on koputtanut", + "hasKnocked": "🚪 {user} on koputtanut", "@hasKnocked": { "placeholders": { "user": {} @@ -2402,5 +2402,13 @@ "importNow": "Tuo nyt", "@importNow": {}, "invite": "Kutsu", - "@invite": {} + "@invite": {}, + "swipeRightToLeftToReply": "Vastaa pyyhkäisemällä oikealta vasemmalle", + "@swipeRightToLeftToReply": {}, + "accessAndVisibility": "Pääsy ja näkyvyys", + "@accessAndVisibility": {}, + "unread": "Lukemattomat", + "@unread": {}, + "noMoreChatsFound": "Lisää keskusteluja ei löytynyt...", + "@noMoreChatsFound": {} } From 01bea4bd173835840144363c26b30aaca302b995 Mon Sep 17 00:00:00 2001 From: Edgars Andersons Date: Fri, 1 Nov 2024 13:56:06 +0000 Subject: [PATCH 129/236] Translated using Weblate (Latvian) Currently translated at 100.0% (674 of 674 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/lv/ --- assets/l10n/intl_lv.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_lv.arb b/assets/l10n/intl_lv.arb index b38871034..816826ae1 100644 --- a/assets/l10n/intl_lv.arb +++ b/assets/l10n/intl_lv.arb @@ -2810,5 +2810,9 @@ "oneOfYourDevicesIsNotVerified": "Viena no ierīcēm nav apliecināta", "@oneOfYourDevicesIsNotVerified": {}, "noticeChatBackupDeviceVerification": "Piezīme: kad visas ierīces tiek savienotas ar tērzēšanas rezerves kopiju, tās tiek automātiski apliecinātas.", - "@noticeChatBackupDeviceVerification": {} + "@noticeChatBackupDeviceVerification": {}, + "continueText": "Turpināt", + "@continueText": {}, + "welcomeText": "Sveicieni! 👋 Šis ir FluffyChat. Tu vari pieteikties jebkurā mājasserverī, kas ir saderīgs ar https://matrix.org. Tad vari tērzēt ar ikvienu. Tas ir milzīgs decentralizētās saziņas tīkls!", + "@welcomeText": {} } From 9019d9c9a6bd978d72f62208d86d595d8e74a916 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 3 Nov 2024 08:12:43 +0100 Subject: [PATCH 130/236] build: Add links to snapcraft.yaml file --- snap/snapcraft.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index b4e353b05..3e654baa1 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -3,6 +3,10 @@ title: FluffyChat base: core24 version: git license: AGPL-3.0 +website: https://fluffychat.im +source-code: https://github.com/krille-chan/fluffychat +issues: https://github.com/krille-chan/fluffychat/issues +donation: https://ko-fi.com/krille summary: The cutest messenger in the Matrix network description: | FluffyChat is an open source, nonprofit and cute matrix messenger app. The app is easy to use but secure and decentralized. From 8ba46a3533df8c66dc6b4115d56c6d0026cd14ad Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 3 Nov 2024 08:20:05 +0100 Subject: [PATCH 131/236] chore: Nicer empty page --- assets/logo_transparent.png | Bin 0 -> 7005 bytes lib/widgets/layouts/empty_page.dart | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 assets/logo_transparent.png diff --git a/assets/logo_transparent.png b/assets/logo_transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..5c0b0839143c4b86880352afce39ef305d0aeabb GIT binary patch literal 7005 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 Date: Sun, 3 Nov 2024 08:59:05 +0100 Subject: [PATCH 132/236] chore: Polish chat bubble colors --- lib/pages/chat/chat_event_list.dart | 3 +- lib/pages/chat/chat_view.dart | 4 +- lib/pages/chat/events/message.dart | 64 +++++++++++-------- .../settings_style/settings_style_view.dart | 17 +++-- 4 files changed, 55 insertions(+), 33 deletions(-) diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index d4614af10..e5f060e5a 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -142,8 +142,7 @@ class ChatEventList extends StatelessWidget { i > 0 && controller.readMarkerEventId == event.eventId, nextEvent: i + 1 < events.length ? events[i + 1] : null, previousEvent: i > 0 ? events[i - 1] : null, - avatarPresenceBackgroundColor: - hasWallpaper ? Colors.transparent : null, + wallpaperMode: hasWallpaper, ), ); }, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 0f7c45570..b50e85cc3 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -317,7 +317,9 @@ class ChatView extends StatelessWidget { alignment: Alignment.center, child: Material( clipBehavior: Clip.hardEdge, - color: theme.colorScheme.surfaceContainerHigh, + color: accountConfig.wallpaperUrl != null + ? theme.colorScheme.surfaceBright + : theme.colorScheme.surfaceContainerHigh, borderRadius: const BorderRadius.all( Radius.circular(24), ), diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index ae4f47c08..eb314996d 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -34,7 +34,7 @@ class Message extends StatelessWidget { final bool highlightMarker; final bool animateIn; final void Function()? resetAnimateIn; - final Color? avatarPresenceBackgroundColor; + final bool wallpaperMode; const Message( this.event, { @@ -52,7 +52,7 @@ class Message extends StatelessWidget { this.highlightMarker = false, this.animateIn = false, this.resetAnimateIn, - this.avatarPresenceBackgroundColor, + this.wallpaperMode = false, super.key, }); @@ -84,7 +84,9 @@ class Message extends StatelessWidget { final ownMessage = event.senderId == client.userID; final alignment = ownMessage ? Alignment.topRight : Alignment.topLeft; - var color = theme.colorScheme.surfaceContainerHigh; + var color = wallpaperMode + ? theme.colorScheme.surfaceBright + : theme.colorScheme.surfaceContainerHigh; final displayTime = event.type == EventTypes.RoomCreate || nextEvent == null || !event.originServerTs.sameEnvironment(nextEvent!.originServerTs); @@ -230,7 +232,7 @@ class Message extends StatelessWidget { name: user.calcDisplayname(), presenceUserId: user.stateKey, presenceBackgroundColor: - avatarPresenceBackgroundColor, + wallpaperMode ? Colors.transparent : null, onTap: () => onAvatarTab(event), ); }, @@ -265,23 +267,20 @@ class Message extends StatelessWidget { ? displayname.color : displayname .lightColorText), - shadows: - avatarPresenceBackgroundColor == - null - ? null - : [ - Shadow( - offset: - const Offset( - 0.0, - 0.0, - ), - blurRadius: 5, - color: theme - .colorScheme - .surface, - ), - ], + shadows: !wallpaperMode + ? null + : [ + Shadow( + offset: const Offset( + 0.0, + 0.0, + ), + blurRadius: 5, + color: theme + .colorScheme + .surface, + ), + ], ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -309,13 +308,26 @@ class Message extends StatelessWidget { : 1, duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, - child: Material( - color: - noBubble ? Colors.transparent : color, - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( + child: Container( + decoration: BoxDecoration( + color: ownMessage + ? null + : noBubble + ? Colors.transparent + : color, borderRadius: borderRadius, + gradient: ownMessage && !noBubble + ? LinearGradient( + colors: [ + theme.colorScheme.primary, + theme.colorScheme.secondary, + ], + begin: Alignment.centerLeft, + end: Alignment.bottomRight, + ) + : null, ), + clipBehavior: Clip.antiAlias, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular( diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index 78ea12b4d..1ea22f0ec 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -211,10 +211,19 @@ class SettingsStyleView extends StatelessWidget { : 12, bottom: 12, ), - child: Material( - color: theme.colorScheme.primary, - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + theme.colorScheme.primary, + theme.colorScheme.secondary, + ], + begin: Alignment.centerLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), ), child: Padding( padding: const EdgeInsets.symmetric( From 6e92693966b9c2c7ec41ecb40bf204dba8c264c9 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 3 Nov 2024 09:41:45 +0100 Subject: [PATCH 133/236] chore: Follow up chat bubble design --- lib/pages/chat/events/message.dart | 3 ++- lib/pages/settings_style/settings_style_view.dart | 15 +++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index eb314996d..5d5b01484 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -320,7 +320,8 @@ class Message extends StatelessWidget { ? LinearGradient( colors: [ theme.colorScheme.primary, - theme.colorScheme.secondary, + theme.colorScheme + .onPrimaryFixedVariant, ], begin: Alignment.centerLeft, end: Alignment.bottomRight, diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index 1ea22f0ec..ed8e5289e 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -216,7 +216,7 @@ class SettingsStyleView extends StatelessWidget { gradient: LinearGradient( colors: [ theme.colorScheme.primary, - theme.colorScheme.secondary, + theme.colorScheme.onPrimaryFixedVariant, ], begin: Alignment.centerLeft, end: Alignment.bottomRight, @@ -253,8 +253,10 @@ class SettingsStyleView extends StatelessWidget { bottom: 12, ), child: Material( - color: theme - .colorScheme.surfaceContainerHighest, + color: accountConfig.wallpaperUrl == null + ? theme + .colorScheme.surfaceContainerHighest + : theme.colorScheme.surfaceBright, borderRadius: BorderRadius.circular( AppConfig.borderRadius, ), @@ -281,7 +283,12 @@ class SettingsStyleView extends StatelessWidget { ), ), ListTile( - title: OutlinedButton( + title: TextButton( + style: TextButton.styleFrom( + backgroundColor: theme.colorScheme.secondaryContainer, + foregroundColor: + theme.colorScheme.onSecondaryContainer, + ), onPressed: controller.setWallpaper, child: Text(L10n.of(context).setWallpaper), ), From 4453048285c4662a9fafa7a65460903118c42e87 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 3 Nov 2024 11:02:06 +0100 Subject: [PATCH 134/236] refactor: Remove unnecessary builder widget --- lib/pages/chat/chat_event_list.dart | 25 +++++++++++++++++-------- lib/pages/chat/chat_view.dart | 15 +-------------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index e5f060e5a..57ac404ab 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -23,9 +23,18 @@ class ChatEventList extends StatelessWidget { @override Widget build(BuildContext context) { + final timeline = controller.timeline; + if (timeline == null) { + return const Center( + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ); + } + final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; - final events = controller.timeline!.events.filterByVisibleInGui(); + final events = timeline.events.filterByVisibleInGui(); final animateInEventIndex = controller.animateInEventIndex; // create a map of eventId --> index to greatly improve performance of @@ -55,12 +64,12 @@ class ChatEventList extends StatelessWidget { (BuildContext context, int i) { // Footer to display typing indicator and read receipts: if (i == 0) { - if (controller.timeline!.isRequestingFuture) { + if (timeline.isRequestingFuture) { return const Center( child: CircularProgressIndicator.adaptive(strokeWidth: 2), ); } - if (controller.timeline!.canRequestFuture) { + if (timeline.canRequestFuture) { return Center( child: IconButton( onPressed: controller.requestFuture, @@ -79,12 +88,12 @@ class ChatEventList extends StatelessWidget { // Request history button or progress indicator: if (i == events.length + 1) { - if (controller.timeline!.isRequestingHistory) { + if (timeline.isRequestingHistory) { return const Center( child: CircularProgressIndicator.adaptive(strokeWidth: 2), ); } - if (controller.timeline!.canRequestHistory) { + if (timeline.canRequestHistory) { return Builder( builder: (context) { WidgetsBinding.instance @@ -105,8 +114,8 @@ class ChatEventList extends StatelessWidget { // The message at this index: final event = events[i]; final animateIn = animateInEventIndex != null && - controller.timeline!.events.length > animateInEventIndex && - event == controller.timeline!.events[animateInEventIndex]; + timeline.events.length > animateInEventIndex && + event == timeline.events[animateInEventIndex]; return AutoScrollTag( key: ValueKey(event.eventId), @@ -137,7 +146,7 @@ class ChatEventList extends StatelessWidget { longPressSelect: controller.selectedEvents.isNotEmpty, selected: controller.selectedEvents .any((e) => e.eventId == event.eventId), - timeline: controller.timeline!, + timeline: timeline, displayReadMarker: i > 0 && controller.readMarkerEventId == event.eventId, nextEvent: i + 1 < events.length ? events[i + 1] : null, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index b50e85cc3..14c70459f 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -287,20 +287,7 @@ class ChatView extends StatelessWidget { Expanded( child: GestureDetector( onTap: controller.clearSingleSelectedEvent, - child: Builder( - builder: (context) { - if (controller.timeline == null) { - return const Center( - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), - ); - } - return ChatEventList( - controller: controller, - ); - }, - ), + child: ChatEventList(controller: controller), ), ), if (controller.room.canSendDefaultMessages && From 133e7ab955ee326734f2f30830a4d57080084947 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 3 Nov 2024 11:11:13 +0100 Subject: [PATCH 135/236] chore: Design adjustments --- lib/config/themes.dart | 4 +--- lib/pages/chat/chat_event_list.dart | 2 +- lib/pages/chat/chat_view.dart | 11 ++++++----- lib/pages/settings_style/settings_style_view.dart | 8 ++++++-- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/config/themes.dart b/lib/config/themes.dart index 2dcfe9dbb..f2335a005 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -77,9 +77,7 @@ abstract class FluffyThemes { ? Typography.material2018().black.merge(fallbackTextTheme) : Typography.material2018().white.merge(fallbackTextTheme) : null, - dividerColor: brightness == Brightness.light - ? Colors.blueGrey.shade50 - : Colors.blueGrey.shade900, + dividerColor: colorScheme.surfaceContainer, popupMenuTheme: PopupMenuThemeData( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppConfig.borderRadius), diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 57ac404ab..e88bec3f2 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -51,7 +51,7 @@ class ChatEventList extends StatelessWidget { child: ListView.custom( padding: EdgeInsets.only( top: 16, - bottom: 8, + bottom: 0, left: horizontalPadding, right: horizontalPadding, ), diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 14c70459f..7b8e3c3ea 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -290,14 +290,15 @@ class ChatView extends StatelessWidget { child: ChatEventList(controller: controller), ), ), + if (controller.showScrollDownButton) + Divider( + height: 1, + color: theme.dividerColor, + ), if (controller.room.canSendDefaultMessages && controller.room.membership == Membership.join) Container( - margin: EdgeInsets.only( - bottom: bottomSheetPadding, - left: bottomSheetPadding, - right: bottomSheetPadding, - ), + margin: EdgeInsets.all(bottomSheetPadding), constraints: const BoxConstraints( maxWidth: FluffyThemes.columnWidth * 2.5, ), diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index ed8e5289e..d73024ac9 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -282,15 +282,19 @@ class SettingsStyleView extends StatelessWidget { ], ), ), + Divider( + color: theme.dividerColor, + ), ListTile( - title: TextButton( + title: TextButton.icon( style: TextButton.styleFrom( backgroundColor: theme.colorScheme.secondaryContainer, foregroundColor: theme.colorScheme.onSecondaryContainer, ), onPressed: controller.setWallpaper, - child: Text(L10n.of(context).setWallpaper), + icon: const Icon(Icons.edit_outlined), + label: Text(L10n.of(context).setWallpaper), ), trailing: accountConfig.wallpaperUrl == null ? null From 6b4ed7bdc8b777e44576bdb9a97c59a942ee1f2e Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 3 Nov 2024 11:22:45 +0100 Subject: [PATCH 136/236] chore: Follow up design --- lib/pages/chat/chat_event_list.dart | 2 +- lib/pages/chat/chat_view.dart | 11 +++++------ lib/pages/chat_list/chat_list_item.dart | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index e88bec3f2..57ac404ab 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -51,7 +51,7 @@ class ChatEventList extends StatelessWidget { child: ListView.custom( padding: EdgeInsets.only( top: 16, - bottom: 0, + bottom: 8, left: horizontalPadding, right: horizontalPadding, ), diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 7b8e3c3ea..14c70459f 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -290,15 +290,14 @@ class ChatView extends StatelessWidget { child: ChatEventList(controller: controller), ), ), - if (controller.showScrollDownButton) - Divider( - height: 1, - color: theme.dividerColor, - ), if (controller.room.canSendDefaultMessages && controller.room.membership == Membership.join) Container( - margin: EdgeInsets.all(bottomSheetPadding), + margin: EdgeInsets.only( + bottom: bottomSheetPadding, + left: bottomSheetPadding, + right: bottomSheetPadding, + ), constraints: const BoxConstraints( maxWidth: FluffyThemes.columnWidth * 2.5, ), diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 1fb0fc8a4..114b9cd68 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -339,7 +339,7 @@ class ChatListItem extends StatelessWidget { : snapshot.data ?? L10n.of(context).emptyChat, softWrap: false, - maxLines: 1, + maxLines: room.hasNewMessages ? 2 : 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontWeight: unread || room.hasNewMessages From 972c58b5ad0571adf6e7017e50ad61e5657959b6 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 3 Nov 2024 11:24:24 +0100 Subject: [PATCH 137/236] refactor: Display two lines on new messages --- lib/pages/chat_list/chat_list_item.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 114b9cd68..d7ce69873 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -339,7 +339,8 @@ class ChatListItem extends StatelessWidget { : snapshot.data ?? L10n.of(context).emptyChat, softWrap: false, - maxLines: room.hasNewMessages ? 2 : 1, + maxLines: + room.notificationCount >= 1 ? 2 : 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontWeight: unread || room.hasNewMessages From 640e2ff25dcaafe10acba64fcfdc9d8898d576c3 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 3 Nov 2024 11:30:20 +0100 Subject: [PATCH 138/236] chore: Design follow up --- lib/pages/chat/events/message.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 5d5b01484..187440bea 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -513,7 +513,8 @@ class Message extends StatelessWidget { Row( children: [ Expanded( - child: Divider(color: theme.colorScheme.secondary), + child: + Divider(color: theme.colorScheme.surfaceContainerHighest), ), Container( margin: const EdgeInsets.symmetric( @@ -522,18 +523,24 @@ class Message extends StatelessWidget { ), padding: const EdgeInsets.symmetric( horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius / 3), + color: theme.colorScheme.surface.withAlpha(128), ), child: Text( L10n.of(context).readUpToHere, style: TextStyle( - fontWeight: FontWeight.bold, fontSize: 12 * AppConfig.fontSizeFactor, color: theme.colorScheme.secondary, ), ), ), Expanded( - child: Divider(color: theme.colorScheme.secondary), + child: + Divider(color: theme.colorScheme.surfaceContainerHighest), ), ], ), From e787051225d5b950ef8f1edb4cdff3133c234235 Mon Sep 17 00:00:00 2001 From: Rex_sa Date: Sat, 2 Nov 2024 12:33:42 +0000 Subject: [PATCH 139/236] Translated using Weblate (Arabic) Currently translated at 100.0% (678 of 678 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ar/ --- assets/l10n/intl_ar.arb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_ar.arb b/assets/l10n/intl_ar.arb index a9c06797b..ecc5df684 100644 --- a/assets/l10n/intl_ar.arb +++ b/assets/l10n/intl_ar.arb @@ -2836,5 +2836,13 @@ "continueText": "استمرار", "@continueText": {}, "welcomeText": "مرحبًا، 👋 معك FluffyChat. يمكنك تسجيل الدخول إلى أي خادم منزلي، وهو متوافق مع https://matrix.org. ثم دردش مع أي شخص. إنها شبكة مراسلة لا مركزية ضخمة!", - "@welcomeText": {} + "@welcomeText": {}, + "blur": "الضبابية:", + "@blur": {}, + "setWallpaper": "تعيين الخلفية", + "@setWallpaper": {}, + "opacity": "التعتيم:", + "@opacity": {}, + "manageAccount": "‫إدارة الحساب‬", + "@manageAccount": {} } From 88f8777a07b9e2476dfc300ff1d2d5df9de94f56 Mon Sep 17 00:00:00 2001 From: Christian Date: Sun, 3 Nov 2024 10:47:02 +0000 Subject: [PATCH 140/236] Translated using Weblate (German) Currently translated at 100.0% (678 of 678 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/ --- assets/l10n/intl_de.arb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_de.arb b/assets/l10n/intl_de.arb index ef7803a60..597cc94b4 100644 --- a/assets/l10n/intl_de.arb +++ b/assets/l10n/intl_de.arb @@ -2837,5 +2837,17 @@ "oneOfYourDevicesIsNotVerified": "Eines deiner Geräte ist nicht verifiziert", "@oneOfYourDevicesIsNotVerified": {}, "noticeChatBackupDeviceVerification": "Hinweis: Wenn du alle deine Geräte mit dem Chat-Backup verbindest, sind sie automatisch verifiziert.", - "@noticeChatBackupDeviceVerification": {} + "@noticeChatBackupDeviceVerification": {}, + "setWallpaper": "Hintergrund ändern", + "@setWallpaper": {}, + "opacity": "Deckkraft:", + "@opacity": {}, + "welcomeText": "Hey Hey 👋 Das ist FluffyChat. Du kannst sich bei jedem Homeserver anmelden, der mit https://matrix.org kompatibel ist. Und dann mit jedem chatten. Das hier ist ein riesiges dezentrales Nachrichtennetzwerk!", + "@welcomeText": {}, + "blur": "Verwischen:", + "@blur": {}, + "manageAccount": "Konto verwalten", + "@manageAccount": {}, + "continueText": "Fortfahren", + "@continueText": {} } From 201fba16e440d289edbd29c4cb0c3c235d1e7c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sat, 2 Nov 2024 20:25:50 +0000 Subject: [PATCH 141/236] Translated using Weblate (Estonian) Currently translated at 99.7% (676 of 678 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/ --- assets/l10n/intl_et.arb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/assets/l10n/intl_et.arb b/assets/l10n/intl_et.arb index 379dabe7a..6582bd7b6 100644 --- a/assets/l10n/intl_et.arb +++ b/assets/l10n/intl_et.arb @@ -1180,7 +1180,7 @@ "type": "text", "placeholders": {} }, - "openCamera": "Ava kaamera", + "openCamera": "Pildista", "@openCamera": { "type": "text", "placeholders": {} @@ -1981,7 +1981,7 @@ "type": "text", "description": "Usage hint for the command /create" }, - "openVideoCamera": "Video salvestamiseks ava kaamera", + "openVideoCamera": "Tee video", "@openVideoCamera": { "type": "text", "placeholders": {} @@ -2836,5 +2836,9 @@ "continueText": "Jätka", "@continueText": {}, "welcomeText": "Tere, tere 👋 See on FluffyChat. Sa võid sisse logida igasse koduserverisse, mis ühildub https://matrix.org serveriga. Ja seejärel saad suhelda kõigiga. Tegemist on ikka väga suure detsentraliseeritud sõnumivõrguga!", - "@welcomeText": {} + "@welcomeText": {}, + "setWallpaper": "Määra taustapildiks", + "@setWallpaper": {}, + "manageAccount": "Halda kasutajakontot", + "@manageAccount": {} } From 80023909e4acedb5ee2da9ba79d4d0cd214490f2 Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Sat, 2 Nov 2024 12:49:17 +0000 Subject: [PATCH 142/236] Translated using Weblate (Basque) Currently translated at 100.0% (678 of 678 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/eu/ --- assets/l10n/intl_eu.arb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_eu.arb b/assets/l10n/intl_eu.arb index 3d932159f..45ce2b3fd 100644 --- a/assets/l10n/intl_eu.arb +++ b/assets/l10n/intl_eu.arb @@ -2832,5 +2832,17 @@ "oneOfYourDevicesIsNotVerified": "Zure gailuetako bat ez dago egiaztatuta", "@oneOfYourDevicesIsNotVerified": {}, "noticeChatBackupDeviceVerification": "Oharra: gailu guztiak txat-babeskopiarekin konektatzen dituzunean, automatikoki egiaztatzen dira.", - "@noticeChatBackupDeviceVerification": {} + "@noticeChatBackupDeviceVerification": {}, + "opacity": "Opakutasuna:", + "@opacity": {}, + "manageAccount": "Kudeatu kontua", + "@manageAccount": {}, + "setWallpaper": "Ezarri horma-irudia", + "@setWallpaper": {}, + "blur": "Lausotu:", + "@blur": {}, + "continueText": "Jarraitu", + "@continueText": {}, + "welcomeText": "Ieup 👋 Ongi etorri FluffyChat-era. https://matrix.org-rekin bateragarria den edozein zerbitzaritan hasi dezakezu saioa eta edonorekin txateatu. Mezularitza-sare deszentralizatu eraraldoia da!", + "@welcomeText": {} } From 0dafd61f28e2c9119b77f7e4d79585736e9236ed Mon Sep 17 00:00:00 2001 From: Bezruchenko Simon Date: Sat, 2 Nov 2024 12:26:12 +0000 Subject: [PATCH 143/236] Translated using Weblate (Ukrainian) Currently translated at 100.0% (678 of 678 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/ --- assets/l10n/intl_uk.arb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_uk.arb b/assets/l10n/intl_uk.arb index d5e5ae451..dd35fd2b1 100644 --- a/assets/l10n/intl_uk.arb +++ b/assets/l10n/intl_uk.arb @@ -2832,5 +2832,17 @@ "oneOfYourDevicesIsNotVerified": "Один із ваших пристроїв не верифікований", "@oneOfYourDevicesIsNotVerified": {}, "noticeChatBackupDeviceVerification": "Примітка: Коли ви під'єднуєте всі свої пристрої до резервної копії бесіди, вони автоматично верифікуються.", - "@noticeChatBackupDeviceVerification": {} + "@noticeChatBackupDeviceVerification": {}, + "continueText": "Продовжити", + "@continueText": {}, + "manageAccount": "Керування обліковим записом", + "@manageAccount": {}, + "welcomeText": "Привіт-привіт 👋 Це FluffyChat. Ви можете увійти на будь-який сервер, сумісний із https://matrix.org. А потім спілкуватися з будь-ким. Це величезна децентралізована мережа для обміну повідомленнями!", + "@welcomeText": {}, + "blur": "Розмиття:", + "@blur": {}, + "opacity": "Прозорість:", + "@opacity": {}, + "setWallpaper": "Встановити шпалери", + "@setWallpaper": {} } From 81445221484ebfd29a636192495e6920c2a59ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=8E=8B=E5=8F=AB=E6=88=91=E6=9D=A5=E5=B7=A1?= =?UTF-8?q?=E5=B1=B1?= Date: Sun, 3 Nov 2024 02:39:35 +0000 Subject: [PATCH 144/236] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (678 of 678 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/zh_Hans/ --- assets/l10n/intl_zh.arb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_zh.arb b/assets/l10n/intl_zh.arb index 717719b4f..6e628dbc8 100644 --- a/assets/l10n/intl_zh.arb +++ b/assets/l10n/intl_zh.arb @@ -2836,5 +2836,13 @@ "welcomeText": "你好呀 👋 欢迎来到 FluffyChat。你可以登录任意兼容 https://matrix.org 的 homeserver,然后和任何人聊天。这是个巨大的去中心化消息网络!", "@welcomeText": {}, "continueText": "继续", - "@continueText": {} + "@continueText": {}, + "blur": "模糊:", + "@blur": {}, + "opacity": "不透明:", + "@opacity": {}, + "setWallpaper": "设置壁纸", + "@setWallpaper": {}, + "manageAccount": "管理账户", + "@manageAccount": {} } From 769baa3d9faba391a4afa3e110d95b4aad5b7b6a Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 3 Nov 2024 12:00:31 +0100 Subject: [PATCH 145/236] chore: Follow up message bubbles --- lib/pages/chat/events/message.dart | 9 ++++++--- lib/pages/settings_style/settings_style_view.dart | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 187440bea..8de6099e8 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -319,9 +319,12 @@ class Message extends StatelessWidget { gradient: ownMessage && !noBubble ? LinearGradient( colors: [ - theme.colorScheme.primary, - theme.colorScheme - .onPrimaryFixedVariant, + theme.brightness == + Brightness.light + ? theme.colorScheme + .onPrimaryFixedVariant + : theme.colorScheme + .primaryFixed, ], begin: Alignment.centerLeft, end: Alignment.bottomRight, diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index d73024ac9..f531b5280 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -216,7 +216,10 @@ class SettingsStyleView extends StatelessWidget { gradient: LinearGradient( colors: [ theme.colorScheme.primary, - theme.colorScheme.onPrimaryFixedVariant, + theme.brightness == Brightness.light + ? theme.colorScheme + .onPrimaryFixedVariant + : theme.colorScheme.primaryFixed, ], begin: Alignment.centerLeft, end: Alignment.bottomRight, From 812a1c047e4bc46cf70ff8132c9ebcfe7a19c150 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 3 Nov 2024 12:13:27 +0100 Subject: [PATCH 146/236] chore: Follow up design --- lib/pages/chat/events/message.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 8de6099e8..6a515d4d2 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -537,7 +537,6 @@ class Message extends StatelessWidget { L10n.of(context).readUpToHere, style: TextStyle( fontSize: 12 * AppConfig.fontSizeFactor, - color: theme.colorScheme.secondary, ), ), ), From da857d6abe2aa491cf1d6b079ec355683eeb9011 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 3 Nov 2024 12:51:22 +0100 Subject: [PATCH 147/236] chore: Follow up design --- lib/pages/chat/chat_view.dart | 4 +--- lib/pages/chat/events/message.dart | 14 +++++--------- lib/pages/settings_style/settings_style_view.dart | 6 ++---- lib/widgets/mxc_image.dart | 2 +- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 14c70459f..30a9687a4 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -304,9 +304,7 @@ class ChatView extends StatelessWidget { alignment: Alignment.center, child: Material( clipBehavior: Clip.hardEdge, - color: accountConfig.wallpaperUrl != null - ? theme.colorScheme.surfaceBright - : theme.colorScheme.surfaceContainerHigh, + color: theme.colorScheme.surfaceContainerHigh, borderRadius: const BorderRadius.all( Radius.circular(24), ), diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 6a515d4d2..dc4790e0f 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -84,9 +84,7 @@ class Message extends StatelessWidget { final ownMessage = event.senderId == client.userID; final alignment = ownMessage ? Alignment.topRight : Alignment.topLeft; - var color = wallpaperMode - ? theme.colorScheme.surfaceBright - : theme.colorScheme.surfaceContainerHigh; + var color = theme.colorScheme.surfaceContainerHigh; final displayTime = event.type == EventTypes.RoomCreate || nextEvent == null || !event.originServerTs.sameEnvironment(nextEvent!.originServerTs); @@ -270,15 +268,13 @@ class Message extends StatelessWidget { shadows: !wallpaperMode ? null : [ - Shadow( - offset: const Offset( + const Shadow( + offset: Offset( 0.0, 0.0, ), - blurRadius: 5, - color: theme - .colorScheme - .surface, + blurRadius: 3, + color: Colors.black, ), ], ), diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index f531b5280..0b73bbc3e 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -256,10 +256,8 @@ class SettingsStyleView extends StatelessWidget { bottom: 12, ), child: Material( - color: accountConfig.wallpaperUrl == null - ? theme - .colorScheme.surfaceContainerHighest - : theme.colorScheme.surfaceBright, + color: + theme.colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular( AppConfig.borderRadius, ), diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index bf007f530..1d5625998 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -140,7 +140,7 @@ class _MxcImageState extends State { return AnimatedCrossFade( crossFadeState: hasData ? CrossFadeState.showSecond : CrossFadeState.showFirst, - duration: FluffyThemes.animationDuration, + duration: const Duration(milliseconds: 128), firstChild: placeholder(context), secondChild: hasData ? Image.memory( From 2f39b1e8f47827ef53c144c36a31a045307f0751 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 3 Nov 2024 13:03:03 +0100 Subject: [PATCH 148/236] chore: Follow up colors --- lib/pages/chat/events/message.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index dc4790e0f..a2b40a7c1 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -315,6 +315,7 @@ class Message extends StatelessWidget { gradient: ownMessage && !noBubble ? LinearGradient( colors: [ + theme.colorScheme.primary, theme.brightness == Brightness.light ? theme.colorScheme From 65fd8a4184f1978119778d146b82403439889b82 Mon Sep 17 00:00:00 2001 From: Krille Date: Sun, 3 Nov 2024 14:39:22 +0100 Subject: [PATCH 149/236] chore: Follow up homeserverpicker UX --- lib/pages/homeserver_picker/homeserver_picker.dart | 8 +++++++- lib/pages/homeserver_picker/homeserver_picker_view.dart | 5 +---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index d0535d77c..95402306b 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -73,12 +73,18 @@ class HomeserverPickerController extends State { ); } - tryCheckHomeserverActionWithoutCooldown([_]) { + void tryCheckHomeserverActionWithoutCooldown([_]) { _checkHomeserverCooldown?.cancel(); _lastCheckedUrl = null; checkHomeserverAction(); } + void onSubmitted([_]) { + if (supportsSso) return ssoLoginAction(); + if (supportsPasswordLogin) return login(); + return tryCheckHomeserverActionWithoutCooldown(); + } + /// Starts an analysis of the given homeserver. It uses the current domain and /// makes sure that it is prefixed with https. Then it searches for the /// well-known information and forwards to the login page depending on the diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index 4cd02cdaa..1edfe1881 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -137,10 +137,7 @@ class HomeserverPickerView extends StatelessWidget { TextField( onChanged: controller.tryCheckHomeserverActionWithCooldown, - onEditingComplete: controller - .tryCheckHomeserverActionWithoutCooldown, - onSubmitted: controller - .tryCheckHomeserverActionWithoutCooldown, + onSubmitted: controller.onSubmitted, onTap: controller.tryCheckHomeserverActionWithCooldown, controller: controller.homeserverController, From cdaaad9c54632be2a2ca15871ecd2eb5caf51083 Mon Sep 17 00:00:00 2001 From: Krille Date: Sun, 3 Nov 2024 15:07:08 +0100 Subject: [PATCH 150/236] chore: Design follow up --- lib/pages/chat/events/message.dart | 23 +++---------------- lib/pages/chat/events/reply_content.dart | 7 +++--- .../chat_details/participant_list_item.dart | 19 +++++++++++---- .../settings_style/settings_style_view.dart | 12 +--------- 4 files changed, 23 insertions(+), 38 deletions(-) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index a2b40a7c1..46586b13c 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -306,27 +306,10 @@ class Message extends StatelessWidget { curve: FluffyThemes.animationCurve, child: Container( decoration: BoxDecoration( - color: ownMessage - ? null - : noBubble - ? Colors.transparent - : color, + color: noBubble + ? Colors.transparent + : color, borderRadius: borderRadius, - gradient: ownMessage && !noBubble - ? LinearGradient( - colors: [ - theme.colorScheme.primary, - theme.brightness == - Brightness.light - ? theme.colorScheme - .onPrimaryFixedVariant - : theme.colorScheme - .primaryFixed, - ], - begin: Alignment.centerLeft, - end: Alignment.bottomRight, - ) - : null, ), clipBehavior: Clip.antiAlias, child: Container( diff --git a/lib/pages/chat/events/reply_content.dart b/lib/pages/chat/events/reply_content.dart index a3f18efee..c77117264 100644 --- a/lib/pages/chat/events/reply_content.dart +++ b/lib/pages/chat/events/reply_content.dart @@ -34,8 +34,8 @@ class ReplyContent extends StatelessWidget { timeline != null ? replyEvent.getDisplayEvent(timeline) : replyEvent; final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor; final color = ownMessage - ? theme.colorScheme.primaryContainer - : theme.colorScheme.primary; + ? theme.colorScheme.tertiaryContainer + : theme.colorScheme.tertiary; return Material( color: backgroundColor ?? @@ -56,6 +56,7 @@ class ReplyContent extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ FutureBuilder( + initialData: displayEvent.senderFromMemoryOrFallback, future: displayEvent.fetchSenderUser(), builder: (context, snapshot) { return Text( @@ -80,7 +81,7 @@ class ReplyContent extends StatelessWidget { maxLines: 1, style: TextStyle( color: ownMessage - ? theme.colorScheme.onPrimary + ? theme.colorScheme.onTertiary : theme.colorScheme.onSurface, fontSize: fontSize, ), diff --git a/lib/pages/chat_details/participant_list_item.dart b/lib/pages/chat_details/participant_list_item.dart index b71f37310..2be366a7c 100644 --- a/lib/pages/chat_details/participant_list_item.dart +++ b/lib/pages/chat_details/participant_list_item.dart @@ -25,7 +25,7 @@ class ParticipantListItem extends StatelessWidget { Membership.leave => L10n.of(context).leftTheChat, }; - final permissionBatch = user.powerLevel == 100 + final permissionBatch = user.powerLevel >= 100 ? L10n.of(context).admin : user.powerLevel >= 50 ? L10n.of(context).moderator @@ -56,14 +56,20 @@ class ParticipantListItem extends StatelessWidget { vertical: 6, ), decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer, + color: user.powerLevel >= 100 + ? theme.colorScheme.tertiary + : theme.colorScheme.tertiaryContainer, borderRadius: BorderRadius.circular( AppConfig.borderRadius, ), ), child: Text( permissionBatch, - style: TextStyle(color: theme.colorScheme.onPrimaryContainer), + style: theme.textTheme.labelSmall?.copyWith( + color: user.powerLevel >= 100 + ? theme.colorScheme.onTertiary + : theme.colorScheme.onTertiaryContainer, + ), ), ), membershipBatch == null @@ -75,7 +81,12 @@ class ParticipantListItem extends StatelessWidget { color: theme.secondaryHeaderColor, borderRadius: BorderRadius.circular(8), ), - child: Center(child: Text(membershipBatch)), + child: Center( + child: Text( + membershipBatch, + style: theme.textTheme.labelSmall, + ), + ), ), ], ), diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index 0b73bbc3e..fe5d236b3 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -213,17 +213,7 @@ class SettingsStyleView extends StatelessWidget { ), child: DecoratedBox( decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - theme.colorScheme.primary, - theme.brightness == Brightness.light - ? theme.colorScheme - .onPrimaryFixedVariant - : theme.colorScheme.primaryFixed, - ], - begin: Alignment.centerLeft, - end: Alignment.bottomRight, - ), + color: theme.colorScheme.primary, borderRadius: BorderRadius.circular( AppConfig.borderRadius, ), From 448a111c4890b453e4ff5341895979d60e6918c2 Mon Sep 17 00:00:00 2001 From: Krille Date: Sun, 3 Nov 2024 16:58:04 +0100 Subject: [PATCH 151/236] feat: Add about server page --- assets/l10n/intl_en.arb | 16 +- lib/config/routes.dart | 12 + lib/pages/settings/settings_view.dart | 12 +- .../settings_homeserver.dart | 58 ++++ .../settings_homeserver_view.dart | 295 ++++++++++++++++++ lib/utils/localized_exception_extension.dart | 5 + 6 files changed, 391 insertions(+), 7 deletions(-) create mode 100644 lib/pages/settings_homeserver/settings_homeserver.dart create mode 100644 lib/pages/settings_homeserver/settings_homeserver_view.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 00325b0ca..9e67e4875 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -25,9 +25,12 @@ "replace": "Replace", "@replace": {}, "about": "About", - "@about": { + "aboutHomeserver": "About {homeserver}", + "@aboutHomeserver": { "type": "text", - "placeholders": {} + "placeholders": { + "homeserver": {} + } }, "accept": "Accept", "@accept": { @@ -2794,5 +2797,12 @@ "blur": "Blur:", "opacity": "Opacity:", "setWallpaper": "Set wallpaper", - "manageAccount": "Manage account" + "manageAccount": "Manage account", + "noContactInformationProvided": "Server does not provide any valid contact information", + "contactServerAdmin": "Contact server admin", + "contactServerSecurity": "Contact server security", + "supportPage": "Support page", + "serverInformation": "Server information:", + "name": "Name", + "version": "Version" } diff --git a/lib/config/routes.dart b/lib/config/routes.dart index f12b27fdd..8c508790f 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -24,6 +24,7 @@ import 'package:fluffychat/pages/settings/settings.dart'; import 'package:fluffychat/pages/settings_3pid/settings_3pid.dart'; import 'package:fluffychat/pages/settings_chat/settings_chat.dart'; import 'package:fluffychat/pages/settings_emotes/settings_emotes.dart'; +import 'package:fluffychat/pages/settings_homeserver/settings_homeserver.dart'; import 'package:fluffychat/pages/settings_ignore_list/settings_ignore_list.dart'; import 'package:fluffychat/pages/settings_multiple_emotes/settings_multiple_emotes.dart'; import 'package:fluffychat/pages/settings_notifications/settings_notifications.dart'; @@ -255,6 +256,17 @@ abstract class AppRoutes { ), ], ), + GoRoute( + path: 'homeserver', + pageBuilder: (context, state) { + return defaultPageBuilder( + context, + state, + const SettingsHomeserver(), + ); + }, + redirect: loggedOutRedirect, + ), GoRoute( path: 'security', redirect: loggedOutRedirect, diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index 40934653f..00b8fe7c2 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -175,12 +175,16 @@ class SettingsView extends StatelessWidget { ), Divider(color: theme.dividerColor), ListTile( - leading: const Icon(Icons.help_outline_outlined), - title: Text(L10n.of(context).help), - onTap: () => launchUrlString(AppConfig.supportUrl), + leading: const Icon(Icons.dns_outlined), + title: Text( + L10n.of(context).aboutHomeserver( + Matrix.of(context).client.userID?.domain ?? 'homeserver', + ), + ), + onTap: () => context.go('/rooms/settings/homeserver'), ), ListTile( - leading: const Icon(Icons.shield_sharp), + leading: const Icon(Icons.privacy_tip_outlined), title: Text(L10n.of(context).privacy), onTap: () => launchUrlString(AppConfig.privacyUrl), ), diff --git a/lib/pages/settings_homeserver/settings_homeserver.dart b/lib/pages/settings_homeserver/settings_homeserver.dart new file mode 100644 index 000000000..76ca08232 --- /dev/null +++ b/lib/pages/settings_homeserver/settings_homeserver.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +import 'package:http/http.dart' as http; +import 'package:matrix/matrix.dart'; + +import '../../widgets/matrix.dart'; +import 'settings_homeserver_view.dart'; + +class SettingsHomeserver extends StatefulWidget { + const SettingsHomeserver({super.key}); + + @override + SettingsHomeserverController createState() => SettingsHomeserverController(); +} + +class SettingsHomeserverController extends State { + Future<({String name, String version, Uri federationBaseUrl})> + fetchServerInfo() async { + final client = Matrix.of(context).client; + final domain = client.userID!.domain!; + final httpClient = client.httpClient; + var federationBaseUrl = Uri(host: domain, port: 8448, scheme: 'https'); + try { + final serverWellKnownResult = await httpClient.get( + Uri.https(domain, '/.well-known/matrix/server'), + ); + final serverWellKnown = jsonDecode(serverWellKnownResult.body); + federationBaseUrl = Uri.https(serverWellKnown['m.server']); + } catch (e, s) { + Logs().w( + 'Unable to fetch federation base uri. Use $federationBaseUrl', + e, + s, + ); + } + + final serverVersionResult = await http.get( + federationBaseUrl.resolveUri( + Uri(path: '/_matrix/federation/v1/version'), + ), + ); + final { + 'server': { + 'name': String name, + 'version': String version, + }, + } = Map>.from( + jsonDecode(serverVersionResult.body), + ); + + return (name: name, version: version, federationBaseUrl: federationBaseUrl); + } + + @override + Widget build(BuildContext context) => SettingsHomeserverView(this); +} diff --git a/lib/pages/settings_homeserver/settings_homeserver_view.dart b/lib/pages/settings_homeserver/settings_homeserver_view.dart new file mode 100644 index 000000000..792d6093e --- /dev/null +++ b/lib/pages/settings_homeserver/settings_homeserver_view.dart @@ -0,0 +1,295 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:matrix/matrix.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/widgets/layouts/max_width_body.dart'; +import '../../widgets/matrix.dart'; +import 'settings_homeserver.dart'; + +class SettingsHomeserverView extends StatelessWidget { + final SettingsHomeserverController controller; + + const SettingsHomeserverView(this.controller, {super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final client = Matrix.of(context).client; + + return Scaffold( + appBar: AppBar( + leading: const Center(child: BackButton()), + title: Text( + L10n.of(context) + .aboutHomeserver(client.userID?.domain ?? 'Homeserver'), + ), + ), + body: MaxWidthBody( + withScrolling: false, + child: SelectionArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text( + L10n.of(context).serverInformation, + style: TextStyle( + color: theme.colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + ), + FutureBuilder( + future: client.getWellknownSupport(), + builder: (context, snapshot) { + final error = snapshot.error; + final data = snapshot.data; + if (error != null) { + return ListTile( + leading: const Icon(Icons.error_outlined), + title: Text( + error.toLocalizedString( + context, + ExceptionContext.checkServerSupportInfo, + ), + style: const TextStyle(fontSize: 14), + ), + ); + } + if (data == null) { + return const Center( + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ); + } + final supportPage = data.supportPage; + final contacts = data.contacts; + if (supportPage == null && contacts == null) { + return ListTile( + leading: const Icon(Icons.error_outlined), + title: Text( + L10n.of(context).noContactInformationProvided, + style: const TextStyle(fontSize: 14), + ), + ); + } + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (supportPage != null) + ListTile( + title: Text(L10n.of(context).supportPage), + subtitle: Text(supportPage), + ), + if (contacts != null) + ...contacts.map( + (contact) { + return ListTile( + title: Text( + contact.role.localizedString( + L10n.of(context), + ), + ), + subtitle: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (contact.emailAddress != null) + TextButton( + onPressed: () {}, + child: Text(contact.emailAddress!), + ), + if (contact.matrixId != null) + TextButton( + onPressed: () {}, + child: Text(contact.matrixId!), + ), + ], + ), + ); + }, + ), + ], + ); + }, + ), + FutureBuilder( + future: controller.fetchServerInfo(), + builder: (context, snapshot) { + final error = snapshot.error; + if (error != null) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outlined, + color: theme.colorScheme.error, + ), + const SizedBox(height: 12), + Text( + error.toLocalizedString(context), + textAlign: TextAlign.center, + style: TextStyle( + color: theme.colorScheme.error, + ), + ), + ], + ); + } + final data = snapshot.data; + if (data == null) { + return const Center( + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ); + } + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text(L10n.of(context).name), + subtitle: Text(data.name), + ), + ListTile( + title: Text(L10n.of(context).version), + subtitle: Text(data.version), + ), + ListTile( + title: const Text('Federation Base URL'), + subtitle: Linkify( + text: data.federationBaseUrl.toString(), + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: theme.colorScheme.primary, + decorationColor: theme.colorScheme.primary, + ), + onOpen: (link) => launchUrlString(link.url), + ), + ), + ], + ); + }, + ), + Divider(color: theme.dividerColor), + FutureBuilder( + future: client.getWellknown(), + initialData: client.wellKnown, + builder: (context, snapshot) { + final error = snapshot.error; + if (error != null) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outlined, + color: theme.colorScheme.error, + ), + const SizedBox(height: 12), + Text( + error.toLocalizedString(context), + textAlign: TextAlign.center, + style: TextStyle( + color: theme.colorScheme.error, + ), + ), + ], + ); + } + final wellKnown = snapshot.data; + if (wellKnown == null) { + return const Center( + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ); + } + final identityServer = wellKnown.mIdentityServer; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text( + 'Client-Well-Known Information:', + style: TextStyle( + color: theme.colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + ), + ListTile( + title: const Text('Base URL'), + subtitle: Linkify( + text: wellKnown.mHomeserver.baseUrl.toString(), + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: theme.colorScheme.primary, + decorationColor: theme.colorScheme.primary, + ), + onOpen: (link) => launchUrlString(link.url), + ), + ), + if (identityServer != null) + ListTile( + title: const Text('Identity Server:'), + subtitle: Linkify( + text: identityServer.baseUrl.toString(), + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: theme.colorScheme.primary, + decorationColor: theme.colorScheme.primary, + ), + onOpen: (link) => launchUrlString(link.url), + ), + ), + ...wellKnown.additionalProperties.entries.map( + (entry) => ListTile( + title: Text(entry.key), + subtitle: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + color: theme.colorScheme.surfaceContainer, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + scrollDirection: Axis.horizontal, + child: Text( + const JsonEncoder.withIndent(' ') + .convert(entry.value), + style: TextStyle( + color: theme.colorScheme.onSurface, + ), + ), + ), + ), + ), + ), + ], + ); + }, + ), + ], + ), + ), + ), + ); + } +} + +extension on Role { + String localizedString(L10n l10n) { + switch (this) { + case Role.mRoleAdmin: + return l10n.contactServerAdmin; + case Role.mRoleSecurity: + return l10n.contactServerSecurity; + } + } +} diff --git a/lib/utils/localized_exception_extension.dart b/lib/utils/localized_exception_extension.dart index 8c1c3a53a..4716dcbdb 100644 --- a/lib/utils/localized_exception_extension.dart +++ b/lib/utils/localized_exception_extension.dart @@ -95,6 +95,10 @@ extension LocalizedExceptionExtension on Object { exceptionContext == ExceptionContext.checkHomeserver) { return L10n.of(context).doesNotSeemToBeAValidHomeserver; } + if (this is FormatException && + exceptionContext == ExceptionContext.checkServerSupportInfo) { + return L10n.of(context).noContactInformationProvided; + } if (this is String) return toString(); if (this is UiaException) return toString(); Logs().w('Something went wrong: ', this); @@ -105,4 +109,5 @@ extension LocalizedExceptionExtension on Object { enum ExceptionContext { changePassword, checkHomeserver, + checkServerSupportInfo, } From c447c2087f87bf64908d876877c5a10bbce0fe96 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 4 Nov 2024 12:48:07 +0100 Subject: [PATCH 152/236] chore: Follow up update snackbar --- lib/utils/show_update_snackbar.dart | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/lib/utils/show_update_snackbar.dart b/lib/utils/show_update_snackbar.dart index 1c4ee8972..1b408ec22 100644 --- a/lib/utils/show_update_snackbar.dart +++ b/lib/utils/show_update_snackbar.dart @@ -11,7 +11,6 @@ abstract class UpdateNotifier { static const String versionStoreKey = 'last_known_version'; static void showUpdateSnackBar(BuildContext context) async { - final theme = Theme.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context); final currentVersion = await PlatformInfos.getVersion(); final store = await SharedPreferences.getInstance(); @@ -19,27 +18,11 @@ abstract class UpdateNotifier { if (currentVersion != storedVersion) { if (storedVersion != null) { - ScaffoldFeatureController? controller; - controller = scaffoldMessenger.showSnackBar( + scaffoldMessenger.showSnackBar( SnackBar( duration: const Duration(seconds: 30), - content: Row( - children: [ - IconButton( - icon: Icon( - Icons.close_outlined, - size: 20, - color: theme.colorScheme.onPrimary, - ), - onPressed: () => controller?.close(), - ), - Expanded( - child: Text( - L10n.of(context).updateInstalled(currentVersion), - ), - ), - ], - ), + showCloseIcon: true, + content: Text(L10n.of(context).updateInstalled(currentVersion)), action: SnackBarAction( label: L10n.of(context).changelog, onPressed: () => launchUrlString(AppConfig.changelogUrl), From 3a8bb47e2c374c31e37d4dbe3ce86e45ab866459 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 4 Nov 2024 13:52:43 +0100 Subject: [PATCH 153/236] chore: Polish login design --- assets/l10n/intl_en.arb | 3 +- assets/login_wallpaper.png | Bin 149839 -> 0 bytes lib/config/app_config.dart | 1 + lib/config/themes.dart | 2 +- lib/widgets/layouts/login_scaffold.dart | 84 +++++++++++------------- 5 files changed, 42 insertions(+), 48 deletions(-) delete mode 100644 assets/login_wallpaper.png diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 9e67e4875..247e010b1 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2804,5 +2804,6 @@ "supportPage": "Support page", "serverInformation": "Server information:", "name": "Name", - "version": "Version" + "version": "Version", + "website": "Website" } diff --git a/assets/login_wallpaper.png b/assets/login_wallpaper.png deleted file mode 100644 index 2ca9db9caf0c8054510e17b406028c97228ac2cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 149839 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 z

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 _privacyUrl; + static const String website = 'https://fluffychat.im'; static const String enablePushTutorial = 'https://github.com/krille-chan/fluffychat/wiki/Push-Notifications-without-Google-Services'; static const String encryptionTutorial = diff --git a/lib/config/themes.dart b/lib/config/themes.dart index f2335a005..d5e08eba7 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -97,7 +97,7 @@ abstract class FluffyThemes { appBarTheme: AppBarTheme( toolbarHeight: FluffyThemes.isColumnMode(context) ? 72 : 56, shadowColor: FluffyThemes.isColumnMode(context) - ? Colors.grey.withAlpha(64) + ? colorScheme.surfaceContainer.withAlpha(64) : null, surfaceTintColor: FluffyThemes.isColumnMode(context) ? colorScheme.surface : null, diff --git a/lib/widgets/layouts/login_scaffold.dart b/lib/widgets/layouts/login_scaffold.dart index 354cf4125..64eecb739 100644 --- a/lib/widgets/layouts/login_scaffold.dart +++ b/lib/widgets/layouts/login_scaffold.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -27,30 +25,23 @@ class LoginScaffold extends StatelessWidget { final isMobileMode = enforceMobileMode || !FluffyThemes.isColumnMode(context); - final scaffold = Scaffold( - key: const Key('LoginScaffold'), - appBar: appBar == null - ? null - : AppBar( - titleSpacing: appBar?.titleSpacing, - automaticallyImplyLeading: - appBar?.automaticallyImplyLeading ?? true, - centerTitle: appBar?.centerTitle, - title: appBar?.title, - leading: appBar?.leading, - actions: appBar?.actions, - backgroundColor: isMobileMode ? null : Colors.transparent, - ), - body: SafeArea(child: body), - backgroundColor: - isMobileMode ? null : theme.colorScheme.surface.withOpacity(0.8), - ); - if (isMobileMode) return scaffold; + if (isMobileMode) { + return Scaffold( + key: const Key('LoginScaffold'), + appBar: appBar, + body: SafeArea(child: body), + ); + } return Container( - decoration: const BoxDecoration( - image: DecorationImage( - fit: BoxFit.cover, - image: AssetImage('assets/login_wallpaper.png'), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + theme.colorScheme.surfaceContainerLow, + theme.colorScheme.surfaceContainer, + theme.colorScheme.surfaceContainerHighest, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, ), ), child: Column( @@ -61,7 +52,6 @@ class LoginScaffold extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Material( - color: Colors.transparent, borderRadius: BorderRadius.circular(AppConfig.borderRadius), clipBehavior: Clip.hardEdge, elevation: theme.appBarTheme.scrolledUnderElevation ?? 4, @@ -70,12 +60,10 @@ class LoginScaffold extends StatelessWidget { constraints: isMobileMode ? const BoxConstraints() : const BoxConstraints(maxWidth: 480, maxHeight: 640), - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 10.0, - sigmaY: 10.0, - ), - child: scaffold, + child: Scaffold( + key: const Key('LoginScaffold'), + appBar: appBar, + body: SafeArea(child: body), ), ), ), @@ -95,18 +83,8 @@ class _PrivacyButtons extends StatelessWidget { @override Widget build(BuildContext context) { - final shadowTextStyle = FluffyThemes.isColumnMode(context) - ? const TextStyle( - color: Colors.white, - shadows: [ - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 3, - color: Colors.black, - ), - ], - ) - : null; + final theme = Theme.of(context); + final shadowTextStyle = TextStyle(color: theme.colorScheme.secondary); return SizedBox( height: 64, child: Padding( @@ -115,9 +93,16 @@ class _PrivacyButtons extends StatelessWidget { mainAxisAlignment: mainAxisAlignment, children: [ TextButton( - onPressed: () => PlatformInfos.showDialog(context), + onPressed: () => launchUrlString(AppConfig.website), child: Text( - L10n.of(context).about, + L10n.of(context).website, + style: shadowTextStyle, + ), + ), + TextButton( + onPressed: () => launchUrlString(AppConfig.supportUrl), + child: Text( + L10n.of(context).help, style: shadowTextStyle, ), ), @@ -128,6 +113,13 @@ class _PrivacyButtons extends StatelessWidget { style: shadowTextStyle, ), ), + TextButton( + onPressed: () => PlatformInfos.showDialog(context), + child: Text( + L10n.of(context).about, + style: shadowTextStyle, + ), + ), ], ), ), From 4b7345d0fe0c664d9c3908a56b92d3d71cf7d749 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 4 Nov 2024 13:58:10 +0100 Subject: [PATCH 154/236] chore: Follow up login page --- .../homeserver_picker/homeserver_picker_view.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index 1edfe1881..ff3045c27 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -15,7 +15,10 @@ import 'homeserver_picker.dart'; class HomeserverPickerView extends StatelessWidget { final HomeserverPickerController controller; - const HomeserverPickerView(this.controller, {super.key}); + const HomeserverPickerView( + this.controller, { + super.key, + }); @override Widget build(BuildContext context) { @@ -25,7 +28,11 @@ class HomeserverPickerView extends StatelessWidget { enforceMobileMode: Matrix.of(context).client.isLogged(), appBar: AppBar( centerTitle: true, - title: Text(L10n.of(context).addAccount), + title: Text( + controller.widget.addMultiAccount + ? L10n.of(context).addAccount + : L10n.of(context).login, + ), actions: [ PopupMenuButton( onSelected: controller.onMoreAction, From a2e7356bd0c3219e75460e319cabaed28335d8a6 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 4 Nov 2024 14:06:07 +0100 Subject: [PATCH 155/236] chore: Follow up homeserver picker --- lib/pages/homeserver_picker/homeserver_picker.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index 95402306b..1948daf8c 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -80,6 +80,7 @@ class HomeserverPickerController extends State { } void onSubmitted([_]) { + if (isLoading) return tryCheckHomeserverActionWithoutCooldown(); if (supportsSso) return ssoLoginAction(); if (supportsPasswordLogin) return login(); return tryCheckHomeserverActionWithoutCooldown(); From 9906668f1c74412d639152b0f4b75d35a9c1d554 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 4 Nov 2024 14:12:58 +0100 Subject: [PATCH 156/236] chore: Follow up appbar shadow --- lib/config/themes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/config/themes.dart b/lib/config/themes.dart index d5e08eba7..2125283d3 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -97,7 +97,7 @@ abstract class FluffyThemes { appBarTheme: AppBarTheme( toolbarHeight: FluffyThemes.isColumnMode(context) ? 72 : 56, shadowColor: FluffyThemes.isColumnMode(context) - ? colorScheme.surfaceContainer.withAlpha(64) + ? colorScheme.surfaceContainer.withAlpha(128) : null, surfaceTintColor: FluffyThemes.isColumnMode(context) ? colorScheme.surface : null, From 58577bb9e8c65df64f1dd44ba908372e742f928e Mon Sep 17 00:00:00 2001 From: Krille Date: Wed, 6 Nov 2024 14:10:31 +0100 Subject: [PATCH 157/236] refactor: Performance boost for avatar widget --- lib/widgets/avatar.dart | 211 +++++++++++++++--------------- lib/widgets/presence_builder.dart | 56 +++++--- 2 files changed, 148 insertions(+), 119 deletions(-) diff --git a/lib/widgets/avatar.dart b/lib/widgets/avatar.dart index c6bcaf9f1..ecd137973 100644 --- a/lib/widgets/avatar.dart +++ b/lib/widgets/avatar.dart @@ -19,7 +19,7 @@ class Avatar extends StatelessWidget { final IconData? icon; final BorderSide? border; - const Avatar({ + Avatar({ this.mxContent, this.name, this.size = defaultSize, @@ -31,122 +31,129 @@ class Avatar extends StatelessWidget { this.border, this.icon, super.key, - }); + }) : fallbackLetters = name?.firstTwoCharsOrFallback ?? '@', + textColor = name?.lightColorAvatar, + noPic = mxContent == null || + mxContent.toString().isEmpty || + mxContent.toString() == 'null'; + + final String fallbackLetters; + final Color? textColor; + final bool noPic; @override Widget build(BuildContext context) { final theme = Theme.of(context); - var fallbackLetters = '@'; - final name = this.name; - if (name != null) { - if (name.runes.length >= 2) { - fallbackLetters = String.fromCharCodes(name.runes, 0, 2); - } else if (name.runes.length == 1) { - fallbackLetters = name; - } - } - final noPic = mxContent == null || - mxContent.toString().isEmpty || - mxContent.toString() == 'null'; - final textColor = name?.lightColorAvatar; - final textWidget = Container( - color: textColor, - alignment: Alignment.center, - child: Text( - fallbackLetters, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: (size / 3).roundToDouble(), - ), - ), - ); final borderRadius = this.borderRadius ?? BorderRadius.circular(size / 2); final presenceUserId = this.presenceUserId; - final container = Stack( - children: [ - SizedBox( - width: size, - height: size, - child: Material( - color: theme.brightness == Brightness.light - ? Colors.white - : Colors.black, - shape: RoundedRectangleBorder( - borderRadius: borderRadius, - side: border ?? BorderSide.none, - ), - clipBehavior: Clip.hardEdge, - child: noPic - ? textWidget - : MxcImage( - client: client, - key: ValueKey(mxContent.toString()), - cacheKey: '${mxContent}_$size', - uri: mxContent, - fit: BoxFit.cover, - width: size, - height: size, - placeholder: (_) => Center( - child: Icon( - Icons.person_2, - color: theme.colorScheme.tertiary, - size: size / 1.5, + + return InkWell( + onTap: onTap, + borderRadius: borderRadius, + child: Stack( + children: [ + SizedBox( + width: size, + height: size, + child: Material( + color: theme.brightness == Brightness.light + ? Colors.white + : Colors.black, + shape: RoundedRectangleBorder( + borderRadius: borderRadius, + side: border ?? BorderSide.none, + ), + clipBehavior: Clip.hardEdge, + child: noPic + ? Container( + color: textColor, + alignment: Alignment.center, + child: Text( + fallbackLetters, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: (size / 3).roundToDouble(), + ), + ), + ) + : MxcImage( + client: client, + key: ValueKey(mxContent.toString()), + cacheKey: '${mxContent}_$size', + uri: mxContent, + fit: BoxFit.cover, + width: size, + height: size, + placeholder: (_) => Center( + child: Icon( + Icons.person_2, + color: theme.colorScheme.tertiary, + size: size / 1.5, + ), ), ), - ), + ), ), - ), - if (presenceUserId != null) - PresenceBuilder( - client: client, - userId: presenceUserId, - builder: (context, presence) { - if (presence == null || - (presence.presence == PresenceType.offline && - presence.lastActiveTimestamp == null)) { - return const SizedBox.shrink(); - } - final dotColor = presence.presence.isOnline - ? Colors.green - : presence.presence.isUnavailable - ? Colors.orange - : Colors.grey; - return Positioned( - bottom: -3, - right: -3, - child: Container( - width: 16, - height: 16, - decoration: BoxDecoration( - color: presenceBackgroundColor ?? theme.colorScheme.surface, - borderRadius: BorderRadius.circular(32), - ), - alignment: Alignment.center, + if (presenceUserId != null) + PresenceBuilder( + client: client, + userId: presenceUserId, + builder: (context, presence) { + if (presence == null || + (presence.presence == PresenceType.offline && + presence.lastActiveTimestamp == null)) { + return const SizedBox.shrink(); + } + final dotColor = presence.presence.isOnline + ? Colors.green + : presence.presence.isUnavailable + ? Colors.orange + : Colors.grey; + return Positioned( + bottom: -3, + right: -3, child: Container( - width: 10, - height: 10, + width: 16, + height: 16, decoration: BoxDecoration( - color: dotColor, - borderRadius: BorderRadius.circular(16), - border: Border.all( - width: 1, - color: theme.colorScheme.surface, + color: + presenceBackgroundColor ?? theme.colorScheme.surface, + borderRadius: BorderRadius.circular(32), + ), + alignment: Alignment.center, + child: Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: dotColor, + borderRadius: BorderRadius.circular(16), + border: Border.all( + width: 1, + color: theme.colorScheme.surface, + ), ), ), ), - ), - ); - }, - ), - ], - ); - if (onTap == null) return container; - return InkWell( - onTap: onTap, - borderRadius: borderRadius, - child: container, + ); + }, + ), + ], + ), ); } } + +extension on String { + String get firstTwoCharsOrFallback { + var fallbackLetters = '@'; + if (runes.length >= 2) { + fallbackLetters = String.fromCharCodes(runes, 0, 2); + } else if (runes.length == 1) { + fallbackLetters = this; + } + + return fallbackLetters; + } +} diff --git a/lib/widgets/presence_builder.dart b/lib/widgets/presence_builder.dart index 2ca963c84..58964ab2c 100644 --- a/lib/widgets/presence_builder.dart +++ b/lib/widgets/presence_builder.dart @@ -1,10 +1,12 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart'; -class PresenceBuilder extends StatelessWidget { +class PresenceBuilder extends StatefulWidget { final Widget Function(BuildContext context, CachedPresence? presence) builder; final String? userId; final Client? client; @@ -17,21 +19,41 @@ class PresenceBuilder extends StatelessWidget { }); @override - Widget build(BuildContext context) { - final userId = this.userId; - if (userId == null) return builder(context, null); - - final client = this.client ?? Matrix.of(context).client; - return FutureBuilder( - future: client.fetchCurrentPresence(userId), - builder: (context, cachedPresenceSnapshot) => StreamBuilder( - stream: client.onPresenceChanged.stream - .where((cachedPresence) => cachedPresence.userid == userId), - builder: (context, snapshot) => builder( - context, - snapshot.data ?? cachedPresenceSnapshot.data, - ), - ), - ); + State createState() => _PresenceBuilderState(); +} + +class _PresenceBuilderState extends State { + CachedPresence? _presence; + StreamSubscription? _sub; + + @override + void initState() { + final client = widget.client ?? Matrix.of(context).client; + final userId = widget.userId; + if (userId != null) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + final presence = await client.fetchCurrentPresence(userId); + setState(() { + _presence = presence; + _sub = client.onPresenceChanged.stream.listen((presence) { + if (!mounted) return; + setState(() { + _presence = presence; + }); + }); + }); + }); + } + + super.initState(); } + + @override + void dispose() { + _sub?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => widget.builder(context, _presence); } From d2c228418277bbf7d0c9d92dd279d19629f77e99 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Wed, 6 Nov 2024 17:25:37 +0100 Subject: [PATCH 158/236] Revert "refactor: Performance boost for avatar widget" This reverts commit 58577bb9e8c65df64f1dd44ba908372e742f928e. --- lib/widgets/avatar.dart | 211 +++++++++++++++--------------- lib/widgets/presence_builder.dart | 56 +++----- 2 files changed, 119 insertions(+), 148 deletions(-) diff --git a/lib/widgets/avatar.dart b/lib/widgets/avatar.dart index ecd137973..c6bcaf9f1 100644 --- a/lib/widgets/avatar.dart +++ b/lib/widgets/avatar.dart @@ -19,7 +19,7 @@ class Avatar extends StatelessWidget { final IconData? icon; final BorderSide? border; - Avatar({ + const Avatar({ this.mxContent, this.name, this.size = defaultSize, @@ -31,129 +31,122 @@ class Avatar extends StatelessWidget { this.border, this.icon, super.key, - }) : fallbackLetters = name?.firstTwoCharsOrFallback ?? '@', - textColor = name?.lightColorAvatar, - noPic = mxContent == null || - mxContent.toString().isEmpty || - mxContent.toString() == 'null'; - - final String fallbackLetters; - final Color? textColor; - final bool noPic; + }); @override Widget build(BuildContext context) { final theme = Theme.of(context); + var fallbackLetters = '@'; + final name = this.name; + if (name != null) { + if (name.runes.length >= 2) { + fallbackLetters = String.fromCharCodes(name.runes, 0, 2); + } else if (name.runes.length == 1) { + fallbackLetters = name; + } + } + final noPic = mxContent == null || + mxContent.toString().isEmpty || + mxContent.toString() == 'null'; + final textColor = name?.lightColorAvatar; + final textWidget = Container( + color: textColor, + alignment: Alignment.center, + child: Text( + fallbackLetters, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: (size / 3).roundToDouble(), + ), + ), + ); final borderRadius = this.borderRadius ?? BorderRadius.circular(size / 2); final presenceUserId = this.presenceUserId; - - return InkWell( - onTap: onTap, - borderRadius: borderRadius, - child: Stack( - children: [ - SizedBox( - width: size, - height: size, - child: Material( - color: theme.brightness == Brightness.light - ? Colors.white - : Colors.black, - shape: RoundedRectangleBorder( - borderRadius: borderRadius, - side: border ?? BorderSide.none, - ), - clipBehavior: Clip.hardEdge, - child: noPic - ? Container( - color: textColor, - alignment: Alignment.center, - child: Text( - fallbackLetters, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: (size / 3).roundToDouble(), - ), - ), - ) - : MxcImage( - client: client, - key: ValueKey(mxContent.toString()), - cacheKey: '${mxContent}_$size', - uri: mxContent, - fit: BoxFit.cover, - width: size, - height: size, - placeholder: (_) => Center( - child: Icon( - Icons.person_2, - color: theme.colorScheme.tertiary, - size: size / 1.5, - ), + final container = Stack( + children: [ + SizedBox( + width: size, + height: size, + child: Material( + color: theme.brightness == Brightness.light + ? Colors.white + : Colors.black, + shape: RoundedRectangleBorder( + borderRadius: borderRadius, + side: border ?? BorderSide.none, + ), + clipBehavior: Clip.hardEdge, + child: noPic + ? textWidget + : MxcImage( + client: client, + key: ValueKey(mxContent.toString()), + cacheKey: '${mxContent}_$size', + uri: mxContent, + fit: BoxFit.cover, + width: size, + height: size, + placeholder: (_) => Center( + child: Icon( + Icons.person_2, + color: theme.colorScheme.tertiary, + size: size / 1.5, ), ), - ), + ), ), - if (presenceUserId != null) - PresenceBuilder( - client: client, - userId: presenceUserId, - builder: (context, presence) { - if (presence == null || - (presence.presence == PresenceType.offline && - presence.lastActiveTimestamp == null)) { - return const SizedBox.shrink(); - } - final dotColor = presence.presence.isOnline - ? Colors.green - : presence.presence.isUnavailable - ? Colors.orange - : Colors.grey; - return Positioned( - bottom: -3, - right: -3, + ), + if (presenceUserId != null) + PresenceBuilder( + client: client, + userId: presenceUserId, + builder: (context, presence) { + if (presence == null || + (presence.presence == PresenceType.offline && + presence.lastActiveTimestamp == null)) { + return const SizedBox.shrink(); + } + final dotColor = presence.presence.isOnline + ? Colors.green + : presence.presence.isUnavailable + ? Colors.orange + : Colors.grey; + return Positioned( + bottom: -3, + right: -3, + child: Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: presenceBackgroundColor ?? theme.colorScheme.surface, + borderRadius: BorderRadius.circular(32), + ), + alignment: Alignment.center, child: Container( - width: 16, - height: 16, + width: 10, + height: 10, decoration: BoxDecoration( - color: - presenceBackgroundColor ?? theme.colorScheme.surface, - borderRadius: BorderRadius.circular(32), - ), - alignment: Alignment.center, - child: Container( - width: 10, - height: 10, - decoration: BoxDecoration( - color: dotColor, - borderRadius: BorderRadius.circular(16), - border: Border.all( - width: 1, - color: theme.colorScheme.surface, - ), + color: dotColor, + borderRadius: BorderRadius.circular(16), + border: Border.all( + width: 1, + color: theme.colorScheme.surface, ), ), ), - ); - }, - ), - ], - ), + ), + ); + }, + ), + ], + ); + if (onTap == null) return container; + return InkWell( + onTap: onTap, + borderRadius: borderRadius, + child: container, ); - } -} - -extension on String { - String get firstTwoCharsOrFallback { - var fallbackLetters = '@'; - if (runes.length >= 2) { - fallbackLetters = String.fromCharCodes(runes, 0, 2); - } else if (runes.length == 1) { - fallbackLetters = this; - } - - return fallbackLetters; } } diff --git a/lib/widgets/presence_builder.dart b/lib/widgets/presence_builder.dart index 58964ab2c..2ca963c84 100644 --- a/lib/widgets/presence_builder.dart +++ b/lib/widgets/presence_builder.dart @@ -1,12 +1,10 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart'; -class PresenceBuilder extends StatefulWidget { +class PresenceBuilder extends StatelessWidget { final Widget Function(BuildContext context, CachedPresence? presence) builder; final String? userId; final Client? client; @@ -19,41 +17,21 @@ class PresenceBuilder extends StatefulWidget { }); @override - State createState() => _PresenceBuilderState(); -} - -class _PresenceBuilderState extends State { - CachedPresence? _presence; - StreamSubscription? _sub; - - @override - void initState() { - final client = widget.client ?? Matrix.of(context).client; - final userId = widget.userId; - if (userId != null) { - WidgetsBinding.instance.addPostFrameCallback((_) async { - final presence = await client.fetchCurrentPresence(userId); - setState(() { - _presence = presence; - _sub = client.onPresenceChanged.stream.listen((presence) { - if (!mounted) return; - setState(() { - _presence = presence; - }); - }); - }); - }); - } - - super.initState(); + Widget build(BuildContext context) { + final userId = this.userId; + if (userId == null) return builder(context, null); + + final client = this.client ?? Matrix.of(context).client; + return FutureBuilder( + future: client.fetchCurrentPresence(userId), + builder: (context, cachedPresenceSnapshot) => StreamBuilder( + stream: client.onPresenceChanged.stream + .where((cachedPresence) => cachedPresence.userid == userId), + builder: (context, snapshot) => builder( + context, + snapshot.data ?? cachedPresenceSnapshot.data, + ), + ), + ); } - - @override - void dispose() { - _sub?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) => widget.builder(context, _presence); } From 2adf2925603f7549eb59c76c0f437f3f786242f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sun, 3 Nov 2024 18:55:00 +0000 Subject: [PATCH 159/236] Translated using Weblate (Estonian) Currently translated at 100.0% (678 of 678 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/ --- assets/l10n/intl_et.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_et.arb b/assets/l10n/intl_et.arb index 6582bd7b6..a3154bf8f 100644 --- a/assets/l10n/intl_et.arb +++ b/assets/l10n/intl_et.arb @@ -2840,5 +2840,9 @@ "setWallpaper": "Määra taustapildiks", "@setWallpaper": {}, "manageAccount": "Halda kasutajakontot", - "@manageAccount": {} + "@manageAccount": {}, + "blur": "Hägusus:", + "@blur": {}, + "opacity": "Läbipaistmatus:", + "@opacity": {} } From 252e2cbbdb301ec8da4f5693df0790dd118022df Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Sun, 3 Nov 2024 20:26:43 +0000 Subject: [PATCH 160/236] Translated using Weblate (Ukrainian) Currently translated at 100.0% (678 of 678 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/ --- assets/l10n/intl_uk.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/l10n/intl_uk.arb b/assets/l10n/intl_uk.arb index dd35fd2b1..63847361d 100644 --- a/assets/l10n/intl_uk.arb +++ b/assets/l10n/intl_uk.arb @@ -2835,7 +2835,7 @@ "@noticeChatBackupDeviceVerification": {}, "continueText": "Продовжити", "@continueText": {}, - "manageAccount": "Керування обліковим записом", + "manageAccount": "Керувати обліковим записом", "@manageAccount": {}, "welcomeText": "Привіт-привіт 👋 Це FluffyChat. Ви можете увійти на будь-який сервер, сумісний із https://matrix.org. А потім спілкуватися з будь-ким. Це величезна децентралізована мережа для обміну повідомленнями!", "@welcomeText": {}, From 246f2be0ebe1e411a4dcc11580215e497bc5701a Mon Sep 17 00:00:00 2001 From: Edgars Andersons Date: Mon, 4 Nov 2024 09:07:53 +0000 Subject: [PATCH 161/236] Translated using Weblate (Latvian) Currently translated at 100.0% (678 of 678 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/lv/ --- assets/l10n/intl_lv.arb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_lv.arb b/assets/l10n/intl_lv.arb index 816826ae1..c78ea7e32 100644 --- a/assets/l10n/intl_lv.arb +++ b/assets/l10n/intl_lv.arb @@ -2814,5 +2814,13 @@ "continueText": "Turpināt", "@continueText": {}, "welcomeText": "Sveicieni! 👋 Šis ir FluffyChat. Tu vari pieteikties jebkurā mājasserverī, kas ir saderīgs ar https://matrix.org. Tad vari tērzēt ar ikvienu. Tas ir milzīgs decentralizētās saziņas tīkls!", - "@welcomeText": {} + "@welcomeText": {}, + "blur": "Aizmiglojums:", + "@blur": {}, + "setWallpaper": "Iestatīt ekrāntapeti", + "@setWallpaper": {}, + "opacity": "Necaurredzamība:", + "@opacity": {}, + "manageAccount": "Pārvaldīt kontu", + "@manageAccount": {} } From edd03757e01479900e58fa0373d253752376ef00 Mon Sep 17 00:00:00 2001 From: Rex_sa Date: Mon, 4 Nov 2024 23:22:27 +0000 Subject: [PATCH 162/236] Translated using Weblate (Arabic) Currently translated at 100.0% (687 of 687 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ar/ --- assets/l10n/intl_ar.arb | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_ar.arb b/assets/l10n/intl_ar.arb index ecc5df684..36fc16962 100644 --- a/assets/l10n/intl_ar.arb +++ b/assets/l10n/intl_ar.arb @@ -2844,5 +2844,28 @@ "opacity": "التعتيم:", "@opacity": {}, "manageAccount": "‫إدارة الحساب‬", - "@manageAccount": {} + "@manageAccount": {}, + "noContactInformationProvided": "لا يقدم السيرفر أي معلومات اتصال صحيحة", + "@noContactInformationProvided": {}, + "contactServerAdmin": "اتصل بمسؤول الخادم", + "@contactServerAdmin": {}, + "contactServerSecurity": "الاتصال بمسؤول أمان ااخادم", + "@contactServerSecurity": {}, + "supportPage": "صفحة الدعم", + "@supportPage": {}, + "serverInformation": "معلومات الخادم:", + "@serverInformation": {}, + "name": "الإسم", + "@name": {}, + "version": "اﻹصدار", + "@version": {}, + "website": "الموقع اﻹلكتروني", + "@website": {}, + "aboutHomeserver": "حول {homeserver}", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + } } From bd8b8d6e9bbd325b52ccda7b9917ebd49d8d64ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 5 Nov 2024 21:53:21 +0000 Subject: [PATCH 163/236] Translated using Weblate (Estonian) Currently translated at 100.0% (687 of 687 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/ --- assets/l10n/intl_et.arb | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_et.arb b/assets/l10n/intl_et.arb index a3154bf8f..6b790d471 100644 --- a/assets/l10n/intl_et.arb +++ b/assets/l10n/intl_et.arb @@ -2844,5 +2844,28 @@ "blur": "Hägusus:", "@blur": {}, "opacity": "Läbipaistmatus:", - "@opacity": {} + "@opacity": {}, + "contactServerSecurity": "Võta ühendust serveri andmeturbe eest vastutajaga", + "@contactServerSecurity": {}, + "supportPage": "Kasutajatugi", + "@supportPage": {}, + "serverInformation": "Serveri teave:", + "@serverInformation": {}, + "name": "Nimi", + "@name": {}, + "version": "Versioon", + "@version": {}, + "noContactInformationProvided": "Server ei jaga asjakohast kontaktteavet", + "@noContactInformationProvided": {}, + "contactServerAdmin": "Võta ühendust serveri haldajaga", + "@contactServerAdmin": {}, + "website": "Veebisait", + "@website": {}, + "aboutHomeserver": "Koduserveri teave: {homeserver}", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + } } From 42c5f09065451ebc737ffb9f74a89eb087c41b20 Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Tue, 5 Nov 2024 21:02:54 +0000 Subject: [PATCH 164/236] Translated using Weblate (Basque) Currently translated at 100.0% (687 of 687 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/eu/ --- assets/l10n/intl_eu.arb | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_eu.arb b/assets/l10n/intl_eu.arb index 45ce2b3fd..118eb283f 100644 --- a/assets/l10n/intl_eu.arb +++ b/assets/l10n/intl_eu.arb @@ -2844,5 +2844,28 @@ "continueText": "Jarraitu", "@continueText": {}, "welcomeText": "Ieup 👋 Ongi etorri FluffyChat-era. https://matrix.org-rekin bateragarria den edozein zerbitzaritan hasi dezakezu saioa eta edonorekin txateatu. Mezularitza-sare deszentralizatu eraraldoia da!", - "@welcomeText": {} + "@welcomeText": {}, + "contactServerAdmin": "Jarri harremanetan zerbitzariaren administratzailearekin", + "@contactServerAdmin": {}, + "aboutHomeserver": "{homeserver}(e)ri buruz", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "supportPage": "Laguntza orria", + "@supportPage": {}, + "serverInformation": "Zerbitzariaren informazioa:", + "@serverInformation": {}, + "name": "Izena", + "@name": {}, + "version": "Bertsioa", + "@version": {}, + "website": "Webgunea", + "@website": {}, + "contactServerSecurity": "Jakinarazi segurtasun arazo bat", + "@contactServerSecurity": {}, + "noContactInformationProvided": "Zerbitzariak ez du harremanetarako informaziorik zehaztu", + "@noContactInformationProvided": {} } From 72d07ba29144ff7e8140ca2c8db209879950c811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jos=C3=A9=20m?= Date: Tue, 5 Nov 2024 06:34:53 +0000 Subject: [PATCH 165/236] Translated using Weblate (Galician) Currently translated at 100.0% (687 of 687 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/gl/ --- assets/l10n/intl_gl.arb | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_gl.arb b/assets/l10n/intl_gl.arb index c56d47d47..674ae4b52 100644 --- a/assets/l10n/intl_gl.arb +++ b/assets/l10n/intl_gl.arb @@ -2832,5 +2832,40 @@ "oneOfYourDevicesIsNotVerified": "Un dos teus dispositivos non está verificado", "@oneOfYourDevicesIsNotVerified": {}, "noticeChatBackupDeviceVerification": "Nota: Cando conectas todos os teus dispositivos á copia de apoio da conversa quedan verificados automaticamente.", - "@noticeChatBackupDeviceVerification": {} + "@noticeChatBackupDeviceVerification": {}, + "blur": "Néboa:", + "@blur": {}, + "opacity": "Opacidade:", + "@opacity": {}, + "contactServerSecurity": "Contacto con Seguridade do servidor", + "@contactServerSecurity": {}, + "aboutHomeserver": "Sobre {homeserver}", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "welcomeText": "Ola! 👋 Isto é FluffyChat. Podes iniciar sesión en calquera servidor compatible con https://matrix.org. Poderás conversar con calquera. Unha enorme rede de mensaxería descentralizada!", + "@welcomeText": {}, + "setWallpaper": "Establecer fondo", + "@setWallpaper": {}, + "manageAccount": "Xestionar conta", + "@manageAccount": {}, + "noContactInformationProvided": "O servidor non proporciona información de contacto válida", + "@noContactInformationProvided": {}, + "contactServerAdmin": "Contacto con Admin do servidor", + "@contactServerAdmin": {}, + "supportPage": "Páxina de axuda", + "@supportPage": {}, + "serverInformation": "Información do servidor:", + "@serverInformation": {}, + "name": "Nome", + "@name": {}, + "version": "Versión", + "@version": {}, + "website": "Páxina web", + "@website": {}, + "continueText": "Continuar", + "@continueText": {} } From e1b584d8b7674ff388ef0e94bb4c6e6341501df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=8E=8B=E5=8F=AB=E6=88=91=E6=9D=A5=E5=B7=A1?= =?UTF-8?q?=E5=B1=B1?= Date: Tue, 5 Nov 2024 00:38:32 +0000 Subject: [PATCH 166/236] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (687 of 687 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/zh_Hans/ --- assets/l10n/intl_zh.arb | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_zh.arb b/assets/l10n/intl_zh.arb index 6e628dbc8..ffd7ddf4f 100644 --- a/assets/l10n/intl_zh.arb +++ b/assets/l10n/intl_zh.arb @@ -2844,5 +2844,28 @@ "setWallpaper": "设置壁纸", "@setWallpaper": {}, "manageAccount": "管理账户", - "@manageAccount": {} + "@manageAccount": {}, + "aboutHomeserver": "关于 {homeserver}", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "version": "版本", + "@version": {}, + "noContactInformationProvided": "服务器未提供任何有效的联系信息", + "@noContactInformationProvided": {}, + "contactServerAdmin": "联系服务器管理员", + "@contactServerAdmin": {}, + "name": "名称", + "@name": {}, + "contactServerSecurity": "联系服务器安全管理", + "@contactServerSecurity": {}, + "supportPage": "支持页面", + "@supportPage": {}, + "serverInformation": "服务器信息:", + "@serverInformation": {}, + "website": "网站", + "@website": {} } From 4620718e10d52d0e064138c521bc13af5dc85e45 Mon Sep 17 00:00:00 2001 From: Linerly Date: Tue, 5 Nov 2024 01:39:01 +0000 Subject: [PATCH 167/236] Translated using Weblate (Indonesian) Currently translated at 100.0% (687 of 687 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/id/ --- assets/l10n/intl_id.arb | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_id.arb b/assets/l10n/intl_id.arb index 76b5a74b7..2e8b98c76 100644 --- a/assets/l10n/intl_id.arb +++ b/assets/l10n/intl_id.arb @@ -2835,5 +2835,36 @@ "welcomeText": "Halo 👋 Ini FluffyChat. Kamu bisa masuk ke homeserver mana pun, yang kompatibel dengan https://matrix.org. Lalu, chat dengan siapa pun. Ini merupakan jaringan perpesanan besar yang terdesentralisasi!", "@welcomeText": {}, "continueText": "Lanjutkan", - "@continueText": {} + "@continueText": {}, + "aboutHomeserver": "Tentang {homeserver}", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "blur": "Buram:", + "@blur": {}, + "contactServerAdmin": "Hubungi admin server", + "@contactServerAdmin": {}, + "opacity": "Opasitas:", + "@opacity": {}, + "setWallpaper": "Atur later belakang", + "@setWallpaper": {}, + "serverInformation": "Informasi server:", + "@serverInformation": {}, + "noContactInformationProvided": "Server tidak menyediakan informasi kontak valid apa pun", + "@noContactInformationProvided": {}, + "supportPage": "Laman dukungan", + "@supportPage": {}, + "version": "Versi", + "@version": {}, + "website": "Situs Web", + "@website": {}, + "manageAccount": "Kelola akun", + "@manageAccount": {}, + "contactServerSecurity": "Hubungi keamanan server", + "@contactServerSecurity": {}, + "name": "Nama", + "@name": {} } From c385a7a893e996b4ffaff5e3c0fa475ab2be0c8e Mon Sep 17 00:00:00 2001 From: Edgars Andersons Date: Tue, 5 Nov 2024 08:38:46 +0000 Subject: [PATCH 168/236] Translated using Weblate (Latvian) Currently translated at 100.0% (687 of 687 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/lv/ --- assets/l10n/intl_lv.arb | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_lv.arb b/assets/l10n/intl_lv.arb index c78ea7e32..ef65456ef 100644 --- a/assets/l10n/intl_lv.arb +++ b/assets/l10n/intl_lv.arb @@ -2822,5 +2822,28 @@ "opacity": "Necaurredzamība:", "@opacity": {}, "manageAccount": "Pārvaldīt kontu", - "@manageAccount": {} + "@manageAccount": {}, + "noContactInformationProvided": "Serveris nesniedz nekādu derīgu saziņas informāciju", + "@noContactInformationProvided": {}, + "serverInformation": "Informācija par serveri:", + "@serverInformation": {}, + "aboutHomeserver": "Par {homeserver}", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "contactServerAdmin": "Sazināties ar servera pārvaldītāju", + "@contactServerAdmin": {}, + "contactServerSecurity": "Sazināties ar servera drošības uzturētājiem", + "@contactServerSecurity": {}, + "supportPage": "Atbalsta lapa", + "@supportPage": {}, + "name": "Nosaukums", + "@name": {}, + "version": "Versija", + "@version": {}, + "website": "Tīmekļvietne", + "@website": {} } From 72ba3424382517a20e50e3645fc6bc2f7ee40851 Mon Sep 17 00:00:00 2001 From: Bezruchenko Simon Date: Wed, 6 Nov 2024 09:19:46 +0000 Subject: [PATCH 169/236] Translated using Weblate (Ukrainian) Currently translated at 100.0% (687 of 687 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/ --- assets/l10n/intl_uk.arb | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_uk.arb b/assets/l10n/intl_uk.arb index 63847361d..8151b4a57 100644 --- a/assets/l10n/intl_uk.arb +++ b/assets/l10n/intl_uk.arb @@ -2844,5 +2844,28 @@ "opacity": "Прозорість:", "@opacity": {}, "setWallpaper": "Встановити шпалери", - "@setWallpaper": {} + "@setWallpaper": {}, + "aboutHomeserver": "Про {homeserver}", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "noContactInformationProvided": "Сервер не надає жодної дійсної контактної інформації", + "@noContactInformationProvided": {}, + "contactServerSecurity": "Зв’язатися з відділом безпеки сервера", + "@contactServerSecurity": {}, + "supportPage": "Сторінка підтримки", + "@supportPage": {}, + "serverInformation": "Інформація про сервер:", + "@serverInformation": {}, + "contactServerAdmin": "Зв’язатися з адміністратором сервера", + "@contactServerAdmin": {}, + "name": "Ім'я", + "@name": {}, + "version": "Версія", + "@version": {}, + "website": "Вебсайт", + "@website": {} } From 2adfadb61d37be88703c268b72d5e89a23814c65 Mon Sep 17 00:00:00 2001 From: Bruno Roh Date: Thu, 7 Nov 2024 03:24:52 +0000 Subject: [PATCH 170/236] Translated using Weblate (Korean) Currently translated at 100.0% (687 of 687 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ko/ --- assets/l10n/intl_ko.arb | 109 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 3 deletions(-) diff --git a/assets/l10n/intl_ko.arb b/assets/l10n/intl_ko.arb index 443fb077f..dbeb4ca6d 100644 --- a/assets/l10n/intl_ko.arb +++ b/assets/l10n/intl_ko.arb @@ -2197,7 +2197,7 @@ "provider": {} } }, - "fileIsTooBigForServer": "서버가 파일을 전송하기 너무 크다고 보고합니다.", + "fileIsTooBigForServer": "전송에 실패했습니다. 서버는 {max}치가 넘는 파일을 지원하지 않습니다.", "@fileIsTooBigForServer": {}, "callingPermissions": "통화 권한", "@callingPermissions": {}, @@ -2708,7 +2708,7 @@ "@knockRestricted": {}, "swipeRightToLeftToReply": "오른쪽에서 왼쪽으로 스와이프해서 답장", "@swipeRightToLeftToReply": {}, - "alwaysUse24HourFormat": "false", + "alwaysUse24HourFormat": "아니요", "@alwaysUse24HourFormat": { "description": "Set to true to always display time of day in 24 hour format." }, @@ -2763,5 +2763,108 @@ "placeholders": { "level": {} } - } + }, + "noChatsFoundHere": "대화가 발견되지 않았습니다. 아래 버튼을 사용하여 새 대화를 시작해 보세요. ⤵️", + "@noChatsFoundHere": {}, + "changeTheVisibilityOfChatHistory": "대화 기록 표시 여부 바꾸기", + "@changeTheVisibilityOfChatHistory": {}, + "changeTheCanonicalRoomAlias": "메인 공개 대화 주소 변경", + "@changeTheCanonicalRoomAlias": {}, + "sendCanceled": "전송 최소됨", + "@sendCanceled": {}, + "homeserverDescription": "모든 데이터는 이메일 제공자와 마찬가지로 Homeserver(이) 에 저장됩니다. 모든 사람과 여전히 소통할 수 있는 동안 사용하고 싶은 Homeserver(이) 를 선택할 수 있습니다. https://matrix.org에서 자세히 알아보세요.", + "@homeserverDescription": {}, + "sendingAttachmentCountOfCount": "{index} ({length}) 전송 중...", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + }, + "serverLimitReached": "서버 한도에 도달했습니다! {seconds}초 기다리는 중...", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + }, + "noContactInformationProvided": "서버가 유효한 연락처 정보를 제공하지 않습니다", + "@noContactInformationProvided": {}, + "welcomeText": "안녕하세요 👋 제 이름은 FluffyChat이에요. 당신은 Htpps://matrix.org에 호환되는 어떤 Homeserver에도 가입할 수 있어요. 그리고 아무나 대화하세요! 저는 큰 대화 네트워크랍니다! 😄", + "@welcomeText": {}, + "changeGeneralChatSettings": "일반 대화 설정 번경하기", + "@changeGeneralChatSettings": {}, + "inviteOtherUsers": "다른 사용자를 이 대화에 초대하기", + "@inviteOtherUsers": {}, + "changeTheChatPermissions": "대화 권한 변경하기", + "@changeTheChatPermissions": {}, + "calculatingFileSize": "파일 크기 계산 중...", + "@calculatingFileSize": {}, + "prepareSendingAttachment": "첨부된 파일 전송 준비 중...", + "@prepareSendingAttachment": {}, + "oneOfYourDevicesIsNotVerified": "당신의 기기 중 하나가 인증되지 않았습니다", + "@oneOfYourDevicesIsNotVerified": {}, + "noticeChatBackupDeviceVerification": "참고: 모든 기기를 대화 백업에 연결하면 자동으로 인증됩니다.", + "@noticeChatBackupDeviceVerification": {}, + "opacity": "불투명:", + "@opacity": {}, + "setWallpaper": "배경화면 정하기", + "@setWallpaper": {}, + "manageAccount": "계정 관리하기", + "@manageAccount": {}, + "aboutHomeserver": "{homeserver}의 대해서", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "contactServerAdmin": "서버 관리자 연락하기", + "@contactServerAdmin": {}, + "contactServerSecurity": "서버 보안관 연락하기", + "@contactServerSecurity": {}, + "supportPage": "페이지 돕기", + "@supportPage": {}, + "name": "이름", + "@name": {}, + "serverInformation": "서버 정보:", + "@serverInformation": {}, + "version": "버전", + "@version": {}, + "website": "웹사이트", + "@website": {}, + "changeTheDescriptionOfTheGroup": "대화의 설명 바꾸기", + "@changeTheDescriptionOfTheGroup": {}, + "sendRoomNotifications": "@room 알림 보내기", + "@sendRoomNotifications": {}, + "chatPermissionsDescription": "이 대화에서 특정 작업에 필요한 파워 레벨을 정의합니다. 파워 레벨 0, 50, 100은 일반적으로 사용자, 관리자, 관리자를 나타내지만, 모든 등급이 가능합니다.", + "@chatPermissionsDescription": {}, + "loginWithMatrixId": "Matrix-ID로 로그인", + "@loginWithMatrixId": {}, + "discoverHomeservers": "Homeserver 찾아보기", + "@discoverHomeservers": {}, + "whatIsAHomeserver": "Homeserver(이) 가 무엇인가요?", + "@whatIsAHomeserver": {}, + "doesNotSeemToBeAValidHomeserver": "호환되는 Homeserver(이) 가 아닌 것 같습니다. URL이 올바르게 입력됐나요?", + "@doesNotSeemToBeAValidHomeserver": {}, + "continueText": "계속하기", + "@continueText": {}, + "updateInstalled": "🎉 새 {version} 가 설치되었습니다!", + "@updateInstalled": { + "type": "text", + "placeholders": { + "version": {} + } + }, + "changelog": "변경 기록", + "@changelog": {}, + "sendingAttachment": "첨부된 파일 전송 중...", + "@sendingAttachment": {}, + "generatingVideoThumbnail": "영상 썸네일 만드는 중...", + "@generatingVideoThumbnail": {}, + "compressVideo": "영상 압축 중...", + "@compressVideo": {}, + "blur": "블러:", + "@blur": {} } From b2e1accf9dc1679f5f11b8e40227c4f67298517a Mon Sep 17 00:00:00 2001 From: Krille Date: Thu, 7 Nov 2024 13:09:21 +0100 Subject: [PATCH 171/236] chore: Follow up homeserver input field --- lib/pages/homeserver_picker/homeserver_picker.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index 1948daf8c..277009951 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -80,7 +80,9 @@ class HomeserverPickerController extends State { } void onSubmitted([_]) { - if (isLoading) return tryCheckHomeserverActionWithoutCooldown(); + if (isLoading || _checkHomeserverCooldown?.isActive == true) { + return tryCheckHomeserverActionWithoutCooldown(); + } if (supportsSso) return ssoLoginAction(); if (supportsPasswordLogin) return login(); return tryCheckHomeserverActionWithoutCooldown(); From 6d4cc45d07d1b3b339252fb8bc736a1b2035ee16 Mon Sep 17 00:00:00 2001 From: Krille Date: Thu, 7 Nov 2024 14:07:58 +0100 Subject: [PATCH 172/236] refactor: Move to upstream geolocator --- android/app/build.gradle | 4 +++ lib/pages/chat/send_location_dialog.dart | 12 ++++--- pubspec.lock | 32 ++++++++++++------- pubspec.yaml | 5 +-- scripts/enable-android-google-services.patch | 18 +++++++---- .../flutter/generated_plugin_registrant.cc | 3 ++ windows/flutter/generated_plugins.cmake | 1 + 7 files changed, 49 insertions(+), 26 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 7520ff2ab..f92f73f38 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -86,3 +86,7 @@ dependencies { //implementation 'com.google.firebase:firebase-messaging:19.0.1' // Workaround for https://github.com/microg/android_packages_apps_GmsCore/issues/313#issuecomment-617651698 implementation 'androidx.multidex:multidex:2.0.1' } + +configurations.all { + exclude group: 'com.google.android.gms' +} \ No newline at end of file diff --git a/lib/pages/chat/send_location_dialog.dart b/lib/pages/chat/send_location_dialog.dart index 31a311f72..bf14460ca 100644 --- a/lib/pages/chat/send_location_dialog.dart +++ b/lib/pages/chat/send_location_dialog.dart @@ -57,13 +57,17 @@ class SendLocationDialogState extends State { Position position; try { position = await Geolocator.getCurrentPosition( - desiredAccuracy: LocationAccuracy.best, - timeLimit: const Duration(seconds: 30), + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.best, + timeLimit: Duration(seconds: 30), + ), ); } on TimeoutException { position = await Geolocator.getCurrentPosition( - desiredAccuracy: LocationAccuracy.medium, - timeLimit: const Duration(seconds: 30), + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.medium, + timeLimit: Duration(seconds: 30), + ), ); } setState(() => this.position = position); diff --git a/pubspec.lock b/pubspec.lock index af7511c4d..421a88055 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -801,42 +801,50 @@ packages: dependency: "direct main" description: name: geolocator - sha256: b8f520252c5c66851295bcc263bc8ae7555501938427f72216ba7688702e261d + sha256: "0ec58b731776bc43097fcf751f79681b6a8f6d3bc737c94779fe9f1ad73c1a81" url: "https://pub.dev" source: hosted - version: "7.7.1" + version: "13.0.1" geolocator_android: - dependency: "direct overridden" + dependency: transitive description: name: geolocator_android - sha256: a4834a98fab5124f2d5b881e62a40ebb4a71d6aad6ad577e047a3ffb69b67dac - url: "https://hanntech-gmbh.gitlab.io/free2pass/flutter-geolocator-floss/" + sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "4.6.1" geolocator_apple: dependency: transitive description: name: geolocator_apple - sha256: "1e8e398cc92151d946a4bbd34e2075885333e42d35ca33e418e7ce7b0a29991e" + sha256: bc2aca02423ad429cb0556121f56e60360a2b7d694c8570301d06ea0c00732fd url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "2.3.7" geolocator_platform_interface: dependency: transitive description: name: geolocator_platform_interface - sha256: "9d6f34a8a4b704d504f34acc5e52d880a7d2caedd99739902d6319179b0336d4" + sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "4.2.4" geolocator_web: dependency: transitive description: name: geolocator_web - sha256: "0b9e0ec13ce2211085cae0055b3516c975bd6cfe2878a20c8f13611f1a259855" + sha256: "2ed69328e05cd94e7eb48bb0535f5fc0c0c44d1c4fa1e9737267484d05c29b5e" url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "4.1.1" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e" + url: "https://pub.dev" + source: hosted + version: "0.2.3" get_it: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0e1e4cc3c..fb3e3ca40 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,7 @@ dependencies: flutter_typeahead: ^5.2.0 flutter_web_auth_2: ^4.0.1 flutter_webrtc: ^0.11.7 - geolocator: ^7.6.2 + geolocator: ^13.0.1 go_router: ^14.3.0 handy_window: ^0.4.0 hive: ^2.2.3 @@ -153,9 +153,6 @@ msix_config: install_certificate: false dependency_overrides: - geolocator_android: - hosted: https://hanntech-gmbh.gitlab.io/free2pass/flutter-geolocator-floss - version: ^1.0.1 # waiting for null safety # Upstream pull request: https://github.com/AntoineMarcel/keyboard_shortcuts/pull/13 keyboard_shortcuts: diff --git a/scripts/enable-android-google-services.patch b/scripts/enable-android-google-services.patch index aca4f69b8..def051189 100644 --- a/scripts/enable-android-google-services.patch +++ b/scripts/enable-android-google-services.patch @@ -1,5 +1,5 @@ diff --git a/android/app/build.gradle b/android/app/build.gradle -index 7520ff2a..ae376d9d 100644 +index f92f73f3..6d389efb 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -2,7 +2,7 @@ plugins { @@ -11,7 +11,7 @@ index 7520ff2a..ae376d9d 100644 } def localProperties = new Properties() -@@ -83,6 +83,6 @@ flutter { +@@ -83,10 +83,10 @@ flutter { } dependencies { @@ -19,6 +19,12 @@ index 7520ff2a..ae376d9d 100644 + implementation 'com.google.firebase:firebase-messaging:19.0.1' // Workaround for https://github.com/microg/android_packages_apps_GmsCore/issues/313#issuecomment-617651698 implementation 'androidx.multidex:multidex:2.0.1' } + + configurations.all { +- exclude group: 'com.google.android.gms' ++ //exclude group: 'com.google.android.gms' + } +\ No newline at end of file diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index d0e0fbc9..0a546da0 100644 --- a/android/app/proguard-rules.pro @@ -100,10 +106,10 @@ index b2fd960a..fdb01a4d 100644 include ":app" \ No newline at end of file diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart -index 039dde89..1cefdd71 100644 +index 1ba2659a..989f458e 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart -@@ -38,7 +38,7 @@ import '../config/setting_keys.dart'; +@@ -39,7 +39,7 @@ import '../config/setting_keys.dart'; import '../widgets/matrix.dart'; import 'platform_infos.dart'; @@ -112,7 +118,7 @@ index 039dde89..1cefdd71 100644 class NoTokenException implements Exception { String get cause => 'Cannot get firebase token'; -@@ -63,7 +63,7 @@ class BackgroundPush { +@@ -64,7 +64,7 @@ class BackgroundPush { final pendingTests = >{}; @@ -122,7 +128,7 @@ index 039dde89..1cefdd71 100644 DateTime? lastReceivedPush; diff --git a/pubspec.yaml b/pubspec.yaml -index 69c80d6e..efd32d89 100644 +index fb3e3ca4..039b2ccc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index eda11f869..83da0badc 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); FlutterWebRTCPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); PasteboardPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PasteboardPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 5e8e211e5..24fc8c78d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows flutter_secure_storage_windows flutter_webrtc + geolocator_windows pasteboard permission_handler_windows record_windows From ae8965b03ffa064fd598c6491f9efb42351128af Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 8 Nov 2024 09:50:24 +0100 Subject: [PATCH 173/236] chore: Follow up send file dialog --- assets/l10n/intl_en.arb | 3 +- lib/pages/chat/send_file_dialog.dart | 176 +++++++++++++-------------- 2 files changed, 84 insertions(+), 95 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 247e010b1..173c33bf4 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2805,5 +2805,6 @@ "serverInformation": "Server information:", "name": "Name", "version": "Version", - "website": "Website" + "website": "Website", + "compressBeforeSending": "Compress before sending" } diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index 8d7fcaad0..95850e09c 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -34,7 +34,7 @@ class SendFileDialog extends StatefulWidget { } class SendFileDialogState extends State { - bool origImage = false; + bool compress = true; /// Images smaller than 20kb don't need compression. static const int minSizeToCompress = 20 * 1024; @@ -60,7 +60,7 @@ class SendFileDialogState extends State { mimeType != null && mimeType.startsWith('video') && length > minSizeToCompress && - !origImage) { + compress) { scaffoldMessenger.showLoadingSnackBar(l10n.compressVideo); file = await xfile.resizeVideo(); scaffoldMessenger.showLoadingSnackBar(l10n.generatingVideoThumbnail); @@ -70,7 +70,7 @@ class SendFileDialogState extends State { file = MatrixFile( bytes: await xfile.readAsBytes(), name: xfile.name, - mimeType: xfile.mimeType, + mimeType: mimeType, ).detectFileType; } @@ -93,7 +93,7 @@ class SendFileDialogState extends State { await widget.room.sendFileEvent( file, thumbnail: thumbnail, - shrinkImageMaxDimension: origImage ? null : 1600, + shrinkImageMaxDimension: compress ? 1600 : null, ); } on MatrixException catch (e) { final retryAfterMs = e.retryAfterMs; @@ -117,7 +117,7 @@ class SendFileDialogState extends State { await widget.room.sendFileEvent( file, thumbnail: thumbnail, - shrinkImageMaxDimension: origImage ? null : 1600, + shrinkImageMaxDimension: compress ? null : 1600, ); } } @@ -156,6 +156,11 @@ class SendFileDialogState extends State { final fileName = widget.files.length == 1 ? widget.files.single.name : L10n.of(context).countFiles(widget.files.length.toString()); + final fileTypes = widget.files + .map((file) => file.name.split('.').last) + .toSet() + .join(', ') + .toUpperCase(); if (uniqueMimeType?.startsWith('image') ?? false) { sendStr = L10n.of(context).sendImage; @@ -171,98 +176,86 @@ class SendFileDialogState extends State { final sizeString = snapshot.data ?? L10n.of(context).calculatingFileSize; - Widget contentWidget; - if (uniqueMimeType?.startsWith('image') ?? false) { - contentWidget = Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Material( - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - elevation: theme.appBarTheme.scrolledUnderElevation ?? 4, - shadowColor: theme.appBarTheme.shadowColor, - clipBehavior: Clip.hardEdge, - child: kIsWeb - ? Image.network( - widget.files.first.path, - fit: BoxFit.contain, - height: 256, - ) - : Image.file( - File(widget.files.first.path), - fit: BoxFit.contain, - height: 256, - ), - ), - ), - const SizedBox(height: 16), - // Workaround for SwitchListTile.adaptive crashes in CupertinoDialog - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CupertinoSwitch( - value: origImage, - onChanged: (v) => setState(() => origImage = v), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - L10n.of(context).sendOriginal, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - Text(sizeString), - ], - ), - ), - ], - ), - ], - ); - } else { - final fileNameParts = fileName.split('.'); - contentWidget = SizedBox( + return AlertDialog.adaptive( + title: Text(sendStr), + content: SizedBox( width: 256, child: Column( mainAxisSize: MainAxisSize.min, children: [ - Row( - children: [ - Icon( - uniqueMimeType == null - ? Icons.description_outlined - : uniqueMimeType.startsWith('video') - ? Icons.video_file_outlined - : uniqueMimeType.startsWith('audio') - ? Icons.audio_file_outlined - : Icons.description_outlined, + const SizedBox(height: 12), + if (uniqueMimeType?.startsWith('image') ?? false) + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius / 2), + clipBehavior: Clip.hardEdge, + child: kIsWeb + ? Image.network( + widget.files.first.path, + fit: BoxFit.contain, + height: 156, + ) + : Image.file( + File(widget.files.first.path), + fit: BoxFit.contain, + height: 156, + ), ), - const SizedBox(width: 8), - Expanded( - child: Text( - fileNameParts.first, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + ), + if (uniqueMimeType?.startsWith('image') != true || + widget.files.length > 1) + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Row( + children: [ + Icon( + uniqueMimeType == null + ? Icons.description_outlined + : uniqueMimeType.startsWith('video') + ? Icons.video_file_outlined + : uniqueMimeType.startsWith('audio') + ? Icons.audio_file_outlined + : Icons.description_outlined, + size: 32, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + fileName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + '$sizeString - $fileTypes', + style: theme.textTheme.labelSmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], ), - if (fileNameParts.length > 1) - Text('.${fileNameParts.last}'), - Text(' ($sizeString)'), - ], - ), + ), // Workaround for SwitchListTile.adaptive crashes in CupertinoDialog if (uniqueMimeType != null && - uniqueMimeType.startsWith('video') && - PlatformInfos.isMobile) + (uniqueMimeType.startsWith('image') || + uniqueMimeType.startsWith('video'))) Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ CupertinoSwitch( - value: origImage, - onChanged: (v) => setState(() => origImage = v), + value: compress, + onChanged: uniqueMimeType.startsWith('video') && + !PlatformInfos.isMobile + ? null + : (v) => setState(() => compress = v), ), const SizedBox(width: 16), Expanded( @@ -271,11 +264,10 @@ class SendFileDialogState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - L10n.of(context).sendOriginal, - style: - const TextStyle(fontWeight: FontWeight.bold), + L10n.of(context).compressBeforeSending, + style: theme.textTheme.labelMedium, + textAlign: TextAlign.left, ), - Text(sizeString), ], ), ), @@ -283,11 +275,7 @@ class SendFileDialogState extends State { ), ], ), - ); - } - return AlertDialog.adaptive( - title: Text(sendStr), - content: contentWidget, + ), actions: [ AdaptiveDialogAction( onPressed: () => From 92f6adfc9e3d4afcbfb2c670fd2e1081192618c8 Mon Sep 17 00:00:00 2001 From: Kimby Date: Sun, 10 Nov 2024 03:52:12 +0000 Subject: [PATCH 174/236] Translated using Weblate (Spanish) Currently translated at 74.6% (513 of 687 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/es/ --- assets/l10n/intl_es.arb | 69 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index f11a7a5e7..31f25dff3 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -282,7 +282,7 @@ "type": "text", "placeholders": {} }, - "chatBackupDescription": "La copia de respaldo del chat está protegida por una clave de seguridad. Procure no perderla.", + "chatBackupDescription": "La copia de respaldo del chat está protegida por una llave de seguridad. Procure no perderla.", "@chatBackupDescription": { "type": "text", "placeholders": {} @@ -662,7 +662,7 @@ "type": "text", "placeholders": {} }, - "inviteText": "{username} te invitó a FluffyChat.\n1. Instale FluffyChat: https://fluffychat.im\n2. Regístrate o inicia sesión \n3. Abra el enlace de invitación: {link}", + "inviteText": "{username} te invitó a FluffyChat.\n1.Visita fluffychat.im e instala la app\n2. Regístrate o inicia sesión\n3. Abre el enlace de invitación:\n{link}", "@inviteText": { "type": "text", "placeholders": { @@ -1667,7 +1667,7 @@ "type": "text", "placeholders": {} }, - "defaultPermissionLevel": "Nivel de permiso predeterminado", + "defaultPermissionLevel": "Nivel de permiso predeterminado para nuevo usuarios", "@defaultPermissionLevel": { "type": "text", "placeholders": {} @@ -2315,5 +2315,66 @@ } }, "commandHint_googly": "Enviar unos ojos saltones", - "@commandHint_googly": {} + "@commandHint_googly": {}, + "noChatsFoundHere": "No se han encontrado chats. Inicia un nuevo chat usando el botón de abajo. ⤵️", + "@noChatsFoundHere": {}, + "joinedChats": "Chats Unidos", + "@joinedChats": {}, + "space": "Espacio", + "@space": {}, + "spaces": "Espacios", + "@spaces": {}, + "block": "Bloquear", + "@block": {}, + "blockListDescription": "Puedes bloquear usuarios que te estén molestando. No podrás recibir mensajes ni invitaciones de chat de los usuarios de tu lista de bloqueo.", + "@blockListDescription": {}, + "aboutHomeserver": "Acerca de {homeserver}", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "unread": "No leídos", + "@unread": {}, + "swipeRightToLeftToReply": "Desliza a la izquierda para responder", + "@swipeRightToLeftToReply": {}, + "hideRedactedMessagesBody": "Si alguien elimina un mensaje, este mensaje ya no será visible en el chat.", + "@hideRedactedMessagesBody": {}, + "hideInvalidOrUnknownMessageFormats": "Esconde formatos de mensajes inválidos o desconocidos", + "@hideInvalidOrUnknownMessageFormats": {}, + "hideRedactedMessages": "Esconde mensajes eliminados", + "@hideRedactedMessages": {}, + "appLockDescription": "Bloquear la aplicación cuando no se use con código pin", + "@appLockDescription": {}, + "alwaysUse24HourFormat": "Falso", + "@alwaysUse24HourFormat": { + "description": "Set to true to always display time of day in 24 hour format." + }, + "accessAndVisibility": "Acceso y visibilidad", + "@accessAndVisibility": {}, + "globalChatId": "ID de chat Global", + "@globalChatId": {}, + "accessAndVisibilityDescription": "A quién se le permite unirse a este chat y cómo se puede descubrir el chat.", + "@accessAndVisibilityDescription": {}, + "calls": "Llamadas", + "@calls": {}, + "customEmojisAndStickers": "Emojis y stickers personalizados", + "@customEmojisAndStickers": {}, + "customEmojisAndStickersBody": "Agrega o comparte emojis y stickers personalizados que se pueden utilizar en cualquier chat.", + "@customEmojisAndStickersBody": {}, + "blockedUsers": "Usuarios bloqueados", + "@blockedUsers": {}, + "blockUsername": "Ignorar nombre de usuario", + "@blockUsername": {}, + "noMoreChatsFound": "No se encontraron más chats...", + "@noMoreChatsFound": {}, + "countChatsAndCountParticipants": "{chats} chats y {participants} participantes", + "@countChatsAndCountParticipants": { + "type": "text", + "placeholders": { + "chats": {}, + "participants": {} + } + } } From f5c21481e68d39d8df87b00fa04eea3c0a0d233a Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 11 Nov 2024 11:38:48 +0100 Subject: [PATCH 175/236] refactor: Migrate to newer keyboard shortcuts package --- lib/pages/chat/chat_input_row.dart | 2 +- lib/pages/chat_list/chat_list_view.dart | 2 +- .../chat_list/client_chooser_button.dart | 2 +- lib/widgets/chat_settings_popup_menu.dart | 2 +- pubspec.lock | 21 +++++++++---------- pubspec.yaml | 8 +------ 6 files changed, 15 insertions(+), 22 deletions(-) diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index b22d905af..cd41989d2 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -3,8 +3,8 @@ import 'package:flutter/services.dart'; import 'package:animations/animations.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; +import 'package:new_keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/platform_infos.dart'; diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 674aa9406..7a928c10f 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -3,8 +3,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; -import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; +import 'package:new_keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 041b83826..16de23732 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -4,8 +4,8 @@ import 'package:flutter/services.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; -import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; +import 'package:new_keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; diff --git a/lib/widgets/chat_settings_popup_menu.dart b/lib/widgets/chat_settings_popup_menu.dart index e7d6142ea..7fc251a10 100644 --- a/lib/widgets/chat_settings_popup_menu.dart +++ b/lib/widgets/chat_settings_popup_menu.dart @@ -6,8 +6,8 @@ import 'package:flutter/services.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; -import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; +import 'package:new_keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'matrix.dart'; diff --git a/pubspec.lock b/pubspec.lock index 421a88055..09bc64138 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1098,15 +1098,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.13" - keyboard_shortcuts: - dependency: "direct main" - description: - path: "." - ref: null-safety - resolved-ref: a3d4020911860ff091d90638ab708604b71d2c5a - url: "https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git" - source: git - version: "0.1.4" latlong2: dependency: "direct main" description: @@ -1299,6 +1290,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + new_keyboard_shortcuts: + dependency: "direct main" + description: + name: new_keyboard_shortcuts + sha256: af1389c7450c29746addfb6da5fcc5e85a83340db02a987d2012ed26c73147e3 + url: "https://pub.dev" + source: hosted + version: "0.1.4" node_preamble: dependency: transitive description: @@ -2324,10 +2323,10 @@ packages: dependency: transitive description: name: visibility_detector - sha256: "15c54a459ec2c17b4705450483f3d5a2858e733aee893dcee9d75fd04814940d" + sha256: ec932527913f32f65aa01d3a393504240b9e9021ecc77123f017755605e48832 url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.2.2" vm_service: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fb3e3ca40..77c0bab6e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,12 +62,12 @@ dependencies: image_picker: ^1.1.0 intl: any just_audio: ^0.9.39 - keyboard_shortcuts: ^0.1.4 latlong2: ^0.9.1 linkify: ^5.0.0 matrix: ^0.34.0 mime: ^1.0.6 native_imaging: ^0.1.1 + new_keyboard_shortcuts: ^0.1.4 opus_caf_converter_dart: ^1.0.1 package_info_plus: ^8.0.2 pasteboard: ^0.2.0 @@ -153,10 +153,4 @@ msix_config: install_certificate: false dependency_overrides: - # waiting for null safety - # Upstream pull request: https://github.com/AntoineMarcel/keyboard_shortcuts/pull/13 - keyboard_shortcuts: - git: - url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git - ref: null-safety win32: 5.5.3 From 39de990fae487ca83bab5b441723956732657df0 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 11 Nov 2024 16:03:52 +0100 Subject: [PATCH 176/236] refactor: Remove keyboard shortcuts This package right now makes the web app nearly unusable as it throws multiple errors on every key press. The package seems to be unmaintained. I tried two other packages and none of them worked. --- lib/pages/chat/chat_input_row.dart | 181 ++++++++---------- lib/pages/chat_list/chat_list_view.dart | 36 ++-- .../chat_list/client_chooser_button.dart | 114 +---------- lib/widgets/chat_settings_popup_menu.dart | 12 +- macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- macos/Runner/AppDelegate.swift | 2 +- pubspec.lock | 16 -- pubspec.yaml | 1 - 9 files changed, 101 insertions(+), 265 deletions(-) diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index cd41989d2..9d4fe4602 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:animations/animations.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; -import 'package:new_keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -96,130 +94,113 @@ class ChatInputRow extends StatelessWidget { ] : [ const SizedBox(width: 4), - KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.altLeft, - LogicalKeyboardKey.keyA, - }, - onKeysPressed: () => - controller.onAddPopupMenuButtonSelected('file'), - helpLabel: L10n.of(context).sendFile, - child: AnimatedContainer( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - height: height, - width: controller.sendController.text.isEmpty ? height : 0, - alignment: Alignment.center, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - child: PopupMenuButton( - icon: const Icon(Icons.add_outlined), - onSelected: controller.onAddPopupMenuButtonSelected, - itemBuilder: (BuildContext context) => - >[ + AnimatedContainer( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + height: height, + width: controller.sendController.text.isEmpty ? height : 0, + alignment: Alignment.center, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + child: PopupMenuButton( + icon: const Icon(Icons.add_outlined), + onSelected: controller.onAddPopupMenuButtonSelected, + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem( + value: 'file', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + child: Icon(Icons.attachment_outlined), + ), + title: Text(L10n.of(context).sendFile), + contentPadding: const EdgeInsets.all(0), + ), + ), + PopupMenuItem( + value: 'image', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + child: Icon(Icons.image_outlined), + ), + title: Text(L10n.of(context).sendImage), + contentPadding: const EdgeInsets.all(0), + ), + ), + if (PlatformInfos.isMobile) PopupMenuItem( - value: 'file', + value: 'camera', child: ListTile( leading: const CircleAvatar( - backgroundColor: Colors.green, + backgroundColor: Colors.purple, foregroundColor: Colors.white, - child: Icon(Icons.attachment_outlined), + child: Icon(Icons.camera_alt_outlined), ), - title: Text(L10n.of(context).sendFile), + title: Text(L10n.of(context).openCamera), contentPadding: const EdgeInsets.all(0), ), ), + if (PlatformInfos.isMobile) PopupMenuItem( - value: 'image', + value: 'camera-video', child: ListTile( leading: const CircleAvatar( - backgroundColor: Colors.blue, + backgroundColor: Colors.red, foregroundColor: Colors.white, - child: Icon(Icons.image_outlined), + child: Icon(Icons.videocam_outlined), ), - title: Text(L10n.of(context).sendImage), + title: Text(L10n.of(context).openVideoCamera), contentPadding: const EdgeInsets.all(0), ), ), - if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'camera', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.purple, - foregroundColor: Colors.white, - child: Icon(Icons.camera_alt_outlined), - ), - title: Text(L10n.of(context).openCamera), - contentPadding: const EdgeInsets.all(0), - ), - ), - if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'camera-video', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - child: Icon(Icons.videocam_outlined), - ), - title: Text(L10n.of(context).openVideoCamera), - contentPadding: const EdgeInsets.all(0), - ), - ), - if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'location', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.brown, - foregroundColor: Colors.white, - child: Icon(Icons.gps_fixed_outlined), - ), - title: Text(L10n.of(context).shareLocation), - contentPadding: const EdgeInsets.all(0), + if (PlatformInfos.isMobile) + PopupMenuItem( + value: 'location', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.brown, + foregroundColor: Colors.white, + child: Icon(Icons.gps_fixed_outlined), ), + title: Text(L10n.of(context).shareLocation), + contentPadding: const EdgeInsets.all(0), ), - ], - ), + ), + ], ), ), Container( height: height, width: height, alignment: Alignment.center, - child: KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.altLeft, - LogicalKeyboardKey.keyE, - }, - onKeysPressed: controller.emojiPickerAction, - helpLabel: L10n.of(context).emojis, - child: IconButton( - tooltip: L10n.of(context).emojis, - icon: PageTransitionSwitcher( - transitionBuilder: ( - Widget child, - Animation primaryAnimation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.scaled, - fillColor: Colors.transparent, - child: child, - ); - }, - child: Icon( - controller.showEmojiPicker - ? Icons.keyboard - : Icons.add_reaction_outlined, - key: ValueKey(controller.showEmojiPicker), - ), + child: IconButton( + tooltip: L10n.of(context).emojis, + icon: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation primaryAnimation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.scaled, + fillColor: Colors.transparent, + child: child, + ); + }, + child: Icon( + controller.showEmojiPicker + ? Icons.keyboard + : Icons.add_reaction_outlined, + key: ValueKey(controller.showEmojiPicker), ), - onPressed: controller.emojiPickerAction, ), + onPressed: controller.emojiPickerAction, ), ), if (Matrix.of(context).isMultiAccount && diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 7a928c10f..5d2494813 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:new_keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; @@ -138,27 +136,19 @@ class ChatListView extends StatelessWidget { behavior: HitTestBehavior.translucent, child: Scaffold( body: ChatListViewBody(controller), - floatingActionButton: KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.controlLeft, - LogicalKeyboardKey.keyN, - }, - onKeysPressed: () => context.go('/rooms/newprivatechat'), - helpLabel: L10n.of(context).newChat, - child: selectMode == SelectMode.normal && - !controller.isSearchMode && - controller.activeSpaceId == null - ? FloatingActionButton.extended( - onPressed: () => - context.go('/rooms/newprivatechat'), - icon: const Icon(Icons.add_outlined), - label: Text( - L10n.of(context).chat, - overflow: TextOverflow.fade, - ), - ) - : const SizedBox.shrink(), - ), + floatingActionButton: selectMode == SelectMode.normal && + !controller.isSearchMode && + controller.activeSpaceId == null + ? FloatingActionButton.extended( + onPressed: () => + context.go('/rooms/newprivatechat'), + icon: const Icon(Icons.add_outlined), + label: Text( + L10n.of(context).chat, + overflow: TextOverflow.fade, + ), + ) + : const SizedBox.shrink(), ), ), ), diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 16de23732..5ec81bd5b 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:new_keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -166,36 +164,10 @@ class ClientChooserButton extends StatelessWidget { children: [ ...List.generate( clientCount, - (index) => KeyBoardShortcuts( - keysToPress: _buildKeyboardShortcut(index + 1), - helpLabel: L10n.of(context).switchToAccount(index + 1), - onKeysPressed: () => _handleKeyboardShortcut( - matrix, - index, - context, - ), - child: const SizedBox.shrink(), - ), - ), - KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.controlLeft, - LogicalKeyboardKey.tab, - }, - helpLabel: L10n.of(context).nextAccount, - onKeysPressed: () => _nextAccount(matrix, context), - child: const SizedBox.shrink(), - ), - KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.controlLeft, - LogicalKeyboardKey.shiftLeft, - LogicalKeyboardKey.tab, - }, - helpLabel: L10n.of(context).previousAccount, - onKeysPressed: () => _previousAccount(matrix, context), - child: const SizedBox.shrink(), + (index) => const SizedBox.shrink(), ), + const SizedBox.shrink(), + const SizedBox.shrink(), PopupMenuButton( onSelected: (o) => _clientSelected(o, context), itemBuilder: _bundleMenuItems, @@ -215,17 +187,6 @@ class ClientChooserButton extends StatelessWidget { ); } - Set? _buildKeyboardShortcut(int index) { - if (index > 0 && index < 10) { - return { - LogicalKeyboardKey.altLeft, - LogicalKeyboardKey(0x00000000030 + index), - }; - } else { - return null; - } - } - void _clientSelected( Object object, BuildContext context, @@ -265,75 +226,6 @@ class ClientChooserButton extends StatelessWidget { } } } - - void _handleKeyboardShortcut( - MatrixState matrix, - int index, - BuildContext context, - ) { - final bundles = matrix.accountBundles.keys.toList() - ..sort( - (a, b) => a!.isValidMatrixId == b!.isValidMatrixId - ? 0 - : a.isValidMatrixId && !b.isValidMatrixId - ? -1 - : 1, - ); - // beginning from end if negative - if (index < 0) { - var clientCount = 0; - matrix.accountBundles - .forEach((key, value) => clientCount += value.length); - _handleKeyboardShortcut(matrix, clientCount, context); - } - for (final bundleName in bundles) { - final bundle = matrix.accountBundles[bundleName]; - if (bundle != null) { - if (index < bundle.length) { - return _clientSelected(bundle[index]!, context); - } else { - index -= bundle.length; - } - } - } - // if index too high, restarting from 0 - _handleKeyboardShortcut(matrix, 0, context); - } - - int? _shortcutIndexOfClient(MatrixState matrix, Client client) { - var index = 0; - - final bundles = matrix.accountBundles.keys.toList() - ..sort( - (a, b) => a!.isValidMatrixId == b!.isValidMatrixId - ? 0 - : a.isValidMatrixId && !b.isValidMatrixId - ? -1 - : 1, - ); - for (final bundleName in bundles) { - final bundle = matrix.accountBundles[bundleName]; - if (bundle == null) return null; - if (bundle.contains(client)) { - return index + bundle.indexOf(client); - } else { - index += bundle.length; - } - } - return null; - } - - void _nextAccount(MatrixState matrix, BuildContext context) { - final client = matrix.client; - final lastIndex = _shortcutIndexOfClient(matrix, client); - _handleKeyboardShortcut(matrix, lastIndex! + 1, context); - } - - void _previousAccount(MatrixState matrix, BuildContext context) { - final client = matrix.client; - final lastIndex = _shortcutIndexOfClient(matrix, client); - _handleKeyboardShortcut(matrix, lastIndex! - 1, context); - } } enum SettingsAction { diff --git a/lib/widgets/chat_settings_popup_menu.dart b/lib/widgets/chat_settings_popup_menu.dart index 7fc251a10..398d606dd 100644 --- a/lib/widgets/chat_settings_popup_menu.dart +++ b/lib/widgets/chat_settings_popup_menu.dart @@ -1,13 +1,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:new_keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'matrix.dart'; @@ -52,15 +50,7 @@ class ChatSettingsPopupMenuState extends State { return Stack( alignment: Alignment.center, children: [ - KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.controlLeft, - LogicalKeyboardKey.keyI, - }, - helpLabel: L10n.of(context).chatDetails, - onKeysPressed: _showChatDetails, - child: const SizedBox.shrink(), - ), + const SizedBox.shrink(), PopupMenuButton( onSelected: (choice) async { switch (choice) { diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 304fc1636..41d360c6f 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -210,7 +210,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 4e6cd6256..7275d672c 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Bool { return true diff --git a/pubspec.lock b/pubspec.lock index 09bc64138..0eae4df53 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1290,14 +1290,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - new_keyboard_shortcuts: - dependency: "direct main" - description: - name: new_keyboard_shortcuts - sha256: af1389c7450c29746addfb6da5fcc5e85a83340db02a987d2012ed26c73147e3 - url: "https://pub.dev" - source: hosted - version: "0.1.4" node_preamble: dependency: transitive description: @@ -2319,14 +2311,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - visibility_detector: - dependency: transitive - description: - name: visibility_detector - sha256: ec932527913f32f65aa01d3a393504240b9e9021ecc77123f017755605e48832 - url: "https://pub.dev" - source: hosted - version: "0.2.2" vm_service: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 77c0bab6e..94d2c1140 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,7 +67,6 @@ dependencies: matrix: ^0.34.0 mime: ^1.0.6 native_imaging: ^0.1.1 - new_keyboard_shortcuts: ^0.1.4 opus_caf_converter_dart: ^1.0.1 package_info_plus: ^8.0.2 pasteboard: ^0.2.0 From 5fc4e1c443152bbc3240ad89571c199778729759 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 11 Nov 2024 17:03:18 +0100 Subject: [PATCH 177/236] build: Update matrix dart sdk to 0.35.0 --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 0eae4df53..b60163d46 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1238,10 +1238,10 @@ packages: dependency: "direct main" description: name: matrix - sha256: e06783394db3a49dbcd98a45803cac7d735a2fb1e3aafef65ddf01e72e16fc83 + sha256: "94a66e563b89fabbeb67f24428f05f4547c6ee98878ec20f647530cbfb6f04db" url: "https://pub.dev" source: hosted - version: "0.34.0" + version: "0.35.0" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 94d2c1140..37fa55d17 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: just_audio: ^0.9.39 latlong2: ^0.9.1 linkify: ^5.0.0 - matrix: ^0.34.0 + matrix: ^0.35.0 mime: ^1.0.6 native_imaging: ^0.1.1 opus_caf_converter_dart: ^1.0.1 From 6c54e552dcd8cfe83887267a44a946c56a721289 Mon Sep 17 00:00:00 2001 From: Krille Date: Thu, 14 Nov 2024 19:17:30 +0100 Subject: [PATCH 178/236] chore: Better FluffyChat Logo for PWA --- web/icons/Icon-192.png | Bin 12430 -> 24004 bytes web/icons/Icon-512.png | Bin 29877 -> 50420 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png index 3781c0638f1d684291160deb5939420a45acd16c..6003f5cf7a22c0c6916b11c6085505bd3d8f6f50 100644 GIT binary patch literal 24004 zcmeFYbx<8&wl;ilcXxLV?(XjHesJgD1b2dKaEIUy0fGbq1cJL;2yP*`z2rA@=g!o3 ztG;)t-uvI2sy^Ml_v&Y@_3X7*?bWqAMomQ)1(6UD005xK%Smaxo%{YA@UUDQzt$lmaUYxf1OJ!idl6>Vq6Z7T|_U$0cpJ`MNdjR(CD zoS$!9Jw84+_w;;yecgU{`%IMFj#+QjHLU(53aPc8DdK+E<@b~4 z^UZ8Tk8@f}@d)1IvBdMS1VdmCMh^>{NaCKN%k{8q*8Wvzudw{%)BbPWs`Hgi)?Zhz z`wG9?F0ErPOOu3xD$+!e#hkzk+qZt#eOf zXwMW(o4ts=ft$~{wZy$5DS?RY_b(4)x5f>oHhU2-QG)w7*V<3JkHDyB{b|+d#h}|W zuT9P?Y$`kn;~vtZ)spV|yR{#7rd!^tGg$mcur- z$)FT#>HgDT%LOa79P4xp8ex5b7lLPyaWJ}@gC#py2Gt5kjYM-tXo(Uc#)HYFCxC)Y zM*wf_);uD&i|syJ>^z=0cOgOu{MEMmuJ64PzOl|XWz{gNJ;yoK@pwYQ4CWmDM*H#3 z_!I>e9fgn5g?#o^%i7gt3oG{ZC1n{otuyuxwe8cEs=?*gGY&4FZ`nKcw?@MXNCV8? z6WCEoa4+NPSUEkIN zPBRQ$x}Fox^&b_E3tpbWY9i`uzGgDKYY3bqJYRS?#T0k77$rc6V}au^NryxfV6^Fp z$SvWh$h{{$M2useNoyuaW9P_HjrJO7U;A|Tt&-!GY;S!2DXRPHtHaM+jaxL%+x_lm zMhAZ(0cBRnBMF#!y3WM&0v-z@$}Z8*oty7=3wmC5k=AGGL~m<2hgqo7zX^6J*?gMs zz9Do9vkNvpZ?E@w+5^UY%(G6F%I|SLrCMxu7Nx*4ba5hV+NHjlg&6;IOTvRv@Uh~9 zCg1alni`_`r-57lZ(tRtNhtbB&PNr_b-`LCvknRD2~&}rVdVk2$>mHMSSo)Qaxz~M zci16bwt8pwv-mv7t&bzGB0>kY4v9yoO!7RpCV`VFh6P22_Q$wKVQmdbz^8cK+g&0a zo{^^g6_^GpFYk(7B$}aSB?Tt*#1TbN-f9rGzt64)aowDkBr{cR_fLEou)=0EFAFve zPqc42WbKbt4;o~eQ$G0*^_i&4B6YdvFi*EFewq}r*tEHb1{sRm<#P;c%7`B8R$_4- zF?D;^;bf>SOr3>+F1+nu#|JAT{U|4A@IcC|o_+yI87qoZzj&CEdX6?2GP)i`SfEHkNR1mQ!!So zS+)MpCQPkCHj2%#+>OhPZt{DcY6Oc1vyK`Ng0dCOS~2wxOf4eyHr1^1t5uZD$C*IG)a6in(=G{F2iBRLvY8 z$11CtAdZ)iZv%7S%?A@q+Nyq4YgIK(OA%T4SZ3vnU#+gt1;iO)#A%2PlZEQTnW-K; zN#pfJ@x1X;k%9uw%Tw^V$;khN3{kBtt?+c3^a}2lu26070A-OsEK75%-mPR0RwV@H zs2gEbSfmPO^}__}WcYXGvo0etFIqq)w%Av?Ph+CPi}o58Qn73jA=FiKw)z0_(7iw9 zbkCTn%_%e7peB1}sH?c9walbWa0S|zfs-lHM};(DSO7Z@-YS&Qb0zO!r=tWWMuAlj zITaFN4k-z6bMl?!IApVc5;eM5l@Yf*VLc61A&(Ly!BSki9c5l^axkELxQQt>TfD~4 z(NJtVWmM=e-MDgoPH2NE`*m-w4MU#PT5OJp%V* z40SiyEFjL))PY}%@TJnvq3++ZECvbJGX7UGWh+N=&E$;(2FD0AJ zp*>4|^t8eYg`8WD^NTvJJcL>RfuqzzYN|tJsUa$%*$Cc_?*F&yJRc;Vcw+3yd;aNC*W+FvT{~0=JBh%BM zqVH$8@UOU86Ab3@`NZN{9bgW zAgvG4%Nx4nf6mW)Zk~Be>n#M12@aLD2DIOdvI=Pk1as`kRzdpwpyNnrwV5+p8-m&h z78n!4xTJHSuND5pEA+C{!?_`PabCo|x#gZcx?!b5yT0G&X>4U@0=N&a9Ce)7if7i`k(@ahi} z77HG9h|x-;gV~S=vAGcc5eCzympDR)xsG*Egu#js^jE=Zb;{p2r$PkB;ju|$tuttC z#D&uEAqgXJ^278V8D2sliBD+QBJAv*EmosdRI_Cb*D}Vg<4mhV)fUiQ+T_quIZ=Eh zu0XYJxDX{5-C*B1-+lqsaqdq%Od+OluF_e&B$6%SeYWJAd!%058@o~Ri! zWKyU078n&d7}5YO#$R|6jfgg`L$_v%JX)U-W^^iLx5g5tQXULQM-SA0{=nQHT8 zMr=5`((ks6Hk=ijO7pCh@2E7G#Y9u8DO6d}LYI0Cks6vx_ER3U%<_FGi5VdM!&k?W zX$d5rP&DHqraSu@NsG)oeLDFMm=d5qJEVJrt9>P3cuI(KxobQ#hnkqS<3rETRN1XK zO&9@)xzku)AVKLnkZNddj`-dM5*pw2&zQ1oYN!OCVi^_pcaZn!6yzW9oc82>llP#= z`m3paSlPW4J&AP~rf;eEe5Y-u+|Gb;#8Y-?P^^$mux;j{mz)oI5D(3l6`#yilUdsS zLG?062$lyXp;x80xyR6ziEkxbTS@xIM^`c8f9EcHzJNBbFFOup(8{1`f@HMMK)Ne3 z&;KPWsu$_6n8*bMG?}hgg58N$m&~Y9(cYPW8s)sT=|z?rcC_qRRq8WfMRl0(I)i)~ ze@KD0f-nP7z}b9R$GGxkp)C%H(r}{(OvzrbU>MplnG)+v&aO(E>DG&tQ2oUFh+qp_ z`a$>nkzcKLiLx-N5T@FWMtN3k@1t2-TMi)lsD?bN$THmLy0WDaMQy7bV7Ai;Wf96) zG)*~#wcEFEw;7=U;pA6jmt1U15I|nlUEE0fMbCcFf=Bh+p7@xWAve+p^+Y4?T2CZY z33WI!@yRRd|NyxP3oFDQao=%=$0t0E!?5AU85@gsd0SdcQb<67NbR96PbJNkncb- z`Ue-(7YPv$$+%G6WlcOfQ7OCDRUKI&SV9F*uK7q(n97W*c~SQLA(GV>=P)WO6>^-# zm6Ci&KZqvBo-h1JTA*AafwYifv!p&N*y=U(Ts6dB24M*7`deLf;D}$${DjB<^{R;qovZcdoF?8X|*B%=`pgp=E_K z!=TqCC7oQ+J{z|e983xlTU1@CdC2EkGJ9S3Vb>L<)Xd9qZot$*sUHYwX+(}CSG8;z z)VR~#OO~3Zi5Z$MJiRxyy78{ZlP-wCZ3&SBr;pOSFSIHu#KYvLOTzDmGbJV9i?s(d zT?_yxB~GM>K4w-C7ge0Qdf3X{6a3&pfBMa+5<01*#dc1})@yg|b&yd&TkvUOtZ>WwDlwHXM~#)^%KAExG*u zkf;~BOKqVZmWC{&@&OX}C9H_m{7b;}aC9Ex1D0zpgU0&@V`0T_TtH1@@DZo`%!%Ql zvh>25ld_9D8ezcuj~I}&fNW%VgDs6b7zWfng-}=98TiXQWZp1shjztutHL((;5ie< zq}bCz^fu>$drHjh*n~cg(Ue#eoeO={slsACm%Nff6&-Fka=fS*Sf& zjFaKPgBL=qP~8dAhaprJOf>86FKhic4KS%q+^i$`7)wSPwIbQL+s^ zIBLU;yBX0clJGJ~JS59F>ixnz(cAN95Pmqm{;oq@n_`ADxJL8l3hu%E z+!`QV68j+G%Sq`9xw#Q;cmxP@-+8>Oey_DcgNRkT_mTs`cWb{ST$a* zu^@c{BKHZF^K5WB7oj2#b$w>L0q>rIRv-UI&8 zx=&a9KZ622dmo1iOq^V(fmtEiqr;O^T(sj*ZpHNcM^I=r{0yi0>GIt{Dl0*)6p4hpKHCF%$FjSh`#m-!Ti zD->#>#i*2st>r$0VCn}5;8be(*jV)y#1;^aHH;F&hcLHRl1-zb5+mkp)IGi~%<+6s z28pacuS&mFl?zL`1rO-X{mjlqUIq^za0kZkh80$>iqDoH|IRoq5xrV?4++dIj12i& zsj=N-k7Q^*daij+iM=og*QE$W>m-3`#LQS5f%fISPF~(L;62ZLfYJC$l&mn_6w5aB zr46$ew^K^|QPK!xm0Jf3f~>R7?~g(_gQxNzw%c)6aD5}Gu2-;&&A8!3uYYNYz9_(WT0ImFd}1wSoBeid&j$^BUWgaW%kI`o4*yqv9wlbYKMLtWblv*9~F znRKZ-kQp~hhYm@-;wZqR0IunE`hh|6rLMsBv_>@jQcm&=xtZ|m#x2=iyhhnnw zuI$?09V(HKl?&NCfrC|Fl2{K<+3RN^#qP%e8~OkW_hBZ`%%U7#*x@3o2So9(sIX^T zM+zA2pz)@KC@-BpZzHKnZc<|IzQ?1<~Iba3b!J(I^Q+x?>MG7o)c)5)1p85JWnN8Wi5w zk8`AkY>ZoG?%{rig`x_Uv$kVOGBS62mog>}e$SvC6Hu-{trNbhG)^`xgHkQ+7F-<+ z*Ds?JstQ-QMfChYkACa;89)GGxxHs-Qlt3=EfEU-^L|GNFeoYJdDSJl$ zq8K0_Sy4OqOx_`a*Mn~RQr}v+ZThSqIQx-mDS{ZE<2HHX$m?H;NTag3TCKNnHdGE3+Aht(>>}b@k9SY+Y_b$GtzCV^XE& zgnDy@W`-#@iTK-5l}!DLYA`_2@I4<)mWTE))Bm zS|BNqP zs8qnJ6_szh&&gLIaBN#dVnb{hTj4gO$5`NNDCsxLNjcv)nA#FH71^Jz3Y&k|?30#= zQIeE~>W#8Kgrxj|rL8uUvro4C^1~3vL|bfvp1wVJRLu82e?;`ug#H6ji(&Y8Fy$+?|!FwF1_u=U%WY$_XH4we5$-v?U-!$f}b8~ ze1}_p#&`5t?REvvkPgc6$l&?}e#IM*0~^b3caGS-?`)ez{O+3oe7H~Hae4ov#W&_w41De~4;u(Puf0+u_*ye9_eI#9OT=oTZPUJ5>m?KH5O^v>0k zl#yPW#l)nPwsZ2lvBTi#$`#u(r~0e#>{z=J??~55hq@4zK1T>M>?jJ#U_3y6AjN|# z&MRzQHGF>0@A0OgNCDBTdY-lPZkbwN#v=gdO`;K{#xx z(3eIeQEShuRIh}t>ox&g+%)+D(r`)9nGA4TRIz3g^T8z8@2az_Y=hH;j<5x@zz;}_ zWQEsVQ}d|g65LxDtvGOxn&o1!ezDp5cg!U1k>D~?tL9EQBm#d|*8>&o4u81zY3CIr zU0y=qZzGyL5>R#su2~pYbnnfo_c1p71s4RqoOU^nBVE*lO$!bYs3A2idP1Y<5kfBy zNVR@|S&8PBKpeIyWUbw$l^zJ$@GbvSr84L26iH84j{p16tiexM8Nt}cl_YjrW&mzZ zjM8k73Pbb7NG@a5bg;o3d}5^Cz6#kL*(urNbKR`(En`Tyo>UyBm_3fg*G!x%@Jpja zbE|{mUU#Wi+sAEsM4S|+<-DS_8U#IuYT(#L<4}iMElTi|Q5AtQaeZVXW>Vz` z%;^i9u|}9vzUrUFM~zSiVprkFvd8N`UZtAYUguJDop2FCOcJY@NB35RPCXQStWI+Pfe^_a zRBmn6m9BgVkYpZx5jp<^AysPZ%4Avu2+KX@=RB@ciK3BA$dwHBg6w+h;w%od)8Ozd zB(S4gc86K;5j4O=azoGL)mbnD;gPh;!QxcIe3AYtE3qclAia4nmX}HNIlSU#ssCY@ zn+65HSLdKuwU;i;Zy+8(&e7a93{srCc(4JKvN2M8-z1G*)o{Cx{ z6I!v0XnzD_O4aAb)kHOAw~BA?#Vq&-zNXn;a%V#9;AwHWDs7i2J1-xZ^D%cyy!7KL z%1c=vZe7&ILjPgl(L6+M_{^gGX#Df?gA@A?ymawBhu$D+e%x97oE7%_w|BOd0t?;K z`aYpv7v;;F)NWFiEA#9LXDy8@++<(yJ@cwh<7p!pn~O{HG;^h9%->hh`mvOy?ev4k z;6J&M+2QT3y*ORP7a9ILWv5nksCg$RvA zLwVBL%C+E0^8|hiT&hSGt|e2(|D^VynldIQ>rU75z5z1^s&q}N_z!*qzt_(Z()~6N z1yuWjwLc(9O)@u2k030kA~eS1^w|zRJX)I;@iVQ#$jt=3{V1a9RDl5I+$AgJNs+#@ zi%Un9-D-|n0>l-v(hY{!u~4lcbsy8mTGWRMiRU|fTQVFd8w}VyT@x;m*V9QBhIHhr zQolaG(}e?F>>XOO{a!)>U!(e_e|&<;(Ls*>qdb^SG)Ar6I>*Dj2pBGp3Dz^dyVdIO~W5>S5D>LxCiVue2x z+pyuY@jcYOXmonubO)_);U1MXgYy!`-+Iv@TVSnbc4v#bUH%|VlkS~&3Ck81juo~>723|FN0lSSZ6M26mdm}tXqDs3wwk#gcF zssM)u+7AbGl|17akUEiddO@{5YE0&|iYJ_NY5N<_lJlq6V!T(Od7Oa4&Fj}5gQOA& zfHxlxR=ybFeIonW6Whl9LP8ij*y}d;<@p+}p&y~rjNkBSp7(>(IlhS;0o$_MVS6V4 zQVGRAZ2n_~0CrpGgs58hmVQsc$a?0bqT#sBUB;qI;Q@W36@B9S?uf+=h&vR=quUCw z3;-XVPzATCu?6Lb>eR$XEX`N&JU2d%`L>t@{WD7qy*dT&Nbhq8&2;vR&4_59?RR>S zxo?NJrJTVsP>6=IqFUhcTHx!{2|bHk?}HbMw$4nJcbBfVp*GFORRf%i`I?DsFzDmDmgs#;>U|Ccp=g_#!%KBCp8>0KW zm<$SEHgizv9m@x$K97L)f#Jn2_{m=)xBCa0?zB(=lx}HR9oz}FHo&}y`hU0t9FzEFR%a!>CYX$(fxl1m_a zLJRbE_*p@_UTTJX5YTvnA8=;lmUP{*;39q)DiAo8QX%W*4{z`X2rcXS=^4ZK&Nvl6D%c9jI#+R6F3gEakAv@HD`EcvY{MMV&Wd<5PA zoIqf6avvv0XAc1%VamU71>UazG_z8Y|0M!;5T?{qRwI{maR-rev2d}l0i}KHyf`RD z5Xptyt*iw!q-6dM@pdOnX$uCs3b3+zdwa8ZbF#R&+px0p^YgQ^aj?~}oPEM@<{DcQs+UpJE?*{#kPk3m(?XzLk0C~80x?6&zy+F=js(*%%l~-2# z2f`nhZ0wv||8nY$ynj(a0kq-)vGH2)@_{V*+5QPa)!ptbD$O1L*{VNKR&P+|mXpK%ZnSCbc}%p0WnTjba|ncIL^U7ci9D*0&m{DF>C%OP~VXMYJYP^K++v#4t8ErM5`d{sK|3fTTTXXQTadKGzIYGQ2AQuk@7tq3jjT2}A z0=)$pFFzlLCI7#od$?GGz0KW0;x=zQzH#*ypMP;hPOoI=?CHbsuhQPOpf{eG*f{>M z!}`DP4-fyF(d-})kcY#Z7s$n7$pf_Dv9JPi^6|3suyb=*vhn_3`}4Pu{2v|q69GVW zcHnp#-_-%0zENdFgq ze-EzzMGbGz|55U9@%taT{zKQl#lXKM{GaIh4_*Hj1OJxrf1>OE8C{6~T_A#--&&p|@x6l0E{j2HZhh_h$r2fe8&ryZ&jLx+S)19?Wz3B!v zL9K1A^9I$m)wr#m^8$sa26&L6O}Dh@5|Hxb{^!jiljn&ZDnjN~Nx4`mJ|>Il$2$Rh zyPRjY9-FtEXK>hv5fVje^#A6GPZ6W%MVtYI&xS4MMl$1{8};)6$`-Z_fJ-6O?5Fh- zkv0Pz4yhU9Y9?ZKvPJc&Psc0|y|_+vBMF;f@4PgPMq^ZGnIDw_)SBkroA%3?VJ}@{ zR9r)$grmOshCo(YqAFJSiUAxnGr)- zx2eJhma;j9=Hn}y>Qs$<*0QIHF&~o87r7)Fqx+?4e!XZRrD>eOtj1?k1pBrr3pz;G z*W)d1dX0LH@DSF`2p+-7TU!7~sPThqfpET4hBg3EFkZ;KL^LcYL4?x}LJDdTN)(O* zF!1dhDr!Jn@*StAr?QqVubS?vlt(kpcwG_i0XzZAz@Ia9pTUXM&>AB71jfQm(`oln_e41d|yi@0UJr5c=s*<;cemigKZ(N%kV z^O2t#LfkLXT81Og9kFD%JUJsjcsXDcy^;71-Nyi4y;7-7w*Ult5j=K5rQ&#ad_>5f zWIWQQe1Ec}A)&91L-uz18&8h8|ks^glBv#-=Qo+>f7cip1tP^ONPNbexYXOGKkR zDFg7JOaWiEr|$Nd#_k>Eo$Fs@Y{Qd3c))SIrm&Zmf3@Vw$Brb0$p>WoE<-o^_F%NN z*w=#E2}Lsn%?z85E!JUR@XO*;*y&wd-ELoLEnEO@(%Sd*uo4{0)n1W<%8gdOJso^bdk zuR7-KZO-$l0@}cMs|y)^$^qH5z8Buj{6S~w0JG3X&y8)&s!`^H z>y7E0pGg5-HPN0-iR_kVfVyrk$gn8tlG)bfRo^}ZhCR_>QShyd0>iEw>W}Z9ty}qc zDNJQ+TfaCyRoLE>L6F{Zy*zZ%A|j1_q^}>gZ^XhR6w}baj`=nX4V|^-5(kx`z)+7Z z;m9X_98kiPMhJoY`|d@iP)p$+=You4Fxxp&P}D8YBizO!$D<)SG1Cz=)=uM;hr1zB zf#tZKHFA#2=e>fr=0 zwRS@?gpdda7sodozjSp9yPaGjet3NcucuwCmRDs)rF*A)*hh+E!$ z#aQ)=G;^yVqL7zoyS24C3xUmvLA{OfM;vjJ1$q7SK9}*w@&x$%BELJ)cUXGiLxRyK zb?Uo2KB{7VI^sYIdYpyjsh`}3Yo8<+rkXxQp>*?_R7uy^0ESndeZM0?jZfKOO!$$h zu4524P=tX)B4KYp45hg%B$-*n*JUxbHnh36Hp;{bw7KFXA?g*(^}lTumXeNO^M2vG zy;p$|0pR+yO$;Y$H#zKMG~JR`ly}0EsLRd-!^oT{ZY=Zi=_#Uybxb5Ga#I=!@{AemRI+V!ig z4oG9O`FhuHjFlzm!c4o5ABx%m8=b82q6hERM-}9|dL1-@TvM7&bM9jHTt%5pbRu`y zA`!z0CSd1z%Ml-jqSZg1)phYLl4&UP7`JAQZ>E8n5CYD}Pu~-WMlUV78pVIxJ9~9; zZo4>~8GrJ2<3ZtW^LjC(!wI-FXF*^PH+l_o``%;L(&hbq3vrJJ;9r)O+$Y;d++KO& z1?SMaKAaQBqp4|LRR6KcanUW4ylf7Mg7|Z`i_hVjO#odmE;G=eVDtL}AyHiZ`ru~% zYs%=T7ypHCUwa(`QHNh$ohQ-P{u2pO{{u^%r(5`OlnHj^?IR>iNJpJ`VME6I@vwqs z_j^>X+UHgtUcY83 zlr0aVi0*&JeNmBQxV>ia^$>;9Ecx&*&@gj{D|a>lt^*AlC!_fwBK>4tXMT78wBg&( zft1II!18X3<<0sjp=nKAUmgXKAVr_^<|1g*I1B- znS-%ZU%@e$7J58GhiEuKn|Y(Tn%wvL*V@vqY_>)j^_J4aNAmWNVY2(&0ui=UJ5HI- zt`&=r#P!=jXYY&Y9c5#Vz{!s5ab2FIEMjSQcjQ_iuE^_V0t>D&^`xs@ zR<;^OQdV-6{e@&K;rJ)XlFMUb_B^8ZIjXaW?nIGZd;Sg%2Hj5iCdV*-c>B2+IxL&t zkW}lAXZC3)-kVUYo6!S zKTSFMn}=b7cZZoi-L`#Qs#!`VPdwB281*9Bm*^e|FUbZn!+Ji?$#b*j<^S&vfx-TO- z@46abR!*hJ`|Le;XPf_U9Pf-~20`iXuadMV96Z0$n!^OGEZXCpUxKv@$94ej%o-f5 z%rZR765Qz|vPpMbO=&*7&g7-l%`}wpq+4jy=Q4TcjHFYYH&&NBqXPYjf4I@0h134C z15<5{abS(Nd!{{+;^x?)epsovoxF0++d9k9C@6S8|0#08T9%iHmss7c{GIovAeGBc z-;ziKiXm_Xh4Q2vK2MHX-=?z*f7Tbcl8~Eq#r!BhH-t$wt^mIchry99k&+%ZSr7z^U9k}z%*f+Kk zy#9=bn;f}|w>JK|E?VI3vmw1pi30nqZL<`E>ABasyXb6q3#x#$b!zlqeYt#ztzu~# z!74|tM3)FJIP}a?ikypM&Eq?(O>`CoZ|@y;MTi|Iz`tCNs^~C#-0a9P*x5Ht6;#%H z%Ah{plI(pI4O%tcU8EgX(0jg-ZQnO?KOFo98y4uJ>PW{eY~C=y;vn#>KK?i+fNV1c zmCf(}L*2|8tyhMOkdVuA8!B^Nr69fe`EWB(xNl}h?j*Y+<>~4xaBwh^-OLO*XW@7K z9<(X*I0Xem=bVPLQm^}L4Kk1A7U=FK`qKmEV#O1Y1RJczsN9-aDI{Fmvp0Pq) zwIbR{&8PiY8`Oeb})nnw_N@JChBl@O@mFHSsm8b4PB?dD%TOMeYs z&lDvjTtI_H$DgC)p}%rhd34_(brp1gT(1#PbJjnq&$M!i3 z5#i8Yc8uX^zm~SwcKL}G_H3_PfmB3<%-__f`~7D)EDU552=%7c95Ga19^x1z3jIR- zVrm@}hlN@$K3fjRehqBiUM=qD-pf#2e-Tq4mT7jLG)AIx>T-SQFt=`Y(~{lXbLAsM z1?j#&I1r|7PS{HFA~lN>MJOCvCp-X$mO^ZwlopqS<8-Yj%kF!Gv=o2&wU+tPRD9G| zcXVZIYVMTlkr)wwMo?A-SDE=uP>q-TTx>lU>%0#A;8e$HjcL-EwEZM#*`F-XvA;Ym z#4S~o%shHj(*MXn8KSde_E|?RD|xHX>o_mA+f8``Ipm~n0}3-mz!WAkM^mVtbC;FR z;AZ^!HUR&fneH`M#neQNe)n83fz*D)@9tHEbYW@AeI^e+43{+*P-ZB zyODygLc6K9cJJtR5{bxg+MFwLB+r#&Uy03S4PJ2dnUZ0qwuA(=0iAHi-rMI{WofMy zG54+Qub%`OT0c2x=HR@WOhDO7jYDKo(ccp|cRg&G5GhE(J=9;BL7lsg2N>13zEUwW zM~`$QTupy@f=wMLBH^EZZEe%zt-Iq2isoQ?6|cYxAzC02(ms!Gb6ej=J+@LnNl9HD z?_mKLJSzj7_QJv>A?1}2&MwALb8vNCTTarTNFHx+2L(=MYQB&5>a`1~!lmVIjeDS; z)t*d2@e!w#LY!tG{eY)VA%$EEA2IA1&jfM?M!HB(_H#qGxA5_ z;vZ69*~x*-#QQ3IPN!=b%2CK(hQk{|{M_MFQ(l$;q9=90I3L`gk^Z~=2*QLtZWihj!WDU+%($#lbVmdF<4q9u(TZhRJj2t(BFg+HO}3 z4Xypu9q5n(mUR&wh2=WxfxbCDP22E~S|8G0%{Yz`e|{%PJzIV_U&?T(V{CnYbkckn zwj<;pOc=u0i}hAXuD?bnpi;5ZRL-I?n`V<4YZ&G2`+hf@LhHV=(Nws+jHUki;cCEt zh>Sfa-l+?E`845a@wI0{1`n2Dc4id?-R0uvi}MCrL-24gJfJ#4C^_VNnvPDJg$+xrBOd2x-^f)WK5_YmBmn2tsJg?y|ugG`4^{8)O4I z@1{ zWUxC^mRY-eMLZW^a^Z#5*nsXvns^2U`EEY0S7kACmrl6}35{aw>pG$6w9O}Km*5FK z-!c*K2z`BLOXI4p+7@tnj1HfB)sX}EZcUY1j&_nS;R$aR%<=M~YUED-4QgdP`Ee*` z=$r`x+mDuGm(Ch2z^Uct%;9Fw(ckX`W#(np+8#9f*eaff^dYm;v;z4()jOXozm-@o zThwo9hmt~q^1p(e0$BlgWDq>A8S)=^n6!o6qy=G-5h*NPyPSWicB{h_*F(Fa@B2xI zqbp?zEpg83(r0-F3Q%lDh@wtb_MgL$hugxwYzO8gCyV_Ix4oXj0UV%wA$SQ`q_>9h ze|Vd(0SV2fc&W=H(wXr`x`yZs6S?xum~x7$7ec?SU0fm;_aV_Gen7^p*AFKU4F>;4 zzy7U#JQ>O~)al&}xBoqarsv^ZUSj>Nq|rR(moH(T9v&^#zE)2-4XMOi{;2fIql}BV za}GOwp$*>iVRp_EaiA_4l4W4xlVyiF=s3)<%NlpWan265dFP8Nf)nhZb*^rNLlqhZ zb+=72%l^T0lyE}4u9qgRUvecK?jZ_&$F&zHI1Z(6^$d@SbGBoTZWluVY9Cv3pW6Li zJ5?C3hZr{h;@bCa?Zn7r3nQ26-CKH(WBJgR8wbZ&%_l3Tg7!1Pf8w_0$r|nqOG4|~ zAEHA_4Qeg4+FnX5I7aa7!`O%$BiX?@bQIy;{eXwOW3S>7p_!9uuBs_4sbPrm86E}( z#oA+E0BmWse8KaFq7UkHL(hPHD8tgU%~Xw}$IowT%UQPt)Vw|405Jt-B(fegny=4U z&w@P__So?3Uk|k?&A+lL&q+*>F1XG^w4P+9@QSvi`k(If9c-)rwRhpxJL=2Mh&%B z-(0N35!qGg`Y-1lD`jC!zC#0&Q#LvIDRC#!gwvm~^7pX@6FBd=tR;`Gk+nVyuEe_5g9)vv&+)laqHnfPogHFXygh7^we zO)NXO^({9L5H0R$1>Rq7PxDms6!z+OZ;%_vs>ZIn_CH}z?9w6&P=?Dvz#{5-|F+d1 zD!-1h@42J*PFtsa{INpf_3bEq`8_G;h_JQ@Y8G0+_9B09+AnEcFI6XHD5P`0#B7BE zB(VfSvex_8K=yh!F@+Oq3aLB0jkozBH}bLS9_UU1h1XWNxMGegLz z2s}XPIS!sVj<{|*jw0#4p18t_myxtVN*RX;DX%d+&lR*9X&9^xdL3K&=sB-bLPiuc zrtF>g?bBH#!P|Phr}>`tXP?+r%j$XdB8QdjPB~t_0q5+@&V`+>uSZFIhIMrX;7Jtj zqO`8<+x~Hg&A95a|KqyHY^T{%KD1H+zx&V^*`JP>0?aS=&zVMoe2))AHmJv;mqB>A z@Q^m+6S<8Snaw%~+p7+X$1nn)&%e*?L$YNf%aPTE$`fE~G+l%!mR$R-f0(nFR?Zi2 z`~?&xEjUd?a9^2f*y#%55S6K4%#-)97jjWq@a0_>{u#_V$uA&sF*wfN9Xlx-x{F0Q zefVw{0_m@5zdrt+o_@7xrMY{WuSd~@y@PkvHbbdmt#@FOl&qKP79ywojtV&ib6|3*G#=D zWG&9V({c#wCPI0+xJCk2w!iL3>0Cm7ruk-~EqDJIe`kUxCf5;S#~I~Mx)R}0&)NQK zk+gJ#a!SfCmQB69DbmMX&^;C{$_FQ|qp-;>6C?^LH6=%%1+tfbJbr#)hau9}ib>+^ zX~SD~9u~?m+l6_*V}1k@qnGC*qZTvBoh}3ZoZ!SqY0MLiOU{lPR31W2Kf3}yeRVekO1LxQ^EhKah)~c=5s=cByn~GVZRl7xN&(_|hT7ufdUPZ)A@fLeU@XhB>`2PAj=f2Om&wcLa zIgiJ6o!5QC1f|_Qw^~1c*{O|sH&$k5_qqnfH}%Hh;~2hgPNQWs7YiK7rmc!58k#zf zA{;zOoudnw{msW-| z`Kh84lgj$mbn6ou{LgR2p=Susf-U!XeLA|LuKf;WM-J{$l*Cel*X^~OIwlAAp>L06GS0=>d3{&3|BEQ^8Zr= z-&}&w_bB8CYY9C`q2~ScbLCQPm_ohh2JNm~5NVhAY*QaFU2~y$8rUgz1dId*CmI)> zj6%*_4R#7K%UKE&j?yYf2Kt7KIJX8>Mb8LCMLNOM0wU(_eJAE?5~j zI5eLxX;TTHP*;4iuW0Bh0Rsd;NL4$(t%2q53&HJqP6~%dLoq`a$Xm$c3i3pwYm#4T zSLA2aA+_p(9G9Bea3jA`&f|93epg9O>qVZd;ISzQzo1%s&6#v2%AY#6`MH~dw-YCb z8KuoI7;KxgDPRtY%0FhVY3b?;#^Hu4iI)lC^nd5GVlei`y4m_%!)MbchnP)`&&?Ch z_b%>TTnx~~@w*(D<=QM`J!sjMMOkh|$6kr~hm}*1Z&*<~JP95Qq~E{>4Cj?KDj9jz zd}a|)%F(Kvdgp)cI8ueSJ-Q%dTPPdoVtwRGT!x5w2{9)0oRRNnwS0@)+$hDw#UZr- zoq+PF;`CSf5ep}KLIbeN;5xoZeFuy`?-mID)ix`??#3!ZM*z|TCM)NMmK2Vbs!$4q z@8xyc=SxIZlLjxn@ZTKzL=WoVVxCw0_ivXpq~(}e6aCmW zQaU!TrtZ`6f@S=~OQe7wh@w?JrM1 zf4QFR3FAq!g|va{?(tc<1=)__F3~lG-s{cuSft{s2VNJAN`}u#lwh;ng1~Xx+a&~s zVJ*33L4*Qnm-^{AK1D-|1F}i!lDM9A_hC&`gFyyc>o&drfFzHijMVaqn}6N-q&&Y3 zX|ZVI^0t~!%UZ%>OFi>Rzngg=k?j|MnrKATAtTizsINw%-n_d__Mk~TdS2{|8Wbug zJU5+`z*i?Fs-}2P{GQ_JaOhs<$HRt!(Y!SaD8u1iM}-bQyRrL~VS+>4;|y9Q7vOYSj(Dn} z#-@?&6Ylz@;9HIvB|D&|#y56Oy^f679Vo%J*AG`{H1mqKH~pJm?j~CPakf4OGsNJQBHKwQk2k~dD8mPD?`!8Fq7I^9E-# zU_`>IrPwlR*uCvF8KqmRVtpmzpqsnMf`|n**=!w?$V}pGdW6)c^FQW#f@k6kM?K_D zsSI96l_;c=jWQfgoi6K7V_Zmg9#&K|X;Obu8loJ!4v&t1>p>0@2c&|oyaOLE+X(Dj z#hwaylxsU^1qqODX>F@IS&%EY_|(nT1E+y1QV{`~pb@A2mp)G2%WU(^*2qEs3vD!r@lAX$5%GlusYMGv8E&q$Pl>`*4Fe0-SMTP z&zJYK(i&LUKaqNp{131^`NdM1>ILSL(RYh3xtr288A&2Emt)v zr@Y&KC4w^Ve!kHDAK2xJKguAR;n74w9l%B?^m)_sc!+XzIv0x}=UDtOFO5G=Ilb$s~IaHb06tZZ4^V8}9br_s!?tEXzMlm>ppg(MjQFhWaMw{WS zB=@cH3dH@Ava;p*ACo(WP7jT=gvqQXA#wX_%&*$6Epx*ZXOQ6sboeWRe-v~9TUt5( zfniZ|xL(KF#K=IFaFJLlb^B92}hie%RsF|4PV#U+pR5f7+$rh4kd@RhFVD4Bt zn89+Xbi+RB!)ywO1d7t!`>H-&D;PPRUGoM6`UVzWH`cx_!--zG8?~!#vE%>MV0zq;*&!C=)3ze%- zw>f+0f(`D7E2r%Pou&1Snr*o9s?y>WP7VV~Q?6^#`t||OxJ$J<9QU6{mx=P-dK0T; z7}fjA6u1)&eGQFSrXkK7 zPZ%&L#gI=#E{dSSk<6VVSshQw`%7fT*=r5+tbCO1tfqy8cf4#=@87wzICrrO1!b0G zFS)_nnaBq%N`L;;&HlIYR4_t|0n_}J&K=R3riQ%ge>)?3r0g{*i=7gsPtqRcaSRlf z_le2%a6`H(vwo%BS^n#hPZrr}Sm+$R;6e3`V6NZb!z>O@{~&-qEX>1?u;Ty8&zPC& z>4F#(WMH!IZF&1Ae&vpVGr^er*4Bve1Dfl+UuJ$mqYf9=jC=|p%?2HIqlsVGx(1Xq z)9!jS5+XT?!vtgVL|GAlKn(shKFCJoDZt9QoKK=8=UY|S#)jy@oBMCy(zx*K@(oHa z$H$|$^5=`m@n4CML1u*^PxFsMj?rpBM_IIy?nC?>C78rUXwTl%k!#<~@wnI^+ z=wIkQty2eQamG^BqIb3cw-A2y4TT zV=uS@Y%=aMM^s3D-NG`ORrVNZ<3@guwc&GPvZBf~s?~oG2@gO%*MXs>^U@SSA))q% zwq+na)8PA8!+1a7H1Rs}J_F|R1R^t+PS%~Q>}~Vhyqxhe%}96Q5@t&t{+;NZY}w7a zou-XISc6QXvJVNR;2!?IdsA1R6VbL$pLX{R?UT;K`R>Rk(FglNM@C*yoUW>Wo!8uL zX9EYJna>bY?Y*x)>Kv<(v%oKhnr?(#{G+-XQu>#CGT8x}IKHqs#Mr4)x{c_qMU{dc z3t7#(9SltCmTl94jN0s60x`H8F?p%5Gl&&NWQwKT`QEuuoDK*dx-j5)A;O_jTEB{y zG(bKc7APIYt+iB4nn{$lY|IB*#!!c+1)s`@8bByRhH1NX|Mq2|@>y()+tNS)@pR^0 z#6ubr3q!0)>uFL+jqF$LsyR>c&JF;a8Nk%Zg(RL`h^V(GW_8_OE z882M1)GcjL?6%!^+6pdsnPO_v<;5CR1r>W{Z)Mke2NVjn$II?%_c!CJl$A{6LF@zl zqGYeBZv(K=sL0pC#kr^~VUg(ASU=ypJgQR6CK!nOXQFTA^S~fZK|zna@euXpIW&w8 z33iIW(?el!`Z`1RtmoxJehU1uI712h$r<>Ao)wa2s(fE1wrAjnR=o4i2Wz zohHu=WU5C_Pe*f*oliv^r(*? zCY{gRznSlAO54q>badK=&c z?2Z5%>!!yc)ds*$JtDoh;FRFsEuoqySA^n<&YfUVl}yWJc#M9A$~aQHkT!rWfLn&j zqFmn3y??di*gwHPr(vEu5)8g3fN_|L#V_fJf!RkrN$tGwXl0`eYhZ%$_0Xpn{gAEM z*QfK;{rAX}!9NtsNQW3HwC7e4J~iF_%}f|=;qpHEppd=fFN=@@^8B5o`C-vBB)z(m z%igEE35J8S!Pp!@hJs=-@N|n`9KRFuc&hD-{+mBQbH6Ppwh^N2+j|m;Q(u&NcbBc8 zsGE(A?M+^k71NwlgtL)j5Rm~xQ${UHC3*IiSNbw=8QKv^K|);5?xoET;)QRkw$)zF z%EnqSt1y?XOFl?2W}#E-l-rwQb`4pel5SFYk9cSLq$z2t;-z}I79y1GMK z*6T^pN*Ym}LR4`EOm=M(LTPd+;c9QT;=5!k^R#{WDr?Y4;fSo@pBKjUClwbx;zy^j#K&2 z&s(sRF`C%fo7u#Ir?%5He^>I4@Cce;nDVfY{!5XL!Pp9PxFDsuAVodVnC{J@O+?Bl zqCMNHx^m|yXzdYHVrAXsld)%#F;29Q$;@AWL_kZ1qV>h~PtON)LRXEUtET4dWrhcp ve0UnRe<2)hzSgFB-~Qj)FlE6-=Ow)%*F<&`>^PkwJ_B^L9zUpn+lBof95e{K literal 12430 zcmb_hg-;wnv%fpEQ2cN$Qe2C>ySux)yBzMtwYa-maVb*VrMSC8arpYZ<(|*^GSiD|o;srj%`W-uwO3E~y_^yCmYD zYq@+XMcC!1cM^43-HGCxdh;%S&UJq%b=UFoENm}&4E`?7tc~znK;Xp&=J=lPiQ#pR zYdCb?Ge^aRw-Xtu7UJ956(@1o1AEj#62R*c5c_`E*aIyv`EYv7SYLICbG=vU5OQ<7 zCGh3ldHe2X!Q&ggyU4Q<&*i0$LeG+Z$B&=rGtV_tm;42VQ;u7td&f_j`<2&vo*Ox} zeHssQ4v8*h>iE=|jxF;2sGor@~bWEVxeW`3dgI(WxL z^6&1fY3Pi@Q_UfV?YloWkh5uXd-Itc8+s-DpZ5J94mO{hA1YRbuin0?SJ75DYlX3t z<`Rer_xl}I^8()=y-PO(%88{@KBrQSXCuk?=$C3z_*Pcm?QuEjZeK58_BG$~1HbJ2 z2^|a(h4n=$55e<|t`8L?;Q5qU#alvvjSFA=Yk<^5zFd6CIIf*N*J_)KJyo+%ANGf& zs8ovoiK%5`RTkp?oO5ZIxfDg3vb9xl2g_(x`GK}|bv44CxeR^lthG&b>x{Vq-@~kp zL)XL1VFX!VpxOM4II+n%?)si^l8fB;+|Gs_Y1-O`6YI*_`hyZ*_YJe9b@xq^DGUYf z-e2XV+5STrSXqJK)N0LU^YyR^nd)`dTl&@w_ZLDBfAjO~1>){^@x^`~o9o}n-)(ZW zcGZ^t3{wYf6;WB3(j5p6{a_IklGq=Ah<8yvNd6{3DyKvmO*b0LBZNy9x*|-+;d^kQ zbMQ{(P;)0Rs`KiR;Uc{rK=e`J4N2>%;3~Y21f` zd~%gC6DT{=r

g!%RZ_!OF>#%MJh6?LES3iCASR5(kY zR9Z@Tr$hucC-3iNMbA!oE?!#Q1)64X(;VWPrn6F)oimLlMdmh|DEVZdhmRfYIa^xl z6(pjSF5SG{AF^!M#8GSRRkhooMo;&bF86f8LubQf-kZ?jig4J=*B3VDX8A)9L45vl zxhE0%#IxNhtJ4R?8S44f3hDxctKDs*UblK3WIy`0h=OX%u?CCuG-g|0Q=_f@spz|8Z8 z+gS47%6CSMLE)~8xI>sFXtSlyvQ_9R*cKelx_x?q48pwX; zk#9BNmnQpPJXOk1x$ARK(d_(A%2lFPPn^B*sm`6%%zY6;b2&lA`7YDVsIV#sOJ5sgVAUqbI;PH3-#2ZAWf~CkYZgiT~X)~!Y@bn z_g8;0@i3jJy6VgYw(SJ{WYFl=gOXvJj$lh!C4s}ytr;hbiJ_1G^Cm#FVongK{NLUyD&qD*7 z_sbn{jjU$ODEjljJgWp;(JK3e z9F}Sp$(D9MmDD)uar?}WA<9B6oJIVzr}ob^3O3w$BIy9sAVuX~oM&l%N&^r=et{iF ziQQa~oOMCz;`oBbG2t`v_O}R{V%6R^h_*1NN!YWv`N~}~Jas-4^FnwgmokiIal{?M zcg^U6ubDB*GiHIk5|Ih*dMG=k8k)>YL|f8HVg(pV?0CG+YyOyRFUrT8G&zdGo!Bex zEO?l(O3^z`eYn#!aOHlyw7+4_jI}AC-m9D}ec;Jh=V^N7M9#J(R&NBoJ2VkSjOfS7 zdK}gAARirmrK8Xl)Aj{D4a=jXKLv0wY4-y2s zeFbSwNi9OTPQjEwmF+*;>!L(?4zM*Q(F*gJh2+1)RJPUK*leaCsM~z4?NK5DTR_&Q zFWLdG5f$e*@@NiGpRTd+nS||jf~))ap`)#p^H$E0<`0VAgdAj<@ZS$y$HP`~+_m zJNp%@jg_FiWX{4-8S;Y8l1((Cpe!qTWaYfH>maH)C`jTyu*;i9hnT|jLo>KxPZ&l^ z?C$zS9+NeB>bWWEOFuS{_x(8<7^;;=VQEx~w5wK~g*4!=ieoATX|dS-72%tGHx=9R z;tQ;Yp{a=x8NAo}WBD3Xd2A|&@u@$0qq-GL{X_i)UQ&-Ofv{p!2}EU90HePgqE3lg zNVV}2wey&rC@M`3fSq@Vo)q;Vo%AdFl*=l~OQ4{`JCG6T^e&>Jm>1gW1B=-*c1I&H6eDd!5$Z3WnBr@(6SV@yY4 zv_8O$i5V6q0Ws?vMo>o2jq$52Trs#hOMCh@Xr<7UCT5O^$9MvAB+d!RS|etZMC>K4 zSbKY$LnGwrt;7_+WYk4PL{?rd+J?Cq?Tk)n8nqDkWz>O&xz+#Fl{!*a*y;z_A%e>c zErV$IEAkYDsbxCh1}1me>PN{7l^g2oEj@O~n5B};fxwUfJt(CJqP>*G07`V^;|Ns5 zI_RhNux~v5fq>V7YZ7@2pYED+sS`#@%}Xt3vWc=_=0H?=*!I z__Ilz+ozC>qKN#41Z^;s>iDkq9khBP_oWSbRX0c&ActTU%&$l+IFi|QBy-cZ^|i)k zHf29ga)UFX4@s08<7p+q!jsk5N_2V~DF|IJ&m7M~JPx9Qb=bNF4iMJJ_ zoh<-%7*t&g`5H1lBm71x%??mmraqQp7FM=QQDpOkVUjFV54K?bCKraPedL7&UZ@SS zQcU;AcS~$y`?4t8i7r3?P4xTd_?r?!-L=@B>}XC7F@q>tHT9B*t`YJl{wAk=3i# z)j;41>b=;FBSQ)UgKz7+BR>Fkg{7*Lo+@a|umbH$W75OXH)!=Fo6qMELl6PVdsM+| zwYYAGAf$;hTE>V7-I0|QwK7;llERdm!P($92_w|*vlZc^*sox9%vPXcL8YP&o}c)- zmB^=U2(?E$g=cag7BF_p8?{sETfu@J?DtY_0J+uF7K_p!9R!$8Q?shL311x0FH}7# z3}tkZXkauC&aeHZ+}K$`i~J^Lh*7@pXJ7=}24-b9H$8M80puZ`@uV`$5~wiM9CTr` z?yrkz^nDfswtgY%4jLg6N3h1V61Z~)-3%|1sKm!Ooe==Ue;_;|iDIb|&me1+t4NLz zCf73!mnUK*OyYEnF(#0=LYAwKB6DU1DF3uFUAS`Q{n59L2S2^-eDdo`k=);D>NnB5$Guyi*y{VcM>9 zA8+DheVh#$*)4B``P_I0cvH(zlP5xv`#mHgj`)KK6eUaeg* zNkKSp24Xu{itvaP6}1+SiLsnb^SM4hc7+)k>hHDkY5pbLvhf0y51)e=|E{H+Pg+3h zW(02)abPzw=rN(^dF<0qT}@UlYw3X)8GqJsPsL+JPkO0iT=Sox_(s>d24xHEKwVSP z-~?=yl+$5X{2@=hhfAD0V&Zj`m=Vujguhabrep=wE-_8`)Umz$4TDl zdk=eDQa5St(gL~*kri%o%6{w&NJ>QCR&Q>kdFIBD$lE%wO4B1bT^za!+(4>-dVPd! zy96i{t|B+G;l^>pw!i-zH^MB8iD;}K6s)888DDRcL2MTa=0Rv=lMWw#iStokOMw+{ z_TXnZ30D`J!>jW7MYyv@%S>|>^kw(8a;;bJ(wZJ^WVJBsbbfYB-|rN;ct~2PCLT~U zGjJ@%N%ceM*5|4%uMnNa2bvdH-GN32qk|7WecK(d!eTm6X`=J78y3G30bQ9uAscHu z_gj#j#lfsMgkkC&n1oI)LCh+|Mq4HF&=3jm12$U2e}jwHmuo#o&xx&?AwJA0jmyjb z=?VuW96b_Iq_x}1b-*Japb+=+^7LfoO_V0rbjfJ%*{{@=TCEgzg@mWIhcr?{?UrHm zW+W5|&j7DX5~uq&eQrLq+N82lN-o7@lnU0)sPGo0rG}`9ZkD*5hnIpm*ffi4eM1(C z18?9Pl=`()M-kWfRM9aolxY|-Es!T3B-ekh~09Z~kU*lzVajsQD*+?A?8jv_AC&WkN~ zMv%UHn9ANl~NI+|+RfsH)DT|-m2m0kgzK$TyvYw)064$2Mzy)3| zzFOuz{c5#M*)gLCxpX1*( z$0%~k&SqHq@0NcCE=tZ$1)Hf1PPbpc0T(1%z*$qS#{`we@ivAY>9%-F+xR&>oV|}^ z8_&Wi@^TLvUa??_u|ssMX~Zr*Hz>lE&m*D8HMdN69N#>4NnGxscq#?(gv23-j{ZH@ zB5P;F;dJNar-Cm%t}EAV@H1 zpkgCNLpV;d6Q9qm1;%umrvc{+mtktVF%iE7eMORriZ0yJcDypFKiS%;stm3qf%k`S za&c3)OGbXkAkZ80a$XE~_{4R10G7(AG8fp8E!*u%l0JR;p@Z393m+%}HaSWKkELS_ zf_;aV_&~N@(T#K;E&<>9*&L_^1xa$$KF@vKAj#QiLK=Gc)v27Qy5q8=l*AzUI^w#r z%P}OD#+CZ}N5&m5vjQEMNAtq4*wp%$$}eWMIqKXo?<$rJ<0>_4>C>FE(1gzJhH=+S z5lLe-8PqR=;z(wyhGg=U@=>9z=ajy$!8HctX|gpeU}w5K1f;}MxR>C~b?wmKpBSkV z0m6*mZ<#-bcm$fSJ9WnqmU@Apa|3C+&TH8r849zfcJ%JhqOF0^r!8MEfoVLwy-8m( z5@}(qoI^oZ{^Fa-P|ps58*H_$i~RD$kk4)@?LcJYzywmW+Lz9b(#F<+six-_pmM)+ zCcbdM=)YPptg(!QDB$D2Bd@D8=^y8lqokG#0Dy?`UjhO$vvB?~;asKU#NhT|z$lF9 zohA**0017Al&FxZ=b!Uz4^LIK=V5^)SsHT4o&1q<3|K6LklKbKBErpW$qiHKSAQB`FamH^GW+IbcjFQ9%kQ;}wVxiNFJ443 z8Eae*Sw0(=T$j`jd{}8?p(G(RMmO-#Xb_SJ0JIP^2|Ou42o(q)1TTmx3J4+rq5faD zUJ)y69pW=Gq|D6^7<&}buP~%q76EdB?}9!gu#Uh(i0&3uJe;G_DbClon%H?&h!f2z zX)3)W93p^kzWoBa33dnK*ozDXP(rGEFP2Nt*4T=n0LbCNkrp>C75RX(IJqMlR-uX z`o0cL0ZalNT%}vUk2sIuWn>a#rsC#SfCf|?h~p^zm1YZ;4BC+GDx)jUM7#b(L_yte zI76x>Sq-8RVi>V40c@>w`!fjF`jZF;x(M!-M6;NfX$_kXhY2jH1~;idv7(#`LJq(d zK5WrQShkpD?0NC=R~v{^9d=}_2-eEoVELW8;{tqKgL&Sahap%_`hbr&R@5VeEh%3w zGr(1k6*9&*WQ~tt!B4G5z6pRHePf!!+WNXhm{%y&x`A@1_4Gy-xDNGCF;06$e1`B5 zr4B7U%T#8?h1G_v1BLzy&L;|P1NK5$XYLOez(~HLZ2n_CS`W+tp@rjvaFe`YJ7)B0 z9aXb~z52F5dM3hz$Qp1X#O|k@ujTfH;c4uPdE71Si9&A|ZCfrL#}(CuOo!QEQ#QmA+{=P@5;JGY)y z7p4?4$|cWJSlt5TcLrEeh)J(TFJ4{+f1yl6@h>PJMXaEv8-y!YJ*&kL1=7)`8_DEB zity(u4Nw5w4KjhA#6i7TPO2MD9qN&_a~EId&5Ojw!#+~XGQ!LNif$ZoZ)-2yo6lFV zd26m_oL_>;gKg)%xvBx41XuCg00HFJk{Z{Asho%DN1%DxLp5zz3DxPB%90JmAwb6R-Gmx{MF&E9EW|(t$oEGS6;-l};sq*I4O3dC zwm_Lx&@H6(RI(M9X?^Tfz9vKe ztbCFYE~Nz?K>6`Xn$TU@`kAQ-^njN0-)%dULbZ;jxpERr+MOmI^*?ttV8~^PdnT!J z_^v_WFtvZs&H_URnsmv3aS)aW)*$pn@T9TTaUwFRMc;8^J9vfnn;+MqbVbwG^~vGC4iSJ%%dV@N z1VT@$C++@$DF>z>Lt-3)_8y8e7SjlJe&HY;D$X}W24!rKN}*=zaxe2gH*^mH?{_)NL4IXy`78@LI~ z<<_Ga-D|_30HUqi;ZHu|?Dt#*z_RJf{u4bfA)V zLt6)0zvca1Ru^ekL0yk6&XIeoZ#ev$$BN&w_&p`}kW0^oabRRC#dB=9aR`95S$iqw za;~h;Ue=bJBZj!YR3>0}8NYqkwY?Lq?}bftnr;MoTK~<9a%*NjY7AyT!fhuv`oerI zR-}vNHS_Ll9hf#JKoqq=%(Cq)G_`T=Dpl$B!(29(`MVL>-|Ii)&8li%cSXb?7A1A! zT4WaB8t4_~u~EWpH{^I~1;@v&z#fC;`nsMqvQ=5znBy%UX9kLMUBDFkobQUPnaS7G zVB_VC!}S>L@L0n@LaVtM-FN)PwZ)=0UOfiyo8Z#QNcys*c2msXXt;mYtW9c!=?p}~ z#?HPCA$J7thRbvwV^U~BMsw_*==7ST0s^XnzoImf;y`b;pe868X;we8x5<}A_e|-Z z-f~0*5h0F$47(#BO1sImsOiWx?uzx109q9$@z6@Fp64(c6mpDCQMinI+wY3xTi#q1 zmsodySUCqG1PK8S*E$5T`dzUEhjMmBMI$K-H5lvc#+>w$Y-d@O$IPDEKn4K&4(7DV zXMty@og*z~UBa1Fm|4r^m2P*#SX9A$|EYoC1lyvfckkJTl2cIH ziRq27CbJmwT{Ir*Q@<0B9Zq0tB>g=7yuFwStN&{;c4fJ85u@Zo$217Wxl#J}gtB>X z6`Qw)4GtJv_2x3J<@$cqZ`|7rl-)7kv;VWIDbXb~6ShynZ=>Vs8TO=RoDhj$2QiEP z+<%7AG31fGX0U?QdB=Slg^`Zk5C7nh?X&@ zC$fSjTvG#5cfE>Xdd9E*P$ju&MR(zIOy5TiXtn0|smSUNYKIf|`WZ@>n@g>ZDX2-- zKeDXy@Hg`Q6xj-oqdlu853QHq1AFq8u6xU8ASzCt;r=q;Z_+aZvyOQJaWVjZ6n z{q5xLcA$Fp8IDx1-2eyOdMpmExm&M=))G2zXwAuISx8EWLIPQDnXknjBM$97FOQMW zJDYgFu%I)F!o|b->4bfrYN>A?%2N3*im_GiN-MWJ8y+P|pCq12;Bc?OsB7xQ_MxUQ zImNo+ludkpP7-+5E7$1*xzr%uAKAX;h!+>v%HTF>(^LaoPphQOHK?t5ReyBUmRP+# zs}~}}F)IU7sJ=k0-&}WWDrtH7%P=1-MK)Pu(57MnmMgQ!k)=5=8yT_e$ORPUOv4bN zkcYpC006YCPtrQKk9z~ns_L%+s0$Gzj$!&MQdP77_|~w5T!XTxzP?#8e66b~LhYU$ z6ap5S+OWaye~9ku@6?;%#nT?`hTNEJuQd)g=uhjbyUsr~e|Ss&)2`FpS~y(1o=!6J zWe$L&{O%22GpbMUV&i_CQ%}a&WDAAnZ0Lbllm>(jT=W~)lk9Hw<~NK^o?(9+PG~IQ zcyZ>B&fEe+D$b^gb3(nB%f17HTHpYW$YwSnB=ZLtg$|Zs zfSW*vqMA_c! z31Q2w+(n22QPZaB#Uv9jAJmPKEVYiSJmnk!g>Djj4O4%!5DjI;aqpb8>s9C0!s zBZZ%Q0!~czDhDw6_n-+Pp}j^p7yLx+bcMN)YAB93m0{-l=I?~(T@CjI1ns^@|OzH1Wkw@u<#fTY8?09Nx_tllGjj5*AUZ+BrED? z%+dJkf~@=QRANi&n3m85(K3O68@+Q!StxtjTQD?Y&V+;?{x;by5!i_0SWIQ@KGyTP z+X)Os!DMiL>Q1tx8O@4m??(J5C*Nv+ZmFX8ckYnWpDvB?cv$n=>#L?L=0H+!srdT% zj?I3pW_{2njIZ-&K<~VeZgyS38-v{-)SUQCp~eerEiD6s$@MD|-IJ|Cmqh{$9#nXf z`AyRdxvRtKmYY~;Fbxjrxk7;E(yHJ>T9#z9d|}Uc+xO0c-SrZI^xZqM~ha zwdmN`1H>gUW?RL2UPeRK0Zu5LjbE|XTn`6d-`{66*^>FXwdj&c^(QDI^&VGqm(o+t z7jeLZ{hv9Cx@I^!&4p2jJRhy)h76L%!tPBMPdT7n#{@1?H`X}Js=n^l{;oswdmO7= z%3v4$^hbyZwg+ahLGJT|z@trVoID|G6Hn{r51ja+%^DSq3qQk{RH)+HZ*mE zgu+eIDImEJW0J`wjwl-JJoh8@@C{4`rg2FieS z`Lssn@OcDgNLy|Oo0h-IpbHA_sCV`Ud>Xcr2t&ec98$>&`@Nn+D{)ziB(h-x0tN8Q zzi@L09UZ}IH-EQy9O*VC6qw*~s4<#ipBf4w6Q}g#Jt&XcCi8A|-5{!6mqdB|ybK=i zbE@Rvoe*DnI#xl4^O&cvme1DNx#>l$)8KwO#h21$uS)ypm$>iQIxM%vQcb7J^M#l9 z#LGgh4IXE{XbhW-(XSDoYKJqMu}6?~qtQPvSiA?gx+eRh(D23_;hfW3H)1j05ig6S z`AA4eIPcoM@zMIk_?xwA4@5@XNqO31J7Ref)B%gX)Z@z2n|>LUpO&~1GKG(ZAwUkV ziE3DFe%V3p7=vpmAmkgnaQ^AY>3Ruz|MVHC4Rk0eq0k7MceiY=`%bPi+R?kI#yk$o zFmp=8wG!IC-&$4{Qy~q9ru64e%o@fa6=SXS9i1#2WgNCQ`Fp4C+o-;#V$|FPDX(xV zb1(nayB#hfXGRa&H|Dq4y7?wk<+(&cDx*0!JvZ%#X8-V5iwnL$HszPyC>txYk8V1> zi*wiZN9$`~T=(r82i!gFOE(=~EOBz4WeI+d0eqf%oFf9^Ata87ik#;(h5@$C__uuA z!Tvx$o$X4|REGJn4$8Kp*TL-Brk{42e($#9&3^y6x5fjLVX{Cc`Y|@B?Arh6hj+Uf zLt6B||7fxJezRlX7J?bgzcTP!9ASDt$Sk_mix=kj-zl7(b}9d`xD)S6n&)q*5q-D_h)g$@zlKZ@$EVTthN&VeSV=B-0$Q*wW*z?ijJwj5*p>; zJljAa(WqXNelF3y^j;XuIQz5xRufcEpsb(q6E^y?YERq{o9QFTrBJ#u! zB=W}-{wev7sNk1IzIS~qE124zw!~N6%T~r>htd7FK~m_y1I8RcW?o91`h?soL&VjBGoyoaczW#K9sfJwCH`mImt@qEhxIiy9S>v~jZVxmTh`yM%j7ERW8dz^ zE0e7wicCDd9p2a7yx_Kpv9*sR-W{QjC-1~o-IL1 zU7mp6bf9(kUr63ORMeF_2XWL%O7`S`6}V$V5YA??dAP~!wkuVSj>Lor!E?s`uDXh)K58hzoH#fYU&K zV;-Tt@Lse4E`ljW-Mr^tdHh95%ttphf_Ng=p^@ewM(fu{?_`_H=YOf7dxD{gG;7f17o0UMk!CyeR)o+I6l&x>Z z--HIjdj}eD<#}*>9SvKT)HtjHtM zey^(`^r(=>RJq$C5)O2g;>p~x2QCLadl_Y!j|3f+4Yc~blzIi|9llM%c3PDPYNN*8 z;a?%;X#WJ-#Zpwk2P*B)?5pr0J3IQz1`Ea`eS_1aY{N-eErcX5_P|pgPq6b35Bvc$ zv*I_RX^^5`;i@R0#Dx<0{O-=whU5|P_5>3w3OJb7vz=nJWB{QGXUvfTTsTQu4zY`( z0pmgivOlH>K&;bFf{23I4mrV{0*362Ftcj`OznaJ82GHQn@|~nnNJXG$e){WXnstc z`B>@Lr8T7SPxFfcn%;naFK>2l7ga(DerD_l>Z-fV{I1ep-Fv_WNPeOHE8ME2i&igQ@_JNlbT$WPzN5~BoE=TIQV?ctjlE$E%WEDl z)#~T@(KX1;_drDsM3uS3H*2%?rN6l7h(`3ga6t{mbVHj=aw65eU%o)L)8t`2bDvA4TUIlykgPh8)=sm2PF!Q>(PUjnpGl1-7X=tleeI4WTe0sfYv znNy(ff40*Gs!^4e_=sS}YrlxF1>7nk7ukn56s#f~<*|SE5m|ZsBG_Nx83{w7sbb=o zpJmOj!Zta2i!=m^2fgA>VJD0Rnl*oRWRM24DQJPf3-{r8$7x4fHBu<3@G*@O;)I{| zPt4GflQEFOE3D{bzZBj+p#uczP*P!V>!!-yrEBt3><<~WX7hV*rEXyt7eGS zm1nU5AF15Y&U59Ba4id*Z9ibOmpuYo`P!ynx9STL&)Q*sEfw++olqOHD%RQG-WpGk zLJTZn*Y^7Ey?P5P9A1b2Vb|P$_B}Z*@nm*aMQt%}F(ktjKU2ZvfW(2AR;&8$B-#ID zY!@s7Kveda8MC&v2$sZk$eefxVT|=cT_%tpJ+(0Uu-)Zsind^DZi=PP0bWX` zqGtu(u!T8AY6w-*4aHSA?1W!fVd_AhW#6X`0hf6p+7B7b)hmxpHOG-NaCLxxpeG+j z`hOC~20TgF_lneOzO7(ICwyoDJgD?RH;wVHq%^Xa`OzO$*aO(Yl7OH*m`Eg=|F3>Z c!Ut5?wEYhAmqo9CO(TGmn7n9>uwl@D0OGY(*#H0l diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png index 8b824e807ae18ad25aa2c9dc18e9faf75cfb77a7..f652f244accfa1435bdb386c608d4300ba34fd06 100644 GIT binary patch literal 50420 zcmeFYbx@qowlzGsJ0ZBcy9^TC9fCUy?(P=co#5{77MuWq;O-FI-64GBcjTT^-+k+? zTlL=m&eTjzKizv)@4b5Ue!6S=30IVtK!V4G2LJ#_Qj(&|008)V5gY&u{r+|1KKld! zkO_FIYB(z!x)Iwu+L>BdgNU6y>_NmJcMDShzZqp zdEOG%=aSZnPpR!@ob6^z`n3(=I&?FRo=a2r28u6S5DufaQO0Y1sx-0rWh zoq3sN8|?^}jxLD!cqQF`sL5CrZn2Fi`k?=4tdrjrJ1$XB{!w$q_wmONJiq9)iwH7r zlzqQPj=Bat%h0ybZX~w4?0E*m+~T(Mi2l4pYh7`+tEGv9&vf>$`=%hbm9tbYH>*_- z7(HBwj|eX#c7ClJufbk(@FWj+{A!hSgA0v9hO$o?sqyg&gnH+}pL|{(a<0;cb7@8_ z!w?-G;0B&uY3hw;Zmk7GH=uPTE(_yfW>nixOkA9FHtYa)e?BGFHbH zY};;|SI!@@n`#<=pKf#2V|CSI=@;MhJ@H$4x6Iq6!Ukffj7w@b^Tb`2V=7K6@mw?P z;3xC!{=An|*D#4M&OFVHitqInZp7=_9sjN%p4|3EZCAb5y~FTZpKU*>edFiq#`Rr8 zrOni&C(g5TUf>bN+R%goUL8{W+J-=f{_CdOr?N+5V%wDydCaz1-)M;y{6Rs)4 z#-!&>(=r9w8o;ZIuCPSy1xn(a{x{|?cRRZ8X9PaVzIE3hH@nR&1ghK`mLGnck!2G= z;8as_`j#vRyZkiiY{@cXP?Tnftf;}2r)vP)VpN@c4hd-0%!VH>7#C@(kq$hHUvIO( zWA7nT#b|6P@Z!Qd508g?I_dJcwDT=K>C@pA7l=)2HIFflYSt;K|9+Cu5fpxc8?anC zG_lOdkqqy$T~#$A_9E?|vq{;=8NQdtTKvg}>3sf2u->xQ9Pc5mvl7WgaPI06l|m|A zuzKOT;19I@F8DLJ&IK=Vo~H%e>Thiu#; z@yh*df_lLae4E_t_hQ=i3zGGD6b{Xf4~}pps{)5vvY=Ed`) z5I<`2tdW#!A!|7y@nl)W0_b{RQAWP9^rt|&ks3T2dE-)!aDe{W7=}TmgX){w?iF0x0~+6 z2W=MmXs1&*H;*sR92ISY9Ub&+%Z**g29eBWJ_D0xEci~eEL;iKu&{dxa4W?xMwl17 za(2uPSOBk+Fq#blhs=(td39f5)U^!F7LnZ*x|%2+cp>?mc*I)@d7Xo9CnJ-jX4@hI@a?Z)k_vT(B)_lvH!|RYE;XsBtKs zx>q&Wn)Z|pTynlo+ay^|@v|LJ)@jOwat{6Gs%&vh$2euWiTcWK1&?Nucc+eidny=* zA}HG2`lCofS$)lLZn8ZP)(`r7zOAG0&?rdIlu$#)0_H`)tSaKm_C1Ot5IsS6MrSVZ zMkHOy8_G78(~XM4jh9^z<{TLyN!iA;RNpl<8}Kw<>E}Z0GcOdW#Hs2Q=pF8~suDQm zALPyHUbsoBHho85Iw8g~m9|e0#9sxCin;98hR)m*K_L# zoijF7%AD~!fUlWYk-Ng{0^S_T?)dAMT?le^N6$zYdGImFD2lWqo4vLRBzdCn_MznUR^vT|LDTa%H zjgbbW$SRh?9G#%BrG+P1X&%h$iG|Y{*sN{DF8$$(~ zZ4q&)5pyA-Y*(neqYL*#sDgWSx$#G{QiNPC{KP8SzgAiyWpQCZ1DEdQ&`gX0u3uD= zvY8!o63N#Ds#1wm&4VK0wzg-3MP3{mXppGv5w9m1hF(QMMmAx)U;G&KWd+q=6wm*3 zhMC9;f(u4r@lb0cfG&kKKt;QeaV^Ml`A6Qv#o|jxAfalFWC$bu0yqVV4R%(ql?SPH zY1{^xh57i=B?3hQD(pu-U}+1B!Cay{a<|pT54}3n({-4Xq-~_T-TRwu@}q&yPnf5a z(c%o4q`Rmjn64pZrI@*oAE*Q2Qv?YUL-R~8gwH8MT{xXJE$nQi_F4$pAQF+&2^`sj zhe#wTZu%TW&9IAPCtpTh0JH_oh+$WEv#D>}Y`YGz+K~!0a8!Je5?Hp1$Z(PT@$J9x zHXkcC%Y%q{1UDcA=`p^bgo7m&CcY%(d5j9vkW<%igz+<>tj*92$WB6%X%vVU3K$gG zmPY&KKig(Aw{OQt4d$XzFMsMEU{|UWld}r|IMdGlv@4_>F4@bIkU|3 zOf@r&HJL^Dv8(fKFdkO^mk6?D9z~e|&RKG7INjRgny7J(O=Ekma&n=|OH4hTNgx3l zS}TGu&Sj{9P88{lHP1vrg zEt+GxH02PX$rNnfu~=B`40@WtXI$QGoJj)3dsQ>La&*=vT5SZj$2L+KTy*pGX6Tim zdLSml7{yHxr#W;G94PAem)oItInb`dTT0Dv7cQ3^9d=iUSSUuQ$bh)8C%LG=;YSFV z4``mP4bqp*^oR2MjF~BdBg9?}-W&`_#I&89N}o&gEGat$boS6vTgFn03kKLBmCeEtaRkOJ3stP5ZzjdNzyqNzs1&pq?)~$D zsq#Tx zEsO5V@mbOOWxhEF&&HO0-i`8sF};Fu?>x##hvvGt7!A&m#fMxVb{~2+EYU(mpW3ZQ z+o^;*Ye>2$j252#Mk!J%t;d+1+de*TM;HOmF31aUX{Q@n zIrWWmt#CG1BT!WPzQmQaJmLz{`O2JLY9?ikf^ZWpdN3}0VvoN5t{B}ay}%LnFlHJL z=5zR)4Z8^8g-@pjFFPJ8dWk6)eVpV|yOh9eYz$oRZvPB3!*B5NUM%9YAIvDg4qsJ6 z?GN^rrs%N}b($Sk$Y=+G^)QwCHb7G*7glE{iZjI z%3N4Gh&ccaLBx!_XJc&UmWq}aD;zqBOOQ&iLfNOa^<|kdH)GFLkwSgwTPFK{v5{a1 z0KcESlwAv30PnY)q@qv~SN1FdopO<{y%gxcb|H`O!cY4{LjVp9qa+SP=$L=^47~|8 z!^s-k%i=@y${ca@P~i^yooE^U5CMQgYj~VTY8&m9bzob4;l(=p3mJ*P_30 zGCGhHDuJs`T!RB%M%hjnOCk{imBfez>J|~^$s628sJT%R+y10|fV3*fR! z@(Uy?)xB~sHkp7ye`J(&8^BO%t&T^}&64W?cIX$S{#pgdd=rMk)^MogH5%|9q+hY2 zR9(}JtA!d=0=1;Tue3km$q7dj>kuj?@xq4fx94TSzZ=Am3ixiSZhgMN=5w`2!=ci@ z@JEseG@(Qx-Jf;K;5+9CC!{Qk;EM73rUdxQdJIX`Rg+{#FK#N2aj^+I<11lj)Oz#a{ zTRy-bt-4IeFB1Cew>>G2TdakYakvQ7t=HYqdtT`UGDUBZx)KO7r^HkXQd8yBMGMnr zuhj3M3a;*WGza$aYc8N>H$9lDQ@@D(sy+749Xjvoyu&-a6v!szhf9L7IaGq}(};_X z9V1$j7^BS)HS}Xk8x0;;y!^UXYVdWhLfvY63Oo(m#c7i85}sPfGWX30V$aRZ@&UgE zh~2;%w2*LxDed;v5|vvxjxQmM5a(IA%p)#0>Q^n~L7p+5je@`>p&3_npKx4t4Nizd z(u;3D6;K#8b0UbXB8oFuWB0cCH{9}_QxRiWXGlo>tV$wWlaD2%g1~l_BuN>KogKIp zhVXPF3P-N4ugy0lwI1&|0Yr+&*$P2Y^_b+Jv5bIrJNB&NVw{?(nVhssTm>NM1`=?&+a>rz(;jek8P0 zMa!H}KmPA_VsacDcxh+?U()FVL*sBX!hfsU)Z{7oo}}k*4SeH-*P$J*@cr2CXYTy; zVFqRpK}6hU8;Z}ig6z{~Y)X&tK)Sr0*s0VNcWz~3#FR^3DV~%qB)vue-Fi`>oIg!>_Tvdwd-yJrtqusI{%QMxJbX1G41!e6VQQ3)gd;DsVHTMhk8P$|idDI_do z;Yc?r$Z1?QEou-)N*(fiSSYa&18$nZTd?x2|1R7QQ`ALpPN?>h)5#NRUa`zqM)%6u z(2^b~oswQS`cWxSVP^stxjSk)N(4ql@xl_XQ1t`{4DqLiMFQ_?%c>xBH&X)i=QfoOdGVoUL5auXdT#ST&&3tW0?!6clRH!yCO zx-3Daoxy5|&Eq&Ml#A7pJ;J5ehf2W;Sv2}8$eyy z)Mcn%Qh7;_@iUg)??bSwR^j{%4N{?$ISV|g2UhOqQD{ivn6Q2ETA?2p0?xL$I5Taq z@pBvw*T|N-%+Y%{w|yx5=E8DFirvWw@LgoiyADNp{ldzE;kXpUH+hGX2uGGcg08Uh z6K#>s%zN-7J`)=V3@p}hi2hyQzK_qVr0{{VvK^v9pL(*h1Al(}EX1Hfk7lFY!+Zl{ zyQw8v?;&cDo&QsbNy2HiqG~L`MV}vM&J_ZPCq$sJ$?5rHVf|tU(!+|JtdV|I2l@!+rksPIb<88C zW=jM1T8VBocSK07hDydrqy0B#NFA<#ombWKddM?o-SfOs`~<_ad{$WCK4aP!KE;H| z^tiJe*hrG2v&f%RbkdVBk{zZ2?Fz1T!r

GhO6GnYqKX75Sj;@GA%l6P5T7R^A3tkQiC8Rc7s{W=jvWb@NpPtJ$D1dPHGqd{@q z{4MVM!Ss16RXPLo@dYBme+Ue#m!J6=3Gli=iOEHo&AHBiQ7M1wN)WX*aRQ~NmYhCL zM_vYL4fiphs>a{O3d6<{GW0s7>>lWlK$=_D&@SecXD;&jG5^D)bncF?R}&v^v>3;LO^d-zSlP^>z9d3%hxSz4 zzzrQXCVNFtL^kLQ!=dm{qrV1h-M|r1@%v=Nm$uj@@oQz*pee;XAXUhu2GY+C{sPL* z!CNtpIR<}^hh9c;DC;m94h=mw(Bla5%HV>cB! z#gXYLM@7sg5up2rUA_+E!pEoQHGF&7J zVv$HIqnb7vIWhWOj4gZiFd2Wj8I}b8dc}SJh~=?k_FqMF?0L54-GZ+**EnlaXWC+Y zP6xRHcMuyj+q{ZMR^=*Vxa^m*#XK~O*u8!IzTX3ipKiaczRnO6M2VeCuTkurhsaQW zEB<1Swp(^tCYGhK_n4CgxOuU0VOm2*T$s*!e~vytB5&*JO&I@D;fy;Nt7hi2Bhoi= z%#4bx`n-L9Nxp+D^WNcz24DihKgjpmox5xLE#Ys%FwP5j12 zz&3@ICBbMWJ4J%k$W%zzUtJlk)iU*CfVO1CBU4RGuhAfw9Xr=TqG zX^m2#-46JI`M`=p40emsoM#AeHw)a{J{;8?V54vPf;h-9GaGYc6m;q#<|SngR$bok zzfdk}H>4xqAKTBSKrOeV;tE}XL8*U3@C=O%q)M8YrYS4&y2`nYc@CZ0o`Z{K)LL6N z*KSu=6AZ=C?6vUB2sn%jfLL*H43QWb51^A6@-f^;y4Od9Z4FsDXMwI+%Q<3DIf4aG zd=wl+Gmjq4j2RI~Q_1x64?J$Gf*5!>WX zygAEiv@W3t2N3GJkJo7aNfxIK2x51xUAZOkgR`A|P5WW}918JNyWEE%9+fAb=$^$b zCX$`RaXrK6DZ%qCPj+6>vQyV&43(wb+}o(Xp$?x=2s{H{nAhPLaDdpm_CYC3F@Mlp zQM*NyC7*ZQ!Wbw=$;r9s;QFJK!tR$${8~>)?pz$1YT=hiP=B10PSF8ov1Rtt&SrK!lE0~^JKBOpL zgeR2}$T4Ax(_OkMTF6jOL_|m&FK|Aae53olmjy476rLS(WP(F&UDHJ?BO&Nv%_BHZ zpRLCiCz6BV!Q{aNR*>bBE@@XZb%*OxUZuHOiR^RiuW{-b3WL9qIKbjuX{Nb$$Pkde zJ;e$PmiXbj0x|mb8~L$CS1I$$I;2n_M~ZOMPsY~ytt~fPms|s9Z@j_DI?W^^T>J*9 zrK8Oe@Vv>m5j2Kcm74n5&b!EBm^)(ij$C^D5-E!2PloNz50?F} zGQmhdPmpO#QfG{Eb2PWzvViy&ZV&u#Fd{j3&^pQS=Ce! z=u_nydhi|xqVWh67Uo#jlk!bU_exT500#ca-ZL<)(uV7cTd zY4|O1?cKsnuFUij-Kr-Cg6hAN(E64?r8UZjSpe@Jh&!&Q)z-<-d_6`)rTIA!3eF#Z|ts^;>9Z zJ0is>8BE28%+Jac(QD3}>7nx$J_M(N(U1hLb9aNPCNLbz!dTWABh8G zy?o=B+BZ?5(O`6r>9ffg-6vuU9leivcL`BMH5iEZPTU#!F4l%7ZIf<{Y8@@IDo_80t5inyb zw*#E43<|fQC$|N1-29`<@?prp>CivjA97U8thgqOe|s7!=l#Hwq)3~YeKZ>a%{sJ> zMMM)A2L`gK@0Y8V-D>+PLe+`am5qE+!;njtfm)^N7tcD+L%EYIpIQ)NIf;Xhv@#^cvtusoZGZme-VKaNgPmPuP@}`Op`)OrSky*_v*GD}&8&JzC3&H*UgVun2qTNcm*=) z*TBb0jQ;QIG;QS=G6CtdZRS*mf=e`qc)-()t0yt+HM`Tvx5$Y%g^$uelvw@;eP^NR zRs<~lB>(^{$U;O!QA$MQA17(v&(CD~#Pdt`3*vv*S82-s48t47ZBoj(EGC9;^&vtr zZx+4^_*Lf#N)9&x^P@y>)8^)o(OmC$O?4P`A=DRugR8x=b3FQ6DyG(eyZvgc+dZM% zIUrBZw0sE-N_C7?`SR=6AJq{7>NojB5ZDr_1F}AhX|g>|-fR6s3mvc%R|5C@2P%$~ z5ZkNfx`Qnyv@Whxo5b}A6`{5hUf*w*Uf6LgmSIW0P(=P?le}3xE6ukvia4=dL#JSmxoTkAA)`6yz zm(|(!4Va{gdN|(ZCMq_a;$8btN5_iS^Sx|CH<7oVV|hP0vMT_9EN$_AvPnZu7HDi| z!)Rz?X9Qw&x3Pae`2+y)eR8)qG`0dc6B~idENuBn&pWzFi7ia{N!2;znC0w6K;{;b zo{k_DPkB{iPb*^{6Vgut@O4(4C+3FI?bz`A;&Q|=S z8gh!nB6f}-Vs=J$MrH;vcMDfmQUQ2kK1UN%pt7j=-yz-a(z-{bTtzduI#N|03((eEZY#S2+Lb$a{DH#Q!(zf5iTm@w=6r98lEG*yT^B zQlk8%fBXfS*cn@x0RJlTaI!M9uyM07FtdUT8Q8fw3>gf$nT;4&3{8yKdDxj*SlCVf z1xm`+$=T4>81x6~9h}kP9mmuVWXx^K#mT?|0)ZIVO<1@Xc#POh7+5%1xj0!t93W#Z zj(>qraI|<=rJ?n|di4j&Y;*qD=xn}w5so88!if!)xQi^0&?h?#+zhl`z!1;oR` zZOr@^)Sqzx3MoqQld>}YOO2wnp|h!-qYXc)oS`wXg35o{s#@59RGba}u*Sm0#=^nF z!_3UX&dJTj!u?-HY9L3acg6hyWnpGyWB;qg#26_44r%zV919ymGZ2%#t=V4o8??tMO)7&;q@8ajjCdt+v0 z2ePmLS=d!sIDzbJKvpg~W)>jxzu4QESeSbJ->m=49%8<~jk%+>(J zh^b{QY+c-G{@vQm9Q4jJ9W(16c9{P65h2g&x!1H$n{5?iKraxKmA4AB;^#2>=zZv{X z+I;8cZ*}kK_&vKb{WHD)owPrR^nda7cX9nM_V5nf5brF zRCEGk7>d?$sZT}H^t4JKF}Xj3EL8oko&KX_<*Ml!Qoxg=;y!pkz$eI8F2WHG0oL`7 z4hVgxcW5Jo8bi9G0g357$O-gs>pAtG8tooOe!+4ltaVwOi{)6 zW($xBup78kPwz3>oXGKrM>L35F4xKqBZh?-;EaJDbbaAKT!r_p%dw?`^hPoV$4yqI z+kp!Q92^3=!KtCo@Blt{*tPZB0^mK@(q*-L6hf#^lazXpJ80p~*&CN;xk_NG2-eEd zKBNkAkb>$kkGnnOo1DZpfORA62eQr6Fyu;A=}tNYdDI}i=3o*3NPiO1zLOU4M~NS& zH88eH(p3pmG~mQ3y#jcx3B&E5FO{5+c>^d!=U078l9m}2R8WI#mwVYaxkOX=5&TsG zZVGOWlfb!XisnZWKY*c*J$~xzUTBO_Q-k0Tho%~E=dkOGjoE)>A^_(n`$mAbKN10)x1DK0({M64@IYhKMlk>uHAgIm8xIsf!g zG(WpdaEtgBrNbW~S($hDFcR=^BEt;jo~oy}F#Xi+21h$*w{`|o9!`(8io^#4X)+o` zOrAN$2H>)%yx<~z6y*`UJ07}s)5eV!KlWQ0daF%*x*(LM5gTwWk9m8$IoB=D-}XwS z@?^lGU*lFU9Hr6%KZ9A5ogX24ASSjfy-O9@jH&coPmn| ztfUZ2itr$6#QvUxhZgP`d6Utfss7OfXn`WinUbi`O`T5YaHl8`>y!!Kb8t&GfqV;g`md#7=r>9R!nkWRF3UU;OpHflnDVHt^c`ge(hR3BS zsTM?BV?w`ub9TIDm6UQZY`|Hx=Cp2ROvYYB$t+Gku!oZ?IE}{_hzds;x%l2Rv0Zrea z2@(s#US%DZuz482(9A?)d&!cBz_Uu-wn|AyJX(DQ?9&#xh5;m3KN=%nXwlOy*3+ls zu9Q|-84lfjNvd9(C~j(c(CY78PLviBQexev+Gq7P;S@n=7#@?MnoEot-5uC{AE4#! zISRaRYp>OsBwCCa8d!-YA3AVxh&P1H^_2ybP`Q^bVgY^sloVHF!qO6N1th!$b>*(k zV8zL!f^)bb{M25}{aCY+Mx~v-Vqb{MAW50iKFMfjK`tJzh_d@i3%P zG)n|;&~UHNqFFqMDDis-QOf8licdUdyr`5r<0TN$(Q!sfM&{5)sXky>%Mil8TbY)6 z?=vcNrJNJ`(ji-n9{p$AbS$J>hS^zmC6C$weh`PXhW7U}28P@o+gR?`fWCq4hPJyw zO<8VN4f?Ttx}tFmus98-kun0dRe1?XuZ|q3^UCc2lFF_}q%4>sPpU zNptls#zG{N~ zdaYvQ3YE8EvxE|m*3;%+QL=ylEpoDZEolNI|1-%=_SeV1v%{wnKNXe2>GS)|%>mizl;S&#&~g^m3lf zUw)wx!bi@e@il!lra~rT`qa?kE5{MP8_-Qtw2sNfXE4+b{OM(D{{Ui=+X*|Gh;?OH z5rLU!`z&oyb?ef@6|Tx~dKtO4xHxiG^-QCy-MfB8d3R=;S48qqgZNTVlSAxQ8ky03 z4J8ls@NV3@?IDmZqe8c!U&6Q{0Om-Vo}gJ+UDtUpxi;IFbC+j+@@MTarCv_&x7OVl zzKf2o)c-8@dy*k4Cut&_ZHU?19ZZVROhMQkN;HA0#4U(02S%%~ebe6_^$r<^2>Ogl zdHnMb_3v9l>w%occ@&qIZvAYBZiM!krukUZrTNsgFIH&C8MFA7CS&ES^Hu9Z0i9K| zL{}Kd7D+~eRmW~i$h4(Ao7;&WKvCG5g1$l}xHK7({Mg|D#H)|PQYH+~bXiT>9i|_z z2{p#o>zlzE&NlEAcz=N6($5TD2JpM=mo+Hm3FRH@5xgdj5Z%Fu?BQw&N>g!+yLo;) zZz^xLh+6X94EowJ#rG+dZ!fRx;M_-UPk>QE)J%R6@sOP%#fGi(^J5rIwKd+o@@Zs; zw73tMnm0J+vTs8=YkfwP&&1(?SA_Exp}B1q1wb>kNI3njsyek^Z;j<&(XQi#ju)Rs zz0id81{0{RG8v1ogqN9joQK-x@f|duo>7sMK2lR9F4~?T&4uDtu+ptbTBa-RP{{lO z76lnY;+PT~z0PoTb$A^-+^p#_==1Cy>he=@#vkjP3;h4)BIC}-duJ}2uwKu|MK;)v` z8pXwXxj9sCS|i#}E8NF@EV3Ca34QsQXY(HvMej)bAb&1s`KilqM2xy;i|q)OB>euW zHanRWgp#o3tIu=D35(%If8;H|*7u(eo);`FY^m{fLGDl6w;Mk4m3m^v#TL9rezS zR5Tymxbu0|zCq7@Mmk}vFnXNMs?<=HqBgkI25SiL$C>{Lw4&yEo6y|Z5tYknWmdWZ zpxeL^TWXZlHd7Kt!+_}~;W z|0M5#&hLs|0)c`|ypIwOyhgFC>PDN`1ys2}$BPbJzTC$IzfE5CSV_!S*l8VulddFo zFRod=&q1S=d7nzHw$Rv*L<8p>b-b#=yncWTxjJ7E5|VmLLUJ+|LwOqWP{I3(3o$@2 z4C48ybbY@+*+}N#qd1LyJCY^c>$jT$PBlMoe+w!s$CoIy_get=*^ccli4WTFv{ouu z3vJPhM7Uu#vX0C`Or))%v=1-xJu9Aj*l@Z%N;L+~x#{9S?fY6r+zy}F1$YrO|3ufB zhF{v$l{1Xij4KHtPyX~sptlKLFsletcnU=2B&JR z>t~)EdR{>lAJ`6E3KPuh3uvU}bm64)YSYDwZ{}3c*)>giRY~Ov{B)>H(AJq}A&&Y1#km9`Q_8s3kT^JC|B_pX4S{K&kWf>4h zS!iL01Q@E2Q}O;7mjVq>%0@&u{#mD#d3_*PhtOA*{(QUH@jGnzsRi29rS3ddlx7kZ zAJD~#irxuqrs$U3p*~~8Bk2A#oFezZ`({g?v$A?`5{!r%dSZTeJ@Yj!y@83a7l|t; zd?;~MbH;R2wP;rSJzo+!hM}|GK7w0os3c!()su-q0`AT%3oP|fA@Luy4ADX_dI+y=u%y5J9{95h9x5xdNzNzrg<}(B1_|M`d=i2`ziu*03rvydiP|^Mcds% zq)?SuU>mVfcA&dUceXH-`y4?o)|Y<6M4x9%jc?`{6`$58@?Hgg+W(?>aDnPq{Bx^1 zc(QLSKI|mtrT}O5H+9~};F&vvJ(@H+PU&2A7|mdPjzv?ikIqLc%k=4WHyy`S(9&{r zYeo+72laIJNW3~M=gtE=`!92Z$s6$}QoG|Nm9h}R@x_)`(@+oWe`AEtY7EEz*6pdM zr$w2sbDyG|uUu7FBOzK(u}^LB!Qm9MY|r|6LN5Q6BWh+}wlJ5NZoV`Yx*1?5iUCG2 zZqFG{fON*q{hLfaH++`;f#3pmGxEM}LCfl6JT6ih1x+P1&N^Gx^N7 zx*j89t`to_r^M9HIVU^O>YU1&TNIcl1pAwktiufUPLJxv_R~^Bn^H0{pCOaLOX!Ny zV=rDK0=5XwO)WnmZ3_-Pbbz^w(R_%RahC!2T}0ceb^HEkB{Q>rW;S#O@$GI}tJ$VB zmfN@N`y!~zWnXrj&q|t|gct9@zE-_MHjKhQhCD+)u~?WS@oJ6ZOZ$U@_Y>C3Ddn;H zY%S=ObiCUd(JY%I?IrMMb*A8h#lt-zac+|@$|AY~Np90P^lx~bIR&GKnIlhxrg?P; zm(U*?Zm4CyvF0ANQ;N&$W4QG7M|qh}k8Vks^8Cei_1>R`Z3*PpuiUDyFiptH!m>G( zK;yALcHP_$eSB<;IZvG%`gMHmoj&zsJ+!z8mvmQrd0s2DafrRGt3nlk`kv9hb=yOn zf7245w4;aiT5f0Obd8M5jU@=q zh(w4N+b_+jq_Mh?Yibg{ujsKTyTSO-Vwb+o15sLIB~9N14WZ%(8nbfRCU`{WUwQez z53n=Pj#PO3iZ4p4E{%K!){Yf(AKv77?>?ze(~d;%l4S(cP#1A}?{#D$eiJR6UK1=J zXszeE3v|nzu*ZtW{oQ>wV#XjdGrKqvGVQ46=}PeI))MY;2o=M_n5h5q1fRgW@xuhX z0NSg}ssk}p@?dr!L9weqEYpvv)~_YC`}NFNKCK~^XJ6LnX0gwlX=Up@g=-ydcs^?G zqw`SjFN4k7umwmo&2!NeHM^Z?c|>5xc&v3XI9lH}PY()6fwRetjiAuhEeZUh(|q$%XcS77y925Q)t{g|T1LM2U@v0WaptDoFlgbgOpT8R+RaLLta{Q91u4UNlR` ze^Q+hZhd(?jr8avdO1RG^4Q4b{{nxp6~s62)GBvsrz=7u9Aq4*?PS( z(a}Cf#6N3Icc;IYm{x7o(7WS}CTq(x8Q=zQK^2Ax$`1wCXkOH7X0kx$(v-AX0D_=&-7S-nmRZuEH z&ndL>pRB-DN}~G?qn3n#yrwbHT2`UTM|p(YZ>prA+?s=<#nKup!?)jB<#c#uRTf1> z-zBuI;tZIhK$b~)_EfwV?g^3;_K6v(;)aHtV&8cR&765^Y&hf5ySMW@bDB8hzWpAt z`IIHVZJT&k!dGany9D6kI%t{yaInnO=5 zO&`hFv>QrocYay~n-%x#PpS7?kTXav1#Y#XXheqDxX33qrf;@PLfvrX#^S}lb&Ze-q{3#kwtv_wvQCR!h9M3TsPrUlOR$Ap=`+RHCnEAfxpk=;#)=p~17rGl*a2qn+t#IRt*G`S%rkaU8S~J2&wT{ow93lc8ctS8Ge7TL~D=I&UtjaopY? z4U_}uw6Wn?D<~IJFka?X$oE$6i>B(<8@=+ z_=&$pd?`_>-9K}1kk$4MR2jY0|tEl)QNPe*#Hm;Ry$aK`j_)M^BCFzBWS zyZIhf*Ewc#du3Tp_i5(@!IH;#+@a8OwGQ_e;%2PYaa#{l<2)X31>qvgwO_O(mg{Wl z(m6`K z(eeC=;r?DRSRH32gGRjp*GzHjNgcr4_O9Ofl5*YC1%V?u{>U}wbRs*7zsm6KjI;dC zW2v(sz-d2bKWPa=O3FpzxfP;S#!44~@77~$Noiu!l62_4c+fPphh0uUvo6}NOI3QQ z*|bkchW7C`7{JW&e9z%R59WOI(p}xuwQtVTAPU*D70w8djr{VV{(CVK!Qk&Tn{O8; zG#_?dbi9`kBOg*P@gUmr!%xxZztv2#EQ z-5c?{AXUkcs4-OKTR#g|KR?0!wYwvet3n_YKZ2$%k<5Ho7`k?U5R>HkO5Rfk2*lJw_ zY3Xj35KyGMLkWomq&ua%mS*YhZoYYc*Y*9qGtbOC=RD_}`#!N_*Ou8O29lB?DXyHU zGM0sm>8Q%@;w4>Th1LI4rAOpK1Z^RmF2b2>P+xW4}g20uS@JMx~iG$!|k z!9HhJ;t?<6>fTS^jlDAZK64k|%|Jjf^Z0az?R8eHJgpsZaf03T?q?mpagC)s#yQ)E zH-fnHM)_OphXU^^y_LlvPfxE~+W#bmg~7^2f10l<)4Y2gbH$EZc+S#r{qtid=lQz@ z`VHRc)rdD2Le2o3N^j@^UGdDPnZMEckE6xT^?rG>UUd6AZv1BcS~YbeNiO33$qPq^ z^Yi0HTkofN3jY9ja)I~r-w`&$Dw2v>q9NI5w8&XK>{lG`H2yFb%9X&W%+2;FDW4AB z61EwsFi|H&KX>u)9T`EoZX~2X8e`A6qo|9p)sN@TaO9zROj?FSvyu zm?O&eZ2DC{Ucb~NV&+WC|1t)frIS|Z{Nk*_#8KAIpt8ptj>~07MPaKcP-k&r`pJzN z6|NC?lsX1(N|Ky`8hD*)MP~`Nunq*p*g)G05`9P-68lNcvqpQx_3DytR#ucd;NVX% zfDCIn_hWAK-cw2wWULYkOyFVx^D(?y$ z&A=$}Q^iM?P6&uHUGC>f6XFl)3vKu+3=PjN6JDCDhm)3YkzldH)BK`hV`pZ+&1U~v zNS91eM9p-^FQE)=Q{|S|W&Py5;d%P@_b;L20Mc`Qt$0%ZKtR@amXUIb*Pl;eb^Udv z0q^@iJuK+nj>?2#jnHtmUzD^%*^lg&U7R`y!_`^d4$Rlrhdn+j9Y&8zE0~$FI=0ni z7dDx+!<$W+0DHdKU7+9iSfXpltUbfrJcM+#Tvo%1v_T&8uCKhQlD_y;k!aXzDI{r3^sXtT+4&ZzjitFN;0&eug7g%v;85Rm2E z=ny6PBz74Qa{2IMlYhrilpgvx87u{-Qyi&$7wVL2jZ2F!EvTYsIsm`LX}>vepENyF=d`V> z`9&3TQB+hydSBGjFZZNK?{GBx!Tf#g0nfk+2_!uArR;kQUfFIf=?6 z{O}^)Z1J<6cdr3AUsxD!baNKII9JtH&N?LK9Amvm1J8@z z(u2>nHTQqqAGK4@3)9CvXOr)sH^~8K>GBETs!C&W&WK^Kh9sI$xwYSa@{5X`M!ox} zrtfdjJsjXKokN^u0sei-O(LXU5xXBAeRZ1@6+Z#Np)+#F4*He;Ta$6eR`11_Q5IH` zWY46bt$xHSWBLu6gHSXY-TorT^A1N!0<)$p-?vNEfTcCI^P5l8Y`zas?ibqZV4O?e z5uL|?3r%bvtxbQXSK(NZkElIP+EEau*y}y~?S|KcKRq4wQ^nNPJ5s7yl z&AFFScOK260CS=!1a35mxavfFTs{BWho5u$B!W&lTejeSQUQ5>&QfrPin_%dwH2Jz z{shIMg2s=JQs^Y?tBbL-{STV%gc?b1Sfo{3^$95sRy>-c;(mv3`ae7nDNVqq?#J^A zVRw$utk%Yw`)`B(_SiN2p$qDCZ@g;k{=XK00&}dr@m;?@=TUBL&sa}NmV~;qLrnSX z!oJz^zS#TO@Fp!?jun+^pY|9lN_`-qFq6sUiXR#}KWB&H?mkjxtUo$v;VYzeEXHyU z20(Cyc>Ry z{~bs3K3!PMZh%)^L1PyE#n4~O4)@F61(4!OvB}IiQiB1%T7=qoK9o->cUiP#r5O0h zv8^E_Y3Q2A&y7JctZ)KGXmT^^Q*!NlR`>0-((Vgbt1&E05BzX5!xPX|$uZ*6Y6PZY zTj^&VbI2|C>99)8u->1n&KmgLYuS2r<(PH-(dffAg-X%WgI+?p)<1zif?=GA8Vw0q z)opjnylt1uWX6xuOx96FF+AsNKS19>-&;=8Jsc}5TtQs}l#44Z!AeB`dhQ?rVKyKk zm7=W2$4K;x3*OP3^UYo3`nsB&=kcc#vHE~d?j?1XIFRN&(m!JZ7`Dly@R%{8^?h4a z0O7nd6w)IOG@<@ip7q+MghH_TWeg8Z4~d4kTmbb}2`kaJcxg7;v{Xu;bZ8nW;NVk- zRaJdU*cqO}mkU`^J-tQU^A(ir9G!@IKGE3r>^e5X?ExpP=d(4|_LP13>>~x2$BDHX zV}<<9zY{5ngbJptETdVT4~9i!&k(RbggFk;_>2ftC=YsCbv^k2q;BAoy z4)=(^`v^Vck*)!CQ-s~ku@QDoy?zl|XbM)=uE;Lfc|t(U&2+hmRhfmj>nXmKB2 zm(k`6%pI0o=UKI1_WfF4dS5X8^*IYKaoYH%0KfdKk5HA3G;YDC^4G|e0HQh`mhO)> zbTwp<8&hXnwQAlY&CO&Y{%qQl=p}yO(3aY&*ZL%@6~D2^%&@T|yv}hGlc?=IKl`kB z=kfbOx0YNaly0@e@-ljIquwiloUS?N9&W48;avr;yynzlAaoO4DyL(nKcN@Gh$tyuH z=ak)Kf`M5%rx#8{(C(BJ1@!D`_t+KUP`~Jbl`pOq;kJ6x7s<)|;&p=^Q13R-8>K4N zQo=*bHD+Cc-Gq{eTFDHc=$#+3;+v%cqhGrIcK?n@?AK5Y97H=&*QNTfJLQR=!zm?b zN9jz2Bw=go8-8I7rp|&0<`1!G-x_&)4S0_fW%D<_X}}}q#(7xciX8u&5W$+rQhTKi z3RII*;2zQi1&$FNSGy0?V90*84BOec)ZYsF6A7YB^}pX7dcGY`f&OfNe-;-TQx#;z zg|83eKb@u>ny)GrMd_)qafrvG3MIDsUU*z1<*J_%b3>ST{@8+v#FNH7H+r1mka6A5 z?{9TGJ;h**BFd$f1$jNtEwc$>73#U(X~XqS4+LkPZWtg3hN*fu z=+}Fcu$1wUIS)-S=Cd7qQZ-H&K8XA!f?0r)`1%uCoul1mV|#`9(00Th?1dd*j_*5} zZfi~1%%=zc4}>GeV~VPd9pFD~TT9HcRh1)@Z&t!{6M5Ld>PDYCG4| zmYtGq+;eUJ@GC;>!*^u^$~HSkCVb1Y2Qsgxf{_|N>^~&=Hr1uJqpjxCGvKy^$wspn zW;T$D&_Z%^BAen*5b>}F{mh07mAe21_qS??r{lYu)~Zuj27}T-8#C=t!NfO|uBS9p z<-I{tBZvh%!Pbnd_M@q$OfC-I2gq%uq719}LN6Joxz+ze|7l!#RGY0de+sQ|L zhkKa)4!j7zI=XkVTaS110WAK~81zNwErPnfzg$G$T9*GpzUO!RJ)wO!5Z9DsoN3#zvBfSI;yW0uh0?Kd77rXvybujQ}GHURSKmx&w<^0-vRR>DGNfrG|9 zv4n3jJE~AsHATz#>Fdb7yT#*0fd&g}3(~LTAlSFCDEZch*I*&fgUYo#aP53#F~(y- z4=ovS=&Oe) z(IHMHoALAjV;EO=`m2hg?bc8->#vVv^BJ=TR`cu2JgA|me@f9Ry(YBxzh+dj{am(4 zYPr&52I8=*=8Jj}t??|oQgYsRPo6j6``QUw87%KkOWs0?kiO`9E*Co>Z5+IZ81E%(FPE-spB{`clf$8>u7 z3%c*y6C<+N|9gi@wvi@+nU?hG__XTYDj;um?AYV)Z9qH^$_9)4e_b_niGsG5mmb^RVwiy#N?EQGpyR0J3(kS-w z&xhZh&FZC%bi2JeIVF2Viit_L{o^6BAE9G=&Cljzuas9WOT0KLF_%&4vCm7K5Z9S| zt(XJvf@x?7bgY(Pqq$vlZ}WGtOTH8+`YRC%RydRsgqq?7JBvPM}opJ+ACOWV%hpvT5=V@#Uq297#BB;C{*;-v=aRp ztd@eb$`U>THC>N4wKb-u)pl!gLWYMN=DBbUfAa`N=x|8ewOTtBcNqp({&723s+zFX z{6ljgN!ZzG?kAzNVv*nv7fB5;D|M6VpCB9r5FP#ehR=EwVsPH6>+xp({)d&MwK{${ zd%eNMFf&23o~woVn~$8T41v&t1N7t8hb@`av(9@yR_)t$(sebNf5(jXmlDXJ*v*Iw z3AM`YkOgfMHKLsBx>uJk%U8V=Yq8?`%;x*-sLlXtm}iBTKKGs%=#RmCoH(3{1^ciG z;#|glI1~UFjtPI1&Ufi`uqoN4Tlk1B)r!L_qieJXr3f9i~>o-E7E|fkU?+J z_KQJWP-VxqrfZ$Byk2=n)Q|8rNAAAb+IzHo#wP2VBpPPVbP;!tG5&F78#KGXKNdGIqIRio+}f?GsKf`v=H)suG7h|z@bn!s zIf!`0_W<07+BJcX0OY~qcZ2YLfS82iL0bj89{3V0U(te{65j;o)BSzmaYR~jU{v@x z2b9(RSi7~0M~Y=a>_8@sCPz>M=s{wdP~;CdZ-e_e0{0%EK6C#W+r|bc&Vor@x>n9&cocrYaqcx%k1s9mYTUS62smTT&`{yDpu9esKr%5vDy;_e9^zez3JT52kP9-s=iX(VfRU(_{| zjU41eD)XoSL&mcAYvFC%wQ&4sxx1)(_De58J9pCJ2d}fCI{7KrHbdhBRk$kb``@+V2oShXnUDfriTdTE@OS5$ zYYAiW)&AQs7qMVs!R8m&`kd&?8@r6{KaR#`{q;P0q2=$Fo4x9nyMLFs9E&Lq6aoQv zzx=^pZ82pBzt}J_$zKy;C>!3cpub+RE93X}Ufya?bFv%wP&74hKVPBsX{PdSUr1G1 zd8l?QNiQ&XJcg!Y@|uE-Ea(2(os+-z{kP-lE@Ta!qatC$UW($hYu1bm#1|g-EF;D; zWr@;^ARp}DV0U>uxx06ko=24I_2q}l3if-f zYv%+mZXD>9cZZ1VWi<3i^lt9Uc6-(9Zo(Xg0gC-}?vuNUb|rY&4BKj4owo7C51X5l z!JN&W*Hf@72g!hPeI@}E{}ezy zK0iQo-{=0$1I3scKX#P(dp35b@`u#MYZ5yvDzYD(PSrvoyZxHWF%%p;XT~qrjS!y0 zSi{2x#^%o|V{gAnbzACejEeCcoh?>C9)82c%X3nlyBhEU(Ghe+8Q4SwYLGQ}*LTv- z&&7x*2$Vk3^(($zwL+HA_V~IK4|)iKw_V-u4;gzqbaRVWcsu)KOM3XB29Mi|&R^?- z@3x!wBY)QE0b|rcn|~_0oKRy8rSbdvU0hVVin=^DqXv$lgUHf!i$Ck(RV2lUle@G( zbiRmYHMQ0-&sI7Y+j-(5=LtCZ+A%`TJ3qkuFC)&1yi=C7wK5+-t9v8xJa<3E2n&mX zFa38Xfzt@L2FM+Fv#1h^lDBCd6%e`1_CaHZ<}K!(Ys{!wlDZXt?pGgh*r0DhyBXmR z)t&)*#n&PNY&u|fv3-2ZL2SzDgm-Dfgw$*d!yZ&Kz91z(8 zYdW7RjnakG)R<%FBFYIOR1$c#Rij`PWv!V_j%)kj2g0qwW-H+Sbs9C7=LM2$m3Qm2 zhB;o{fOoFjKS1 z%EjNwro~9XKTa|5WI#*>*GN=uEFg0JFbw7H2%TH`-v?jQ7lnUHxg}u2+pp46G7cnP z1V9lOZ-Qf~EoU$e)?R!Uo7+L7^vnE}8o3N<<4$GMRYX>a_`?m;Ox8qsp zAdx%C$;z_Zt=zdo8ONcY*W26I6a&T4LWT4*UdVuJQPQ+x)2nn494AxZbtrvV2!0m| zlxeB&$gN3vd&DE(l4N3u%nKFo-Vy}9=Z99b4ZnvPErJj1FCHqIH-Cs_%ud5;yxD`P zFTII7ayMteMjn`_7ug1klU&`-8S;xI z*Y(RA7q4`#;wP^@ed22~nVTN#xkC>ghtz3eKA4;p+IFoSr}07aiYGFb7v8q8I^crv zc&bJJ{>Nm{t>5rrg!XOFuz~|b5J(T6?4vND?S({G>8Q7o(eRhAAGDdQ6%LJYz6Eq2 zbIEY{SPh|u`ux>bN0#y_?s%>lNG0t)biM+(O<4?O`C;@#&Y+2R~$){uN?)6%) z!|TAXEp2SH=M+;##^4jEUDh@}XvPql)c{tMB023mNYc0Ss4dG#K@GTEgAuXuiW7ip zUIDi{HU3WK#ikVkg}BAC)S`c$)g?dLkG9u*-A#r7^n!i+8M$M%U~%yodAQnI3{&i_r$__HKZngoqb&^d!^=m zXeh~F)IfiQ;yNjB%PQjYJJi5Mc@>ARqOSt~`s!DYbfqsQ{H%HZz;bY~5N6FjriSwk zVO7a4qCRJ_6?1N-CkDmVHeN@~09S!q@V<)+$vGZYg@p@y#u~oq+NtF9fwW z@7-b4d^6GmySEgWhc1geu!`j;a7Qv_e`;>>5(MbKabZ)`v0rpq740ruk4FL$0ErDi z6M>l!GE?5$5CR0I4y#RHm-T_ zmKmNkv{`(Wi5{UA5-Mq%Lpj7p;QFqvP>VQ{g$9brSb9uspv1Hbb0@Y=m<6;)s&~ty z1wM&?-pk%)@->XGXhS91M`i)&%qgG&z0I7~640G@Rz{X$Ml<#?y{ixDtaNLjMhyur zL@JagipYHR_>R)wWGyClP~ z#kRz^z!eGh(3$%hWsZOv%tej*n$HvLKS|I*Z^-q>_@2GOB_5rhp728I>r>#6A-=z|apitFa#EdeV79Vboq zx-ot3GAa&eE~v3UClkWdG~eB;wC3j1I1z_Nhm{m|&6ucJG2P@sFE3IX8})+%pq@<^ zSpqRPv4mCu(MZOUMR$JSpKEEBMnhe>!zda`x1ma)v!LBJvsI+1zHJN8>ULTE!bzNr zPuHu~23x(l*JrJ(-}rSzUP( zkHLic5uA4xm7Q8Q>rfxX5cW}6&o-S@-`MZEQ@Pa6gPb zh>cM}EHF3^sQzu-I5tWUyFSo?E(xItjwg*3crCS&wYi^*g}qAy0Xu+g`B_Jl>*QXx zb~}ap&f;g4^o2m?u^SoDKkswZGbe1JMw^Lr7|0f|e>e)oFGwElFa}P}tVlMUrJ~uv z@g?_*czbugkt;*C@GKq&zcbIW%772UfWZ94?`FLi$#d>FoTMYCVnckdwOY?lU-b0> zibN2e!B9^fTE?bYQ&^g)f&Bv{CqXejk6m3QYHmqycKLzRB1N#A*|6TKRilJe9poz` zGdOMhboa4+nPZO}cx!f>G%WwwRvmK0HoOg47~rkzOQ$X7SHISP*m1s?h9e=o?)QQP%keeEjSogg?UyeYS1N;aA!by<(9O?4RX)c$fN zlQk{A1`e`iqmZ%mPa;Ix$M!ES#!ChY=~;mJ0nX1kz>;D#7G^KTsA3MxI(HB$J0?u7L+*36b++P_rNt&W=Llb(zEqlm^W{vtphQ z$eQvfotX^P@SlFN{lZEv5e8lsL~3r`SGU5pYM)a>7+W2Y15scVWdTl43soY43mks$ z+4*J+r)xx7fJo(An+C{qidrGPrtWMLMtO{M)s0U-FwE_k=Q)liFAjfR8+v= zN&I4IUuwaqa@{b-Ue!blu-$A0>t2St+j8Y|^5<7~dJziHWm9RHoPTF~!i_IEd9 zrW661n4AXh0XRGA$9{^Bv#TT@O{p0u0yK5p6eww?EM0}%wL>5u_5dHsuH5l#O*NT} zkNg%n@8nR+K54AYZm-a@1|}lTt2#eBHwSBQJL~lgaDE7epUfEuO!K%a7_WTxNlw6} zz4*n4j<|?-hp>R>bID?11AReRdP9Czx%(?#RAVX==w`aSVvlZm5ug(La~V<4?z{O* zKsJV16f+eKKcL*u)>iRhrR?uD^Opp`@kv%u-5w2s>uOP+Jq8cW+7CQL(ghs;+^$Z5 z%gMJc!YMH_wA+`o^km-BcmF~QR9J2Lb{+IX+@}_n9X)@AOY~D|YMrwk^0VOL#x(tj zcL3{E*$tuJq_E?eudAa-KyRv~jm^O8AU_ii{Zhvj`f4ioCeeM7|Ev~wn1=Px?c5G# zO0!0!t;9`+=ihmP>M`cEI+5*P!WAl72$AXvp@4lY2RY#hp`D9HSJHjaB~)C2cSu4$ z2emIB&OX$3jcX7Eu|nbV*K$M$>iko#S2?`68K zTw3=PP$7pgR)H#qoZf*RNd?idAZ!I15ff%%U+Qn#jR4G`Zqoi{HKE{>Uri-ik^YYh zBIFOCr2&4KD9X1ZclAhM+W6PWm2{6Bpn}9#hlL4QAO&WiKa512-`_+`RC3L2Xax-D z^mk}jBso#2w7MKL&OWTy2xB`K`({4pXYRu!ly?6;{E)O84BiF#RriKxD)gHP8U3V z;&}il456z(EI|*#wr2HqRoH1iB~D+6NgqJMmH_HYeJ=r>6k5I|1dyI)h)37W#VM6=D$ylFr_!?qnKBDSk% zbi6*pI2w~j|2ZfCU<;C?;4vn}W*$bsF0oe4_PB9DvQhvwH7#SS!$tR##F#jkfbi9S zQlUyU9U0~gk59?LRlJ7OY7BA$&!r~4lF+xp{7)gk&ZvV7S@i#1X^mc}#L7U+%cnQR zkSpz@ge<|P9@rOHBy5|IxM?!|$4@3oRdOg=0{9&9o~rlVy@fnqv=BH_c=ayeOa&{k zMtBG~tQkQ~qoj!#V^&?8er(K`Y{0@rsqaS4cz-ex$CFI8sQkCCju*z~3Ehy(OZ?s_ zs*frk;xCh_Q9n}U@oi$ba|d*(@$mc#dl5ilEtr)8{fpFKQQDmAR}r%O$o;$5N&#Qu zzs;{y8p+?dAm+1_W0Rg22mYcj4tV_dr{n)zCN&tGAm2w2pS%~l@)ociNTO+23`*n* zMn==8FQg{f|4IE=3LzPZFUup>lO8DT z!~VY(K!>^3$Oq`i_H4wOTht9%j~8W7riftGy+%e0rv8WL84)F~8OCGh1r$MI%B@l1 zg2eL4Q9WQC4)xB1hi98u&Wet2fD7YHe@_WIZ>N5?e6i?2nluB1tUzz#A*52EP{>aXF)Vs9RzTFObdB}~UIX|5iknCe3j}*w7QMm+E&o9D z`eB~v!2+hhBI{G39L;Q`MGZH{0QT(?nYT>nWRg2FM=jV?%3YVV!I+sKJ{u6_q83uU ztZiakT$j*}s^#sRmJqWVAoz^W$(HMvcH5b*3BIKG2+r*D_@ixws@JW{B zF9IY-jsVTGHb)~e@FV{%gAFtbybh{hEm%od0k&(FX4RQ`zXKpSGkJVztTF{6^3C^L z0Qq$by+qo*2JT_gER2qz-@{%NDwsZ)^abW2{G9zxPB_iHx5u36x%OT~)PjjWV;YSR z`NJ*SF+Q2q2t_w@bqqc80Pc{iM-LAI9&i|W(N5nwv2wG$!XQZ>Au!G!n3GRaAAAFB z038;8^OtR#&`i(H_mjL|22&Q^EafNaF|{%x=C(_B9CYXe^;+A9V~*{x&gRe+p#Z|i z8Mp;|eC5|Mbag3{hk$fD%>NS#Qs_<4HSs^hJ|(8LLdPtnD%lcbfEJkp8t78|ZG4peJkItbQK`f?V;TsSIHN*@hkrpjy0!l>;7xDal*CS?$S4FT@oYjSB> zmtX`%1@v4}yMmcnR-3Gwgc{f;ZJYH5A5j&={Rt|kqO$%kt?zWkQH6HEKH(?`_#nE% zVxSZftMq|h(={-zL%*ZG1|~^Q3LF+r@^(#IL)6CvIhlRIrr4F8GdkD%w1md{uz~#i z`4_P74uz4d^VOQChojC(4@=!9!%cKEGe7<1Tg1F?$f(GO#j$<^DL8FflfKkA3f6@v zA77hABHXO_)|>N{D%Ky3-h`7QzJaw?St#DDT!>@88|yw?nMqp~Y8P_dj!eWLM1Hr1 z8g94Z@5^d$|Gd%qN%$1qM2Jg^jTLcKO8CWZ6>asC3B&3dvCG5MWW3;2r24REDZXMf zHgMxauLc=E6uL8$qCGx69VXtE7K_J(yfUsAgL~i+RhB}YaWRGzfMqa+cjGFow8Zh6 zH0lr-ng4F$v|d;A3c$@p1T?%Nn25*vXm28!LGje)< z)>YoNQfM7J=9Mp~%4YP)x;`d;Ho`$(n zM}WS=?uw8cL3rOQ?Rm)S=IV>mt+qK#fx7l|vJh*1J0NCoqB*5Y%k&*E8Etk#oS)yD z@T%$V00?Wszu9(3kzrOrKjQvr2%7)(o)SNPnfiErWY>u{lpO%<3*L|Rk##CnJA{IU z)n7l%%?opzuqrGO3-k_-6qs2jju=bF49A{_0+-}Q;Z_6tMC0ri1mi|PMyebkwB zc8V8=v>9Gj&-d1)Z=R9*afg6eD1&#{h?Gb&GS+>gM@f%QLD+(orJn}x0Hcs;o*3MB ztltfw!M|nS56ZDt-Gl_?fSAD#H}!3?x`l<Rdd~ZQPBedjO2oCC_V*+Vg$IQqOIGO|J7M+rcRQrV z3hd7iv|AY+d&D4|3;_z0z2+oAat(#NB56?*)>kCDE3g{29qF**#|4-$%v1ZW}+Yu{pO z`d-6l5|(^>=mlCr){c?jFr2i~^<@@| z5)k7^OW1a85fs1hy2AqNHK$ru_IPDmlH=DgK`(k{>Usxm@0m$!O#hHl-*OUAVd)P5 z^B>T&1J0jLFvyG&&-;y2p3CYc!EdDBzCC*Dv-(8N!}4UCP?*R8^w=q9i(2&C$`7o- zVuopwV8TdpuzS3fvDl=^@wxLsUR93!iH09Y5zp5}(qN&Qc7LW=8Z+my`0S(g63)n- zLsv$ruT2`veDBnc=s1`AvfA-(-)8Z>fOQSS*tMZ^qu6a{g@P}Xq@Voqj^$1??YTEEF2tjwDRdH#u0u$M)lb=%QUsbV3<>i+C21fd$N%O*GFW@vv0P<%dEi4b^`(2EF1GkN(xN(y|mQdp! z4M4t2(St@o+wL%dFJR8O22z_$l2WzZJ=@HH@GN8o_r17EDrkRvelT|Rjmn$E z>|+FlMUAZX>Z@9euhIYr<^xQ+890K|v-Op}wxxnEW{*tDQyptsu2ccn_nx9gv!#p! zcam0^r#hCyjOfvMp^;Ze=9ns*S(l*-fT@yG`4i<1@h$ZLk*c zowBofzk@#BlZ*O=gCq=|KECQBYwW3o$#=at2;-87)OPBhyosk0v8xwDN(%-CuWEcn zN83fO*D*81C}+o_jEL6~zwFlRbyu`Mcv^{*SevCGcOrxACB3EZ+elm$vY#F<1P)yV zM|^$OmZg|wa=5Jyt=e73ietirFZFN{`pQAUAAU-3vDNJ&&ZEpxAebyp!`=@az7Sh# zhsYLiwr!rjN!wsZwxXkN1k0WzuaWav==CJE`wJEvClTrBaOyUIhu{ta^|60OjY9-_ z{0_tPEw4+7jWW00(%~q>Mt=D}ibg1*yww^yV7HUk(!e+qOL@Z}Gd%Y%0zW{Pe`sz0 zI3~!O2{;QY6^@a9_8gpkx3J@Ch;Vz~VHQQEG2sZ?(oy#2W(4o$vc0A&zZhdbt@D3s zw!+7TGLf!1rMlkt|FfRl{fjhuH_V3@uF)CHz+2U50s*bI$gUThh7Vu!YoKKFs+&j`Ch8C<4iF-cTKs%i(A>%bd8PecT8#WsVs^tB>eJW zB7aZX*ni{Bkn`UZ_t=N_sJeH|Gq}T?=g`HPU(P>y#Dy~Wm`B`*Qs`Be0CPEA_Z}-H zqSTeMtWx~8sNMg0tYe+n_cC*hRuQrUF}c?aw=Jzm(AxZ+khWpkKOPX5UW?!u0mAv! z#f4E-t+@vlOnYix$fxXJG!fRa-q!6=RGw;JAj~mlPI9|H#4UmvXpxkaUN?V%#@H)P zP1r#lZ7Ypr+!csZREzarJlBb)cv|)b2C`32aSMEbQ<3bFhzZq8sIi*OtD;zBReXBJ zS*+LQ^x9q2Cwz!dM(W^lX0#G=IHM!^oBCy$+vN)kTno#I>Vdk8=ZLtCzM>U@ag_Gd z(c|CA3ofICgm1+f#}X9VKsELkff`ioK2>3z+ZbUUuT}>JWlqx@Sd^e)Rq#1{B_w*x zfbUAR&}KMhqMf?P&ZP5>BgLD_NndIBSfr*sK=rizTCe%3*EbAHM%8W4j}vt$)0P0)MMR%tICJ7ZwJB_}PQ6#6seZ zeVzUyd!^O?XnCKg9wx~T0z6I{+bMVp?OJ%WaG0jkO*<-MqTI^OSnWLX53h=4pAm!; zOs$4VC!R+aa1%hK<%Kq;0!lrY0`>9uZ~Rtm4F`$KSlScrVaZq;ll`LHVs=RptH(pO5$1hBHv0L3Mk^)^kaX`I=<@>E3xdEr+!WkRP6gdIO=jY;aqCxghqcLt zJJ(Ul7z)f#e8=b8`W;SImVq`t`k7J*3-UY1)+amAQ_yn(l8cDL{kg6*k-^fc?^+{$ zPQcL}1-6Z{0@a?5?PLD)5QPe_l%3s^2-*H$K@58V0$7+;OLx9_?_W5b3r1$#<{py1 zxSK}AZ=2!#7J?X2$nsRTOtK#>{Y*-6;^6MjHRFOrBBn7;CDqoux@w5qojWbq_&xQE zhhUTub}b81QgSjr=ZV#6hhBT8`vWe+2$RXS4Xsl%?bl}Q)gW4R0JwGap;Y2j#*Fg) z`W>H<(iy2MNg~LmJc#sSyO6o*ghi?Gm<;>pcgJpjFksx^zV6;Babl_G+hTdJ+}^Tk z3x7QJHk;&)e?h+eV!Ev3&a*jaYx(@74+j@nE+sxjwHktH*}J#cIY!f7|C98eOVrib zv`|!zA5AB)kkq`pOiGYf{8_TPTM@V~h5hmqBzV7irL(#wcf=Bw;xZcAJuvM20~G{> z7GaSXImPR%HFOq#0sv9CK&e~3#RCmL$2akEaz0@f+ZZvf{@~+HFI+n?PCfu{3`fv= zd_FlC371znE4!ih1)9Du&P&_5w$KYD`MzJMZn+v~D(nS{V$l9lT*MH~OZ=LGs}G=` zGL(|$ud6P?sr+WdIZftFKqU5!6E{tT>9jV_(!igH;+HA4soH5fqZf^ z<@WZnw!87TmjvL&2%#4cqS$JD@!&(m-E`TY0f`SONbcG!JDCDOoA}tOVpU^YBRO^D+4WF26m5s=7K@&0v&v~_2AZ$d#Y__<#xM334-q?(cUi7iD zFGuk{S!o23Rbs&L{YFtiyG)MGM6r`6Q0Be z_17PPaRon7y*}A4lgopELS=7aurp5R6OZoYGog`>C9iV0q>CkxwPqYv@7;hW0vdNM z6hh1!0NNvw2YiR0I5x*DcVk&fAmf%jE`n2;0agpbrp(t}H(@4@fvsR*ww%`>H6h2` zzlRG?;NRE(q1oDr1SBBa>5KfR;M3M5xzt0ZV!Df%QsgQeH8M!DxsJy*cSG+wf^+_ zy5_mMRV?=CqH&`$8wONPSE!Ei_#PmCFuQ>jraVz}vc=32U|(aD8?^b40)=4W-H}vL zsXB*SEk9+sEe|D>E*iwe$h@!{J{JF5VPQsG^^*NkPy_R!<$92#qXw7Y_Ng^2bDX2| z42%q!EJUtN&k9_0b}6@9uM?JF|I>L_5q+(uA}Og~%Q=u$s%1AOUjy|#SvK_ScjKw8 z&(3S*OI=)=RzJFGx>(Px>x$j(4ZtB6hmCIby z*c(=vp+S;7X*R@HncJ^50D>Dau2 zyI~TK%-mCl>TJdC2yI9P*~e%5MXXGue<9~s_WE<%F-5Ovj3YvVBwghdgv(La^-Rc} z(gh*7AjXfVWys>_hk`KQ1_P{KW#WH#DOj}l5fysbrbB4|26BGsUw>}(|NOnqUc~6o zCV%tNr%VsY*;I8AM)G@9VJNDIP9kNIT5Q>~ETsa`=+&sE!VFWv+vi*$gTbhR><3zs zvT9sy?Q_A7>FY^7voQasLt<)n)@7K2jF|U1=Py4{A~S9INzn50+%*+_$FY6*1iQ!Q zd?9ae@IFNM)v<0X`I*bGui2DEkOUJfK@016L6>B>|JtB%!}=5z#g3FLy7A))&8h;B zIzt8?`2l%YN5P-+mej4?s6`_p5uY<8%p!Ng$|}%oEPA_{6s!Izbd*-WENSHCad)er zO{?!y>m&JSmOg14e0uI0lqIkHckn*o>AB@kE4`m7XLy<6BL6g`qPFKn;EV5>{YY*n z#5Z84un^%)%c<=5%$M0{y#>7(o6zl{j*1#s@p_gNC{yX(DfTLgFrI3YCp%ivRAgyu zOI{#_wT;bUAs@GXd)wbU_wjN}sLoC3J?0T~O}kr5weP%M<{g-lbXXu!lZ2sYYby zCLCpq*DW9DUaM?2mX++C5K%p`SjV@>KU3{;=VAAddU=f5K~)Pg`tQ5rm?acEInlzp z4wn`rMC~{J+P7doxv$wLTlmw;ynG8em_5F$)xOsCUo)*y9vp+*PG%)rz;S)-rv#8i_B@xwZ+!I*9Q*!=c|t$8JsEX(lAH;~!pIL-3m|1?cUAU))XW-t_&R9y z`V0w_aY6pL(5n{&_+dE+75SqbFsL&<mIl3>*Z!g-w`G3t_^X*x`n0 z*S6}%VP4(i5BgHU`V0iET>X27mz&NUUG+yJ9bG#*7lN-5yI+HkjJqwaocZj=cD|XJ z``q^bZuHWnNnC8In(M0npkKH$N*~Hsh<=beFwJ#~iB^(;k>%|+lX24ZibCbzL-fB= zf$PI)nJL@vJ(k!&ylq~nE#Tks&CpE_(f*%Ziv`+gpa#eLb z9Jj0HQol6}87&B1l6LXS1e4Y{=5l z&ttlFUQKOsMdOPgcsxYW=mI8HdAK_&t6P^Q75BIO4}N{1rlvAFK(Z=s*p4DEFb(#S z?^s#T-xs^=_IU(D9imZL!wYr`@^2q9bEuI$5AP7pcR2G?dN%{xSsvek_h)ULF3Y>zq|(d~y0?h3qJTnP-2YkR zLX`Y6thE@cDLZD)o&jrei4X&}vX!Gd?2?cB8yQ!vV; z?iVwYq0a~tb#)(Ic0O{)s)3k|hxL1M;uk`z_)s zEzgB{ocVaHzLzCr(K*bm8KN6DTe&x3zSX-4)hUTYHYSFx{Q%qUmDH0|3s~V^Sv- zLf>+^e_G8I_(b1TN_-Q{^Ss|kG+CZl42_r;U&Ou$J}kZkLUG4k)mtzly@@Qdm488&wP5>R{XW{Fn;wXuK*U32Kz<-jg_u;clYxt zaL1`~jXh*`a(0$~tr&fCN#Zgbcg3$+_tlpLoyyTmfz@wzOCB3cBDAySB0zX1BzrWX zO5-m*`FKwL2VAao`sT}#=)w@2(@Pre-L~HqH-S-7Y*SIWV34$b(*hG9r zxtUKU)ZT8lxwGjY`Q(#Ti1=sg>*6B!eDPlcM5{1w)8Kk?#q?xmaq&zdMEOA0+fMva z*D}MdsHCakSvcB`&IGM$*iD-%{otT_Tkn=v=A_}r)(W{f zbs0rl+k0>tV>EMjCQnT_Nkz|B6ZD6o2G+~IJ>CajR(`k&DN=$jk`(%B^#PgilLPl*xz8lOe`>Y zi#{9fV~=tD+3xyUQYFUua(7c^*W;N4)57U2tlRN&@i-qFF|+QeF2%pTqj~xHwJ>rt z{+7{d!HW`z@M=!g-NjNmQ3i-AW@G}|V%X#5~ z^r>;0J?K&wakw}@R%O^!v(utAKB`I*)M^qn`t$Uz(c9vN{@`j9)vwKD)Kjs46zVM< zJC8BtP^II0`03_iGLdJl!P|1^Xfb%@4WKZU($3(K$_Q#y;1qg zBYFwyLGQt3%53m^TJR({$jtUWHjr=DGn?wQS@Mm*q-A?X+Mf5~NxGOO8HwG#I6t+- z#7lC=Tq0b{NnPrzA7)oD>8uWS^N|FO^0E7_+ylDoF8}V%wq3ky#i##*s-YVv6e}7k zo}D89J?ZsZ=c+Fp=J_!TMEcMhrdtgbNa$;~0-vWq4=>TZF7_+uytj{@hTiO>KmA~Q zbulXO?6Tofk>&41Fp>bbt zl#`2tYD2H@ku6=#>&r33WbG}d3G%lKs!3(DbTLTrIcX#Lh@j`&+k`_b@qqY2@EO2R z`Z+=%YWtr?+JGK=@@-u5eJ_W4=dl|?~Bdt3w2u02^$gXSLj{5mr_6Va4kc`H7Q_NY$d?X$5rjk z(hCbi4cT0TIxZ&W5do*$_d*bGo+&jWw6wu@Kms}zNB(#ho!b%)d#j{8`*_@R77xLA z*CTl~6VQIMhv#BUhR_sTOJb#Ic!gy`Ct!)qdEPEX%Kep5LwS#K5@~hUZ^UZ;%uHtjVU?|Rvz4PvFf7xaXwPyZ+BVZds&M5z9FVR>uv z&Ae|+*&_~$9|+7P&0{`+Xix#ImZ_O5mZ_%z!<0)1DXRR%g9ONpgC&)<5&f-26mkH0 zlu0V1XsooyfVBG8!ch1jfgyt>RtJ2p|7unBO)M?gHPE;$^w$Zg5IrLK0v8ae83!@C zm|6r-fWu?xqt2dFPDx~WygLU?qay1O%rwIUQTg?ijkq_7&k#I~x4<$fZLfQuSl|&E zRgDCjcxZXFPo5;dKGTzr}zRFg+Fd@I|ul>Ji3Z z*3%?7X_vm{tLUR$&jxlDQu3jptYTu%*`G6lw^L;<%Zks{iW;Kzq+az4U$O$wK`A*W zBO+w9&7{0%x0>Jq8p1%`?c#dFMv?r%{7ae>>En>g@NG3%)5ih|h-F2yPcs0nPBg{= zcY995u0T)L$*GS|6lK;*2!OrT_$~DHanW$M2gV5lM3<~IK%-IzQ8V#N0GdJ&73PM# zOdJOw0C-$4ejJP7L>N}!Gj;uVy3GP`Y)`E(+@1S11}#sfx_=En?1m$&us?E;|1Afh zah3@Ld+SEwa9QF+Kntcfe97~*!kl<1DvAax8WZ8AL{BR zn$KdDap;nxT;s3^0D#bOt1xY6^-fctcYTWRD!9ObJIK@y0hYR_SOiF+8j(2Ky;?X_ zCVjunv*bJj_?u{mA90TYqbmpA51Hl?v10a#j8al!IYYuw&6x9qKMQ>*zn*FCmwU0V zCMNHZ;TeB1?MRJ&Xnk**o5M$IWd|%~{k=&DIlAIF`}0#?t0|a6$^)bcs?SlD5r?V( zIg14JPCcaUtDH_wv1hM?kRVr#m{)GH(nE-tu()u z*FwssaQI5WXd!}VtRh)(6C86jaOOs!@A#jWy>7qlcbhlrNnrp`k)y%{rEtqS5tSRc@uQVia34j4K_lVl&LJ=NHq z>jG9Ozr)RT?@<0(O$M6j%73otCzllARvwNRfpH zHlzrituACyb#@;uPA-6C!VKR9v;p>C4c}bVffU(i{XL6cK@y_gHR-)Rp+>pS=}5IE zIKb3mk1GDbbHVSdAx6xa4%AI$Q^q3rNp^m$R_Jaa4x=U!Y{uaNeb*q1OZjA#Y<1v8 zGV#%`_cDr#uq6rnT32J1AitT(#eWr5TuN|v!lP(MNt)7YkE33E0P|&}W24XLPcO{v zYd%Ky6sMI6x%Gjg5?zo(J@^&iuJ9$9{#p}yJVP2chhm_Cba4Rzo&mU%y?L|Vlke!2 zpK*$8>dP50_%uM5jD=d#)Q{0bJ6>EBH6dMn(ROMx5D%mQj!9Qbu{IVjDptUu`SD~EUO+_LIBzBV_i#9*U>>Q9`_tc6bH45;?lJfpOult z*@AIW#s&i+ExA7i1?C+_O~Rem@GIDqVBr)lFtc5)E9lsF_qR@MUUOajqsA~oILazZ z*_3wef8b%9D0hm}rJ-nPkVGW)fhQ;~3#_a7%0xwv=3wo)P{$2ETDF^?zx!q8E6oe> z*J5vc1%d^3+c+TwNM9_0gO18;By*%#m`J!3JXD-RPoWP8f(F^+mg|rJlh&$^yPw{~ zE%@6n8;Oe}0Y{>+N|eC#_8IUz9E9O|tHBm*036GVJ>Rsc_1ki@s|h(;r&Wa@a<;tk zd7nUH6;N)iihN5LErWc8m0uhtiy||O4UHR}$JT_5R?jq6TsKk$M*JT^&oQ^WP_mh;yN0y+3C5P8$SP6vFy@X> zhXMxK+a#r|a#@vF{l`rb(HSx?Nx8fk0N4@XC(bZS$*bH0UWHal zN{7v6n+T5P*Ts0`6EgCIINPBv(?OsHGR9m?`dU~TlM|3!YNVp zvqA1cF4g^n0lRvP1@3xC=CGJBWt9z~a!2>t9F5CisoGM?5JdLD*9FAR<1L%>2xkzq zR$PFRI6M{}Vk$%-{4wpMJDY8r6m)Z*hmzOm_Xsxj!SB3Gc!4XZPJK>bG!vCgNfuRq z7FJfKW7VWq@(owSVa1SJv|7lKWzrUNz3^!7{I9=lCSCiDNfCHB>x)PTB2qbmQj8lv zTE__qfGEQpa66gNGedo8fMj)Y3IgJWDjfSb=*7vQ>83(Q{w5|K$NpHVsedgPKs)dz zi<|FSljCXi6NWb8rz9bJNqZnqek?;jRY>1xtCpr+R=ikR$GQt?sp0ibv*YFy|@IpqGnU}^S1^^C>ko?Q96!f9rE#M;$OmqM82Jlwl}t4ouPfY@KJj6 z=;*?29dYFO)`vrf#XH{qTK{2acF>vsvTmzRIE>-+thCh6xby9Gg?XM)KB6$yn(u^K1>;&M_f0%#K369&>NMvtI|nMip6)2H2A z(iy2^kqAKSO96ey8rKi5w6FduzClahxufre^lH&(P)_?+^kn{(lO=di*rK~9%zGY0 z!p?4{7L7^Olc6B$V)?FWlvp(phdQd5SDiR|5|ktZ$2=d!rnXWYAftLJT&ZT@xeia@ z=yzz%;ck1wHJrIxylO#;u#PtTT0cLo+Y8$A)=%$l=JCGNuyXaPyM+>=-cJC-GfO-r zjqWW4_2bIod~5Bl8FTQ_Z9f+?V3_#rirN(lMNLhWrcxTZKQ0p_L>;VQ}j zdi5WQ6am+T{-*OTdjx$HM5b7L<{&<-YK6UFI5g#9)mU^D*1LuOr$tc-9bVQIK?HE6Sqg6u- zkiMjzAW@*{RJ`W(_qP*_egmd(1tBbNQP*T?XjWQp03qy=tGz>6rmL0L1wb5OageIU zPCN&c*l=vkGxxxDw|@0!mP;o_Z=u&@sEmZan#u**4kOi2=EJvOwknmU9<3j);V%$7(wCKoc?ipyaW`!z2J!Q@y5PL0#BxRs{R5 zQYmj`NC=S|!w6r3yH%ZwGvIqn8uCa8^px3^1|6=LKc1-?WMDvKm2_Q<>s^jDz!5(E z2Z%vYni01iF_a#r_bgpOqhxK;6g{ApT z7;+OA*cO8?bm<(5s#pNl4FwAO6v0);Vt@im!Vsi!V;X1NMF2L;#1w(dP)-rBdMD;P zP9#~?X#}e56@l^Nt|{{BDNR$2?IFNd(|7NB!BR{^!H3N+*=f+QqmgTGe6l`j~$dgF$;z5;efel!$-3oWVmS>Hnl{uI3J@eXF! zCIAu+qi^5}-{-RMJaHd?y289mEk!5K3PV=uY_2QnAwuWJkmf)V1`MLKB;{d4CAQ!2x(Kia zRgDriQ>1BPAOIQ=IeYaZ9Sx5*2Fd$(LVG(-bxSDL94eg{qZYv*ib>u0ysVM3Oihc` zqR?Mc5BQMVG!SMK0K+xCkm2&6CfNCu-|GlyaqKgpoLp6LUa{f1 z(U?jp*2+n>wSsnM!hWv0U34XP5z2xm1Zr}%mr_WkvIR7qUHvmE87+1I##3uA?0 zR*R-@a%EU~%RV;&h(qOMbaL9?%?Z%~VoJVie#VPU_t zu`wbfXveU=J@#H=fmdB?;8~B5K}J6>X{z0Zfa|mjYXN^`Lqn;<;W$n^w-jkbsH|F& zfkTxvt|ZB_X)iK3YN3Xg(MSXM$1~xkv;4iugy6o zc6x*NmrRCNxVT(<%K`6woG4Jflsl>`?}iyc;fA`9TiFcm`R?{iUt))EC3Nqf3$x8p zag?&}9a>Lk+oB*_K8X3{ddDXDuN31F+ixDXP}^zGOCSEXdjxc`b?Kcf#txjCTtOj= z-@o^k7i0OwncmQMsO^s42BrXx-I&$vkKjiP%ztd4`lMRks2ggqJiJP!^0w?7@QueT zNH@rrQ2UqC#|l~i8*t95J_nZsD6-?gCE$rbow4h} zuTs6;Qmzw}a&ndnIgBL}E$eMt+FDsYoGaXGN$+eI=ntk|VSy^Lf*<}vQ9#)W!#!%y ze#Y5Bx}rp60dxUIOW{M|VMrA(IM8tAlk@gi#O74>e5oR$&U))yIEMg>%BE?>P#>;^ z)hh^sjf%-T-eJcDVF^FISLRGlW3e#VI$eM8sC94n((QhWo7{lUohFh!=RXa5nb!s8 z+=GHl)%1;yw;Mi7z;2%3e=iRxPrzw+g?tJV30%9k1!C;^ejxKL5+rw<;@npbnat7CABRZGtY zBJhPKil9?f1(XGj4bLJzH#Tyfu3A!8)zQM?VcxlXxv{MG#pCNs_*=ynh#-~9LzS<< zK@$aOO~B2n&h24s;aC>lnur8YpWu6nRKvHQFXWY`5^;0zql3IKS5YkzwRfB+C;=sU ztwhd(+M{ebP~3yigArsr4giOR^!8HF+L-8poQS+)x6~c(R4TCcji}j}orSXbEe2%m zKe^SA)sU)lNehZ2Q}H1TdVwa_`oInDL2w6E6(iRI~}QPY-2U`IVl0%g#d9YC_9$9?=$W@^ihi+un zSePGNr_Ce`mHu1648Xfw#W-`|3!7k`VCA@E073s6;sA>kbEv`%OEHo1K&?G$Ht zNkU`{i4;Qq=>;}CRyf7_$>NY6&*Da32r|T7fB)j3%5tD7uwm+fkmx2TJWUN9`RB=z zKZukbJ@_W;Y96<}WYfqfZS-tYXYImM3@kZG%Xfm48az(~1Qg;KLHAe{ATd;ZMthxp z?qUS^#*r@yA0kBNY=*D2`JZ6Jo)sCzKd!PjoBriKnE9v?txmnmQjK3s%|2NETuYVv zLxktdI>Kpjl=K9|LEHLauT(mo=mE|sMo79>xN57@_|$BF#t6EY3GIs*1UZo&r@UEh zbfo}VvN=I*-b>ON`7f+_4mRX4k1zFz;$%SG@bN(&lAcZMWmz^CJvPW-rI~UtE=lY0 zW>c%!@j2f1X15!qgJ;#+{>m%5raP)TD+j&y@3StXzRhX;7oo+##WP=5r|yk(i&PIQ zKAi7YjsfxcT53kEi82hs2ed+*fqv5G(RUrKC_+|p!uG?I3Qk7arRBGa?eBapE3ci27}z1?;h|@^dj0&%FlqPuXfJihu)d*5 zf3%2=U30UJgH_ukg;@QR%X@l{Jd!wpot2<9ZaR?ph|San5#38>EKI+etvFp`b8Ide z-n@t=65{%7(hlb-6BA{{WVj<0aAX{mCrwwJ4>MiQd2JQ;~m?_(i3keL>l)44ye?uH9jH9xJF^lFA+dIl=iq09Wc_=|Sr^P7PL>v3M*?dvVN z-%qwqVwTyq{NPb}@>C=yO;7~eY(=%GK+|vILw%0-qK7@0XBGo8xbeU%Ev@FQDQY;6Ml1C z*1qv1tx{j>Tu7vv@LFVk8t`#c;FSu<-}ESCYFHj6$t{~Vrwa{z{%>(IfnQuf&JVSS zbh^}Dg7P_QE|?a4jz0-fQahGfM&x8P28kZ$5f!}$k0~eZT+>MThui-a7sxW}LpVx3 zZ*lrHhiw17M?%9Lt@zRo1ADW?VL{wC*eRk7U9?MzIt)0`Ry&jmfS8Z+6P?T=2T>pk zh&`6WS8r}ItJbp>XC{{?M3}^yswl-;Ht7+ zHdt{w+cP{7q2CQjNNrQ|Q>RS_z|n?d=f^asbi$X94Y$pM>qGU)pM*=beV%Km7UoX4 zZ*)~3|4PX(p6*zY!59335A|KPja`{ua$@hC9|_FW@nwFBLTG6JKn70&xjehJPDp9_jmF<~X{2CP_SYP}v5zix9q$## zs%O!Vj@o1GOv@2Qx#HQp2kNMvTpRw)G)a)R$U6ytcafzT zSD1ZhmrZPJ$tM-Xz?b~C8)C!QhY`=2A)v0?RJJWhSSd1@kal$OothUHwZKJ6XA>V+ zV$ycFl6WRGLA>6i(dubqwD^P6)OQTGud;7GrLxznWO+KFmffFvwB^5Y<&vP%sE8Px zj5->#ZyH}FI?xGoM$~aWYANmdf6e`v!ucjnQgG`012L_uv4F>mXT4G6K;0x}>WTysO5pdJ39%82%5}KUIXMOAUS<*|{k-LQTZ|&;u5VrbD zQkW&1hJ+V^!mr=c?r7C$DKJkN?x$ZNIcQF>d^oB~B`c-MV=oVbnwSZ8yP()^An5vQkzl_ zwGyNe9Cei5X~T$;M?d+`^ZOSQT?JzvMS+GW<@ys!=~~I0xiBWJiZm>zDxQyC)Vo98 z8Fh5tZ-LUi7JeSOAY0j6{+$0c#d?r;lvbh&FNIxi>pd7wBG@alP5oN26S9Lilm)$6 zo@Mb=QVcog>+5m+YzmfLoS-}j=jFXG9~W-%avTa98`}lnR^F?^W*WiSo3?d~zkl(lD52tGOkFF%rr`S&*&T74JmE#T{2Fd% zaROw1mbm*9IJVk{sQe#lKcL+Cex3cXEtqv*$%s?MCRV;i+^= zl~AdzIM63q1KDIc^#(ionm=qkdk|h@XDPa7WDKZ4j0gNRxgg|pdy(Z7Ai)c8s6pJo zSpLgsEM8C^E1Ijx?K%8_OFg9b+8HBSs9YWpZRxC8Nd!tizJEYIc|u1qi8~+O1#$lO zC62e{bHC&E;INppp!$dX_Uk{~J&YmRuD0)lwnAI;CPyiJOoj-06)!;c8cFi_h#w%j z#2kO&RCQ;%yg5#1%JP~2;c$D5TsEac_(YH+oFk!qHxrpytchfLNs%T@=_T%CMuo@Z z46(ly;9{zu4?jK(i2HVFCE-gm~H5Zq7N18PR1Hnq_&oFR}Kq;Mh}yQAG(Ux2Q+HK#`tb-u>{&q&DoD;HYZ%>=lfsh zX4d*Eb5vytzB0|PnyLt<62;2HWDNdHBB}>%gH@H_l{AdnEsW@EalSn0x@3@_rD&Q!Od6m)6a!c(iF>rHc!}Fs;xP3S>V1*t=mV_--9B9R8E;s(iCa6n;?_>UV$jT? z|KF{FXCfa7Q`-OC1n%+b(Y$Zj@OQ?@0$QzASBIICF7jw$nCO z=iNFvIAmFm`PJ`s4$Os&P2%Kns5z|{={`%U(x|oYED&*ROVf8ZP?_A5+&)PyMuwlu zFx60Jnd1BXiENpsXWw^xMohxijr6L_WaQ=D zB$>#^pr(wG5zE~vm@rndyG*1)%QSt+)+laW(3W-Zb(7)`J@t??%`)T`L&|yb%@(^M z{_5o^}Fr2E9__A)!)_hFenbOK?_0+<2%kQ6!X7A?H zInC!Zo|{W7M;`Ekv--Ukof3m6GAc-udaDx5yQO2h*CjkBWn=q)MDPn4q+TB)tck92 zc`EIWPhM8fH>90s@|1Y`77%EsQ@nYutj+F*eni2`JGV+`Jq%z8)#bynUVJaH-aT0Y zX{nW!VCHh+qrDMZY!@@1@+p-iluq ze*0Fm&F5!fnBT2VLc6RD3vBa8Olmzbou^qfn@By-{NLJ`M~tP}h3U5To4Ei7mv;0(RJsQlZ4zl97>u8pXuz5bpwmSIJus=$`%<*t@d=Z}q2q;@4( zm&uh`^*LXnp+~cmrLrUm3B4|pp=ZtgA%*IK&$xFwn%+O445RwCwno@$zE>tnRQwiG zrkDUzW~+7Xi`nE)+gSl6(&d8%d_K{EWqSV%l8g-`&)B}jyq@mv_ zL%%gP9O=-X2NtiII}Pwy#eajCTgGD+mo%axZF?70SGP{Mf>-MA>Ne+xj(cOud>L;F zd#Alt;6rWPd?!TfUF~Ln{72Y{gPgY$mj;jVZm8j06GMuxpB6p?`(|cizA~tNgm9zW zpQW`9<~d@q*j9M+z>~r`)Xo)oWPJD%jG#%MUp5m{BGa#g2Ft?UB`bk>6jiNPJ>MR= z!Lh{`Cac<_+W8arP7}!-k5TRRB`RYiF<-y2Y6-r38%AdV8Ijs~#3MK$W;omRpsIWqEZY+fC7>jf&0$XlJ6|cMfF10Ca z<7T~CwKB)X8+{08+TBR^Pq#p4>wdIaq5tL|f<4%IIM$AIBZSMzTG?=gTNG|1U#!BO zqDyc^bri6wgi-8=HBSg*Eow}|xN~MVuxInSUZ;?yU%fB;yOB*G*X36R){JdT-9s+a zq%*dG@ky_YTR*AM>Y##;w|^lK$b&iO-r`!OnRdQjLewHWMb1N?9dc*Xd|qcv{pGkg zhsra5cfLR#Fn8STvW};;z#HsqrSJG__660c|9VR!6DbTr$I8CFKrbgMpzd19RLLNk zXwzGI&bvnWk#bd9NslfMj}gaTeRW)~J6CmZc4ke-O0Alm!OvqueIJH&@C>!(4@Y~Uj?4jzW+%b~Yn5r;->4IM zwQdsKqo(E`tZeL*b+-tUjlN+ahK#s)+(3?$i$eFTp;p@~6W>Yydy&1&Xn}up3Vd@J z139AbA|TciuQ;0EWxEkeHsrqv=r-Q!zQEaWo3c|IqGKhJ`E|w+L@2`)PWE$=qFgRi zt*(ASG4GB=d1CkjQoBU|EOEv1D8>4Kb^Pg`k8+MtCz4i+GJFMMj=uBkuA)s@m~I|h z(#F(JIfc8Ec{Pe+J@MIDL)f!^f1UVRj{aGjLzNUzH5i;HiAwi3GxZy=-Nz3o#d|XJ z`&-9*R!Kcm78uZnEWm6qZu-=0^!CitA+}$BSa7%Cv7VO~eu0a9>cMI+g2M8A2-3b!hopX6V+@Eki z-P+pSsj01xbpU{Q8^Qoc2yd@zkJ(26ARzYA)OJxf zai;`3IapZRnp3)Xg3T$-J*+JNz+-tW-7?NhASD#)jMoX5nCd3bAbw_?BDUeFF8#6X zS4A2K;Q*Hkmr+E@&>8Jv^`Y9|=#K3b&ta-aU+=KU2cug95t! zG;=2Eo?|2}%DH}WBSThw$9^yDFSNBoZsG@RwK;Pz^)@=#89zf!KdsO^sSe(~x_=Zd zybFEwd{Of%HwqUt+POaR-Q@al_kC`GS(G&&+Fngj`00n+9z*nnHaqm$z8Y)s$nret zVQhnxpyJZK?q7EE_g3uqv*J4zh!O0y*?Aw+;?{!EGvPD;dd>=FEi;7 z`688MWj|v3&S;nXkIIZEbhUk@bDyU@0R@ZYEXwO2gO7d~EdBfD{f6H1%cO^P(dzN? zhb@BPCBKJZn^1BpomSSunm#1H%zfX`{&yO}wU^{OQMW$x6$9j{j4JZgV6#U172*fI z3w|!qxEwR&3EwB@ZaiZ5^*|}JP18UPzb7aEo4;b7t=_4UxIRxYcso(Q^uzK-5BX#h z;=vo9BPs!6hB-<{-8Az<#(ur4KR3^>TqD!^nwRV7Ck7nT?!J71`*QaL6B6NViGUM> zc8|>*j_ng$8zxRlf=nDEtQ(bt7LvJR5zmp9eDKXy?QSoVINQuDj`p8fjr@sOsNc7* zR`C@-IBzsG3c@}%Wjc-1F0AUGdg3yb?W%ubfoWB%EKav6YiwFRvYJ;Psccx*J+ixA zL`NI#yk6Ej^Nl$`;3L+z0Y7gh5tWQ_UoqE@^FEPAU+P%@)%t8;cf;eh+7uXR;I?LW zC!!2SEaX4j)le2Wx#nVRbUP`|aA>`0S~wJ}%?-jDS82%Ago};s)@#Scc zHfWQE$?_|6vG_nar?`ad-_hoHb_*&{7`5^(4qTE?HKq&KT()l}@9@+IXmb$<4ksD_?A`45F2?{W-w zwEs||iM*G$-_PhAbChkR**^AQG%F|T;Ic99FWrq@y=lF*eTlztc@!=i&|;IZ4kvBX zs#Gi&N?c=)ak-mxE@yls%HOEDsw}azVN%bYeXXsX&}N?^>YSFseR*S4p5Tr^$KNI4 zeolL$jM2D+iEm}u!m@03v;3S{9I?FhN?FQ3dNeas;p|mDu*dE>*zc!&J&9{_8+Nf7 zy^)?V>D!P}y^ns4#9gQ||3$lOJz83|`H0~f+r`S_s4GG}{&_&c%5JiCV}F+$65z#V z+r)Ktkm$me(87ICD3McUXXG$0X!0<3QpKq&+Io^qv^;#&kZE5V8?6e$^5guSkKT-tVzjuZzh^jh%kS0{8O4FNyg7kG zd)>p&@r+pYqDXf)`SHST0s$yiIz1HzJk>atZd-f5Q=O?XaTo4RMC}JCbA*bL?B$x( zr23uzN`WduP0S( zQoaZ#^)xYJh#%f-RXo9Xh{#-^?Kj1QRPLEpviCekAIv_jFz@l?F(6>07Z-^*$s3oY zrwe?3Z)H|oo2qfoT}0yg$h*t|Zj4WJEy+sGFG4NZGNfgKzhKpce$cc2%NF{CmH0h9 z>!Z7Ulxpmx>99SA(rH=x7$BOX!fraIL$V`UvMn&q@H=aE)$rCT@aYBD?XHo>g7rvW zZ_<=#XSTyV4r`j9YcFYt<@eUYxU^@!-6fj$Hc*xPNusK5O0F4hPm%Gf*Z%Z;_P;>e z(z;H=zwzUKMrxDOgEW2x>W=3Es@~hQSJ(TEMExG(;#8|dq(7zLNaZ+SR^=0wmON(Mnnu(T8sqOsH7m+*l zgFiWZukL`mT$OSLH!AfL{@^q_AE~_?&{Ji{C-weC{T%&u?jx!@j7AJjkk$RKYzcKu zi}5U+zJFiAVUo3UU2D=LoU&g=XD_JilBG5t>koQeSQP$fExvl0o($$2M0`3x#?8Vr z$k*(p=n{Xt|ro-9lH@V)6v6!m&N#Ooh-V&RahDy1d`46;^F zgu>Xu)XWLmqCe?j?iWmlG>~IRZl}X+VzUTG32#`*;H9<-jB}@jlayga;5&@{58zxd+=|IL*uwT( zcs%_zyvgYg{LLRvSt4?!C5lrranaOZn!Oe#<4JbaKtdta%|Ka{RHl~Fn=Z~%^jDY; z4T>zUOvFe5Y(qO|?3Y70%!9Jpt$F6u)5;Qg9qH_=nNdv~Q~ zkfd~vuq>8Kn<-!ZK^M7Ql&%2sOi=VNgl?m z`74+e&&xT=Bq9Lwni8`(l9v_CTt(~PtK33!cFZ}pG;mSRp9Ct=T~q_m;{+m+kQK+# z8eZ0S)8Jy~s_w^68=4kZtN?W8>BJL;H-^N?C7oaiwy}pGe$d(G4E2g0b)i6>s-q96 zo=&hrL2yd|m#ZCnJ7GmN8qf=@8wpEdPDhC(tN@QoqpNC`UuWnQ>C+NNuJFJuuB_lSJ(cz8)YB=$ZVsf`m(EX z%lp!hN`V}Ifzhd&&0TLIpo(mbgXy`Cf^kp!sk1Yc(|wJ)M^r&ad<#^Nac^ zdMxAM(u8Kq74_a>q9nM>1*-VD-T{~|PF;@@aLh!&RW?RW;`P~IR zx$^-Hd6o*^`BcWg?L&>t3{)wHe74>*78<=b;wCzz>s+Ldt^3%kXu$VNtjt+l@R_J* z-qyxVPUF}M!&x1a>lDC%`N)54$#d0Usl}Zi4+)B+fioS%k=Fd_AC%U(d?fmM-*?Ac z4u43QqDIpo&6Gw=zxWKVi8lEc<&U7GuaV}v%79NntaJ>)cmVit9cFD8HG++4lfqB_ zXvDCIG<3v$0dxCS1CU#si66@Atce=i-SehIcH%wZ42e-ioK{8>hVAV{0P z0ZpY(dZWSX1W^)Qy;T7p_$|FFzSh&7Y4^b@Kuxb_%S0H)Q1d8^ z5bd|PljeCHy51DZ0agW?uPB9V&>V9$X#aWz|4Kxz^K!6B&QVNqE4KxgjD+Xib-fy78lt(!npv6ClfQQJL5?I(slnuT@Km zO#X9l3AWY?Qo$$p?BpX_^?#Fe9`2_kHtyK;ONFa z=!Be1+1KdeeY!W<)k(T79`ZF25vKU*Vg=%d;3|m;%9zQA1`5;2RCi7fJcEG~aeaAc ztVkgvjcqBcQgxyH!VEF|^-ea3UHtsL9^E|lY){BLTkB&=%>e-*Om~)9=@Vv_9LRpz z`4RprF%L@DZ={wU`wJN^`d<%iPCPU12b2|0%F zGAlIu4hfs26UY&jHgzUYS$j9ONd)6O3@jpbdZsm6f;uCA4YG0bh-UtYf5 zApdZ^sqOGhw*kkbI}Y<>s4>ZE>O0g1Y!8{OTgyh`psqqzrBRr{+~HJCMazL8Z}ExT z3moZ~3bk_1X`O$N1}f#exFAS=hy_)Ml@QU#2kO;trZoi^ehw z%P*rUil9k<^TFlZ-&JtrK^)RRK|~C9DKHN#B65*PC)evra6j+ZdQj>6YFmws?B{UK zlLvp`DqfrxoFuDl#ZZWo4HLXZ%^Hqmc2`En0tBTZCqxn=aI!W0&`rYTND%*+nrdKh z_uI}c&@ckNfoOSJACv;$ig87?xC(3Ik?e=E2=G2Sy9}0E+(gvj?*6K)GP!( zM&j#aH_aKbG%GS6<44kFgUZ2)rfdVJKLv0AYla=>>h}UR@#TLcy%xf|GF)B>Nh#$LVgp9FZk3;@p9FzZpx05c~kp_JkqCibh z7Ox3AEw%q+qc%0!`jr8>2-7jBmHZAu zWk-mRF;LlP?CA}!Nq|F_EZ2cVmdd2SQ3$Nv$@q%A%vWxyBM)^DsHt9b??1~_8J|LzN-FOzA=;T#!@j@ zprk#~(aTIDkzkU|4#LpGJB(w|^xlgGPSAJ4_fCOeK z=63yE!)ap+MlMU#$34}!!>~SqF3P-~vkHmPam+Un^1`8`H~uOUv8(&KhG}B*Nw@S+ zw}0+RHmu_Eq0yw+ix!axR=Q8ZOS4I^b-Iia5e}v;_mgse>DKSV-CNs3&by_-`owY> ze2q6@5~gs}ed_V8g#Waq3AI*Eg05(KK@$}3#`o@8XR}xChMyISWOu13(7h)E2VUc4 z4|O9qf92j`*UIsNg@+MfQ)1OAs8PRK-Nw^Iq?QCG%7;x6zc2!;6SL>Z`3lmoEn03U zb!7YfZtRmew6#JaQBtqj^aQ5MUV9aRM-gzORzgK~D2V0<3J0o07FOb)y_V_TOSepq zUDB_Kbi_zZ6>siIZeOWi9IV{EXO*@x9CZ%y4W3U>?+>>g)*N}!7d?Y z>HV=8*@3GrD<;~0^?E85Rcc;4GZv(KR7YTDiLU_?0w*EXRwp`+9XFh#uG5-F!a8iUr zE@H2gR`L>Lq}FPFYkG41EuKJQ*S+EVgVkNtK`)~m{xx*_l7SBPHLR%&jeFPTw6Aof zJ~nzFL@)m$6@Q{J2J?OeyC36u_EHb?X(-#qfDQcv$Rd2K2&2rVe^B<1+jeetxWARz z?sRZ5^)<1V%y<#aVwHj*ADgHvU@SY+%L zur68r?(PXQ;p-Gg{t?-rq2Zu)gk_U_`{bl!ewXq8|FNE1ul?&0}78GuCp% zOyN?3Oa`_8U@kag8bNvjcB81%di&5CD_3NMXPezI-}M`aJQffa)7>p?yM-6`TVkfQ zqWGXO7l9-kmmOT*ZIx)G5B^j5yzOz<)x(lH%krx3<*?3|g7`(^-810m)&1=OUj6QVNA7+GROx7O zms^rfx~z;ACQ7n)e+V(cg89xc@Gcd14q!NMF$NrYOM_ zPigzK0U(A$gVLmtS6hoWD-3N@U;K-ij9prV6ss0QA2rx3eVaZ3Y=E zr{fF&s5t-KARs-H@NE*&MP5k;aTgvJlZ^!hKBo)-C;@qC2~CgX<2Cn8&A)ljiwS`l zOYdO#LKXv6IP0KX(=W579JMrb12lB4k&NBf-ZRe5Ne7*g=|6w|<}R4hHf1g?eTM~i z*tt-|j5>15dy=_9Vfp!s^ipV6+s55dBd5BXWwz&@@Wz#9Uz44*v~;06hZYmHI*!nGrIF~9SH%y*cy+lcMb5}j+Cu@@bM2-TivzCnv&51tOlW4`y zF%G4urNeY9n7pSHsgUohk%UvYGAbyJCI3_hhW3Do)7j7+v^o3 z?nb#LzMd>Mq5vp>0V?2Zv9{9MgZZ1}whOWZ;Wp-0#j0L3a0M0#pvxGx2)uxnc+1R} z-9`WV)m|ajIjn>q2QDJVqXf`PnBYKp$dCK#l9{W{aUMuCWCo(1i*I%GhsWvh`jFYM zLmMOxf*dHzR09K`FTcAH=+4V3VL!u`Z-2*h`P6rU0!Nj&jj#cayL0V=5w(Fd23Wud z$ooavK^y`Q1MKMASU&&^Kqfoj^6UBmf(}XD22tW+fS9%ejd@gw&II`L*(s?+>{e?s zh&YXxYr9Y4Hr6nE4##sqiinRT!!zrs&xq&GPX?HH04}i32e^EDjt);k%%=S&JNw2C^1m@HOz{P8K+yNyDXeIY3ELsV_&J1llW0?i5@B*yUCB_{C=`U{ zS^be>^YT8nUplk)B_GY#6Ez@Xn@{en<+Kb@Tm+zh6kmN;Eu2p_)Z6vwpfX|280(+mge&If&9B@{N)WY@perWfu8P4+O zW;FlESA5uoozZpQ>T5Qt&XNl;)O(>C@_YN|fjnk~Wkdq7-80WWuM%L=oT4Y~IFV&l zoP6ANjULQ9d(W#XY&qX~c5h4=rf}9JhtB~fTe)u!PzaRaXNt0q*G z2IfL&+X2SROU;oP!5+40f#-9b$JkmrY``ASA=Pn#XgPm0Yv}9@hnm#w$x-KqyXjj zgdg`g&K)6r7Y&OmohZ++Kcl&XbJu|cZkNnqJ4@IvFwFq9mLzZt!;0W`Qm!kZyo7-c zDpl?n0rR{%w7zQr?)lEyHaT*2`(V;+76V3zxQ;vbr8=9QQ@I;Y)FsUAcf{~5VXm|; z1}~eUJP4OXYBp%vdD{vW2qIu0LhymknrLUn+oO^Eh?d3YJCGM~ISER$GYlaQM+{r* z@BT~sO0U!HuZim$F&;)Efoy=Y3+z=FE6A{5CNdBOn9jrHH|huL2YMj!bCJWHg2)ej zIMk&Q=41VU7PNisd1U^KE)VcPWaa%A)NzRqHroWlMFDdU-iIQ}?RWE#rJWrd{wH4# zEy=SZn9P$kJYbR(D0vq%Jeuap#80nR8S7uV`_c#+38)_Qhu>`cdp9}Tq*0V4iVj7~ ztgK*z9By6qlf2&j+QEMp=RdbK3cW`kr{e%L$@K@?$ow@-=ye>|`HNNVUG_HwDfZ** z>Sxd0N5qZ&?XIT`#Sq2Ifq?RHsJ5(5%;%krO7g%fumu4WzvM3Diq2n+Hi zc1f;T?p8;zDLfgy|!6^Gf&E z_hx7G04kQ*XSX}cj4;l`d^1Syb*b^+IerJa?$M!7FPM=1eou7%G#(p592LTgow~_9 z!yL1*MAH_11%Q==(P(ZuPsRgT(>`xh{d_9+&JO!2M zD2be1d$2+JA)Sy*W~*ydM>><_-z1cL?+BDwN$s%i#uKZpN7D9{R|-WrkUDkBe^OIF8FQ^rnUU?N8%z zfUOkAQ4MyUz2wYHyL$2#L~`-+qu%LcD$vmXin&QjXMXqFMp*eF2U$Pd`NxfFru{|L zsE-(+xjicD3BONxY}IyyM0(}r(4Qt%K$7_T5#xNv(&#D33xI>QL^YPr+=5_&n+Upa z7PI4okjb4ny09&uT}d6-3XXnd1O#6gemBCQU^?4ccuwNBdhQ`Y@W8W_%cURjXBa

;OOa+_C#PUHywO)vYsE9W3h^>Obku4zKt} zBrvZbHn5g@D{O;jw+15j!g`Kjh+W}a)||9AXFnGNaXj7;hTU4or|<<@cXlG?E_`X~ z6rv`>WV%k1OXtiikcS3x#jT&nyCHWFLMi>4MM*42A2jJ_Tfw~YZ9K2O)klG6I=E*h z42lJJ*C@&+k-8;!xp4dZ|Gc+FfGTpoY>7;cTd{tUwZet&o!Ms33n6u$*I_Q~P4{lY zQu2WHa#aQ6U~uivGg^<>spG630ClQZLuT32W4|v(qDDVuv4Wa@)>+pnFZ(iT=wQ z|Dv|4eF7ejHD7-J%hU_6bzLh>Vobr*L+p6YQ|;NfF@sg_RG8bx--oDeK-fWjJSX+G zyLirf(E;!IpP01Iakg)6MTqZ}IzWr&jArO;XB2fvB(hxva~yy4kYCvcUYU=VK>5il z^|hyH|GB|=2dbJ#;t&3;CLB;NCY&_Uf|pa$A?ptz>T}piJcQ1*UgU(G(t!UaJPYcMJAUuO86IGpIkR#~-!*z9{XTal zCnvBT>MeM=U6FF`&J*G(ECEirTKis)=l-FMrW(#Kf~!H2!KrkgwI(d5=cUB$*#WT3 z{@uNOqpV);@4yDc3J5%wAx-!0I}TWsxO2aYJU~XeUndq6L%*jUHUdpMu3V^P{w;AS zpf_e17rb|IWm#J(S&!lCN=?WlL)#b`+{qg~9u{=(AI)(WW=8Z2aRz@jY4S0)TcN|R zj|^d6)-!A1k+rsV>Cz=BXy6${Y8;IC$TAIXY3-gG`3^VD327F~wC<_1?R_6D*DljC zM>lJQWz$$oJzg}G<*PXGQ&9duoWq^^2QPw299*Uq6BtUXFKOJ4A5 zB^3Tkw{&@3@?@MhO2r*#)ZHwdvD@P2k)+#@0@&7K|7##&$D(#yjmBlHh+#Q?Fk&rr zdAP!sx;;($4B{&u0Z5j0sT;V%txIhR{)uxTGLbBG79}V#5i>Gy44s|P#UJ&a3?GbV zVey;onmMMR>LM@pp9Dk3tj~3oGVPK!fO(2DSGRA>PXn;(ZSC(C`QIY8`HS^>yE<}AFQ{!_8 zAUhYESDxn{!Yylbtf|OhSZ8E4Na6V*k|vUQ?6Meqb+(SQY+|j^3|N;@!eu_5zPJEf zW|1so`;J(f&>znQjO{0=J3=rb!Oq>&xyPDEr@b&aLGegoLHP&#`Un}8{3*gMaEu(y zCvM`R#-gmOm2ZnJO;LQ%&K%e3d3FBZG~M4xayF79&*VYpJel(5IWWX3JaD93E6*B* z6HAy?(1j??U-;65GU%l)Db zQ3nbEc*b^7yf-m!u6Loa`(bZ#T}0cc_nY$Culk&l%m~~CO~Ju*Y5e>1UD{N-SIv|o zIz63kzM}N&)`*bj4Q4WK4975w(~g^^{zO15m)WBkzus}S3sQtp=9tG0bcNeH3oFwk zpc>1F+#}}u&T&>eBjRUQQrv4pQOsvfG2rXo`Fk5axkZ?f2;-jQd}_UeDBcSomCyNM zwz7HWc|S`qu~k>@RqQ^w6@61G+oc<9pqkws&_`)jW((l3N zz`4ZvkjIRXalGH9zx@h_rkfq&?{lM$LFSbyB1G)f?`j016rmLLFK@0IdtLv!Ce?K;b+cE(QZZ;jc@xmId#6?-+?;T)dVuYKJ(bd)1iUnaY(ZVcR#hq zRE7j2HPkWIWRb%F;Qq~87tl@t+_y5P;^*C;>{I0(Da0(ZU5QhTxU#3rG=zx!mPbuu za@YgR6+R?&a^@~jv=sv9ZcZxeC!yBh9+?si{ggKHN#nS1g}s=aM1dXc+W}U5w$;(S zoA9OfPUIQ8lB*9vpo@24lJn!Yz}IabNmmS9`>WlhhsFa+Tbe}-73J?EwqCiHmBeRl zjy5SF0Po!g>U%5^p^<$~=bbo9Ss6FEc<#J+N+bq^J3AAZ0Dh+=Vxj04eJ7bm(mN4z zFL;gOFEx%QpcIz1flJz_#fE%*|LlY+XZR>bx^o}2`~67P*(^9g)?+Gq_Uo6|bAAOP z2c3iBIM);HRYdXYt%XU=kAGmZrg25pm!O%+$>``3+~4r2e4Rpt7EWvyDq&sR z-hQeA;&*8lbt!i0m}H!#Qxtn2j#B=64p!3>(G*aEf+ljdy4kC_U6bt?yAG-Yx1X&*-a<`pR%G{abd5%w@z$YEI;g#I%RhpHZye+KF4;OFCFX6j2|v%}sB&~T z4A|n3BYE)OZA&yX4<@>$X}oRDKxx@jF+(dsZ9?GZPLZj!Sl4+Y>ch-52?O&`!JaY0 z8omJDj&1X!$vRY=NTE@+Z3rQM(E2%&?{%SM1cwLB>R0qTyIU}{e&M{x!Duu>?F9*e zt1^7Wc=-X3`?IXp6E0AvqM;PSIKx#>Ajr2cagQAQ(6_^P_24kJxw*_N}&8= zGMJN7q|0Eii(oIjcXtXWs+1l>CQFSkn)-dC%Y`iG$-kbFVYJ7^{FYXPZ*VYe|Hr%N zU-il$%4lgS{G@8O!4o%gzakFwuWOx*LcrjGRgc7A;K?aSM7 zGF03S<&B=1_?Mwo&B!+~DC;1WTI~-t{g`bSgGX%;1WCS#b{$k!Xyr#d@zBI6B0<l&pG3VorS6CCr4~$;ES?_Z47m9uANQoo&=ZQ-1!AdUGpS z^giGo-7DW;9G3S*nQ8P7c>soJcQjL#XodrHf>zrEN9bk;^ziMOR!=)+?AdeW{*H;H z?$LE`?~$P|Bj!j}&!PGaKF8fk`-2gGJR-w+4Zd?RGZ0F#%OmGa$ez=r*mJ;SwY&<0 zU0PvCqm!DLpv71401j@B(D8soEWnf(c5pv1;RSs=1zl_PN(>eJTqCfMB!8pvs>;TntURw& zk*0(Q9Hyh*7F>vT3zkL&yPU&pw>Z&s!T|RNK(3cblG#*4m2~JIc|co1<+Jx|@mC)! z87~ndNPo2d`beG>&(Z=z8bV+$D$~W?J__*20Gf;+au^HcyUq(7R8q&L$dc4RD_$t_}Z$om^cxiiZ)$_5$9RkeN$w;}FN^zz;bki~YI$ z`4f$`K5KC02noMWDQM(Rb{^jh`}u>2c0vf0;Vq8_OL+l;Lrt(0S8KVK~*&7UdK=}wl6541cZ*x6y()}2W$uP%OEn{tB(5;XN8 zO2J_Tc`p3!)Lhk1ogIs~KqjWLWKyolZ+}Jn&rNdN9~p3Gn!2gVTI6{tN$cwB$rPTW z1(#RMlITEmd5DzlS4k%>VjqAAnk;h(iAcV3m=-$;e$u{cx@TEyq}*l#ai7XV6N696 zbueiNtf`=Xfw=OoH%#9&mf3!4F@bGY=&+l>w6Ev0>Fc5{(?_t7-$byKQzH^+mQ=>P zC~zISf4%FSu@mLeQJZzpsu+4*TI=WmAjOV;hkvS=+I!jC_2uvev`G(r>WK@N#kt{H z^$XrBKF%ynGJ03o!W|!D&@R18c;j6VL-ZwXAxY#-Iyw!!;f_z(k7gL4)!RA7H)cY5 zqd>p=qfMwsz?K?7jqS2--_J+`9ZmT2CAREp zfO0Vs4Y2<%p&@n4I(Mk|SyA3dOgEAyQ8=NdKcrk?c=*P0?x>DAdVXFmKNX)s6I~sU z`xMGphPE ztTd@XyXBlbed$JwD(G-g{-ebdKeq8CG8?w$3y&Nq*BKL%E$!=^I+FV+uJ_p<9!=F? zPW&XnO>vGcp^!rn)Hdc2yo z4h|x+zcQ?Bt@^mpot%7dQ~$D;=7&s`+!b{gfmoX(t%K~ zS1=aY(J-*{J4GZ7{r(x1nlFV&sZmWad>-v>veRA3td&l+#zyn%VF#zNzCkxpsJFv# zqrDO>n`nEa9COrQ5i5}J^(_gPyy}v;P}9*j&3m z_VF^j{Tv=$e?4wsZJ>#gxE8rtmvjx^e6vNLwabK`z)&Rm;#4@{+hJ4j_AjzrW0FhjH$)NPjfwjpV>Ah9`O*D z+KS_1%ZVdRO$OQ}gIWkhSaPOtJ_*>U`_m~3#BL}0I5|Cg2(!N7jZTr#@sl~>`=fqa zJ^vq>Fj}+;nl*L@{>3>2udNA)-OXVa1j%Zedpnz_bi8bKxln#g0eJhcQM8WzyK{I- zdWxt&o!Xyc{`)xGuOnsnOS1FK2@b!vE_`UH0HMXUk%F0&UOq{&F6BSWHZR|`DXph0 zkg-udjCk&9VR0MLK;m_tIu@n;(lgwLTo`ngs z$qJpW?=oyBc@|${oU55E|5eN|oFplfiu@U94A(x_r1kuZAP{Pw1y7kcjSC1okbfM# zoLroKvps6o=AHvH(G>%ebcDBgr}2v#s5r~U<8NWs7C*{`4dK8GOU#}#l~uZz6m!RG zIaJT=5V}|qKuZY|Hb(<^2jpJ>?2K3&tEv0fz@of8bfIdwFHvU2^AdB#%Jy49OLw>$ zeG3C2Ldz=Mn|6sE6kl_f3TjXEVa~p@uXSvFuYHjJHDcs&Om&AXQIx>-JF>{^M{i%& zKeDabT^?d2pGo(Mg$A7ad2kpQhm}SRv`4omdp-ue;1C0DdF3fE;`;~_O_7O^w7f3T zq!;g_9R&~$1?XQ~69+Vz=lSX^qjYs$+^{x7REEC0^{rDxU3)K-bB-Wkovs-o#cXi_ zfmRrwWfR*!%|4(JYR_JY02~><-bcv0^xVL)>RU|3Z-@!0db8E<6UKK<%Y_~Bf7WC@ zuN@rDm!rLF&@+P6zpoo$3DB~pN~rnsT|paDm;0O_9^Lm<*Sm8qVFS!-ExpMMyJ5sR zt(JMmy>uyIjuo}1FMrzV$4Q=J{% zv7tDO0=mz|%@^v$3oPQVEX-})zWMP8n1G!$c%uUk?W~$0Ui4plO;f9cpB9h!YpPR> zHbioGM#{mh=xB{EREYq2&hROj>+`j@v-kDWM=B29$_kb1*b)1iw_+-(3X^J5sYABV z;@64s-#$-m%lNOio7gFxen7Q~LwN*E%LP07eAViN879k7!kOg??-SF}ko+-enMJO_ zSvCyhqG787P7cW^TmEl$fyk?G-xdSvzQcANh3%wAcLdGcYRN*4|5@3DL!I4Xb9qX~ zdt`&0Zdl8fFStP53T%8$WI$KI@U5Ll_ts{%f$aS$dNtSA&*EfyREe6hnBg9mQyORR zHFgC{F%sFkZ zGBoCnt4?HVI%}n?4VNS9PudxPj>^i;svQ%uaT6@$tqUSs?G-K} zv!j6zO%-B6E0xV^(&~e68)?c*@%2{mi<@biLmXmKuu`!QQpK$KHnVY4%m{Rx%$aW- zkpj2$|Mpw-5lH55N1Odm<|^-xVbZZFd@JAm~cd1yV0#c{Mz-> z4(V@2YBKm+x_=oY#XaL>;gKPg8n1>->#Ikk&+n-cO+UYxRNlv4dPh6daI*>nH}Zxwu8EP0VPFW=P^dwUCRhMxQ>YQLcIL!2Sw zXX=y&ZTg<5e3hv+<;~#BUKS2W?~5&cAxLmB0TOvm%Pp2~VYmvC((&i3rIfK(vyd~3Ax_ZN59w(+AE zb*v6cb>SaF6q8`?(J&r*F|>EF(v7!9($it)OZPri_71yOd)rysQz1s9Zaa$S`0xQZ zzu++FE)g5JKQ48h?1qD2B-oZ8zDQ5YyoJu1*p!=hZbuuH2A$#B%v&I1da(9cB8>Q# zh9pQQ0ND0f+>Ptro{F}LgrlGY>w+k$tf^ORHAE>>J7D}%p<^kr;cs^Oo=C+3bF$D~ z9+X?&L|51Qv$*G!CDwM$$Egn}L1qIqL<)BEb*3Th^X&97^J%!NvYu$8Z8P`9rC z)j4lScIu!2XO%N>33fDK1>?Sey#e->Cep&j)+bAiA1QwmGj?W3=%^Si`)$KwSaTaH zj`-+q8trV?)MaQg&nq7=v5l73CJRIV>-wk{NdN(mBR-wZ>vux#nX5j1$Qp=P(YjtG z(lkX>@1xZ$ak*OZD@LFeC;voj^^7uN{`xz>19qSaU{WiY#$#ID9#hDFGo>~~$8 z7_!A*i_ia8qV&2Z-+)hC2tPPx8a5XA8MfYw6QunKH|&=J}`FXJbmp+ z8>%D+-~%*JK*-&lr$WmkXQzhBe@{=x;MviFhxgHTQlO+#QcBuqDG@X!7tMAb!jjn? zFXP{~qdjUmoPQ#Gv&kFv=#_NC?G9__)rUVYZ%h}LE^L1TT~lTw?ay~i?hEG`?f3}`3ySls#bXf8&NoQBb~{b7 zap2KHNdy8YhO)Hp&w)`zaEZRFr5#?ee396Tx-C44v{dB;lo+GYo|K@rWLcx z!KW1P#>D>A>Y`h3?Te~+$ePeMmMr)%r~`y~IINd={1y;fo;@QTH7) zZk|TdpYlfexIIK&T6fT=My?M*te#kw{R-+fwQN?GyBoZ9CMWOhq0cTKV-Hmf{eUZe zisJP2kNCpf8LKexZyKG=hkv}azM9(l&{RfP+*5pmct7HhQfqgj7WL@KuI_B_NrT5{ zj{+%z)X~j&0)AiQbHjX?v8>mq?)a$8=X~g@J3l?CF}^29~I8B@Au?4JXF!3B zRzjp377&nLx)!)E-+TXn`#ksVPw&poJ9EyQm{T*`E@J9?;8ah|x%(|$WXB9vj2QFu z{V`gkSTy;qeu-tuDQSE}(X4u&CXbZt6A!%kK>SuN=*shu$4F|v7>gZeOu71 zv2e*Dd6^abI<_McSAz4-9^FAuho8ICRb;ICf^Bh_Q#GRz3@SjgT0Sx8+eb#`)hk1$ zT>PH1e+B-CuzV7O>FbiSu-Na}!-76gm9S9&{(b7|KOlebr3=SM$yC*&VNWSTq4F$Y z@Q`Ba7ca)ftaBIdC;)O+eBQoUTO+V#(1a2Wm4~*~>&p|N5(gAsu$80finN75Hy&Y3 zu;&Q3)nx!@Shr*<+YiBlg+=O@QCHzhd?P0#Qza;u=&lu_42Q};kD+l7>hE5ZSkz!) zN%R+3p$A?hk~r%E|M&PbU%WK zO?X=nAqb*xU*~@X5+p_q6#Bb$^L}l?SXkO;q%ElAha9x+L6{V&eTnRJ{sUil@i_RA zi{5|tZG|=*m4LAhSmVi6jcF^PM*$?dFru)36T?AZq{XU$8IPgZFJRAMbC?5VXrzj^ zv%-g3E+FrDVO=w}t#Iyd9B{F+^Cgn1z0NCv1nqMSXKrk=Pk9oAXwzQMA0%!Ej@M)$ zbMWUc*s6CgPS4mEZ+L*biT#^sDTZJzi2Mt-m2`OP(73mdlnXu$Sc={Gf=M0vJ;&bSV=5Q$s=a>4hJ@f#;AHXc}q{)+{}ZxGI&feH|UC zGEy9CZmtMnY9BH^S&#c*49_V7>stLMeBj*wGkLz|=OZnI(fz8%8^CIJN@zjy)cKIw z8s>WAP>@NA4of^J^IFY;%KJMyRc!lS2)o6ry?F6AIgrSpy!F2C`PFu-6hp(R6It7< zd=?gW?OQ+^zh~ke37PpgPt9T}SPtY`SD1xmv4-NpJl9MSB+Y+M!(q*cAUpfqNELnE zm9m04_n}iA3PAYx2%PheA%um6bBuU_K)UoB7@UnT17MZNU5dvnEC+kl7a9**uJwVR zDWq-`7ra%*_j8&MxTT;C9nh3O4IgG1jx14KvCfMZOO`k&8f4WyMrGQ|CsWU1`mlLj zkiM?=P!~u;@gX?M(`dcBw&Ocl;y_h!AgDz5_d$S{r(_JARyIr9nZhRK?wkD`DJ#5q3lSa+2y6?HwndK;X6QY6-}lgB?)S`c%v6q% zlR+%h&;*%Fqo;?jP|~Qx_wplP0h)g>F(Tl}6~uyC5Rf3ECVq*-0c|dc0_mdj(c>jb z0M7oX!%vu5-UFDxNcbmpYlY-M;#=6E&kJAT)!rgOUfokigDji`U)rHiba|!W#S0l0S&Ls0Aj}J8e zur+AUiJ4`r)&wkD{f;0FhwYD(%8!Vm0md%LK~VW5t_32`5&dusZsJoQ@7rOADmb3X z*epU3p#_KTQGt@T+jpmi)Z(TDg;8Xc8Oe&U)uG1+etC+a=rU(-K=Do|m2aa$X09#d zeT2%#FW;oD@G8(0Y2Rz7j^__5!3Z^yIJ24){7&-VT?EDifg98a%k9YL?lmlag;((t zCg^9mwchLPa%QES;AmXZu0E^_K@=vT`≫heq2oDu#IBa<<}NBeAauibbMI#_ftb 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 From da70152d08be5c0e1213f0980a6ee83126ab8759 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Nov 2024 21:41:56 +0000 Subject: [PATCH 179/236] build: (deps): bump samuelmeuli/action-snapcraft from 2 to 3 Bumps [samuelmeuli/action-snapcraft](https://github.com/samuelmeuli/action-snapcraft) from 2 to 3. - [Release notes](https://github.com/samuelmeuli/action-snapcraft/releases) - [Commits](https://github.com/samuelmeuli/action-snapcraft/compare/v2...v3) --- updated-dependencies: - dependency-name: samuelmeuli/action-snapcraft dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d471abb4f..aed76cd8e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -191,7 +191,7 @@ jobs: - name: Check out Git repository uses: actions/checkout@v4 - name: Install Snapcraft - uses: samuelmeuli/action-snapcraft@v2 + uses: samuelmeuli/action-snapcraft@v3 - name: Get Tag Name id: tag_name run: echo "::set-output name=tag::$(echo ${GITHUB_REF#refs/tags/})" From 9fab7630a39429802030b98d4d51b4e1c1263d46 Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 15 Nov 2024 07:50:19 +0100 Subject: [PATCH 180/236] chore: Follow up send file dialog --- assets/l10n/intl_en.arb | 2 +- lib/pages/chat/send_file_dialog.dart | 41 ++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 173c33bf4..c3eae0523 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2806,5 +2806,5 @@ "name": "Name", "version": "Version", "website": "Website", - "compressBeforeSending": "Compress before sending" + "sendUncompressed": "Send uncompressed" } diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index 95850e09c..ce3ae7ecf 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -149,7 +149,7 @@ class SendFileDialogState extends State { var sendStr = L10n.of(context).sendFile; final uniqueMimeType = widget.files - .map((file) => file.mimeType ?? lookupMimeType(file.path)) + .map((file) => file.mimeType ?? lookupMimeType(file.name)) .toSet() .singleOrNull; @@ -250,23 +250,42 @@ class SendFileDialogState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - CupertinoSwitch( - value: compress, - onChanged: uniqueMimeType.startsWith('video') && - !PlatformInfos.isMobile - ? null - : (v) => setState(() => compress = v), - ), + if ({TargetPlatform.iOS, TargetPlatform.macOS} + .contains(theme.platform)) + CupertinoSwitch( + value: !compress, + onChanged: uniqueMimeType.startsWith('video') && + !PlatformInfos.isMobile + ? null + : (v) => setState(() => compress = !v), + ) + else + Switch.adaptive( + value: !compress, + onChanged: uniqueMimeType.startsWith('video') && + !PlatformInfos.isMobile + ? null + : (v) => setState(() => compress = !v), + ), const SizedBox(width: 16), Expanded( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + L10n.of(context).sendUncompressed, + style: theme.textTheme.titleMedium, + textAlign: TextAlign.left, + ), + ], + ), Text( - L10n.of(context).compressBeforeSending, - style: theme.textTheme.labelMedium, - textAlign: TextAlign.left, + ' ($sizeString)', + style: theme.textTheme.labelSmall, ), ], ), From 22023450d5fdb8acb8cd9b406a2cd1c8b18c49c4 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Fri, 15 Nov 2024 16:27:26 +0100 Subject: [PATCH 181/236] feat: Add markdown context actions for text input --- assets/l10n/intl_en.arb | 8 +- lib/pages/chat/input_bar.dart | 3 + lib/utils/markdown_context_builder.dart | 102 ++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 lib/utils/markdown_context_builder.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index c3eae0523..6bba787fc 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2806,5 +2806,11 @@ "name": "Name", "version": "Version", "website": "Website", - "sendUncompressed": "Send uncompressed" + "sendUncompressed": "Send uncompressed", + "boldText": "Bold text", + "italicText": "Italic text", + "strikeThrough": "Strikethrough", + "pleaseFillOut": "Please fill out", + "invalidUrl": "Invalid url", + "addLink": "Add link" } diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 1d34c7055..f203c3d41 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -9,6 +9,7 @@ import 'package:pasteboard/pasteboard.dart'; import 'package:slugify/slugify.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/utils/markdown_context_builder.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import '../../widgets/avatar.dart'; @@ -456,6 +457,8 @@ class InputBar extends StatelessWidget { builder: (context, controller, focusNode) => TextField( controller: controller, focusNode: focusNode, + contextMenuBuilder: (c, e) => + markdownContextBuilder(c, e, controller), contentInsertionConfiguration: ContentInsertionConfiguration( onContentInserted: (KeyboardInsertedContent content) { final data = content.data; diff --git a/lib/utils/markdown_context_builder.dart b/lib/utils/markdown_context_builder.dart new file mode 100644 index 000000000..bf33f948b --- /dev/null +++ b/lib/utils/markdown_context_builder.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; + +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +Widget markdownContextBuilder( + BuildContext context, + EditableTextState editableTextState, + TextEditingController controller, +) { + final value = editableTextState.textEditingValue; + final selectedText = value.selection.textInside(value.text); + final buttonItems = editableTextState.contextMenuButtonItems; + final l10n = L10n.of(context); + + return AdaptiveTextSelectionToolbar.buttonItems( + anchors: editableTextState.contextMenuAnchors, + buttonItems: [ + ...buttonItems, + if (selectedText.isNotEmpty) ...[ + ContextMenuButtonItem( + label: l10n.link, + onPressed: () async { + final input = await showTextInputDialog( + context: context, + title: l10n.addLink, + okLabel: l10n.ok, + cancelLabel: l10n.cancel, + textFields: [ + DialogTextField( + validator: (text) { + if (text == null || text.isEmpty) { + return l10n.pleaseFillOut; + } + try { + text.startsWith('http') + ? Uri.parse(text) + : Uri.https(text); + } catch (_) { + return l10n.invalidUrl; + } + return null; + }, + hintText: 'www...', + keyboardType: TextInputType.url, + ), + ], + ); + final urlString = input?.singleOrNull; + if (urlString == null) return; + final url = urlString.startsWith('http') + ? Uri.parse(urlString) + : Uri.https(urlString); + final selection = controller.selection; + controller.text = controller.text.replaceRange( + selection.start, + selection.end, + '[$selectedText](${url.toString()})', + ); + ContextMenuController.removeAny(); + }, + ), + ContextMenuButtonItem( + label: l10n.boldText, + onPressed: () { + final selection = controller.selection; + controller.text = controller.text.replaceRange( + selection.start, + selection.end, + '**$selectedText**', + ); + ContextMenuController.removeAny(); + }, + ), + ContextMenuButtonItem( + label: l10n.italicText, + onPressed: () { + final selection = controller.selection; + controller.text = controller.text.replaceRange( + selection.start, + selection.end, + '*$selectedText*', + ); + ContextMenuController.removeAny(); + }, + ), + ContextMenuButtonItem( + label: l10n.strikeThrough, + onPressed: () { + final selection = controller.selection; + controller.text = controller.text.replaceRange( + selection.start, + selection.end, + '~~$selectedText~~', + ); + ContextMenuController.removeAny(); + }, + ), + ], + ], + ); +} From 2c5d6e4851e1cf9de004e7258c309e10deaf702c Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 17 Nov 2024 10:50:51 +0100 Subject: [PATCH 182/236] build: Update flutter to 3.24.5 --- .github/workflows/versions.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/versions.env b/.github/workflows/versions.env index 6519d6b5c..2049a048e 100644 --- a/.github/workflows/versions.env +++ b/.github/workflows/versions.env @@ -1,2 +1,2 @@ -FLUTTER_VERSION=3.24.3 +FLUTTER_VERSION=3.24.5 JAVA_VERSION=17 From 325431c8f668d376f1ff5976c79116dfc115aef4 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 17 Nov 2024 11:36:59 +0100 Subject: [PATCH 183/236] build: Remove snapcraft build workaround --- snap/snapcraft.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 3e654baa1..096a1353c 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -73,10 +73,6 @@ parts: fluffychat: plugin: flutter source: . - override-build: | - # Workaround for Flutter build error: - rm -rf build - craftctl default build-packages: - libjsoncpp-dev - curl From 5fc75ac307c1b10124f612968c5541770337520f Mon Sep 17 00:00:00 2001 From: Krille Date: Tue, 19 Nov 2024 12:15:13 +0100 Subject: [PATCH 184/236] chore: Better error message when join room failed --- assets/l10n/intl_en.arb | 3 ++- lib/utils/localized_exception_extension.dart | 6 ++++++ lib/widgets/future_loading_dialog.dart | 11 +++++------ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 6bba787fc..c8da22f32 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2812,5 +2812,6 @@ "strikeThrough": "Strikethrough", "pleaseFillOut": "Please fill out", "invalidUrl": "Invalid url", - "addLink": "Add link" + "addLink": "Add link", + "unableToJoinChat": "Unable to join chat. Maybe the other party has already closed the conversation." } diff --git a/lib/utils/localized_exception_extension.dart b/lib/utils/localized_exception_extension.dart index 4716dcbdb..8bd493246 100644 --- a/lib/utils/localized_exception_extension.dart +++ b/lib/utils/localized_exception_extension.dart @@ -101,6 +101,11 @@ extension LocalizedExceptionExtension on Object { } if (this is String) return toString(); if (this is UiaException) return toString(); + + if (exceptionContext == ExceptionContext.joinRoom) { + return L10n.of(context).unableToJoinChat; + } + Logs().w('Something went wrong: ', this); return L10n.of(context).oopsSomethingWentWrong; } @@ -110,4 +115,5 @@ enum ExceptionContext { changePassword, checkHomeserver, checkServerSupportInfo, + joinRoom, } diff --git a/lib/widgets/future_loading_dialog.dart b/lib/widgets/future_loading_dialog.dart index 11b867dbf..4b5a44478 100644 --- a/lib/widgets/future_loading_dialog.dart +++ b/lib/widgets/future_loading_dialog.dart @@ -17,9 +17,9 @@ Future> showFutureLoadingDialog({ required Future Function() future, String? title, String? backLabel, - String Function(dynamic exception)? onError, bool barrierDismissible = false, bool delay = true, + ExceptionContext? exceptionContext, }) async { final futureExec = future(); final resultFuture = ResultFuture(futureExec); @@ -44,7 +44,7 @@ Future> showFutureLoadingDialog({ future: futureExec, title: title, backLabel: backLabel, - onError: onError, + exceptionContext: exceptionContext, ), ); return result ?? @@ -58,14 +58,14 @@ class LoadingDialog extends StatefulWidget { final String? title; final String? backLabel; final Future future; - final String Function(dynamic exception)? onError; + final ExceptionContext? exceptionContext; const LoadingDialog({ super.key, required this.future, this.title, - this.onError, this.backLabel, + this.exceptionContext, }); @override LoadingDialogState createState() => LoadingDialogState(); @@ -91,8 +91,7 @@ class LoadingDialogState extends State { Widget build(BuildContext context) { final exception = this.exception; final titleLabel = exception != null - ? widget.onError?.call(exception) ?? - exception.toLocalizedString(context) + ? exception.toLocalizedString(context, widget.exceptionContext) : widget.title ?? L10n.of(context).loadingPleaseWait; return AlertDialog.adaptive( From b6668cee6f56de4b8ab8071c0cd8ee66ae0da718 Mon Sep 17 00:00:00 2001 From: Krille Date: Tue, 19 Nov 2024 12:31:45 +0100 Subject: [PATCH 185/236] chore: Follow up join room --- lib/pages/chat/chat_view.dart | 2 ++ lib/pages/chat_list/chat_list.dart | 1 + 2 files changed, 3 insertions(+) diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 30a9687a4..5ce433180 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -1,5 +1,6 @@ import 'dart:ui' as ui; +import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:flutter/material.dart'; import 'package:badges/badges.dart'; @@ -135,6 +136,7 @@ class ChatView extends StatelessWidget { showFutureLoadingDialog( context: context, future: () => controller.room.join(), + exceptionContext: ExceptionContext.joinRoom, ); } final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0; diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 22868fa54..553c979df 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -170,6 +170,7 @@ class ChatListController extends State await room.join(); await waitForRoom; }, + exceptionContext: ExceptionContext.joinRoom, ); if (joinResult.error != null) return; } From 18d4a5d39ac276bd6b5e0a1926f402fffc900bea Mon Sep 17 00:00:00 2001 From: Krille Date: Tue, 19 Nov 2024 12:51:48 +0100 Subject: [PATCH 186/236] chore: Make error dialog show full error --- lib/widgets/future_loading_dialog.dart | 28 ++++++-------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/lib/widgets/future_loading_dialog.dart b/lib/widgets/future_loading_dialog.dart index 4b5a44478..80804f692 100644 --- a/lib/widgets/future_loading_dialog.dart +++ b/lib/widgets/future_loading_dialog.dart @@ -95,30 +95,14 @@ class LoadingDialogState extends State { : widget.title ?? L10n.of(context).loadingPleaseWait; return AlertDialog.adaptive( + title: Icon( + Icons.error_outline_outlined, + color: Theme.of(context).colorScheme.error, + size: 48, + ), content: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 256), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (exception == null) - const CircularProgressIndicator.adaptive() - else - Icon( - Icons.error_outline_outlined, - color: Theme.of(context).colorScheme.error, - size: 48, - ), - const SizedBox(width: 20), - Expanded( - child: Text( - titleLabel, - maxLines: 2, - textAlign: TextAlign.left, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), + child: Text(titleLabel), ), actions: exception == null ? null From 46d62fdf11d596096675c4e0a5d12049733da170 Mon Sep 17 00:00:00 2001 From: Krille Date: Tue, 19 Nov 2024 13:16:40 +0100 Subject: [PATCH 187/236] chore: Follow up loading dialog --- lib/widgets/future_loading_dialog.dart | 29 ++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/widgets/future_loading_dialog.dart b/lib/widgets/future_loading_dialog.dart index 80804f692..405b8335e 100644 --- a/lib/widgets/future_loading_dialog.dart +++ b/lib/widgets/future_loading_dialog.dart @@ -95,14 +95,31 @@ class LoadingDialogState extends State { : widget.title ?? L10n.of(context).loadingPleaseWait; return AlertDialog.adaptive( - title: Icon( - Icons.error_outline_outlined, - color: Theme.of(context).colorScheme.error, - size: 48, - ), + title: exception == null + ? null + : Icon( + Icons.error_outline_outlined, + color: Theme.of(context).colorScheme.error, + size: 48, + ), content: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 256), - child: Text(titleLabel), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (exception == null) ...[ + const CircularProgressIndicator.adaptive(), + const SizedBox(width: 20), + ], + Expanded( + child: Text( + titleLabel, + textAlign: exception == null ? TextAlign.left : null, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), ), actions: exception == null ? null From 3d7cd800b9842189bb5f83563f776f08e8046e7e Mon Sep 17 00:00:00 2001 From: Krille Date: Tue, 19 Nov 2024 13:32:15 +0100 Subject: [PATCH 188/236] chore: Follow up loading dialog --- lib/widgets/future_loading_dialog.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/widgets/future_loading_dialog.dart b/lib/widgets/future_loading_dialog.dart index 405b8335e..feffd3b3e 100644 --- a/lib/widgets/future_loading_dialog.dart +++ b/lib/widgets/future_loading_dialog.dart @@ -114,6 +114,7 @@ class LoadingDialogState extends State { Expanded( child: Text( titleLabel, + maxLines: 4, textAlign: exception == null ? TextAlign.left : null, overflow: TextOverflow.ellipsis, ), From b440d564013992c773c67a6a6a586590e357928a Mon Sep 17 00:00:00 2001 From: krille-chan Date: Tue, 19 Nov 2024 17:11:46 +0100 Subject: [PATCH 189/236] build: Snapcraft from local build file --- .github/workflows/main_deploy.yaml | 23 +++++++++++++++++++++++ snap/snapcraft.yaml | 8 +++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main_deploy.yaml b/.github/workflows/main_deploy.yaml index 1a817f006..4a8e0f39d 100644 --- a/.github/workflows/main_deploy.yaml +++ b/.github/workflows/main_deploy.yaml @@ -80,3 +80,26 @@ jobs: bundle update fastlane bundle exec fastlane deploy_internal_test cd .. + + deploy_snapcraft: + strategy: + matrix: + arch: [ x64, arm64 ] + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }} + runs-on: ${{ matrix.arch == 'arm64' && 'self-hosted' || 'ubuntu-latest'}} + steps: + - uses: actions/checkout@v4 + - run: cat .github/workflows/versions.env >> $GITHUB_ENV + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install git wget curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 libssl-dev libwebkit2gtk-4.1-dev -y + - name: Install Flutter + run: | + git clone --branch ${{ env.FLUTTER_VERSION }} https://github.com/flutter/flutter.git + ./flutter/bin/flutter doctor + - run: ./flutter/bin/flutter pub get + - run: ./flutter/bin/flutter build linux --target-platform linux-${{ matrix.arch }} --release + - name: Install Snapcraft + uses: samuelmeuli/action-snapcraft@v3 + - run: snapcraft + - run: snapcraft push ./*.snap \ No newline at end of file diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 096a1353c..55d76a165 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -71,11 +71,9 @@ parts: - zenity-integration fluffychat: - plugin: flutter - source: . - build-packages: - - libjsoncpp-dev - - curl + plugin: dump + source: ./build/linux/*/release + source-type: local stage-packages: - libsecret-1-dev - libjsoncpp-dev From 56f6f7cd220f7426da795a5a9648585033728457 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Tue, 19 Nov 2024 17:21:12 +0100 Subject: [PATCH 190/236] chore: Follow up build snap --- .github/workflows/main_deploy.yaml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main_deploy.yaml b/.github/workflows/main_deploy.yaml index 4a8e0f39d..eeaa5fcba 100644 --- a/.github/workflows/main_deploy.yaml +++ b/.github/workflows/main_deploy.yaml @@ -85,8 +85,6 @@ jobs: strategy: matrix: arch: [ x64, arm64 ] - env: - SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }} runs-on: ${{ matrix.arch == 'arm64' && 'self-hosted' || 'ubuntu-latest'}} steps: - uses: actions/checkout@v4 @@ -99,7 +97,8 @@ jobs: ./flutter/bin/flutter doctor - run: ./flutter/bin/flutter pub get - run: ./flutter/bin/flutter build linux --target-platform linux-${{ matrix.arch }} --release - - name: Install Snapcraft - uses: samuelmeuli/action-snapcraft@v3 - - run: snapcraft - - run: snapcraft push ./*.snap \ No newline at end of file + - uses: snapcore/action-build@v1 + id: build + - uses: snapcore/action-publish@v1 + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }} \ No newline at end of file From 12bb71708e4dd28ade2ecc5e0faf3689cced5183 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Wed, 20 Nov 2024 05:50:20 +0100 Subject: [PATCH 191/236] chore: Follow up snapcraft in ci --- .github/workflows/main_deploy.yaml | 1 + snap/snapcraft.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main_deploy.yaml b/.github/workflows/main_deploy.yaml index eeaa5fcba..69f1b71b0 100644 --- a/.github/workflows/main_deploy.yaml +++ b/.github/workflows/main_deploy.yaml @@ -97,6 +97,7 @@ jobs: ./flutter/bin/flutter doctor - run: ./flutter/bin/flutter pub get - run: ./flutter/bin/flutter build linux --target-platform linux-${{ matrix.arch }} --release + - run: mv ./build/linux/${{ matrix.arch }}/release ./build/linux/ - uses: snapcore/action-build@v1 id: build - uses: snapcore/action-publish@v1 diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 55d76a165..0eb4305bc 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -72,7 +72,7 @@ parts: fluffychat: plugin: dump - source: ./build/linux/*/release + source: ./build/linux/release source-type: local stage-packages: - libsecret-1-dev From d8cf6dfb13a766e68d97b13a366f2452929c941c Mon Sep 17 00:00:00 2001 From: krille-chan Date: Wed, 20 Nov 2024 06:22:14 +0100 Subject: [PATCH 192/236] build: Revert build snapcraft changes --- .github/workflows/main_deploy.yaml | 23 ----------------------- snap/snapcraft.yaml | 18 ++++++++++-------- 2 files changed, 10 insertions(+), 31 deletions(-) diff --git a/.github/workflows/main_deploy.yaml b/.github/workflows/main_deploy.yaml index 69f1b71b0..1a817f006 100644 --- a/.github/workflows/main_deploy.yaml +++ b/.github/workflows/main_deploy.yaml @@ -80,26 +80,3 @@ jobs: bundle update fastlane bundle exec fastlane deploy_internal_test cd .. - - deploy_snapcraft: - strategy: - matrix: - arch: [ x64, arm64 ] - runs-on: ${{ matrix.arch == 'arm64' && 'self-hosted' || 'ubuntu-latest'}} - steps: - - uses: actions/checkout@v4 - - run: cat .github/workflows/versions.env >> $GITHUB_ENV - - name: Install dependencies - run: sudo apt-get update && sudo apt-get install git wget curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 libssl-dev libwebkit2gtk-4.1-dev -y - - name: Install Flutter - run: | - git clone --branch ${{ env.FLUTTER_VERSION }} https://github.com/flutter/flutter.git - ./flutter/bin/flutter doctor - - run: ./flutter/bin/flutter pub get - - run: ./flutter/bin/flutter build linux --target-platform linux-${{ matrix.arch }} --release - - run: mv ./build/linux/${{ matrix.arch }}/release ./build/linux/ - - uses: snapcore/action-build@v1 - id: build - - uses: snapcore/action-publish@v1 - env: - SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }} \ No newline at end of file diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 0eb4305bc..a670a2a72 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -3,10 +3,6 @@ title: FluffyChat base: core24 version: git license: AGPL-3.0 -website: https://fluffychat.im -source-code: https://github.com/krille-chan/fluffychat -issues: https://github.com/krille-chan/fluffychat/issues -donation: https://ko-fi.com/krille summary: The cutest messenger in the Matrix network description: | FluffyChat is an open source, nonprofit and cute matrix messenger app. The app is easy to use but secure and decentralized. @@ -71,14 +67,20 @@ parts: - zenity-integration fluffychat: - plugin: dump - source: ./build/linux/release - source-type: local + plugin: flutter + source: . + override-build: | + # Workaround for Flutter build error: + rm -rf build + craftctl default + build-packages: + - libjsoncpp-dev + - curl stage-packages: - libsecret-1-dev - libjsoncpp-dev - libssl-dev - - libwebkit2gtk-4.1-dev + #- libwebkit2gtk-4.1-dev slots: dbus-svc: From 1c5c491ceb76ddbded320e7453866ddfc5e048ae Mon Sep 17 00:00:00 2001 From: Krille Date: Wed, 20 Nov 2024 15:48:17 +0100 Subject: [PATCH 193/236] build: Try downgrading flutter web auth --- lib/pages/chat/chat_view.dart | 2 +- .../homeserver_picker/homeserver_picker.dart | 2 +- linux/flutter/generated_plugin_registrant.cc | 4 ---- linux/flutter/generated_plugins.cmake | 1 - macos/Flutter/GeneratedPluginRegistrant.swift | 2 -- pubspec.lock | 16 ++++------------ pubspec.yaml | 2 +- windows/flutter/generated_plugin_registrant.cc | 3 --- windows/flutter/generated_plugins.cmake | 1 - 9 files changed, 7 insertions(+), 26 deletions(-) diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 5ce433180..73d85fe3d 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -1,6 +1,5 @@ import 'dart:ui' as ui; -import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:flutter/material.dart'; import 'package:badges/badges.dart'; @@ -18,6 +17,7 @@ import 'package:fluffychat/pages/chat/pinned_events.dart'; import 'package:fluffychat/pages/chat/reactions_picker.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; import 'package:fluffychat/utils/account_config.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; import 'package:fluffychat/widgets/connection_status_header.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index 277009951..a50083f7a 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -169,7 +169,7 @@ class HomeserverPickerController extends State { final result = await FlutterWebAuth2.authenticate( url: url.toString(), callbackUrlScheme: urlScheme, - options: FlutterWebAuth2Options(useWebview: !isDefaultPlatform), + options: const FlutterWebAuth2Options(), ); final token = Uri.parse(result).queryParameters['loginToken']; if (token?.isEmpty ?? false) return; diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index a3aeaed6f..b5155de25 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,7 +7,6 @@ #include "generated_plugin_registrant.h" #include -#include #include #include #include @@ -24,9 +23,6 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) desktop_drop_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); - g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); - desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); g_autoptr(FlPluginRegistrar) dynamic_color_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 6c1d6283d..dab6fedfc 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_drop - desktop_webview_window dynamic_color emoji_picker_flutter file_selector_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 3a4881965..50894732e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,7 +8,6 @@ import Foundation import appkit_ui_element_colors import audio_session import desktop_drop -import desktop_webview_window import device_info_plus import dynamic_color import emoji_picker_flutter @@ -40,7 +39,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppkitUiElementColorsPlugin.register(with: registry.registrar(forPlugin: "AppkitUiElementColorsPlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) - DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin")) diff --git a/pubspec.lock b/pubspec.lock index b60163d46..7725343a7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -294,14 +294,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.3" - desktop_webview_window: - dependency: transitive - description: - name: desktop_webview_window - sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0" - url: "https://pub.dev" - source: hosted - version: "0.2.3" device_info_plus: dependency: "direct main" description: @@ -759,18 +751,18 @@ packages: dependency: "direct main" description: name: flutter_web_auth_2 - sha256: "8f59c9fa71b5affb322cb7103b836cd0ced89c9c50c66f82b523b7d339018dc3" + sha256: "4d3d2fd3d26bf1a26b3beafd4b4b899c0ffe10dc99af25abc58ffe24e991133c" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "3.1.2" flutter_web_auth_2_platform_interface: dependency: transitive description: name: flutter_web_auth_2_platform_interface - sha256: "222264d4979e9372c90e441736a62d800481e4a9c860cc2c235d1d605a118a2b" + sha256: e8669e262005a8354389ba2971f0fc1c36188481234ff50d013aaf993f30f739 url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "3.1.0" flutter_web_plugins: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 37fa55d17..ce1b0ee05 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,7 +49,7 @@ dependencies: flutter_shortcuts: git: https://github.com/krille-chan/flutter_shortcuts.git flutter_typeahead: ^5.2.0 - flutter_web_auth_2: ^4.0.1 + flutter_web_auth_2: ^3.1.1 flutter_webrtc: ^0.11.7 geolocator: ^13.0.1 go_router: ^14.3.0 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 83da0badc..eb0b6129f 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,7 +7,6 @@ #include "generated_plugin_registrant.h" #include -#include #include #include #include @@ -25,8 +24,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { DesktopDropPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopDropPlugin")); - DesktopWebviewWindowPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); EmojiPickerFlutterPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 24fc8c78d..627fe7248 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_drop - desktop_webview_window dynamic_color emoji_picker_flutter file_selector_windows From a493020fa86a4b76cb2d6523000b03d8168d82f5 Mon Sep 17 00:00:00 2001 From: Krille Date: Wed, 20 Nov 2024 16:13:40 +0100 Subject: [PATCH 194/236] chore: add hint in pubspec.yaml regarding flutter_web_auth_2 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index ce1b0ee05..33df8aac0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,7 +49,7 @@ dependencies: flutter_shortcuts: git: https://github.com/krille-chan/flutter_shortcuts.git flutter_typeahead: ^5.2.0 - flutter_web_auth_2: ^3.1.1 + flutter_web_auth_2: ^3.1.1 # Version 4 blocked by https://github.com/MixinNetwork/flutter-plugins/issues/379 flutter_webrtc: ^0.11.7 geolocator: ^13.0.1 go_router: ^14.3.0 From a49540d3e72083abc451f9ae4da1827269002b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Mon, 11 Nov 2024 13:26:10 +0000 Subject: [PATCH 195/236] Translated using Weblate (Estonian) Currently translated at 100.0% (688 of 688 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/ --- assets/l10n/intl_et.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_et.arb b/assets/l10n/intl_et.arb index 6b790d471..aab9eea30 100644 --- a/assets/l10n/intl_et.arb +++ b/assets/l10n/intl_et.arb @@ -2867,5 +2867,7 @@ "placeholders": { "homeserver": {} } - } + }, + "compressBeforeSending": "Paki enne saatmist kokku", + "@compressBeforeSending": {} } From 20d9741fe46a3d1abf341b40c66bd0cc4e8fe7a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jos=C3=A9=20m?= Date: Mon, 11 Nov 2024 08:28:26 +0000 Subject: [PATCH 196/236] Translated using Weblate (Galician) Currently translated at 100.0% (688 of 688 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/gl/ --- assets/l10n/intl_gl.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_gl.arb b/assets/l10n/intl_gl.arb index 674ae4b52..20614bb38 100644 --- a/assets/l10n/intl_gl.arb +++ b/assets/l10n/intl_gl.arb @@ -2867,5 +2867,7 @@ "website": "Páxina web", "@website": {}, "continueText": "Continuar", - "@continueText": {} + "@continueText": {}, + "compressBeforeSending": "Comprimir antes de enviar", + "@compressBeforeSending": {} } From 97d47dcb4c23df19b68bca70ceb81d5b34236c6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=8E=8B=E5=8F=AB=E6=88=91=E6=9D=A5=E5=B7=A1?= =?UTF-8?q?=E5=B1=B1?= Date: Tue, 12 Nov 2024 00:26:05 +0000 Subject: [PATCH 197/236] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (688 of 688 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/zh_Hans/ --- assets/l10n/intl_zh.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_zh.arb b/assets/l10n/intl_zh.arb index ffd7ddf4f..30e30d3a0 100644 --- a/assets/l10n/intl_zh.arb +++ b/assets/l10n/intl_zh.arb @@ -2867,5 +2867,7 @@ "serverInformation": "服务器信息:", "@serverInformation": {}, "website": "网站", - "@website": {} + "@website": {}, + "compressBeforeSending": "发送前压缩", + "@compressBeforeSending": {} } From dac49a94ab5f93b470b7cf80ebe54612aca6f37b Mon Sep 17 00:00:00 2001 From: Linerly Date: Tue, 12 Nov 2024 00:33:00 +0000 Subject: [PATCH 198/236] Translated using Weblate (Indonesian) Currently translated at 100.0% (688 of 688 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/id/ --- assets/l10n/intl_id.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_id.arb b/assets/l10n/intl_id.arb index 2e8b98c76..31d7997de 100644 --- a/assets/l10n/intl_id.arb +++ b/assets/l10n/intl_id.arb @@ -2866,5 +2866,7 @@ "contactServerSecurity": "Hubungi keamanan server", "@contactServerSecurity": {}, "name": "Nama", - "@name": {} + "@name": {}, + "compressBeforeSending": "Kompres sebelum mengirim", + "@compressBeforeSending": {} } From 45c135d64bbaa500e86ef3208283d59417e418a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aindri=C3=BA=20Mac=20Giolla=20Eoin?= Date: Mon, 11 Nov 2024 11:15:01 +0000 Subject: [PATCH 199/236] Translated using Weblate (Irish) Currently translated at 100.0% (688 of 688 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ga/ --- assets/l10n/intl_ga.arb | 973 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 948 insertions(+), 25 deletions(-) diff --git a/assets/l10n/intl_ga.arb b/assets/l10n/intl_ga.arb index 5d01bfef6..f37d97432 100644 --- a/assets/l10n/intl_ga.arb +++ b/assets/l10n/intl_ga.arb @@ -14,7 +14,7 @@ "type": "text", "placeholders": {} }, - "wallpaper": "Cúlbhrat", + "wallpaper": "Cúlbhrat:", "@wallpaper": { "type": "text", "placeholders": {} @@ -202,7 +202,7 @@ "type": "text", "placeholders": {} }, - "ok": "togha", + "ok": "Ceart go leor", "@ok": { "type": "text", "placeholders": {} @@ -528,7 +528,7 @@ "type": "text", "placeholders": {} }, - "activatedEndToEndEncryption": "Thosaigh {username} an criptiú ó dheireadh go deireadh", + "activatedEndToEndEncryption": "🔐 {username} criptithe deireadh go deireadh gníomhachtaithe", "@activatedEndToEndEncryption": { "type": "text", "placeholders": { @@ -540,7 +540,7 @@ "type": "text", "placeholders": {} }, - "acceptedTheInvitation": "Ghlac {username} leis an cuireadh", + "acceptedTheInvitation": "👍 Ghlac {username} leis an gcuireadh", "@acceptedTheInvitation": { "type": "text", "placeholders": { @@ -579,7 +579,7 @@ "type": "text", "placeholders": {} }, - "invitedUser": "Thug {username} cuireadh do {targetName}", + "invitedUser": "📩 thug {username} cuireadh do {targetName}", "@invitedUser": { "type": "text", "placeholders": { @@ -649,7 +649,7 @@ "type": "text", "placeholders": {} }, - "defaultPermissionLevel": "Leibhéal ceada réamhshocraithe", + "defaultPermissionLevel": "Leibhéal ceada réamhshocraithe d'úsáideoirí nua", "@defaultPermissionLevel": { "type": "text", "placeholders": {} @@ -664,12 +664,12 @@ "type": "text", "placeholders": {} }, - "compareNumbersMatch": "Déan comparáid idir na huimhreacha seo a leanas agus déan cinnte go bhfuil na huimhreacha seo a leanas ag teacht le huimhreacha an ghléis eile:", + "compareNumbersMatch": "Cuir na huimhreacha i gcomparáid le do thoil", "@compareNumbersMatch": { "type": "text", "placeholders": {} }, - "compareEmojiMatch": "Déan comparáid agus déan cinnte go bhfuil an emoji seo a leanas comhoiriúnach le emoji an ghléis eile:", + "compareEmojiMatch": "Cuir na emojis i gcomparáid le do thoil", "@compareEmojiMatch": { "type": "text", "placeholders": {} @@ -729,7 +729,7 @@ }, "chatHasBeenAddedToThisSpace": "Cuireadh comhrá leis an spás seo", "@chatHasBeenAddedToThisSpace": {}, - "chatBackupDescription": "Tá do chúltaca comhrá daingnithe le heochair slándála. Déan cinnte nach gcaillfidh tú é.", + "chatBackupDescription": "Tá do sheanteachtaireachtaí slán le eochair athshlánaithe. Le do thoil déan cinnte nach gcaillfidh tú é.", "@chatBackupDescription": { "type": "text", "placeholders": {} @@ -831,7 +831,7 @@ "type": "text", "placeholder": {} }, - "createdTheChat": "Rinne {username} an comhrá", + "createdTheChat": "💬 chruthaigh {username} an comhrá", "@createdTheChat": { "type": "text", "placeholders": { @@ -1212,7 +1212,7 @@ }, "scanQrCode": "Scan cód QR", "@scanQrCode": {}, - "inviteText": "Thug {username} cuireadh duit chuig FluffyChat.\n1. Suiteáil FluffyChat: https://fluffychat.im\n2. Cláraigh nó sínigh isteach\n3. Oscail an nasc cuiridh: {link}", + "inviteText": "Thug {ainm úsáideora} cuireadh duit chuig FluffyChat.\n1. Tabhair cuairt ar fluffychat.im agus a shuiteáil an app\n2. Cláraigh nó sínigh isteach\n3. Oscail an nasc cuirí:\n {link}", "@inviteText": { "type": "text", "placeholders": { @@ -1228,7 +1228,7 @@ "server2": {} } }, - "noGoogleServicesWarning": "Dealraíonn sé nach bhfuil aon seirbhísí google agat ar do ghuthán. Sin cinneadh maith le do phríobháideacht! Chun fógraí brú a fháil i FluffyChat molaimid https://microg.org/ nó https://unifiedpush.org/ a úsáid.", + "noGoogleServicesWarning": "Is cosúil nach bhfuil Firebase Cloud Messaging ar fáil ar do ghléas. Chun fógraí brú a fháil fós, molaimid ntfy a shuiteáil. Le ntfy nó soláthraí Unified Push eile is féidir leat fógraí brú a fháil ar bhealach atá slán ó thaobh sonraí. Is féidir leat ntfy a íoslódáil ón PlayStore nó ó F-Droid.", "@noGoogleServicesWarning": { "type": "text", "placeholders": {} @@ -1253,7 +1253,7 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "Teachtaireacht nua i FluffyChat", + "newMessageInFluffyChat": "💬 Teachtaireacht nua in FluffyChat", "@newMessageInFluffyChat": { "type": "text", "placeholders": {} @@ -1309,7 +1309,7 @@ "type": "text", "placeholders": {} }, - "kicked": "Chaith {username} {targetName} amach", + "kicked": "👞 chiceáil {username} {targetName}", "@kicked": { "type": "text", "placeholders": { @@ -1317,7 +1317,7 @@ "targetName": {} } }, - "kickedAndBanned": "Chaith {username} amach agus chuir cosc ar {targetName} freisin", + "kickedAndBanned": "🙅 chiceáil {username} agus chuir sé cosc ar {targetName}", "@kickedAndBanned": { "type": "text", "placeholders": { @@ -1325,7 +1325,7 @@ "targetName": {} } }, - "joinedTheChat": "Tháinig {username} isteach sa chomhrá", + "joinedTheChat": "Tháinig 👋 {username} isteach sa chomhrá", "@joinedTheChat": { "type": "text", "placeholders": { @@ -1509,7 +1509,7 @@ "type": "text", "placeholders": {} }, - "wipeChatBackup": "Glan do cúltaca comhrá a chruthú eochair slándála nua?", + "wipeChatBackup": "An bhfuil fonn ort cúltaca do chomhrá a scriosadh chun eochair athshlánaithe nua a chruthú?", "@wipeChatBackup": { "type": "text", "placeholders": {} @@ -1534,7 +1534,7 @@ "type": "text", "placeholders": {} }, - "userLeftTheChat": "D'fhág {username} an comhrá", + "userLeftTheChat": "🚪 D'fhág {username} an comhrá", "@userLeftTheChat": { "type": "text", "placeholders": { @@ -1597,7 +1597,7 @@ "type": "text", "placeholders": {} }, - "sharedTheLocation": "Roinn {username} an suíomh", + "sharedTheLocation": "Roinn {username} a suíomh", "@sharedTheLocation": { "type": "text", "placeholders": { @@ -1631,21 +1631,21 @@ "senderName": {} } }, - "sentAVideo": "Sheol {username} físeán", + "sentAVideo": "🎥 sheol {username} físeán", "@sentAVideo": { "type": "text", "placeholders": { "username": {} } }, - "sentASticker": "Sheol {username} greamán", + "sentASticker": "😊 sheol {username} greamán", "@sentASticker": { "type": "text", "placeholders": { "username": {} } }, - "sentAPicture": "Sheol {username} pictiúr", + "sentAPicture": "🖼️ sheol {username} pictiúr", "@sentAPicture": { "type": "text", "placeholders": { @@ -1886,14 +1886,14 @@ "type": "text", "placeholders": {} }, - "sentAnAudio": "Sheol {username} fuaim", + "sentAnAudio": "🎤 sheol {username} fuaim", "@sentAnAudio": { "type": "text", "placeholders": { "username": {} } }, - "sentAFile": "Sheol {username} comhad", + "sentAFile": "📁 sheol {username} comhad", "@sentAFile": { "type": "text", "placeholders": { @@ -1949,5 +1949,928 @@ "@openVideoCamera": { "type": "text", "placeholders": {} - } + }, + "fileHasBeenSavedAt": "Sábháladh an comhad ag {path}", + "@fileHasBeenSavedAt": { + "type": "text", + "placeholders": { + "path": {} + } + }, + "editBundlesForAccount": "Cuir cuachta in eagar don chuntas seo", + "@editBundlesForAccount": {}, + "globalChatId": "Aitheantas comhrá domhanda", + "@globalChatId": {}, + "hideMemberChangesInPublicChatsBody": "Ná taispeáin san amlíne comhrá má théann duine isteach i gcomhrá poiblí nó má fhágann sé nó sí é chun inléiteacht a fheabhsú.", + "@hideMemberChangesInPublicChatsBody": {}, + "pleaseEnterRecoveryKey": "Cuir isteach d'eochair athshlánaithe le do thoil:", + "@pleaseEnterRecoveryKey": {}, + "sender": "Seoltóir", + "@sender": {}, + "noOneCanJoin": "Ní féidir le duine ar bith páirt a ghlacadh", + "@noOneCanJoin": {}, + "noOtherDevicesFound": "Níor aimsíodh aon ghléas eile", + "@noOtherDevicesFound": {}, + "profileNotFound": "Níorbh fhéidir an t-úsáideoir a aimsiú ar an bhfreastalaí. B'fhéidir go bhfuil fadhb nasctha ann nó nach bhfuil an t-úsáideoir ann.", + "@profileNotFound": {}, + "inviteGroupChat": "📨 Tabhair cuireadh comhrá grúpa", + "@inviteGroupChat": {}, + "knocking": "Cnagadh", + "@knocking": {}, + "addChatOrSubSpace": "Cuir comhrá nó fo-spás leis", + "@addChatOrSubSpace": {}, + "thisDevice": "An gléas seo:", + "@thisDevice": {}, + "formattedMessages": "Teachtaireachtaí formáidithe", + "@formattedMessages": {}, + "verifyOtherDevice": "🔐 Fíoraigh gléas eile", + "@verifyOtherDevice": {}, + "commandHint_ignore": "Déan neamhaird d'aitheantas na maitríse sonraithe", + "@commandHint_ignore": {}, + "completedKeyVerification": "{sender} fíorú eochair críochnaithe", + "@completedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "changeTheCanonicalRoomAlias": "Athraigh an príomhsheoladh comhrá poiblí", + "@changeTheCanonicalRoomAlias": {}, + "importEmojis": "Iompórtáil Emoji", + "@importEmojis": {}, + "start": "Tosaigh", + "@start": {}, + "commandHint_dm": "Cuir tús le comhrá díreach\nÚsáid --no-cription chun criptiúchán a dhíchumasú", + "@commandHint_dm": { + "type": "text", + "description": "Usage hint for the command /dm" + }, + "invalidServerName": "Ainm freastalaí neamhbhailí", + "@invalidServerName": {}, + "addToBundle": "Cuir le bundle", + "@addToBundle": {}, + "redactedBy": "Arna chur in eagar ag {username}", + "@redactedBy": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "addToSpaceDescription": "Roghnaigh spás chun an comhrá seo a chur leis.", + "@addToSpaceDescription": {}, + "markAsRead": "Marcáil mar léite", + "@markAsRead": {}, + "enterRoom": "Iontráil seomra", + "@enterRoom": {}, + "deviceKeys": "Eochracha gléis:", + "@deviceKeys": {}, + "allSpaces": "Gach spás", + "@allSpaces": {}, + "searchForUsers": "Cuardaigh @users...", + "@searchForUsers": {}, + "removeFromBundle": "Bain as an mbeart seo", + "@removeFromBundle": {}, + "recoveryKeyLost": "Eochair athshlánaithe caillte?", + "@recoveryKeyLost": {}, + "reactedWith": "D'fhreagair {sender} le {reaction}", + "@reactedWith": { + "type": "text", + "placeholders": { + "sender": {}, + "reaction": {} + } + }, + "youInvitedBy": "📩 Thug {user} cuireadh duit", + "@youInvitedBy": { + "placeholders": { + "user": {} + } + }, + "doNotShowAgain": "Ná taispeáin arís", + "@doNotShowAgain": {}, + "pleaseEnterANumber": "Iontráil uimhir níos mó ná 0", + "@pleaseEnterANumber": {}, + "unbanUserDescription": "Beidh an t-úsáideoir in ann dul isteach sa chomhrá arís má dhéanann siad iarracht.", + "@unbanUserDescription": {}, + "pleaseEnterYourCurrentPassword": "Iontráil do phasfhocal reatha le do thoil", + "@pleaseEnterYourCurrentPassword": {}, + "newPassword": "Pasfhocal nua", + "@newPassword": {}, + "subspace": "Fospás", + "@subspace": {}, + "decline": "Meath", + "@decline": {}, + "forwardMessageTo": "Seol teachtaireacht ar aghaidh chuig {roomName}?", + "@forwardMessageTo": { + "type": "text", + "placeholders": { + "roomName": {} + } + }, + "sendReadReceipts": "Seol admhálacha léite", + "@sendReadReceipts": {}, + "formattedMessagesDescription": "Taispeáin ábhar saibhir teachtaireachta cosúil le téacs trom ag baint úsáide as marcáil síos.", + "@formattedMessagesDescription": {}, + "verifyOtherUser": "🔐 Fíoraigh úsáideoir eile", + "@verifyOtherUser": {}, + "dehydrateTorLong": "Maidir le húsáideoirí TOR, moltar an seisiún a onnmhairiú roimh dhúnadh an fhuinneog.", + "@dehydrateTorLong": {}, + "numChats": "Comhráite {number}", + "@numChats": { + "type": "number", + "placeholders": { + "number": {} + } + }, + "hidePresences": "Folaigh Liosta Stádais?", + "@hidePresences": {}, + "jump": "Léim", + "@jump": {}, + "reportErrorDescription": "😭 Ó, a mhac go deo. Chuaigh rud éigin mícheart. Más mian leat, is féidir leat an fabht seo a thuairisciú do na forbróirí.", + "@reportErrorDescription": {}, + "setTheme": "Socraigh téama:", + "@setTheme": {}, + "invalidInput": "Ionchur neamhbhailí!", + "@invalidInput": {}, + "kickUserDescription": "Ciceáiltear an t-úsáideoir as an gcomhrá ach níl cosc air. I gcomhráite poiblí, is féidir leis an úsáideoir teacht ar ais ag am ar bith.", + "@kickUserDescription": {}, + "startConversation": "Tosaigh comhrá", + "@startConversation": {}, + "commandHint_sendraw": "Seol json amh", + "@commandHint_sendraw": {}, + "leaveEmptyToClearStatus": "Fág folamh chun do stádas a ghlanadh.", + "@leaveEmptyToClearStatus": {}, + "pleaseChooseAStrongPassword": "Roghnaigh pasfhocal láidir", + "@pleaseChooseAStrongPassword": {}, + "publicLink": "Nasc poiblí", + "@publicLink": {}, + "joinSpace": "Glac páirt sa spás", + "@joinSpace": {}, + "initAppError": "Tharla earráid agus an feidhmchlár á thosú", + "@initAppError": {}, + "requestedKeyVerification": "D'iarr {sender} fíorú eochrach", + "@requestedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "incomingMessages": "Teachtaireachtaí isteach", + "@incomingMessages": {}, + "transparent": "Trédhearcach", + "@transparent": {}, + "voiceCall": "Glao gutha", + "@voiceCall": {}, + "widgetVideo": "Físeán", + "@widgetVideo": {}, + "errorAddingWidget": "Earráid agus an ghiuirléid á cur leis.", + "@errorAddingWidget": {}, + "emojis": "Emojis", + "@emojis": {}, + "reportUser": "Déan tuairisc ar úsáideoir", + "@reportUser": {}, + "custom": "Saincheaptha", + "@custom": {}, + "supposedMxid": "Ba cheart go mbeadh sé seo {mxid}", + "@supposedMxid": { + "type": "text", + "placeholders": { + "mxid": {} + } + }, + "commandHint_markasgroup": "Marcáil mar ghrúpa", + "@commandHint_markasgroup": {}, + "dismiss": "Díbhe", + "@dismiss": {}, + "newGroup": "Grúpa nua", + "@newGroup": {}, + "newSpace": "Spás nua", + "@newSpace": {}, + "compressBeforeSending": "Comhbhrú roimh sheoladh", + "@compressBeforeSending": {}, + "confirmMatrixId": "Deimhnigh d’ID Maitrís chun do chuntas a scriosadh.", + "@confirmMatrixId": {}, + "hideMemberChangesInPublicChats": "Cuir athruithe ball i gcomhráite poiblí i bhfolach", + "@hideMemberChangesInPublicChats": {}, + "confirmEventUnpin": "An bhfuil tú cinnte an t-imeacht a dhíphionnáil go buan?", + "@confirmEventUnpin": {}, + "hugContent": "Tugann {senderName} barróg duit", + "@hugContent": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "unread": "Neamhléite", + "@unread": {}, + "noChatsFoundHere": "Níor aimsíodh aon chomhrá anseo fós. Cuir tús le comhrá nua le duine éigin tríd an gcnaipe thíos a úsáid. ⤵️", + "@noChatsFoundHere": {}, + "separateChatTypes": "Comhráite Díreacha agus Grúpaí ar Leith", + "@separateChatTypes": { + "type": "text", + "placeholders": {} + }, + "videoWithSize": "Físeán ({size})", + "@videoWithSize": { + "type": "text", + "placeholders": { + "size": {} + } + }, + "messageInfo": "Eolas teachtaireachta", + "@messageInfo": {}, + "messageType": "Cineál Teachtaireachta", + "@messageType": {}, + "pleaseEnterRecoveryKeyDescription": "Chun do sheanteachtaireachtaí a dhíghlasáil, cuir isteach d'eochair athshlánaithe a gineadh i seisiún eile. NÍ do phasfhocal í d'eochair athshlánaithe.", + "@pleaseEnterRecoveryKeyDescription": {}, + "openChat": "Oscail Comhrá", + "@openChat": {}, + "unsupportedAndroidVersionLong": "Éilíonn an ghné seo leagan Android níos nuaí. Seiceáil le haghaidh nuashonruithe nó tacaíocht Lineage OS.", + "@unsupportedAndroidVersionLong": {}, + "experimentalVideoCalls": "Glaonna físe turgnamhacha", + "@experimentalVideoCalls": {}, + "switchToAccount": "Athraigh go cuntas {number}", + "@switchToAccount": { + "type": "number", + "placeholders": { + "number": {} + } + }, + "addWidget": "Cuir giuirléid leis", + "@addWidget": {}, + "widgetUrlError": "Ní URL bailí é seo.", + "@widgetUrlError": {}, + "invitedBy": "📩 Cuireadh ó {user}", + "@invitedBy": { + "placeholders": { + "user": {} + } + }, + "youKicked": "👞 Chiceáil tú {user}", + "@youKicked": { + "placeholders": { + "user": {} + } + }, + "youUnbannedUser": "Unbanned tú {user}", + "@youUnbannedUser": { + "placeholders": { + "user": {} + } + }, + "userWouldLikeToChangeTheChat": "Ba mhaith le {user} páirt a ghlacadh sa chomhrá.", + "@userWouldLikeToChangeTheChat": { + "placeholders": { + "user": {} + } + }, + "knock": "Cnoc Mhuire", + "@knock": {}, + "storeInSecureStorageDescription": "Stóráil an eochair aisghabhála i stóráil slán an ghléis seo.", + "@storeInSecureStorageDescription": {}, + "countFiles": "Comhaid {count}", + "@countFiles": { + "placeholders": { + "count": {} + } + }, + "foregroundServiceRunning": "Tá an fógra seo le feiceáil nuair atá an tseirbhís tulra ag rith.", + "@foregroundServiceRunning": {}, + "screenSharingDetail": "Tá do scáileán á roinnt agat i FuffyChat", + "@screenSharingDetail": {}, + "callingPermissions": "Ceadanna a ghlaoch", + "@callingPermissions": {}, + "callingAccount": "Cuntas ag glaoch", + "@callingAccount": {}, + "callingAccountDetails": "Ceadaíonn FluffyChat an aip dhiailiú android dúchais a úsáid.", + "@callingAccountDetails": {}, + "appearOnTopDetails": "Ceadaíonn sé don aip a bheith ar bharr (ní gá má tá socrú Fluffychat agat cheana féin mar chuntas glao)", + "@appearOnTopDetails": {}, + "otherCallingPermissions": "Micreafón, ceamara agus ceadanna FluffyChat eile", + "@otherCallingPermissions": {}, + "hideUnimportantStateEvents": "Folaigh imeachtaí stáit gan tábhacht", + "@hideUnimportantStateEvents": {}, + "disableEncryptionWarning": "Ar chúiseanna slándála ní féidir leat criptiú a dhíchumasú i gcomhrá, áit ar cumasaíodh é roimhe seo.", + "@disableEncryptionWarning": {}, + "sorryThatsNotPossible": "Tá brón orm... nach féidir a dhéanamh", + "@sorryThatsNotPossible": {}, + "reopenChat": "Comhrá a athoscailt", + "@reopenChat": {}, + "noBackupWarning": "Rabhadh! Gan cúltaca comhrá a chumasú, caillfidh tú rochtain ar do theachtaireachtaí criptithe. Moltar go mór an cúltaca comhrá a chumasú ar dtús sula logálann tú amach.", + "@noBackupWarning": {}, + "fileIsTooBigForServer": "Ní féidir seol! Ní thacaíonn an freastalaí ach le ceangaltáin suas le {max}.", + "@fileIsTooBigForServer": { + "type": "text", + "placeholders": { + "max": {} + } + }, + "jumpToLastReadMessage": "Léim go dtí an teachtaireacht léite is déanaí", + "@jumpToLastReadMessage": {}, + "readUpToHere": "Léigh suas go dtí seo", + "@readUpToHere": {}, + "openLinkInBrowser": "Oscail nasc sa bhrabhsálaí", + "@openLinkInBrowser": {}, + "signInWithPassword": "Sínigh isteach le pasfhocal", + "@signInWithPassword": {}, + "pleaseTryAgainLaterOrChooseDifferentServer": "Bain triail eile as níos déanaí nó roghnaigh freastalaí eile.", + "@pleaseTryAgainLaterOrChooseDifferentServer": {}, + "signInWith": "Sínigh isteach le {provider}", + "@signInWith": { + "type": "text", + "placeholders": { + "provider": {} + } + }, + "invitePrivateChat": "📨 Tabhair cuireadh comhrá príobháideach", + "@invitePrivateChat": {}, + "wrongPinEntered": "Tháinig biorán mícheart isteach! Bain triail eile as i {seconds} soicind...", + "@wrongPinEntered": { + "type": "text", + "placeholders": { + "seconds": {} + } + }, + "archiveRoomDescription": "Bogfar an comhrá go dtí an chartlann. Beidh úsáideoirí eile in ann a fheiceáil gur fhág tú an comhrá.", + "@archiveRoomDescription": {}, + "removeDevicesDescription": "Beidh tú logáilte amach as an ngléas seo agus ní bheidh tú in ann teachtaireachtaí a fháil a thuilleadh.", + "@removeDevicesDescription": {}, + "roomUpgradeDescription": "Déanfar an comhrá a athchruthú ansin leis an leagan seomra nua. Cuirfear in iúl do gach rannpháirtí go gcaithfidh siad aistriú chuig an gcomhrá nua. Is féidir leat tuilleadh eolais a fháil faoi leaganacha seomra ag https://spec.matrix.org/latest/rooms/", + "@roomUpgradeDescription": {}, + "banUserDescription": "Beidh cosc ar an úsáideoir ón gcomhrá agus ní bheidh sé in ann dul isteach sa chomhrá arís go dtí go mbeidh siad gan chosc.", + "@banUserDescription": {}, + "makeAdminDescription": "Nuair a dhéanann tú an riarachán úsáideora seo, b'fhéidir nach mbeidh tú in ann é seo a chealú mar go mbeidh na ceadanna céanna acu agus atá agat.", + "@makeAdminDescription": {}, + "learnMore": "Faigh tuilleadh eolais", + "@learnMore": {}, + "yourGlobalUserIdIs": "Is é d'aitheantas úsáideora domhanda: ", + "@yourGlobalUserIdIs": {}, + "noUsersFoundWithQuery": "Ar an drochuair ní fhéadfaí aon úsáideoir a aimsiú le \"{query}\". Seiceáil le do thoil an ndearna tú typo.", + "@noUsersFoundWithQuery": { + "type": "text", + "placeholders": { + "query": {} + } + }, + "searchChatsRooms": "Cuardaigh #chats, @users...", + "@searchChatsRooms": {}, + "createGroupAndInviteUsers": "Cruthaigh grúpa agus tabhair cuireadh d'úsáideoirí", + "@createGroupAndInviteUsers": {}, + "groupCanBeFoundViaSearch": "Is féidir teacht ar ghrúpa trí chuardach", + "@groupCanBeFoundViaSearch": {}, + "wrongRecoveryKey": "Tá brón orm... Ní cosúil gurb é seo an eochair aisghabhála ceart.", + "@wrongRecoveryKey": {}, + "databaseMigrationBody": "Fan, le do thoil. B'fhéidir go dtógfaidh sé seo nóiméad.", + "@databaseMigrationBody": {}, + "select": "Roghnaigh", + "@select": {}, + "passwordsDoNotMatch": "Ní mheaitseálann pasfhocail", + "@passwordsDoNotMatch": {}, + "searchIn": "Cuardaigh i gcomhrá \"{chat}\"...", + "@searchIn": { + "type": "text", + "placeholders": { + "chat": {} + } + }, + "passwordIsWrong": "Tá do phasfhocal iontrála mícheart", + "@passwordIsWrong": {}, + "files": "Comhaid", + "@files": {}, + "databaseBuildErrorBody": "Ní féidir bunachar sonraí SQlite a thógáil. Déanann an aip iarracht an bunachar sonraí oidhreachta a úsáid anois. Tuairiscigh an earráid seo do na forbróirí ag {url}. Is í an teachtaireacht earráide: {error}", + "@databaseBuildErrorBody": { + "type": "text", + "placeholders": { + "url": {}, + "error": {} + } + }, + "sessionLostBody": "Cailltear do sheisiún. Tuairiscigh an earráid seo do na forbróirí ag {url}. Is í an teachtaireacht earráide: {error}", + "@sessionLostBody": { + "type": "text", + "placeholders": { + "url": {}, + "error": {} + } + }, + "restoreSessionBody": "Déanann an aip iarracht anois do sheisiún a chur ar ais ón gcúltaca. Tuairiscigh an earráid seo do na forbróirí ag {url}. Is í an teachtaireacht earráide: {error}", + "@restoreSessionBody": { + "type": "text", + "placeholders": { + "url": {}, + "error": {} + } + }, + "verifyOtherUserDescription": "Má fhíoraíonn tú úsáideoir eile, is féidir leat a bheith cinnte go bhfuil a fhios agat cé leis a bhfuil tú ag scríobh i ndáiríre. 💪\n\nNuair a thosaíonn tú fíorú, feicfidh tú féin agus an t-úsáideoir eile aníos san aip. Ansin feicfidh tú sraith emojis nó uimhreacha a chaithfidh tú a chur i gcomparáid lena chéile.\n\nIs é an bealach is fearr chun é seo a dhéanamh ná bualadh le chéile nó glao físe a thosú. 👭", + "@verifyOtherUserDescription": {}, + "sendTypingNotificationsDescription": "Is féidir le rannpháirtithe eile i gcomhrá a fheiceáil nuair atá teachtaireacht nua á clóscríobh agat.", + "@sendTypingNotificationsDescription": {}, + "verifyOtherDeviceDescription": "Nuair a fhíoraíonn tú gléas eile, is féidir leis na gléasanna sin eochracha a mhalartú, do shlándáil fhoriomlán a mhéadú. 💪 Nuair a thosaíonn tú fíorú, beidh preabfhuinneog le feiceáil san aip ar an dá ghléas. Ansin feicfidh tú sraith emojis nó uimhreacha a chaithfidh tú a chur i gcomparáid lena chéile. Is fearr an dá ghléas a bheith áisiúil sula dtosaíonn tú ar an bhfíorú. 🤳", + "@verifyOtherDeviceDescription": {}, + "acceptedKeyVerification": "{sender} glacadh le fíorú eochair", + "@acceptedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "canceledKeyVerification": "{sender} cealaithe fíorú eochrach", + "@canceledKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "commandHint_unignore": "Unignore an ID maitrís tugtha", + "@commandHint_unignore": {}, + "restricted": "Srianta", + "@restricted": {}, + "goToSpace": "Téigh go dtí an spás: {space}", + "@goToSpace": { + "type": "text", + "space": {} + }, + "markAsUnread": "Marcáil mar gan léamh", + "@markAsUnread": {}, + "moderatorLevel": "{level} - Modhnóir", + "@moderatorLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "adminLevel": "{level} - Riarachán", + "@adminLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "inviteOtherUsers": "Tabhair cuireadh d'úsáideoirí eile chuig an gcomhrá seo", + "@inviteOtherUsers": {}, + "changeTheChatPermissions": "Athraigh na ceadanna comhrá", + "@changeTheChatPermissions": {}, + "changeTheVisibilityOfChatHistory": "Athraigh infheictheacht stair an chomhrá", + "@changeTheVisibilityOfChatHistory": {}, + "chatPermissionsDescription": "Sainmhínigh cén leibhéal cumhachta is gá le haghaidh gníomhartha áirithe sa chomhrá seo. De ghnáth bíonn na leibhéil chumhachta 0, 50 agus 100 ag déanamh ionadaíochta d'úsáideoirí, do mhodhnóirí agus do riarthóirí, ach is féidir aon ghrádú a dhéanamh.", + "@chatPermissionsDescription": {}, + "changelog": "ChangelogName", + "@changelog": {}, + "sendCanceled": "Cealaíodh seoladh", + "@sendCanceled": {}, + "loginWithMatrixId": "Logáil isteach le Matrix-ID", + "@loginWithMatrixId": {}, + "discoverHomeservers": "Faigh amach faoi fhreastalaithe baile", + "@discoverHomeservers": {}, + "whatIsAHomeserver": "Cad is freastalaí baile ann?", + "@whatIsAHomeserver": {}, + "homeserverDescription": "Stóráiltear do chuid sonraí go léir ar an bhfreastalaí baile, díreach cosúil le soláthraí ríomhphoist. Is féidir leat an freastalaí baile is mian leat a úsáid a roghnú, agus is féidir leat cumarsáid a dhéanamh le gach duine fós. Foghlaim níos mó ag https://matrix.org.", + "@homeserverDescription": {}, + "calculatingFileSize": "Méid an chomhaid á ríomh...", + "@calculatingFileSize": {}, + "sendingAttachment": "Iatán á sheoladh...", + "@sendingAttachment": {}, + "generatingVideoThumbnail": "Mionsamhail físe á cruthú...", + "@generatingVideoThumbnail": {}, + "compressVideo": "Físeán á chomhbhrú...", + "@compressVideo": {}, + "sendingAttachmentCountOfCount": "Ceangaltán {index} de {length} á sheoladh...", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + }, + "prepareSendingAttachment": "Ullmhaigh an t- iatán á sheoladh...", + "@prepareSendingAttachment": {}, + "serverLimitReached": "Sroicheadh teorainn an fhreastalaí! Ag fanacht {seconds} soicind...", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + }, + "oneOfYourDevicesIsNotVerified": "Ní fhíoraítear ceann de do ghléasanna", + "@oneOfYourDevicesIsNotVerified": {}, + "noticeChatBackupDeviceVerification": "Nóta: Nuair a nascann tú do ghléasanna go léir leis an gcúltaca comhrá, déantar iad a fhíorú go huathoibríoch.", + "@noticeChatBackupDeviceVerification": {}, + "continueText": "Lean ar aghaidh", + "@continueText": {}, + "welcomeText": "Hey Hey 👋 Is é seo FluffyChat. Is féidir leat síniú isteach in aon fhreastalaí baile, atá comhoiriúnach leis https://matrix.org. Agus ansin comhrá a dhéanamh le duine ar bith. Is líonra teachtaireachtaí díláraithe ollmhór é!", + "@welcomeText": {}, + "blur": "Doiléirigh:", + "@blur": {}, + "opacity": "Teimhneacht:", + "@opacity": {}, + "setWallpaper": "Socraigh cúlbhrat", + "@setWallpaper": {}, + "manageAccount": "Bainistigh cuntas", + "@manageAccount": {}, + "noContactInformationProvided": "Ní sholáthraíonn an freastalaí aon fhaisnéis teagmhála bhailí", + "@noContactInformationProvided": {}, + "contactServerAdmin": "Déan teagmháil le admin an fhreastalaí", + "@contactServerAdmin": {}, + "contactServerSecurity": "Déan teagmháil le slándáil an fhreastalaí", + "@contactServerSecurity": {}, + "supportPage": "Leathanach tacaíochta", + "@supportPage": {}, + "name": "Ainm", + "@name": {}, + "version": "Leagan", + "@version": {}, + "website": "Suíomh Gréasáin", + "@website": {}, + "messagesStyle": "Teachtaireachtaí:", + "@messagesStyle": {}, + "setColorTheme": "Socraigh téama datha:", + "@setColorTheme": {}, + "openGallery": "Oscail gailearaí", + "@openGallery": {}, + "users": "Úsáideoirí", + "@users": {}, + "youBannedUser": "Chuir tú cosc ar {user}", + "@youBannedUser": { + "placeholders": { + "user": {} + } + }, + "user": "Úsáideoir", + "@user": {}, + "databaseMigrationTitle": "Tá an bunachar sonraí optamaithe", + "@databaseMigrationTitle": {}, + "hasKnocked": "🚪 Tá {user} tar éis cnagadh", + "@hasKnocked": { + "placeholders": { + "user": {} + } + }, + "report": "tuairisc", + "@report": {}, + "invite": "Tabhair cuireadh", + "@invite": {}, + "publicSpaces": "Spásanna poiblí", + "@publicSpaces": {}, + "whyIsThisMessageEncrypted": "Cén fáth nach féidir an teachtaireacht seo a léamh?", + "@whyIsThisMessageEncrypted": {}, + "pinMessage": "PIN go seomra", + "@pinMessage": {}, + "dehydrate": "Easpórtáil seisiún agus gléas wipe", + "@dehydrate": {}, + "dehydrateTor": "Úsáideoirí TOR: Seisiún easpórtála", + "@dehydrateTor": {}, + "commandHint_markasdm": "Marcáil mar sheomra teachtaireachta dírí don ID Maitrís tugtha", + "@commandHint_markasdm": {}, + "googlyEyesContent": "Seolann {senderName} súile googly chugat", + "@googlyEyesContent": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "commandHint_cuddle": "Seol cuddle", + "@commandHint_cuddle": {}, + "commandHint_hug": "Seol barróg", + "@commandHint_hug": {}, + "startFirstChat": "Cuir tús le do chéad chomhrá", + "@startFirstChat": {}, + "encryptThisChat": "Criptigh an comhrá seo", + "@encryptThisChat": {}, + "importNow": "Iompórtáil anois", + "@importNow": {}, + "sendTypingNotifications": "Seol fógraí clóscríofa", + "@sendTypingNotifications": {}, + "addChatDescription": "Cuir cur síos ar an gcomhrá leis...", + "@addChatDescription": {}, + "chatPermissions": "Ceadanna comhrá", + "@chatPermissions": {}, + "emoteKeyboardNoRecents": "Beidh mothúcháin a úsáideadh le déanaí le feiceáil anseo ...", + "@emoteKeyboardNoRecents": { + "type": "text", + "placeholders": {} + }, + "chatDescriptionHasBeenChanged": "Athraíodh cur síos ar an gcomhrá", + "@chatDescriptionHasBeenChanged": {}, + "pushNotificationsNotAvailable": "Níl fógraí brú ar fáil", + "@pushNotificationsNotAvailable": {}, + "publish": "Foilsigh", + "@publish": {}, + "changeGeneralChatSettings": "Athraigh socruithe ginearálta comhrá", + "@changeGeneralChatSettings": {}, + "sendRoomNotifications": "Seol fógraí @room", + "@sendRoomNotifications": {}, + "changeTheDescriptionOfTheGroup": "Athraigh an cur síos ar an gcomhrá", + "@changeTheDescriptionOfTheGroup": {}, + "aboutHomeserver": "Maidir le {homeserver}", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "cuddleContent": "Cuireann {senderName} do chudacht", + "@cuddleContent": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "countChatsAndCountParticipants": "{chats} comhrá agus {participants} rannpháirtí", + "@countChatsAndCountParticipants": { + "type": "text", + "placeholders": { + "chats": {}, + "participants": {} + } + }, + "noMoreChatsFound": "Níor aimsíodh a thuilleadh comhráite...", + "@noMoreChatsFound": {}, + "joinedChats": "Glacadh páirt i gcomhráite", + "@joinedChats": {}, + "space": "Spás", + "@space": {}, + "spaces": "Spásanna", + "@spaces": {}, + "directChat": "Comhrá díreach", + "@directChat": {}, + "redactedByBecause": "Athbhreithnithe ag {username} mar: \"{reason}\"", + "@redactedByBecause": { + "type": "text", + "placeholders": { + "username": {}, + "reason": {} + } + }, + "recoveryKey": "Eochair athshlánaithe", + "@recoveryKey": {}, + "setChatDescription": "Socraigh cur síos ar an gcomhrá", + "@setChatDescription": {}, + "presenceStyle": "Láithreacht:", + "@presenceStyle": { + "type": "text", + "placeholders": {} + }, + "presencesToggle": "Taispeáin teachtaireachtaí stádais ó úsáideoirí eile", + "@presencesToggle": { + "type": "text", + "placeholders": {} + }, + "time": "Am", + "@time": {}, + "removeFromSpace": "Bain as spás", + "@removeFromSpace": {}, + "placeCall": "Cuir glaoch", + "@placeCall": {}, + "unsupportedAndroidVersion": "Leagan Android gan tacaíocht", + "@unsupportedAndroidVersion": {}, + "previousAccount": "Cuntas roimhe seo", + "@previousAccount": {}, + "widgetJitsi": "Jitsi le chéile", + "@widgetJitsi": {}, + "widgetCustom": "Saincheaptha", + "@widgetCustom": {}, + "widgetName": "Ainm", + "@widgetName": {}, + "usersMustKnock": "Ní mór d'úsáideoirí cnag a chur ar", + "@usersMustKnock": {}, + "noPublicLinkHasBeenCreatedYet": "Níor cruthaíodh aon nasc poiblí go fóill", + "@noPublicLinkHasBeenCreatedYet": {}, + "storeSecurlyOnThisDevice": "Stóráil go daingean ar an ngléas seo", + "@storeSecurlyOnThisDevice": {}, + "userLevel": "{level} - Úsáideoir", + "@userLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "updateInstalled": "🎉 Nuashonraigh {version} suiteáilte!", + "@updateInstalled": { + "type": "text", + "placeholders": { + "version": {} + } + }, + "serverInformation": "Eolas freastalaí:", + "@serverInformation": {}, + "knockRestricted": "Cnoc Mhuire srianta", + "@knockRestricted": {}, + "createGroup": "Cruthaigh grúpa", + "@createGroup": {}, + "noChatDescriptionYet": "Níl aon chur síos ar an gcomhrá cruthaithe fós.", + "@noChatDescriptionYet": {}, + "shareInviteLink": "Roinn an nasc cuireadh", + "@shareInviteLink": {}, + "notifyMeFor": "Cuir in iúl dom le haghaidh", + "@notifyMeFor": {}, + "passwordRecoverySettings": "Socruithe athshlánaithe pasfhocal", + "@passwordRecoverySettings": {}, + "widgetEtherpad": "Nóta téacs", + "@widgetEtherpad": {}, + "youKickedAndBanned": "🙅 Chiceáil tú agus chuir tú cosc ar {user}", + "@youKickedAndBanned": { + "placeholders": { + "user": {} + } + }, + "publicChatAddresses": "Seoltaí comhrá poiblí", + "@publicChatAddresses": {}, + "createNewAddress": "Cruthaigh seoladh nua", + "@createNewAddress": {}, + "groupName": "Ainm an ghrúpa", + "@groupName": {}, + "bundleName": "Ainm an bheartáin", + "@bundleName": {}, + "enterSpace": "Iontráil spás", + "@enterSpace": {}, + "wasDirectChatDisplayName": "Comhrá folamh (bhí {oldDisplayName})", + "@wasDirectChatDisplayName": { + "type": "text", + "placeholders": { + "oldDisplayName": {} + } + }, + "oneClientLoggedOut": "Tá duine de do chliaint logáilte amach", + "@oneClientLoggedOut": {}, + "overview": "Forbhreathnú", + "@overview": {}, + "unverified": "Neamhfhíoraithe", + "@unverified": {}, + "widgetNameError": "Tabhair ainm taispeána, le do thoil.", + "@widgetNameError": {}, + "youRejectedTheInvitation": "Dhiúltaigh tú don chuireadh", + "@youRejectedTheInvitation": {}, + "youHaveWithdrawnTheInvitationFor": "Tharraing tú siar an cuireadh do {user}", + "@youHaveWithdrawnTheInvitationFor": { + "placeholders": { + "user": {} + } + }, + "youInvitedToBy": "📩 Tugadh cuireadh duit trí nasc chuig:\n{alias}", + "@youInvitedToBy": { + "placeholders": { + "alias": {} + } + }, + "youInvitedUser": "📩 Thug tú cuireadh do {user}", + "@youInvitedUser": { + "placeholders": { + "user": {} + } + }, + "unlockOldMessages": "Díghlasáil seanteachtaireachtaí", + "@unlockOldMessages": {}, + "saveKeyManuallyDescription": "Sábháil an eochair seo de láimh trí dialóg nó gearrthaisce comhroinnte an chórais a spreagadh.", + "@saveKeyManuallyDescription": {}, + "storeInAndroidKeystore": "Stóráil i Android KeyStore", + "@storeInAndroidKeystore": {}, + "storeInAppleKeyChain": "Stóráil i Apple KeyChain", + "@storeInAppleKeyChain": {}, + "appearOnTop": "Le feiceáil ar an mbarr", + "@appearOnTop": {}, + "newSpaceDescription": "Ligeann spásanna duit do chomhráite a chomhdhlúthú agus pobail phríobháideacha nó phoiblí a thógáil.", + "@newSpaceDescription": {}, + "chatCanBeDiscoveredViaSearchOnServer": "Is féidir comhrá a aimsiú tríd an gcuardach ar {server}", + "@chatCanBeDiscoveredViaSearchOnServer": { + "type": "text", + "placeholders": { + "server": {} + } + }, + "nothingFound": "Níor aimsíodh aon rud...", + "@nothingFound": {}, + "searchMore": "Cuardaigh tuilleadh...", + "@searchMore": {}, + "gallery": "Gailearaí", + "@gallery": {}, + "alwaysUse24HourFormat": "bréagach", + "@alwaysUse24HourFormat": { + "description": "Set to true to always display time of day in 24 hour format." + }, + "importFromZipFile": "Iompórtáil ó chomhad .zip", + "@importFromZipFile": {}, + "exportEmotePack": "Easpórtáil Emote pacáiste mar .zip", + "@exportEmotePack": {}, + "replace": "Ionadaigh", + "@replace": {}, + "appLockDescription": "Cuir glas ar an aip nuair nach bhfuil sé in úsáid le cód bioráin", + "@appLockDescription": {}, + "swipeRightToLeftToReply": "Svaidhpeáil ar dheis ar chlé chun freagra a thabhairt", + "@swipeRightToLeftToReply": {}, + "commandHint_discardsession": "Scrios an seisiún", + "@commandHint_discardsession": { + "type": "text", + "description": "Usage hint for the command /discardsession" + }, + "allRooms": "Gach Comhrá Grúpa", + "@allRooms": { + "type": "text", + "placeholders": {} + }, + "block": "Bloc", + "@block": {}, + "blockListDescription": "Is féidir leat bac a chur ar úsáideoirí atá ag cur isteach ort. Ní bheidh tú in ann teachtaireachtaí nó cuireadh seomra ar bith a fháil ó na húsáideoirí ar do liosta bloc pearsanta.", + "@blockListDescription": {}, + "blockUsername": "Déan neamhaird de ainm úsáideora", + "@blockUsername": {}, + "inviteContactToGroupQuestion": "Ar mhaith leat cuireadh a thabhairt do {contact} chuig an gcomhrá \"{groupName}\"?", + "@inviteContactToGroupQuestion": {}, + "tryAgain": "Bain triail eile as", + "@tryAgain": {}, + "optionalRedactReason": "(Roghnach) An chúis leis an teachtaireacht seo a chur in eagar...", + "@optionalRedactReason": {}, + "dehydrateWarning": "Ní féidir an gníomh seo a chealú. Cinntigh go stórálann tú an comhad cúltaca go sábháilte.", + "@dehydrateWarning": {}, + "hydrateTor": "Úsáideoirí TOR: Iompórtáil easpórtáil seisiún", + "@hydrateTor": {}, + "hydrateTorLong": "An ndearna tú do sheisiún a easpórtáil an uair dheireanach ar TOR? Iompórtáil go tapa é agus leanúint ar aghaidh ag comhrá.", + "@hydrateTorLong": {}, + "hydrate": "Athchóirigh ó chomhad cúltaca", + "@hydrate": {}, + "commandHint_googly": "Seol roinnt súile googly", + "@commandHint_googly": {}, + "notAnImage": "Ní comhad íomhá é.", + "@notAnImage": {}, + "userRole": "Ról an úsáideora", + "@userRole": {}, + "minimumPowerLevel": "Is é {level} an t-íosleibhéal cumhachta.", + "@minimumPowerLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "isReadyForKeyVerification": "Tá {sender} réidh le haghaidh fíorú eochair", + "@isReadyForKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "startedKeyVerification": "Thosaigh {sender} fíorú eochrach", + "@startedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "stickers": "Greamáin", + "@stickers": {}, + "discover": "Faigh amach", + "@discover": {}, + "unreadChatsInApp": "{appname}: {unread} comhráite gan léamh", + "@unreadChatsInApp": { + "type": "text", + "placeholders": { + "appname": {}, + "unread": {} + } + }, + "noDatabaseEncryption": "Ní thacaítear le criptiú bunachar sonraí ar an ardán seo", + "@noDatabaseEncryption": {}, + "thereAreCountUsersBlocked": "Faoi láthair tá bac curtha ar úsáideoirí {count}.", + "@thereAreCountUsersBlocked": { + "type": "text", + "count": {} + }, + "sendReadReceiptsDescription": "Is féidir le rannpháirtithe eile i gcomhrá a fheiceáil nuair a bhíonn teachtaireacht léite agat.", + "@sendReadReceiptsDescription": {}, + "doesNotSeemToBeAValidHomeserver": "Ní cosúil gur freastalaí baile comhoiriúnach é. URL mícheart?", + "@doesNotSeemToBeAValidHomeserver": {}, + "indexedDbErrorTitle": "Saincheisteanna mód príobháideach", + "@indexedDbErrorTitle": {}, + "indexedDbErrorLong": "Ar an drochuair níl an stóráil teachtaireachta cumasaithe sa mhód príobháideach de réir réamhshocraithe.\nTabhair cuairt le do thoil\n - faoi: config\n - socraithe dom.indexedDB.privateBrowsing.enabled go fíor\nSeachas sin, ní féidir FluffyChat a rith.", + "@indexedDbErrorLong": {}, + "nextAccount": "An chéad chuntas eile", + "@nextAccount": {}, + "youJoinedTheChat": "Chuaigh tú isteach sa chomhrá", + "@youJoinedTheChat": {}, + "youAcceptedTheInvitation": "👍 Ghlac tú leis an gcuireadh", + "@youAcceptedTheInvitation": {}, + "screenSharingTitle": "comhroinnt scáileáin", + "@screenSharingTitle": {}, + "accessAndVisibility": "Rochtain agus infheictheacht", + "@accessAndVisibility": {}, + "accessAndVisibilityDescription": "Cé a bhfuil cead aige páirt a ghlacadh sa chomhrá seo agus conas is féidir an comhrá a aimsiú.", + "@accessAndVisibilityDescription": {}, + "calls": "Glaonna", + "@calls": {}, + "customEmojisAndStickers": "Emojis agus greamáin saincheaptha", + "@customEmojisAndStickers": {}, + "customEmojisAndStickersBody": "Cuir leis nó roinn emojis nó greamáin saincheaptha is féidir a úsáid in aon chomhrá.", + "@customEmojisAndStickersBody": {}, + "chatDescription": "Cur síos ar an gcomhrá", + "@chatDescription": {}, + "hideRedactedMessages": "Folaigh teachtaireachtaí curtha in eagar", + "@hideRedactedMessages": {}, + "hideRedactedMessagesBody": "Má athghníomhaíonn duine éigin teachtaireacht, ní bheidh an teachtaireacht seo le feiceáil sa chomhrá a thuilleadh.", + "@hideRedactedMessagesBody": {}, + "hideInvalidOrUnknownMessageFormats": "Folaigh formáidí teachtaireachta neamhbhailí nó anaithnid", + "@hideInvalidOrUnknownMessageFormats": {}, + "blockedUsers": "Úsáideoirí bactha", + "@blockedUsers": {}, + "redactMessageDescription": "Athrófar an teachtaireacht do gach rannpháirtí sa chomhrá seo. Ní féidir é seo a chealú.", + "@redactMessageDescription": {}, + "noKeyForThisMessage": "Féadfaidh sé seo tarlú má seoladh an teachtaireacht sular shínigh tú isteach ar do chuntas ag an ngléas seo.\n\nIs féidir freisin gur chuir an seoltóir bac ar do ghléas nó go ndeachaigh rud éigin mícheart leis an nasc idirlín.\n\nAn bhfuil tú in ann an teachtaireacht a léamh ar sheisiún eile? Ansin is féidir leat an teachtaireacht a aistriú uaidh! Téigh go Socruithe > Gléasanna agus cinntigh go bhfuil do ghléasanna fíoraithe a chéile. Nuair a osclaíonn tú an seomra an chéad uair eile agus an dá sheisiún sa tulra, déanfar na heochracha a tharchur go huathoibríoch.\n\nNár mhaith leat na heochracha a chailleadh agus tú ag logáil amach nó ag aistriú gléasanna? Déan cinnte go bhfuil an cúltaca comhrá cumasaithe agat sna socruithe.", + "@noKeyForThisMessage": {} } From 6ae9b9d486ac2c022394246ea7e6eb8c4b42456a Mon Sep 17 00:00:00 2001 From: Rex_sa Date: Tue, 12 Nov 2024 11:52:35 +0000 Subject: [PATCH 200/236] Translated using Weblate (Arabic) Currently translated at 100.0% (688 of 688 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ar/ --- assets/l10n/intl_ar.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_ar.arb b/assets/l10n/intl_ar.arb index 36fc16962..2a808e238 100644 --- a/assets/l10n/intl_ar.arb +++ b/assets/l10n/intl_ar.arb @@ -2867,5 +2867,7 @@ "placeholders": { "homeserver": {} } - } + }, + "compressBeforeSending": "ضغط قبل الإرسال", + "@compressBeforeSending": {} } From ab2ae47cbd87bda091ada95756a1f96924476a26 Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Tue, 12 Nov 2024 16:20:48 +0000 Subject: [PATCH 201/236] Translated using Weblate (Basque) Currently translated at 100.0% (688 of 688 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/eu/ --- assets/l10n/intl_eu.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_eu.arb b/assets/l10n/intl_eu.arb index 118eb283f..5f062acc4 100644 --- a/assets/l10n/intl_eu.arb +++ b/assets/l10n/intl_eu.arb @@ -2867,5 +2867,7 @@ "contactServerSecurity": "Jakinarazi segurtasun arazo bat", "@contactServerSecurity": {}, "noContactInformationProvided": "Zerbitzariak ez du harremanetarako informaziorik zehaztu", - "@noContactInformationProvided": {} + "@noContactInformationProvided": {}, + "compressBeforeSending": "Konprimatu bidali baino lehen", + "@compressBeforeSending": {} } From 9a066edf4089b8529748346c3607ce619d664f52 Mon Sep 17 00:00:00 2001 From: Bezruchenko Simon Date: Tue, 12 Nov 2024 18:55:57 +0000 Subject: [PATCH 202/236] Translated using Weblate (Ukrainian) Currently translated at 100.0% (688 of 688 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/ --- assets/l10n/intl_uk.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_uk.arb b/assets/l10n/intl_uk.arb index 8151b4a57..be7f2da0e 100644 --- a/assets/l10n/intl_uk.arb +++ b/assets/l10n/intl_uk.arb @@ -2867,5 +2867,7 @@ "version": "Версія", "@version": {}, "website": "Вебсайт", - "@website": {} + "@website": {}, + "compressBeforeSending": "Стиснути перед відправленням", + "@compressBeforeSending": {} } From 16f471af94a1ea79406c8ad9432379ee4267d3dc Mon Sep 17 00:00:00 2001 From: Edgars Andersons Date: Tue, 12 Nov 2024 11:24:26 +0000 Subject: [PATCH 203/236] Translated using Weblate (Latvian) Currently translated at 100.0% (688 of 688 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/lv/ --- assets/l10n/intl_lv.arb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/assets/l10n/intl_lv.arb b/assets/l10n/intl_lv.arb index ef65456ef..c7312a88d 100644 --- a/assets/l10n/intl_lv.arb +++ b/assets/l10n/intl_lv.arb @@ -26,7 +26,7 @@ "type": "text", "placeholders": {} }, - "theyMatch": "Tie sakrīt", + "theyMatch": "Tās sakrīt", "@theyMatch": { "type": "text", "placeholders": {} @@ -1648,7 +1648,7 @@ "user": {} } }, - "theyDontMatch": "Tie nesakrīt", + "theyDontMatch": "Tās nesakrīt", "@theyDontMatch": { "type": "text", "placeholders": {} @@ -2845,5 +2845,7 @@ "version": "Versija", "@version": {}, "website": "Tīmekļvietne", - "@website": {} + "@website": {}, + "compressBeforeSending": "Saspiest pirms nosūtīšanas", + "@compressBeforeSending": {} } From f8b7f7474fe5d6a22a0a83ab4d5ef11f48c6c7e3 Mon Sep 17 00:00:00 2001 From: Angelo Schirinzi Date: Fri, 15 Nov 2024 20:12:30 +0000 Subject: [PATCH 204/236] Translated using Weblate (Italian) Currently translated at 100.0% (688 of 688 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/it/ --- assets/l10n/intl_it.arb | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_it.arb b/assets/l10n/intl_it.arb index 85b04ad97..4c1d9262b 100644 --- a/assets/l10n/intl_it.arb +++ b/assets/l10n/intl_it.arb @@ -2831,5 +2831,42 @@ "space": {} }, "markAsUnread": "Contrassegna come non letto", - "@markAsUnread": {} + "@markAsUnread": {}, + "compressBeforeSending": "Comprimi prima di inviare", + "@compressBeforeSending": {}, + "aboutHomeserver": "Informazioni su {homeserver}", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "continueText": "Continua", + "@continueText": {}, + "welcomeText": "Hey Hey 👋 Questa è FluffyChat. Puoi accedere a qualsiasi homeserver compatibile con https://matrix.org. E poi chattare con chiunque. È un'enorme rete di messaggistica decentralizzata!", + "@welcomeText": {}, + "blur": "Sfocatura:", + "@blur": {}, + "opacity": "Opacità:", + "@opacity": {}, + "setWallpaper": "Imposta sfondo", + "@setWallpaper": {}, + "manageAccount": "Gestisci account", + "@manageAccount": {}, + "noContactInformationProvided": "Il server non fornisce alcuna informazione di contatto valida", + "@noContactInformationProvided": {}, + "contactServerAdmin": "Contatta l'amministratore del server", + "@contactServerAdmin": {}, + "contactServerSecurity": "Contatta la sicurezza del server", + "@contactServerSecurity": {}, + "supportPage": "Pagina di supporto", + "@supportPage": {}, + "serverInformation": "Informazioni sul server:", + "@serverInformation": {}, + "name": "Nome", + "@name": {}, + "version": "Versione", + "@version": {}, + "website": "Sito web", + "@website": {} } From 3203ffffaeba3176e1aa267b071fc5ad29fdfede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sun, 17 Nov 2024 08:05:11 +0000 Subject: [PATCH 205/236] Translated using Weblate (Estonian) Currently translated at 100.0% (694 of 694 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/ --- assets/l10n/intl_et.arb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_et.arb b/assets/l10n/intl_et.arb index aab9eea30..09a5b8243 100644 --- a/assets/l10n/intl_et.arb +++ b/assets/l10n/intl_et.arb @@ -2869,5 +2869,19 @@ } }, "compressBeforeSending": "Paki enne saatmist kokku", - "@compressBeforeSending": {} + "@compressBeforeSending": {}, + "pleaseFillOut": "Palun täida", + "@pleaseFillOut": {}, + "invalidUrl": "Vigane võrguaadress", + "@invalidUrl": {}, + "addLink": "Lisa link", + "@addLink": {}, + "strikeThrough": "Läbikriipsutatud kiri", + "@strikeThrough": {}, + "sendUncompressed": "Saada pakkimata kujul", + "@sendUncompressed": {}, + "boldText": "Paks kiri", + "@boldText": {}, + "italicText": "Kaldkiri", + "@italicText": {} } From fb9197d6ba02ce6510dfd87d2d08ce3582930036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=8E=8B=E5=8F=AB=E6=88=91=E6=9D=A5=E5=B7=A1?= =?UTF-8?q?=E5=B1=B1?= Date: Sun, 17 Nov 2024 02:01:41 +0000 Subject: [PATCH 206/236] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (694 of 694 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/zh_Hans/ --- assets/l10n/intl_zh.arb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_zh.arb b/assets/l10n/intl_zh.arb index 30e30d3a0..4567f7f42 100644 --- a/assets/l10n/intl_zh.arb +++ b/assets/l10n/intl_zh.arb @@ -2869,5 +2869,19 @@ "website": "网站", "@website": {}, "compressBeforeSending": "发送前压缩", - "@compressBeforeSending": {} + "@compressBeforeSending": {}, + "italicText": "文件倾斜", + "@italicText": {}, + "strikeThrough": "删除线", + "@strikeThrough": {}, + "pleaseFillOut": "请填写", + "@pleaseFillOut": {}, + "addLink": "添加链接", + "@addLink": {}, + "sendUncompressed": "无压缩发送", + "@sendUncompressed": {}, + "boldText": "文本加粗", + "@boldText": {}, + "invalidUrl": "无效 url", + "@invalidUrl": {} } From 9376e49ef95da9e0c3330583f1d9594d657a5e6a Mon Sep 17 00:00:00 2001 From: Rex_sa Date: Mon, 18 Nov 2024 05:09:00 +0000 Subject: [PATCH 207/236] Translated using Weblate (Arabic) Currently translated at 100.0% (694 of 694 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ar/ --- assets/l10n/intl_ar.arb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_ar.arb b/assets/l10n/intl_ar.arb index 2a808e238..e99bafd90 100644 --- a/assets/l10n/intl_ar.arb +++ b/assets/l10n/intl_ar.arb @@ -2869,5 +2869,19 @@ } }, "compressBeforeSending": "ضغط قبل الإرسال", - "@compressBeforeSending": {} + "@compressBeforeSending": {}, + "sendUncompressed": "إرسال غير مضغوط", + "@sendUncompressed": {}, + "boldText": "خط غامق", + "@boldText": {}, + "italicText": "خط مائل", + "@italicText": {}, + "strikeThrough": "يتوسطه خط", + "@strikeThrough": {}, + "invalidUrl": "رابط غير صحيح", + "@invalidUrl": {}, + "addLink": "إضافة رابط", + "@addLink": {}, + "pleaseFillOut": "من فضلك قم بتعبئته", + "@pleaseFillOut": {} } From fae481edeb0a307d30efc6554f0676de24198906 Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Mon, 18 Nov 2024 16:33:58 +0000 Subject: [PATCH 208/236] Translated using Weblate (Basque) Currently translated at 100.0% (694 of 694 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/eu/ --- assets/l10n/intl_eu.arb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_eu.arb b/assets/l10n/intl_eu.arb index 5f062acc4..3e9735bc2 100644 --- a/assets/l10n/intl_eu.arb +++ b/assets/l10n/intl_eu.arb @@ -2869,5 +2869,19 @@ "noContactInformationProvided": "Zerbitzariak ez du harremanetarako informaziorik zehaztu", "@noContactInformationProvided": {}, "compressBeforeSending": "Konprimatu bidali baino lehen", - "@compressBeforeSending": {} + "@compressBeforeSending": {}, + "boldText": "Testu lodia", + "@boldText": {}, + "italicText": "Testu etzana", + "@italicText": {}, + "invalidUrl": "URL baliogabea", + "@invalidUrl": {}, + "addLink": "Gehitu esteka", + "@addLink": {}, + "sendUncompressed": "Bidali konprimatu gabe", + "@sendUncompressed": {}, + "strikeThrough": "Zirrimarra", + "@strikeThrough": {}, + "pleaseFillOut": "Bete ezazu", + "@pleaseFillOut": {} } From 891caf939388370bcf303e9a0d7a833a1a8fdc67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aindri=C3=BA=20Mac=20Giolla=20Eoin?= Date: Mon, 18 Nov 2024 08:42:22 +0000 Subject: [PATCH 209/236] Translated using Weblate (Irish) Currently translated at 100.0% (694 of 694 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ga/ --- assets/l10n/intl_ga.arb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_ga.arb b/assets/l10n/intl_ga.arb index f37d97432..727468864 100644 --- a/assets/l10n/intl_ga.arb +++ b/assets/l10n/intl_ga.arb @@ -2872,5 +2872,19 @@ "redactMessageDescription": "Athrófar an teachtaireacht do gach rannpháirtí sa chomhrá seo. Ní féidir é seo a chealú.", "@redactMessageDescription": {}, "noKeyForThisMessage": "Féadfaidh sé seo tarlú má seoladh an teachtaireacht sular shínigh tú isteach ar do chuntas ag an ngléas seo.\n\nIs féidir freisin gur chuir an seoltóir bac ar do ghléas nó go ndeachaigh rud éigin mícheart leis an nasc idirlín.\n\nAn bhfuil tú in ann an teachtaireacht a léamh ar sheisiún eile? Ansin is féidir leat an teachtaireacht a aistriú uaidh! Téigh go Socruithe > Gléasanna agus cinntigh go bhfuil do ghléasanna fíoraithe a chéile. Nuair a osclaíonn tú an seomra an chéad uair eile agus an dá sheisiún sa tulra, déanfar na heochracha a tharchur go huathoibríoch.\n\nNár mhaith leat na heochracha a chailleadh agus tú ag logáil amach nó ag aistriú gléasanna? Déan cinnte go bhfuil an cúltaca comhrá cumasaithe agat sna socruithe.", - "@noKeyForThisMessage": {} + "@noKeyForThisMessage": {}, + "sendUncompressed": "Seol neamh-chomhbhrúite", + "@sendUncompressed": {}, + "boldText": "Téacs trom", + "@boldText": {}, + "italicText": "Téacs iodálach", + "@italicText": {}, + "strikeThrough": "Stailc tríd", + "@strikeThrough": {}, + "addLink": "Cuir nasc leis", + "@addLink": {}, + "pleaseFillOut": "Líon amach le do thoil", + "@pleaseFillOut": {}, + "invalidUrl": "URL neamhbhailí", + "@invalidUrl": {} } From 90959dfdf600f424820d2cf5ddbdf8fa775f76c1 Mon Sep 17 00:00:00 2001 From: Linerly Date: Tue, 19 Nov 2024 11:21:36 +0000 Subject: [PATCH 210/236] Translated using Weblate (Indonesian) Currently translated at 100.0% (694 of 694 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/id/ --- assets/l10n/intl_id.arb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_id.arb b/assets/l10n/intl_id.arb index 31d7997de..3328eba9f 100644 --- a/assets/l10n/intl_id.arb +++ b/assets/l10n/intl_id.arb @@ -2868,5 +2868,19 @@ "name": "Nama", "@name": {}, "compressBeforeSending": "Kompres sebelum mengirim", - "@compressBeforeSending": {} + "@compressBeforeSending": {}, + "strikeThrough": "Coret", + "@strikeThrough": {}, + "pleaseFillOut": "Silakan isi", + "@pleaseFillOut": {}, + "addLink": "Tambahkan tautan", + "@addLink": {}, + "invalidUrl": "URL tidak valid", + "@invalidUrl": {}, + "sendUncompressed": "Kirim tanpa kompresi", + "@sendUncompressed": {}, + "boldText": "Teks tebal", + "@boldText": {}, + "italicText": "Teks miring", + "@italicText": {} } From 9c96b28d940992206610464e4512757e73ed6551 Mon Sep 17 00:00:00 2001 From: Edgars Andersons Date: Tue, 19 Nov 2024 08:02:48 +0000 Subject: [PATCH 211/236] Translated using Weblate (Latvian) Currently translated at 100.0% (694 of 694 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/lv/ --- assets/l10n/intl_lv.arb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_lv.arb b/assets/l10n/intl_lv.arb index c7312a88d..b242f8ae8 100644 --- a/assets/l10n/intl_lv.arb +++ b/assets/l10n/intl_lv.arb @@ -2847,5 +2847,19 @@ "website": "Tīmekļvietne", "@website": {}, "compressBeforeSending": "Saspiest pirms nosūtīšanas", - "@compressBeforeSending": {} + "@compressBeforeSending": {}, + "boldText": "Teksts treknrakstā", + "@boldText": {}, + "strikeThrough": "Pārsvītrots", + "@strikeThrough": {}, + "invalidUrl": "Nederīgs URL", + "@invalidUrl": {}, + "addLink": "Pievienot saiti", + "@addLink": {}, + "italicText": "Teksts slīprakstā", + "@italicText": {}, + "pleaseFillOut": "Lūgums aizpildīt", + "@pleaseFillOut": {}, + "sendUncompressed": "Sūtīt nesaspiestu", + "@sendUncompressed": {} } From 6a2b43133e79598c72421cad93bf0afc1c3dde9a Mon Sep 17 00:00:00 2001 From: Rex_sa Date: Wed, 20 Nov 2024 14:17:30 +0000 Subject: [PATCH 212/236] Translated using Weblate (Arabic) Currently translated at 100.0% (695 of 695 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ar/ --- assets/l10n/intl_ar.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_ar.arb b/assets/l10n/intl_ar.arb index e99bafd90..b1848ba5f 100644 --- a/assets/l10n/intl_ar.arb +++ b/assets/l10n/intl_ar.arb @@ -2883,5 +2883,7 @@ "addLink": "إضافة رابط", "@addLink": {}, "pleaseFillOut": "من فضلك قم بتعبئته", - "@pleaseFillOut": {} + "@pleaseFillOut": {}, + "unableToJoinChat": "يتعذر الانضمام إلى الدردشة. ربما يكون الطرف الآخر قد أغلق المحادثة بالفعل.", + "@unableToJoinChat": {} } From 068bbb6de9bea1ac9db9ca34652630432344c681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 20 Nov 2024 11:04:32 +0000 Subject: [PATCH 213/236] Translated using Weblate (Estonian) Currently translated at 100.0% (695 of 695 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/ --- assets/l10n/intl_et.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_et.arb b/assets/l10n/intl_et.arb index 09a5b8243..0048eff77 100644 --- a/assets/l10n/intl_et.arb +++ b/assets/l10n/intl_et.arb @@ -2883,5 +2883,7 @@ "boldText": "Paks kiri", "@boldText": {}, "italicText": "Kaldkiri", - "@italicText": {} + "@italicText": {}, + "unableToJoinChat": "Vestlusega liitumine ei õnnestu. Võib-olla on teine osapool juba vestluse sulgenud.", + "@unableToJoinChat": {} } From 94eaa9983ef328668011faaeb0a9e7b734613c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=8E=8B=E5=8F=AB=E6=88=91=E6=9D=A5=E5=B7=A1?= =?UTF-8?q?=E5=B1=B1?= Date: Wed, 20 Nov 2024 09:22:50 +0000 Subject: [PATCH 214/236] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (695 of 695 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/zh_Hans/ --- assets/l10n/intl_zh.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_zh.arb b/assets/l10n/intl_zh.arb index 4567f7f42..c91a6d666 100644 --- a/assets/l10n/intl_zh.arb +++ b/assets/l10n/intl_zh.arb @@ -2883,5 +2883,7 @@ "boldText": "文本加粗", "@boldText": {}, "invalidUrl": "无效 url", - "@invalidUrl": {} + "@invalidUrl": {}, + "unableToJoinChat": "无法加入聊天。可能其他方面已经关闭了对话。", + "@unableToJoinChat": {} } From 12b012a1339c193011665d3497984ed60daaa4aa Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 20 Nov 2024 15:35:08 +0000 Subject: [PATCH 215/236] Translated using Weblate (Irish) Currently translated at 99.8% (694 of 695 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ga/ --- assets/l10n/intl_ga.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/l10n/intl_ga.arb b/assets/l10n/intl_ga.arb index 727468864..798a58157 100644 --- a/assets/l10n/intl_ga.arb +++ b/assets/l10n/intl_ga.arb @@ -1212,7 +1212,7 @@ }, "scanQrCode": "Scan cód QR", "@scanQrCode": {}, - "inviteText": "Thug {ainm úsáideora} cuireadh duit chuig FluffyChat.\n1. Tabhair cuairt ar fluffychat.im agus a shuiteáil an app\n2. Cláraigh nó sínigh isteach\n3. Oscail an nasc cuirí:\n {link}", + "inviteText": "Thug {username} cuireadh duit chuig FluffyChat.\n1. Tabhair cuairt ar fluffychat.im agus a shuiteáil an app\n2. Cláraigh nó sínigh isteach\n3. Oscail an nasc cuirí:\n {link}", "@inviteText": { "type": "text", "placeholders": { From aa3d03bb3bd796749da21dace979a1c864c29e6e Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 20 Nov 2024 15:35:55 +0000 Subject: [PATCH 216/236] Translated using Weblate (German) Currently translated at 99.5% (692 of 695 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/ --- assets/l10n/intl_de.arb | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_de.arb b/assets/l10n/intl_de.arb index 597cc94b4..cd14b9911 100644 --- a/assets/l10n/intl_de.arb +++ b/assets/l10n/intl_de.arb @@ -2849,5 +2849,38 @@ "manageAccount": "Konto verwalten", "@manageAccount": {}, "continueText": "Fortfahren", - "@continueText": {} + "@continueText": {}, + "noContactInformationProvided": "Der Server stellt keine gültigen Kontaktinformationen bereit", + "@noContactInformationProvided": {}, + "contactServerAdmin": "Serveradministrator kontaktieren", + "@contactServerAdmin": {}, + "name": "Name", + "@name": {}, + "version": "Version", + "@version": {}, + "website": "Website", + "@website": {}, + "aboutHomeserver": "Über {homeserver}", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "sendUncompressed": "Unkomprimiert senden", + "@sendUncompressed": {}, + "boldText": "Fetter Text", + "@boldText": {}, + "invalidUrl": "Ungültige URL", + "@invalidUrl": {}, + "addLink": "Link hinzufügen", + "@addLink": {}, + "unableToJoinChat": "Chat kann nicht beigetreten werden. Möglicherweise hat die Gegenseite das Gespräch bereits beendet.", + "@unableToJoinChat": {}, + "italicText": "Kursiver Text", + "@italicText": {}, + "strikeThrough": "Durchgestrichen", + "@strikeThrough": {}, + "pleaseFillOut": "Bitte ausfüllen", + "@pleaseFillOut": {} } From 98b4bec41b5ed5715de2c1e8aba611c8e7f4321b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Vosp=C4=9Bl?= Date: Sun, 17 Nov 2024 18:44:44 +0100 Subject: [PATCH 217/236] fix: dont use thumbnails for emoticons --- lib/pages/chat/events/html_message.dart | 12 ++++++++---- lib/pages/chat/events/message_reactions.dart | 1 + lib/pages/chat/input_bar.dart | 1 + lib/pages/chat/sticker_picker_dialog.dart | 1 + lib/pages/settings_emotes/settings_emotes_view.dart | 1 + 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 4d5bcdc8c..fa2d918a6 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -272,14 +272,18 @@ class ImageExtension extends HtmlExtension { final width = double.tryParse(context.attributes['width'] ?? ''); final height = double.tryParse(context.attributes['height'] ?? ''); + final actualWidth = width ?? height ?? defaultDimension; + final actualHeight = height ?? width ?? defaultDimension; + return WidgetSpan( child: SizedBox( - width: width ?? height ?? defaultDimension, - height: height ?? width ?? defaultDimension, + width: actualWidth, + height: actualHeight, child: MxcImage( uri: mxcUrl, - width: width ?? height ?? defaultDimension, - height: height ?? width ?? defaultDimension, + width: actualWidth, + height: actualHeight, + isThumbnail: (actualWidth * actualHeight) > (256 * 256), ), ), ); diff --git a/lib/pages/chat/events/message_reactions.dart b/lib/pages/chat/events/message_reactions.dart index 656f650c6..d91b765b2 100644 --- a/lib/pages/chat/events/message_reactions.dart +++ b/lib/pages/chat/events/message_reactions.dart @@ -122,6 +122,7 @@ class _Reaction extends StatelessWidget { width: 20, height: 20, animated: false, + isThumbnail: false, ), if (count > 1) ...[ const SizedBox(width: 4), diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index f203c3d41..150a7ebc0 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -276,6 +276,7 @@ class InputBar extends StatelessWidget { : null, width: size, height: size, + isThumbnail: false, ), const SizedBox(width: 6), Text(suggestion['name']!), diff --git a/lib/pages/chat/sticker_picker_dialog.dart b/lib/pages/chat/sticker_picker_dialog.dart index eaa066413..96f62b3d4 100644 --- a/lib/pages/chat/sticker_picker_dialog.dart +++ b/lib/pages/chat/sticker_picker_dialog.dart @@ -92,6 +92,7 @@ class StickerPickerDialogState extends State { width: 128, height: 128, animated: true, + isThumbnail: false, ), ), ); diff --git a/lib/pages/settings_emotes/settings_emotes_view.dart b/lib/pages/settings_emotes/settings_emotes_view.dart index fb94b748c..1c6912196 100644 --- a/lib/pages/settings_emotes/settings_emotes_view.dart +++ b/lib/pages/settings_emotes/settings_emotes_view.dart @@ -247,6 +247,7 @@ class _EmoteImage extends StatelessWidget { fit: BoxFit.contain, width: size, height: size, + isThumbnail: false, ), ); } From 44a7c9e30e66938db391c196de7bf812dd639d82 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Wed, 20 Nov 2024 18:57:28 +0100 Subject: [PATCH 218/236] chore: Improve presence performance --- lib/widgets/presence_builder.dart | 53 +++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/lib/widgets/presence_builder.dart b/lib/widgets/presence_builder.dart index 2ca963c84..7aacc6f86 100644 --- a/lib/widgets/presence_builder.dart +++ b/lib/widgets/presence_builder.dart @@ -1,10 +1,12 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart'; -class PresenceBuilder extends StatelessWidget { +class PresenceBuilder extends StatefulWidget { final Widget Function(BuildContext context, CachedPresence? presence) builder; final String? userId; final Client? client; @@ -17,21 +19,38 @@ class PresenceBuilder extends StatelessWidget { }); @override - Widget build(BuildContext context) { - final userId = this.userId; - if (userId == null) return builder(context, null); - - final client = this.client ?? Matrix.of(context).client; - return FutureBuilder( - future: client.fetchCurrentPresence(userId), - builder: (context, cachedPresenceSnapshot) => StreamBuilder( - stream: client.onPresenceChanged.stream - .where((cachedPresence) => cachedPresence.userid == userId), - builder: (context, snapshot) => builder( - context, - snapshot.data ?? cachedPresenceSnapshot.data, - ), - ), - ); + State createState() => _PresenceBuilderState(); +} + +class _PresenceBuilderState extends State { + CachedPresence? _presence; + StreamSubscription? _sub; + + void _updatePresence(CachedPresence? presence) { + setState(() { + _presence = presence; + }); + } + + @override + void initState() { + super.initState(); + final client = widget.client ?? Matrix.of(context).client; + final userId = widget.userId; + if (userId != null) { + client.fetchCurrentPresence(userId).then(_updatePresence); + _sub = client.onPresenceChanged.stream + .where((presence) => presence.userid == userId) + .listen(_updatePresence); + } + } + + @override + void dispose() { + _sub?.cancel(); + super.dispose(); } + + @override + Widget build(BuildContext context) => widget.builder(context, _presence); } From 1f5aac6a1e67d4332df10aca091f2e173468a809 Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Wed, 20 Nov 2024 22:22:39 +0000 Subject: [PATCH 219/236] Translated using Weblate (Basque) Currently translated at 100.0% (695 of 695 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/eu/ --- assets/l10n/intl_eu.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_eu.arb b/assets/l10n/intl_eu.arb index 3e9735bc2..be2044b3c 100644 --- a/assets/l10n/intl_eu.arb +++ b/assets/l10n/intl_eu.arb @@ -2883,5 +2883,7 @@ "strikeThrough": "Zirrimarra", "@strikeThrough": {}, "pleaseFillOut": "Bete ezazu", - "@pleaseFillOut": {} + "@pleaseFillOut": {}, + "unableToJoinChat": "Ezin da txatera batu. Agian besteak elkarrizketa itxiko zuen honezkero.", + "@unableToJoinChat": {} } From 0c847fc437332a1f43a47c4d9636ec2fa30bdf69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jos=C3=A9=20m?= Date: Thu, 21 Nov 2024 09:55:50 +0000 Subject: [PATCH 220/236] Translated using Weblate (Galician) Currently translated at 100.0% (695 of 695 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/gl/ --- assets/l10n/intl_gl.arb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_gl.arb b/assets/l10n/intl_gl.arb index 20614bb38..e3e70e764 100644 --- a/assets/l10n/intl_gl.arb +++ b/assets/l10n/intl_gl.arb @@ -2869,5 +2869,21 @@ "continueText": "Continuar", "@continueText": {}, "compressBeforeSending": "Comprimir antes de enviar", - "@compressBeforeSending": {} + "@compressBeforeSending": {}, + "sendUncompressed": "Enviar sen comprimir", + "@sendUncompressed": {}, + "italicText": "Cursiva", + "@italicText": {}, + "strikeThrough": "Riscar", + "@strikeThrough": {}, + "pleaseFillOut": "Por favor completa", + "@pleaseFillOut": {}, + "invalidUrl": "URL non válido", + "@invalidUrl": {}, + "boldText": "Resaltar texto", + "@boldText": {}, + "addLink": "Engadir ligazón", + "@addLink": {}, + "unableToJoinChat": "Non se puido acceder. Pode que a outra parte xa pechase a conversa.", + "@unableToJoinChat": {} } From 694bed7a7db8ce6ba2eb0b22a1622da1830c1124 Mon Sep 17 00:00:00 2001 From: Angelo Schirinzi Date: Thu, 21 Nov 2024 11:13:31 +0000 Subject: [PATCH 221/236] Translated using Weblate (Italian) Currently translated at 100.0% (695 of 695 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/it/ --- assets/l10n/intl_it.arb | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_it.arb b/assets/l10n/intl_it.arb index 4c1d9262b..7a9403b9d 100644 --- a/assets/l10n/intl_it.arb +++ b/assets/l10n/intl_it.arb @@ -2055,7 +2055,7 @@ "@homeserver": {}, "callingPermissions": "Permessi di chiamata", "@callingPermissions": {}, - "readUpToHere": "Leggi fino a qui", + "readUpToHere": "Letto fino a qui", "@readUpToHere": {}, "start": "Inizio", "@start": {}, @@ -2868,5 +2868,21 @@ "version": "Versione", "@version": {}, "website": "Sito web", - "@website": {} + "@website": {}, + "sendUncompressed": "Invia non compresso", + "@sendUncompressed": {}, + "boldText": "Testo in grassetto", + "@boldText": {}, + "italicText": "Testo in corsivo", + "@italicText": {}, + "strikeThrough": "Barrato", + "@strikeThrough": {}, + "pleaseFillOut": "Si prega di compilare", + "@pleaseFillOut": {}, + "invalidUrl": "URL non valido", + "@invalidUrl": {}, + "addLink": "Aggiungi collegamento", + "@addLink": {}, + "unableToJoinChat": "Impossibile partecipare alla chat. Forse l'altra parte ha già chiuso la conversazione.", + "@unableToJoinChat": {} } From 127da9a054063ee43a3955d0d5e018668d8661fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aindri=C3=BA=20Mac=20Giolla=20Eoin?= Date: Fri, 22 Nov 2024 09:57:40 +0000 Subject: [PATCH 222/236] Translated using Weblate (Irish) Currently translated at 100.0% (695 of 695 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ga/ --- assets/l10n/intl_ga.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_ga.arb b/assets/l10n/intl_ga.arb index 798a58157..3dd2c9011 100644 --- a/assets/l10n/intl_ga.arb +++ b/assets/l10n/intl_ga.arb @@ -2886,5 +2886,7 @@ "pleaseFillOut": "Líon amach le do thoil", "@pleaseFillOut": {}, "invalidUrl": "URL neamhbhailí", - "@invalidUrl": {} + "@invalidUrl": {}, + "unableToJoinChat": "Ní féidir páirt a ghlacadh sa chomhrá. B’fhéidir go bhfuil an comhrá dúnta cheana féin ag an bpáirtí eile.", + "@unableToJoinChat": {} } From 8fc7c13f2ab069b7ae2cccda5d926ea8f0bebef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=28Alexandr1995=29?= Date: Sun, 24 Nov 2024 01:41:31 +0000 Subject: [PATCH 223/236] Translated using Weblate (Russian) Currently translated at 100.0% (695 of 695 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ru/ --- assets/l10n/intl_ru.arb | 70 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_ru.arb b/assets/l10n/intl_ru.arb index af876a086..56c1b1467 100644 --- a/assets/l10n/intl_ru.arb +++ b/assets/l10n/intl_ru.arb @@ -2266,7 +2266,7 @@ "@noOtherDevicesFound": {}, "reportErrorDescription": "😭 О, нет. Что-то пошло не так. При желании вы можете сообщить об этой ошибке разработчикам.", "@reportErrorDescription": {}, - "report": "пожаловаться", + "report": "сообщить", "@report": {}, "allRooms": "Все группы", "@allRooms": { @@ -2817,5 +2817,71 @@ "discoverHomeservers": "Список домашних серверов", "@discoverHomeservers": {}, "joinedChats": "Вступленные чаты", - "@joinedChats": {} + "@joinedChats": {}, + "serverInformation": "Информация о сервере:", + "@serverInformation": {}, + "sendingAttachmentCountOfCount": "Отправляю... {index} {length}...", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + }, + "welcomeText": "Привет. Это FluffyChat. Вы можете подписаться на любой сервер, который совместим с https://matrix.org. А потом поболтать с кем нибудь. Это огромная децентрализованная сеть обмена сообщениями!", + "@welcomeText": {}, + "noContactInformationProvided": "Сервер не предоставляет никакой правдивой контактной информации", + "@noContactInformationProvided": {}, + "aboutHomeserver": "О сервере {homeserver}", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "boldText": "Жирный шрифт", + "@boldText": {}, + "strikeThrough": "Перечёркнутый", + "@strikeThrough": {}, + "pleaseFillOut": "Пожалуйста, заполните", + "@pleaseFillOut": {}, + "sendUncompressed": "Отправлять без зжатия", + "@sendUncompressed": {}, + "invalidUrl": "Не верный URL", + "@invalidUrl": {}, + "addLink": "Добавить ссылку", + "@addLink": {}, + "italicText": "Italic", + "@italicText": {}, + "unableToJoinChat": "Невозможно присоединиться к чату. Возможно, другая сторона уже закончила разговор.", + "@unableToJoinChat": {}, + "serverLimitReached": "Ограничения сервера. Ожидайте{seconds} секунд...", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + }, + "continueText": "Продолжить", + "@continueText": {}, + "blur": "Размытие:", + "@blur": {}, + "opacity": "Прозрачность:", + "@opacity": {}, + "setWallpaper": "Установить обои", + "@setWallpaper": {}, + "manageAccount": "Управление аккаунтом", + "@manageAccount": {}, + "contactServerAdmin": "Админ сервера", + "@contactServerAdmin": {}, + "contactServerSecurity": "Безопасность контактов сервера", + "@contactServerSecurity": {}, + "supportPage": "Поддержка", + "@supportPage": {}, + "name": "Имя", + "@name": {}, + "version": "Версия", + "@version": {}, + "website": "Сайт", + "@website": {} } From 51ee72c5045ee57b5ef2bf19f1df8e8c0405b42b Mon Sep 17 00:00:00 2001 From: Bezruchenko Simon Date: Sat, 23 Nov 2024 11:41:19 +0000 Subject: [PATCH 224/236] Translated using Weblate (Ukrainian) Currently translated at 100.0% (695 of 695 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/ --- assets/l10n/intl_uk.arb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_uk.arb b/assets/l10n/intl_uk.arb index be7f2da0e..3bc8dcda4 100644 --- a/assets/l10n/intl_uk.arb +++ b/assets/l10n/intl_uk.arb @@ -2869,5 +2869,21 @@ "website": "Вебсайт", "@website": {}, "compressBeforeSending": "Стиснути перед відправленням", - "@compressBeforeSending": {} + "@compressBeforeSending": {}, + "sendUncompressed": "Відправити без стиснення", + "@sendUncompressed": {}, + "boldText": "Жирний текст", + "@boldText": {}, + "italicText": "Курсивний текст", + "@italicText": {}, + "strikeThrough": "Перекреслений текст", + "@strikeThrough": {}, + "pleaseFillOut": "Будь ласка, заповніть", + "@pleaseFillOut": {}, + "invalidUrl": "Невірний URL", + "@invalidUrl": {}, + "addLink": "Додати посилання", + "@addLink": {}, + "unableToJoinChat": "Неможливо приєднатися до чату. Можливо, інша сторона вже закрила розмову.", + "@unableToJoinChat": {} } From 3acbff0b440e6d9672a9503e672362905b4bd186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=28Alexandr1995=29?= Date: Sun, 24 Nov 2024 02:05:47 +0000 Subject: [PATCH 225/236] Translated using Weblate (Catalan) Currently translated at 95.1% (661 of 695 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ca/ --- assets/l10n/intl_ca.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/l10n/intl_ca.arb b/assets/l10n/intl_ca.arb index d1e781a84..f0fb26747 100644 --- a/assets/l10n/intl_ca.arb +++ b/assets/l10n/intl_ca.arb @@ -2254,7 +2254,7 @@ }, "importEmojis": "Importa emojis", "@importEmojis": {}, - "wasDirectChatDisplayName": "La sala buida ( va ser {oldDisplayName})", + "wasDirectChatDisplayName": "La sala buida ( va ser {oldDisplayName})", "@wasDirectChatDisplayName": { "type": "text", "placeholders": { From a40eab573d5628bd5ecb1fcc4a2a108fd3cd5616 Mon Sep 17 00:00:00 2001 From: Krille Date: Sun, 24 Nov 2024 17:28:07 +0100 Subject: [PATCH 226/236] build: Bump version --- CHANGELOG.md | 92 ++++++++++++++++++++++++++++++++++++++++------------ pubspec.yaml | 2 +- 2 files changed, 73 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 273594b59..4e2b41307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,65 @@ +## v1.23.0 +- design: Highlight emoji only messages (Krille) +- design: New login design (Krille) +- docs: fix snapstore badge on website (Krille) +- feat: Add about server page (Krille) +- feat: Add default chat wallpaper (Krille) +- feat: Add markdown context actions for text input (krille-chan) +- feat: Better wallpapers with blur and opacity sliders and improved styles page (krille-chan) +- feat: Display warning banner on unverified devices (krille-chan) +- feat: New audio message design with displayed body (krille-chan) +- feat: Nicer room creation UI (krille-chan) +- feat: Open account manage url when using MAS (krille-chan) +- feat: Sending multiple files at once (krille-chan) +- feat: Swipe to archive rooms (Krille) +- fix: Bypass image compression in flutter_file_picker (q234rty) +- fix: dont use thumbnails for emoticons (Marek Vospěl) +- fix: Public rooms always publicly visible even when turned off on creation (krille-chan) +- fix: Wait for room invite before open in pushhelper (krille-chan) +- refactor: Better future loading dialog without flickering (krille-chan) +- refactor: Display two lines on new messages (krille-chan) +- refactor: Improve delete device UX flow (krille-chan) +- refactor: Load bytes from sending files later to not let app crash (krille-chan) +- refactor: Migrate to newer keyboard shortcuts package (Krille) +- refactor: Move to upstream geolocator (Krille) +- refactor: Performance boost for avatar widget (Krille) +- refactor: Remove duplicated navigator workaround (krille-chan) +- refactor: Remove keyboard shortcuts (Krille) +- refactor: Remove unnecessary builder widget (krille-chan) +- refactor: Reuse flutter local notifications object (krille-chan) +- refactor: Use adaptive dialog action (Krille) +- refactor: Use file selector on linux (krille-chan) +- refactor: Use non nullable localizations builder and lazy load on web (krille-chan) +- Revert "chore: Follow up new chat design" (Krille) +- Revert "feat: Add default chat wallpaper" (Krille) +- Revert "refactor: Performance boost for avatar widget" (krille-chan) +- Translated using Weblate (Arabic) (Rex_sa) +- Translated using Weblate (Basque) (xabirequejo) +- Translated using Weblate (Catalan) (fadelkon) +- Translated using Weblate (Chinese (Simplified Han script)) (大王叫我来巡山) +- Translated using Weblate (Czech) (Michal Bedáň) +- Translated using Weblate (Estonian) (Priit Jõerüüt) +- Translated using Weblate (Finnish) (Priit Jõerüüt) +- Translated using Weblate (Galician) (josé m) +- Translated using Weblate (German) (Christian) +- Translated using Weblate (German) (Ettore Atalan) +- Translated using Weblate (German) (Peter Wallerius) +- Translated using Weblate (Hungarian) (Zentropivity) +- Translated using Weblate (Indonesian) (Linerly) +- Translated using Weblate (Irish) (Aindriú Mac Giolla Eoin) +- Translated using Weblate (Irish) (Christian) +- Translated using Weblate (Italian) (Angelo Schirinzi) +- Translated using Weblate (Korean) (Bruno Roh) +- Translated using Weblate (Korean) (kdh8219) +- Translated using Weblate (Latvian) (Edgars Andersons) +- Translated using Weblate (Latvian) (GGLVXD) +- Translated using Weblate (Russian) (Pavel Kozhukhov) +- Translated using Weblate (Russian) (v1s7) +- Translated using Weblate (Spanish) (Kimby) +- Translated using Weblate (Turkish) (Oğuz Ersen) +- Translated using Weblate (Ukrainian) (Bezruchenko Simon) +- Translated using Weblate (Ukrainian) (Ihor Hordiichuk) + ## v1.22.0 FluffyChat v1.22.0 brings a new design for spaces, replaces the bottom navigation bar with filter chips and makes it finally possible to play ogg audio messages on iOS. A lot of other fixes and improvements have also been added to this release. @@ -5,26 +67,6 @@ FluffyChat v1.22.0 brings a new design for spaces, replaces the bottom navigatio FluffyChat also now uses the new authenticated media endpoints if the server supports Matrix v1.11 or mentions the msc with the key `org.matrix.msc3916.stable` in the `unstable_features`. -- build: (deps): bump docker/build-push-action from 5 to 6 (dependabot[bot]) -- build(deps): bump rexml from 3.2.8 to 3.3.3 in /ios (dependabot[bot]) -- build: Remove permissions for screensharing until it is fixed (Krille) -- build: Update android target sdk to 34 (Krille) -- build: Update dependencies after release (krille-chan) -- build: Update to Flutter 3.22.3 (krille-chan) -- build: Update to Matrix SDK 0.32.0 (Krille) -- chore: Bring back add to space feature (Krille) -- chore: Bring back navrail (krille-chan) -- chore: Bring back separate chat types (krille-chan) -- chore: Chat permissions page follow up (krille-chan) -- chore: Do not hide error on file sending (Krille) -- chore: Improved create group and space design (Krille) -- chore: Make VOIP plugin less noisy in logs (krille-chan) -- chore: Move default PR template to correct dir (krille-chan) -- chore: nicer bottom sheets (krille-chan) -- chore: Nicer empty chat list placeholder (krille-chan) -- chore: Polish public room bottom sheet (krille-chan) -- chore: Show short forms of months and week days in UI (krille-chan) -- chore: Sligthly improve chat permissions page design (krille-chan) - design: Add snackbar with link to changelog on new version (Krille) - docs: Update privacy policy (krille-chan) - feat: Support for matrix auth media endpoints @@ -32,6 +74,16 @@ mentions the msc with the key `org.matrix.msc3916.stable` in the `unstable_featu - feat: New spaces and chat list design (krille-chan) - feat: Record voice message with opus/ogg if supported (Krille) - feat: Send voice messages from web (Krille) +- feat: Add about server page (Krille) +- feat: Add default chat wallpaper (Krille) +- feat: Add markdown context actions for text input (krille-chan) +- feat: Better wallpapers with blur and opacity sliders and improved styles page (krille-chan) +- feat: Display warning banner on unverified devices (krille-chan) +- feat: New audio message design with displayed body (krille-chan) +- feat: Nicer room creation UI (krille-chan) +- feat: Open account manage url when using MAS (krille-chan) +- feat: Sending multiple files at once (krille-chan) +- feat: Swipe to archive rooms (Krille) - fix: Display only available join rules (Krille) - fix: Path correct userId to ignore list (krille-chan) - fix: Scroll to event missing the position (Krille) diff --git a/pubspec.yaml b/pubspec.yaml index 33df8aac0..9667542a4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: fluffychat description: Chat with your friends. publish_to: none # On version bump also increase the build number for F-Droid -version: 1.22.0+3535 +version: 1.23.0+3536 environment: sdk: ">=3.0.0 <4.0.0" From 363eabbe1cda3190b6868add5aca7e0f2787a217 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 25 Nov 2024 15:13:51 +0100 Subject: [PATCH 227/236] chore: Follow up send file dialog for images --- assets/l10n/intl_en.arb | 9 +++- lib/pages/chat/send_file_dialog.dart | 65 +++++++++++++++++----------- 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index c8da22f32..440f5d15e 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -1689,6 +1689,13 @@ "type": "text", "placeholders": {} }, + "sendImages": "Send {count} image", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + }, "sendMessages": "Send messages", "@sendMessages": { "type": "text", @@ -2806,7 +2813,7 @@ "name": "Name", "version": "Version", "website": "Website", - "sendUncompressed": "Send uncompressed", + "compress": "Compress", "boldText": "Bold text", "italicText": "Italic text", "strikeThrough": "Strikethrough", diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index ce3ae7ecf..0ced2032e 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -163,7 +163,11 @@ class SendFileDialogState extends State { .toUpperCase(); if (uniqueMimeType?.startsWith('image') ?? false) { - sendStr = L10n.of(context).sendImage; + if (widget.files.length == 1) { + sendStr = L10n.of(context).sendImage; + } else { + sendStr = L10n.of(context).sendImages(widget.files.length); + } } else if (uniqueMimeType?.startsWith('audio') ?? false) { sendStr = L10n.of(context).sendAudio; } else if (uniqueMimeType?.startsWith('video') ?? false) { @@ -187,25 +191,33 @@ class SendFileDialogState extends State { if (uniqueMimeType?.startsWith('image') ?? false) Padding( padding: const EdgeInsets.only(bottom: 16.0), - child: Material( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius / 2), - clipBehavior: Clip.hardEdge, - child: kIsWeb - ? Image.network( - widget.files.first.path, - fit: BoxFit.contain, - height: 156, - ) - : Image.file( - File(widget.files.first.path), - fit: BoxFit.contain, - height: 156, + child: SizedBox( + height: 256, + child: ListView.builder( + itemCount: widget.files.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, i) => Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Material( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 2, ), + clipBehavior: Clip.hardEdge, + child: kIsWeb + ? Image.network( + widget.files[i].path, + height: 256, + ) + : Image.file( + File(widget.files[i].path), + height: 256, + ), + ), + ), + ), ), ), - if (uniqueMimeType?.startsWith('image') != true || - widget.files.length > 1) + if (uniqueMimeType?.startsWith('image') != true) Padding( padding: const EdgeInsets.only(bottom: 16.0), child: Row( @@ -253,19 +265,19 @@ class SendFileDialogState extends State { if ({TargetPlatform.iOS, TargetPlatform.macOS} .contains(theme.platform)) CupertinoSwitch( - value: !compress, + value: compress, onChanged: uniqueMimeType.startsWith('video') && !PlatformInfos.isMobile ? null - : (v) => setState(() => compress = !v), + : (v) => setState(() => compress = v), ) else Switch.adaptive( - value: !compress, + value: compress, onChanged: uniqueMimeType.startsWith('video') && !PlatformInfos.isMobile ? null - : (v) => setState(() => compress = !v), + : (v) => setState(() => compress = v), ), const SizedBox(width: 16), Expanded( @@ -277,16 +289,17 @@ class SendFileDialogState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - L10n.of(context).sendUncompressed, + L10n.of(context).compress, style: theme.textTheme.titleMedium, textAlign: TextAlign.left, ), ], ), - Text( - ' ($sizeString)', - style: theme.textTheme.labelSmall, - ), + if (!compress) + Text( + ' ($sizeString)', + style: theme.textTheme.labelSmall, + ), ], ), ), From ae0999e6ce8c44a3c8267ef09651d7be9d61881b Mon Sep 17 00:00:00 2001 From: Krille Date: Tue, 26 Nov 2024 08:54:10 +0100 Subject: [PATCH 228/236] chore: Follow up send multiple images --- lib/pages/chat/send_file_dialog.dart | 39 +++++++++++++++------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index 0ced2032e..f54f84d27 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -193,25 +193,28 @@ class SendFileDialogState extends State { padding: const EdgeInsets.only(bottom: 16.0), child: SizedBox( height: 256, - child: ListView.builder( - itemCount: widget.files.length, - scrollDirection: Axis.horizontal, - itemBuilder: (context, i) => Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Material( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 2, + child: Center( + child: ListView.builder( + shrinkWrap: true, + itemCount: widget.files.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, i) => Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Material( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 2, + ), + clipBehavior: Clip.hardEdge, + child: kIsWeb + ? Image.network( + widget.files[i].path, + height: 256, + ) + : Image.file( + File(widget.files[i].path), + height: 256, + ), ), - clipBehavior: Clip.hardEdge, - child: kIsWeb - ? Image.network( - widget.files[i].path, - height: 256, - ) - : Image.file( - File(widget.files[i].path), - height: 256, - ), ), ), ), From ef85d48a47be541f2ad70a897d995993ef98ab2c Mon Sep 17 00:00:00 2001 From: Krille Date: Tue, 26 Nov 2024 13:21:13 +0100 Subject: [PATCH 229/236] build: Add android build workaround for new flutter version --- android/build.gradle | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index bc157bd1a..baaf5430d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,11 +6,34 @@ allprojects { } rootProject.buildDir = '../build' + +// Workaround for building with Flutter 3.24 +// See: https://github.com/flutter/flutter/issues/153281#issuecomment-2292201697 subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" + afterEvaluate { project -> + if (project.extensions.findByName("android") != null) { + Integer pluginCompileSdk = project.android.compileSdk + if (pluginCompileSdk != null && pluginCompileSdk < 31) { + project.logger.error( + "Warning: Overriding compileSdk version in Flutter plugin: " + + project.name + + " from " + + pluginCompileSdk + + " to 31 (to work around https://issuetracker.google.com/issues/199180389)." + + "\nIf there is not a new version of " + project.name + ", consider filing an issue against " + + project.name + + " to increase their compileSdk to the latest (otherwise try updating to the latest version)." + ) + project.android { + compileSdk 31 + } + } + } + } } subprojects { - project.evaluationDependsOn(':app') + project.buildDir = "${rootProject.buildDir}/${project.name}" + project.evaluationDependsOn(":app") } tasks.register("clean", Delete) { From 8f483c64c025bffcd918ae1e802f0dfece17322f Mon Sep 17 00:00:00 2001 From: krille-chan Date: Fri, 29 Nov 2024 19:12:33 +0100 Subject: [PATCH 230/236] build: Use file selector to save files --- .../matrix_file_extension.dart | 22 +++++++++---------- snap/snapcraft.yaml | 5 ----- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart index 97dc1d8d9..b65aa9d33 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:file_selector/file_selector.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:share_plus/share_plus.dart'; @@ -19,21 +20,18 @@ extension MatrixFileExtension on MatrixFile { return; } - final downloadPath = await FilePicker.platform.saveFile( - dialogTitle: L10n.of(context).saveFile, - fileName: name, - type: filePickerFileType, - bytes: bytes, + final location = await getSaveLocation( + suggestedName: name, + confirmButtonText: L10n.of(context).saveFile, ); + final downloadPath = location?.path; if (downloadPath == null) return; - if (PlatformInfos.isDesktop) { - final result = await showFutureLoadingDialog( - context: context, - future: () => File(downloadPath).writeAsBytes(bytes), - ); - if (result.error != null) return; - } + final result = await showFutureLoadingDialog( + context: context, + future: () => File(downloadPath).writeAsBytes(bytes), + ); + if (result.error != null) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index a670a2a72..717f4ecb2 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -61,11 +61,6 @@ parts: build-packages: - g++ - zenity-integration: - plugin: nil - stage-snaps: - - zenity-integration - fluffychat: plugin: flutter source: . From 0dc8a015d311ccfac456ed88fc55f7027c638952 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Fri, 29 Nov 2024 19:25:49 +0100 Subject: [PATCH 231/236] chore: Follow up save file on desktop --- .../matrix_file_extension.dart | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart index b65aa9d33..648a0ca69 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart @@ -1,9 +1,9 @@ import 'dart:io'; +import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:file_selector/file_selector.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:share_plus/share_plus.dart'; @@ -20,18 +20,27 @@ extension MatrixFileExtension on MatrixFile { return; } - final location = await getSaveLocation( - suggestedName: name, - confirmButtonText: L10n.of(context).saveFile, - ); - final downloadPath = location?.path; + final downloadPath = !PlatformInfos.isMobile + ? (await getSaveLocation( + suggestedName: name, + confirmButtonText: L10n.of(context).saveFile, + )) + ?.path + : await FilePicker.platform.saveFile( + dialogTitle: L10n.of(context).saveFile, + fileName: name, + type: filePickerFileType, + bytes: bytes, + ); if (downloadPath == null) return; - final result = await showFutureLoadingDialog( - context: context, - future: () => File(downloadPath).writeAsBytes(bytes), - ); - if (result.error != null) return; + if (PlatformInfos.isDesktop) { + final result = await showFutureLoadingDialog( + context: context, + future: () => File(downloadPath).writeAsBytes(bytes), + ); + if (result.error != null) return; + } ScaffoldMessenger.of(context).showSnackBar( SnackBar( From 625680ee2e4a6dc0e02e7fbb640784b4842b351e Mon Sep 17 00:00:00 2001 From: krille-chan Date: Fri, 29 Nov 2024 19:34:53 +0100 Subject: [PATCH 232/236] chore: Adjust default linux window height --- linux/my_application.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/my_application.cc b/linux/my_application.cc index 7bddef81c..986be4ea3 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -60,7 +60,7 @@ static void my_application_activate(GApplication* application) { gtk_window_set_title(window, "FluffyChat"); } - gtk_window_set_default_size(window, 864, 680); + gtk_window_set_default_size(window, 864, 720); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); From 9bd6399cd56ba3c59839cdf8290478b4452ade35 Mon Sep 17 00:00:00 2001 From: Krille Date: Sat, 30 Nov 2024 11:10:33 +0100 Subject: [PATCH 233/236] refactor: Update to new receive sharing intent package --- android/app/src/main/AndroidManifest.xml | 27 ++ .../ShareViewController.swift | 347 +----------------- ios/Podfile | 5 + ios/Runner.xcodeproj/project.pbxproj | 42 ++- ios/Runner/Info.plist | 2 +- lib/pages/chat_list/chat_list.dart | 56 +-- .../matrix_file_extension.dart | 2 +- pubspec.lock | 4 +- pubspec.yaml | 2 +- 9 files changed, 119 insertions(+), 368 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 149424e6a..d5dfef41a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -49,10 +49,14 @@ + + + + @@ -61,38 +65,61 @@ android:scheme="https" android:host="matrix.to"/> + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/FluffyChat Share/ShareViewController.swift b/ios/FluffyChat Share/ShareViewController.swift index 96d921432..4d10e6bd8 100644 --- a/ios/FluffyChat Share/ShareViewController.swift +++ b/ios/FluffyChat Share/ShareViewController.swift @@ -1,335 +1,14 @@ -import UIKit -import Social -import MobileCoreServices -import Photos - -class ShareViewController: SLComposeServiceViewController { - // TODO: IMPORTANT: This should be your host app bundle identifier - let hostAppBundleIdentifier = "im.fluffychat.app" - let sharedKey = "ShareKey" - var sharedMedia: [SharedMediaFile] = [] - var sharedText: [String] = [] - let imageContentType = kUTTypeImage as String - let videoContentType = kUTTypeMovie as String - let textContentType = kUTTypeText as String - let urlContentType = kUTTypeURL as String - let fileURLType = kUTTypeFileURL as String; - - override func isContentValid() -> Bool { - return true - } - - override func viewDidLoad() { - super.viewDidLoad(); - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. - if let content = extensionContext!.inputItems[0] as? NSExtensionItem { - if let contents = content.attachments { - for (index, attachment) in (contents).enumerated() { - if attachment.hasItemConformingToTypeIdentifier(imageContentType) { - handleImages(content: content, attachment: attachment, index: index) - } else if attachment.hasItemConformingToTypeIdentifier(textContentType) { - handleText(content: content, attachment: attachment, index: index) - } else if attachment.hasItemConformingToTypeIdentifier(fileURLType) { - handleFiles(content: content, attachment: attachment, index: index) - } else if attachment.hasItemConformingToTypeIdentifier(urlContentType) { - handleUrl(content: content, attachment: attachment, index: index) - } else if attachment.hasItemConformingToTypeIdentifier(videoContentType) { - handleVideos(content: content, attachment: attachment, index: index) - } - } - } - } - } - - override func didSelectPost() { - print("didSelectPost"); - } - - override func configurationItems() -> [Any]! { - // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. - return [] - } - - private func handleText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { - attachment.loadItem(forTypeIdentifier: textContentType, options: nil) { [weak self] data, error in - - if error == nil, let item = data as? String, let this = self { - - this.sharedText.append(item) - - // If this is the last item, save imagesData in userDefaults and redirect to host app - if index == (content.attachments?.count)! - 1 { - let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") - userDefaults?.set(this.sharedText, forKey: this.sharedKey) - userDefaults?.synchronize() - this.redirectToHostApp(type: .text) - } - - } else { - self?.dismissWithError() - } - } - } - - private func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { - attachment.loadItem(forTypeIdentifier: urlContentType, options: nil) { [weak self] data, error in - - if error == nil, let item = data as? URL, let this = self { - - this.sharedText.append(item.absoluteString) - - // If this is the last item, save imagesData in userDefaults and redirect to host app - if index == (content.attachments?.count)! - 1 { - let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") - userDefaults?.set(this.sharedText, forKey: this.sharedKey) - userDefaults?.synchronize() - this.redirectToHostApp(type: .text) - } - - } else { - self?.dismissWithError() - } - } - } - - private func handleImages (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { - attachment.loadItem(forTypeIdentifier: imageContentType, options: nil) { [weak self] data, error in - - if error == nil, let url = data as? URL, let this = self { - - // Always copy - let fileName = this.getFileName(from: url, type: .image) - let newPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! - .appendingPathComponent(fileName) - let copied = this.copyFile(at: url, to: newPath) - if(copied) { - this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .image)) - } - - // If this is the last item, save imagesData in userDefaults and redirect to host app - if index == (content.attachments?.count)! - 1 { - let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") - userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) - userDefaults?.synchronize() - this.redirectToHostApp(type: .media) - } - - } else { - self?.dismissWithError() - } - } - } - - private func handleVideos (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { - attachment.loadItem(forTypeIdentifier: videoContentType, options: nil) { [weak self] data, error in - - if error == nil, let url = data as? URL, let this = self { - - // Always copy - let fileName = this.getFileName(from: url, type: .video) - let newPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! - .appendingPathComponent(fileName) - let copied = this.copyFile(at: url, to: newPath) - if(copied) { - guard let sharedFile = this.getSharedMediaFile(forVideo: newPath) else { - return - } - this.sharedMedia.append(sharedFile) - } - - // If this is the last item, save imagesData in userDefaults and redirect to host app - if index == (content.attachments?.count)! - 1 { - let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") - userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) - userDefaults?.synchronize() - this.redirectToHostApp(type: .media) - } - - } else { - self?.dismissWithError() - } - } - } - - private func handleFiles (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { - attachment.loadItem(forTypeIdentifier: fileURLType, options: nil) { [weak self] data, error in - - if error == nil, let url = data as? URL, let this = self { - - // Always copy - let fileName = this.getFileName(from :url, type: .file) - let newPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! - .appendingPathComponent(fileName) - let copied = this.copyFile(at: url, to: newPath) - if (copied) { - this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .file)) - } - - if index == (content.attachments?.count)! - 1 { - let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") - userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) - userDefaults?.synchronize() - this.redirectToHostApp(type: .file) - } - - } else { - self?.dismissWithError() - } - } - } - - private func dismissWithError() { - print("[ERROR] Error loading data!") - let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert) - - let action = UIAlertAction(title: "Error", style: .cancel) { _ in - self.dismiss(animated: true, completion: nil) - } - - alert.addAction(action) - present(alert, animated: true, completion: nil) - extensionContext!.completeRequest(returningItems: [], completionHandler: nil) - } - - private func redirectToHostApp(type: RedirectType) { - let url = URL(string: "ShareMedia://dataUrl=\(sharedKey)#\(type)") - var responder = self as UIResponder? - let selectorOpenURL = sel_registerName("openURL:") - - while (responder != nil) { - if (responder?.responds(to: selectorOpenURL))! { - let _ = responder?.perform(selectorOpenURL, with: url) - } - responder = responder!.next - } - extensionContext!.completeRequest(returningItems: [], completionHandler: nil) - } - - enum RedirectType { - case media - case text - case file - } - - func getExtension(from url: URL, type: SharedMediaType) -> String { - let parts = url.lastPathComponent.components(separatedBy: ".") - var ex: String? = nil - if (parts.count > 1) { - ex = parts.last - } - - if (ex == nil) { - switch type { - case .image: - ex = "PNG" - case .video: - ex = "MP4" - case .file: - ex = "TXT" - } - } - return ex ?? "Unknown" - } - - func getFileName(from url: URL, type: SharedMediaType) -> String { - var name = url.lastPathComponent - - if (name.isEmpty) { - name = UUID().uuidString + "." + getExtension(from: url, type: type) - } - - return name - } - - func copyFile(at srcURL: URL, to dstURL: URL) -> Bool { - do { - if FileManager.default.fileExists(atPath: dstURL.path) { - try FileManager.default.removeItem(at: dstURL) - } - try FileManager.default.copyItem(at: srcURL, to: dstURL) - } catch (let error) { - print("Cannot copy item at \(srcURL) to \(dstURL): \(error)") - return false - } - return true - } - - private func getSharedMediaFile(forVideo: URL) -> SharedMediaFile? { - let asset = AVAsset(url: forVideo) - let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded() - let thumbnailPath = getThumbnailPath(for: forVideo) - - if FileManager.default.fileExists(atPath: thumbnailPath.path) { - return SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) - } - - var saved = false - let assetImgGenerate = AVAssetImageGenerator(asset: asset) - assetImgGenerate.appliesPreferredTrackTransform = true - // let scale = UIScreen.main.scale - assetImgGenerate.maximumSize = CGSize(width: 360, height: 360) - do { - let img = try assetImgGenerate.copyCGImage(at: CMTimeMakeWithSeconds(600, preferredTimescale: Int32(1.0)), actualTime: nil) - try UIImage.pngData(UIImage(cgImage: img))()?.write(to: thumbnailPath) - saved = true - } catch { - saved = false - } - - return saved ? SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) : nil - - } - - private func getThumbnailPath(for url: URL) -> URL { - let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "==", with: "") - let path = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")! - .appendingPathComponent("\(fileName).jpg") - return path - } - - class SharedMediaFile: Codable { - var path: String; // can be image, video or url path. It can also be text content - var thumbnail: String?; // video thumbnail - var duration: Double?; // video duration in milliseconds - var type: SharedMediaType; - - - init(path: String, thumbnail: String?, duration: Double?, type: SharedMediaType) { - self.path = path - self.thumbnail = thumbnail - self.duration = duration - self.type = type - } - - // Debug method to print out SharedMediaFile details in the console - func toString() { - print("[SharedMediaFile] \n\tpath: \(self.path)\n\tthumbnail: \(self.thumbnail)\n\tduration: \(self.duration)\n\ttype: \(self.type)") - } - } - - enum SharedMediaType: Int, Codable { - case image - case video - case file - } - - func toData(data: [SharedMediaFile]) -> Data { - let encodedData = try? JSONEncoder().encode(data) - return encodedData! - } +// If you get no such module 'receive_sharing_intent' error. +// Go to Build Phases of your Runner target and +// move `Embed Foundation Extension` to the top of `Thin Binary`. +import receive_sharing_intent + +class ShareViewController: RSIShareViewController { + + // Use this method to return false if you don't want to redirect to host app automatically. + // Default is true + override func shouldAutoRedirect() -> Bool { + return false + } + } - -extension Array { - subscript (safe index: UInt) -> Element? { - return Int(index) < count ? self[Int(index)] : nil - } -} \ No newline at end of file diff --git a/ios/Podfile b/ios/Podfile index fac4d8176..4c88a1141 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -32,6 +32,11 @@ target 'Runner' do use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + + # From package https://pub.dev/packages/receive_sharing_intent + target 'FluffyChat Share' do + inherit! :search_paths + end end post_install do |installer| diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 9d57b73aa..27e9bef00 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 609046320A2D7D2B0D36583B /* Pods_FluffyChat_Share.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9EB6E6475A19949A37A2634 /* Pods_FluffyChat_Share.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -56,10 +57,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 09545B0C8C397F94966EA956 /* Pods-FluffyChat Share.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FluffyChat Share.debug.xcconfig"; path = "Target Support Files/Pods-FluffyChat Share/Pods-FluffyChat Share.debug.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 50DEFC207B70632D9C56ED78 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 58F7B95D036AD8E67B27588D /* Pods-FluffyChat Share.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FluffyChat Share.profile.xcconfig"; path = "Target Support Files/Pods-FluffyChat Share/Pods-FluffyChat Share.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 76737C9A857D5FD6D2634A3F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; @@ -80,7 +83,9 @@ C137635D2AD1446100A8F905 /* notification.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = notification.caf; sourceTree = ""; }; C149567B25C7274F00A16396 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; C149567D25C7276200A16396 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + C9EB6E6475A19949A37A2634 /* Pods_FluffyChat_Share.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FluffyChat_Share.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EA246783222E02DD03959891 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + F3778959E67CDA0CDB0D97BC /* Pods-FluffyChat Share.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FluffyChat Share.release.xcconfig"; path = "Target Support Files/Pods-FluffyChat Share/Pods-FluffyChat Share.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -96,6 +101,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 609046320A2D7D2B0D36583B /* Pods_FluffyChat_Share.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -106,6 +112,7 @@ isa = PBXGroup; children = ( 50DEFC207B70632D9C56ED78 /* Pods_Runner.framework */, + C9EB6E6475A19949A37A2634 /* Pods_FluffyChat_Share.framework */, ); name = Frameworks; sourceTree = ""; @@ -177,6 +184,9 @@ 76737C9A857D5FD6D2634A3F /* Pods-Runner.debug.xcconfig */, EA246783222E02DD03959891 /* Pods-Runner.release.xcconfig */, 9DB2F3524376810E74C799A8 /* Pods-Runner.profile.xcconfig */, + 09545B0C8C397F94966EA956 /* Pods-FluffyChat Share.debug.xcconfig */, + F3778959E67CDA0CDB0D97BC /* Pods-FluffyChat Share.release.xcconfig */, + 58F7B95D036AD8E67B27588D /* Pods-FluffyChat Share.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -195,9 +205,9 @@ 97C146EC1CF9000F007C117D /* Resources */, C1005C4D261071B5002F4F32 /* Embed App Extensions */, 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, F9C8EE392B9AB471149C306E /* [CP] Embed Pods Frameworks */, 064CBD7CE0D4CD6850C6880A /* [CP] Copy Pods Resources */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, ); buildRules = ( ); @@ -213,6 +223,7 @@ isa = PBXNativeTarget; buildConfigurationList = C1005C51261071B5002F4F32 /* Build configuration list for PBXNativeTarget "FluffyChat Share" */; buildPhases = ( + 67579C1EA0B5C7B918473158 /* [CP] Check Pods Manifest.lock */, C1005C3E261071B5002F4F32 /* Sources */, C1005C3F261071B5002F4F32 /* Frameworks */, C1005C40261071B5002F4F32 /* Resources */, @@ -320,7 +331,29 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; + }; + 67579C1EA0B5C7B918473158 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-FluffyChat Share-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; 8C9CCA7C5C45651F90C7BFDD /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -357,7 +390,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; F9C8EE392B9AB471149C306E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -693,6 +726,7 @@ }; C1005C4E261071B5002F4F32 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 09545B0C8C397F94966EA956 /* Pods-FluffyChat Share.debug.xcconfig */; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -727,6 +761,7 @@ }; C1005C4F261071B5002F4F32 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = F3778959E67CDA0CDB0D97BC /* Pods-FluffyChat Share.release.xcconfig */; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -758,6 +793,7 @@ }; C1005C50261071B5002F4F32 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 58F7B95D036AD8E67B27588D /* Pods-FluffyChat Share.profile.xcconfig */; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f53992f99..c94401f48 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -33,7 +33,7 @@ im.fluffychat.app.uris CFBundleURLSchemes - ShareMedia + ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) im.fluffychat matrix diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 553c979df..22f1982f7 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -26,7 +25,6 @@ import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../../utils/account_bundles.dart'; import '../../config/setting_keys.dart'; -import '../../utils/matrix_sdk_extensions/matrix_file_extension.dart'; import '../../utils/url_launcher.dart'; import '../../utils/voip/callkeep_manager.dart'; import '../../widgets/fluffy_chat_app.dart'; @@ -196,20 +194,14 @@ class ChatListController extends State // Share content into this room final shareContent = Matrix.of(context).shareContent; if (shareContent != null) { - final shareFile = shareContent.tryGet('file'); + final shareFile = shareContent.tryGet('file'); if (shareContent.tryGet('msgtype') == 'chat.fluffy.shared_file' && shareFile != null) { await showDialog( context: context, useRootNavigator: false, builder: (c) => SendFileDialog( - files: [ - XFile.fromData( - shareFile.bytes, - name: shareFile.name, - mimeType: shareFile.mimeType, - ), - ], + files: [shareFile], room: room, outerContext: context, ), @@ -432,16 +424,32 @@ class ChatListController extends State ? SelectMode.share : SelectMode.normal; - void _processIncomingSharedFiles(List files) { + void _processIncomingSharedMedia(List files) { if (files.isEmpty) return; - final file = File(files.first.path.replaceFirst('file://', '')); + + if (files.length > 1) { + Logs().w( + 'Received ${files.length} incoming shared media but app can only handle the first one', + ); + } + + // We only handle the first file currently + final sharedMedia = files.first; + + // Handle URIs and Texts, which are also passed in path + if (sharedMedia.type case SharedMediaType.text || SharedMediaType.url) { + return _processIncomingSharedText(sharedMedia.path); + } + + final file = XFile( + sharedMedia.path.replaceFirst('file://', ''), + mimeType: sharedMedia.mimeType, + ); Matrix.of(context).shareContent = { 'msgtype': 'chat.fluffy.shared_file', - 'file': MatrixFile( - bytes: file.readAsBytesSync(), - name: file.path, - ).detectFileType, + 'file': file, + if (sharedMedia.message != null) 'body': sharedMedia.message, }; context.go('/rooms'); } @@ -473,18 +481,14 @@ class ChatListController extends State if (!PlatformInfos.isMobile) return; // For sharing images coming from outside the app while the app is in the memory - _intentFileStreamSubscription = ReceiveSharingIntent.getMediaStream() - .listen(_processIncomingSharedFiles, onError: print); + _intentFileStreamSubscription = ReceiveSharingIntent.instance + .getMediaStream() + .listen(_processIncomingSharedMedia, onError: print); // For sharing images coming from outside the app while the app is closed - ReceiveSharingIntent.getInitialMedia().then(_processIncomingSharedFiles); - - // For sharing or opening urls/text coming from outside the app while the app is in the memory - _intentDataStreamSubscription = ReceiveSharingIntent.getTextStream() - .listen(_processIncomingSharedText, onError: print); - - // For sharing or opening urls/text coming from outside the app while the app is closed - ReceiveSharingIntent.getInitialText().then(_processIncomingSharedText); + ReceiveSharingIntent.instance + .getInitialMedia() + .then(_processIncomingSharedMedia); // For receiving shared Uris _intentUriStreamSubscription = linkStream.listen(_processIncomingUris); diff --git a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart index 648a0ca69..9327632cb 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart @@ -1,9 +1,9 @@ import 'dart:io'; -import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:file_selector/file_selector.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:share_plus/share_plus.dart'; diff --git a/pubspec.lock b/pubspec.lock index 7725343a7..6e60615bf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1630,10 +1630,10 @@ packages: dependency: "direct main" description: name: receive_sharing_intent - sha256: "912bebb551bce75a14098891fd750305b30d53eba0d61cc70cd9973be9866e8d" + sha256: ec76056e4d258ad708e76d85591d933678625318e411564dcb9059048ca3a593 url: "https://pub.dev" source: hosted - version: "1.4.5" + version: "1.8.1" record: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 9667542a4..8a4f4ba52 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -77,7 +77,7 @@ dependencies: provider: ^6.0.2 punycode: ^1.0.0 qr_code_scanner: ^1.0.1 - receive_sharing_intent: 1.4.5 # Update needs more work + receive_sharing_intent: ^1.8.1 record: ^5.1.2 scroll_to_index: ^3.0.1 share_plus: ^10.0.2 From 69d4c50b138c36f9d454ef5060b113480e03edd4 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sat, 30 Nov 2024 14:19:57 +0100 Subject: [PATCH 234/236] chore: Do not display sender prefix for DM rooms in notification ticker --- lib/utils/push_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart index 05f0e90c7..3b1194690 100644 --- a/lib/utils/push_helper.dart +++ b/lib/utils/push_helper.dart @@ -266,7 +266,7 @@ Future _tryPushHelper( ticker: event.calcLocalizedBodyFallback( matrixLocals, plaintextBody: true, - withSenderNamePrefix: true, + withSenderNamePrefix: !event.room.isDirectChat, hideReply: true, hideEdit: true, removeMarkdown: true, From 4b2c35567568aaf519715405d9f0fc31ec64d718 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 9 Dec 2024 15:12:36 -0500 Subject: [PATCH 235/236] fluffychat merge --- lib/pangea/widgets/chat/message_audio_card.dart | 2 ++ linux/flutter/generated_plugin_registrant.cc | 4 ++++ linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 +++- 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index 08a9e9adf..9cdf41cfb 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -197,6 +197,8 @@ class MessageAudioCardState extends State { sectionEndMS: sectionEndMS, color: Theme.of(context).colorScheme.onPrimaryContainer, setIsPlayingAudio: widget.setIsPlayingAudio, + fontSize: + AppConfig.messageFontSize * AppConfig.fontSizeFactor, ) : const CardErrorWidget( error: "Null audio file in message_audio_card", diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 99fe9d9d6..20d1ee2b9 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) handy_window_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "HandyWindowPlugin"); handy_window_plugin_register_with_registrar(handy_window_registrar); + g_autoptr(FlPluginRegistrar) open_file_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin"); + open_file_linux_plugin_register_with_registrar(open_file_linux_registrar); g_autoptr(FlPluginRegistrar) pasteboard_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin"); pasteboard_plugin_register_with_registrar(pasteboard_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index de98e488d..9ed28a52d 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_linux flutter_webrtc handy_window + open_file_linux pasteboard record_linux sentry_flutter diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 94d578564..5bf97e482 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -25,6 +25,7 @@ import in_app_purchase_storekit import just_audio import macos_ui import macos_window_utils +import open_file_mac import package_info_plus import pasteboard import path_provider_foundation @@ -34,7 +35,7 @@ import rive_common import sentry_flutter import share_plus import shared_preferences_foundation -import sqflite +import sqflite_darwin import sqlcipher_flutter_libs import text_to_speech_macos import url_launcher_macos @@ -64,6 +65,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin")) MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) + OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) From 6d59faaa89ba7b77e12803d4e7830efbabd455d0 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 9 Dec 2024 15:42:53 -0500 Subject: [PATCH 236/236] matrix dart sdk updates --- .../settings_homeserver_view.dart | 54 ++++++++----------- .../settings_notifications.dart | 2 - lib/pangea/controllers/pangea_controller.dart | 1 - 3 files changed, 22 insertions(+), 35 deletions(-) diff --git a/lib/pages/settings_homeserver/settings_homeserver_view.dart b/lib/pages/settings_homeserver/settings_homeserver_view.dart index 3e006ef6c..9ad83346c 100644 --- a/lib/pages/settings_homeserver/settings_homeserver_view.dart +++ b/lib/pages/settings_homeserver/settings_homeserver_view.dart @@ -12,7 +12,6 @@ import 'package:url_launcher/url_launcher_string.dart'; import '../../widgets/matrix.dart'; import 'settings_homeserver.dart'; -/// MERGE TODO class SettingsHomeserverView extends StatelessWidget { final SettingsHomeserverController controller; @@ -48,10 +47,7 @@ class SettingsHomeserverView extends StatelessWidget { ), ), FutureBuilder( - // #Pangea - // future: client.getWellknownSupport(), - future: Future.value(null), - // Pangea# + future: client.getWellknownSupport(), builder: (context, snapshot) { final error = snapshot.error; final data = snapshot.data; @@ -74,21 +70,17 @@ class SettingsHomeserverView extends StatelessWidget { ), ); } - // #Pangea - const supportPage = null; - const contacts = null; - // final supportPage = data.supportPage; - // final contacts = data.contacts; - // if (supportPage == null && contacts == null) { - // return ListTile( - // leading: const Icon(Icons.error_outlined), - // title: Text( - // L10n.of(context).noContactInformationProvided, - // style: const TextStyle(fontSize: 14), - // ), - // ); - // } - // Pangea# + final supportPage = data.supportPage; + final contacts = data.contacts; + if (supportPage == null && contacts == null) { + return ListTile( + leading: const Icon(Icons.error_outlined), + title: Text( + L10n.of(context).noContactInformationProvided, + style: const TextStyle(fontSize: 14), + ), + ); + } return Column( mainAxisSize: MainAxisSize.min, children: [ @@ -290,15 +282,13 @@ class SettingsHomeserverView extends StatelessWidget { } } -// #Pangea -// extension on Role { -// String localizedString(L10n l10n) { -// switch (this) { -// case Role.mRoleAdmin: -// return l10n.contactServerAdmin; -// case Role.mRoleSecurity: -// return l10n.contactServerSecurity; -// } -// } -// } -// Pangea# +extension on Role { + String localizedString(L10n l10n) { + switch (this) { + case Role.mRoleAdmin: + return l10n.contactServerAdmin; + case Role.mRoleSecurity: + return l10n.contactServerSecurity; + } + } +} diff --git a/lib/pages/settings_notifications/settings_notifications.dart b/lib/pages/settings_notifications/settings_notifications.dart index 8ac6d2831..85e4320d1 100644 --- a/lib/pages/settings_notifications/settings_notifications.dart +++ b/lib/pages/settings_notifications/settings_notifications.dart @@ -101,8 +101,6 @@ class SettingsNotificationsController extends State { }); try { await Matrix.of(context).client.setPushRuleEnabled( - // MERGE TODO: upgrade SDK and remove this first argument - 'global', item.type, item.key, enabled, diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index 42d4ad9fe..3bfc5fd56 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -403,7 +403,6 @@ class PangeaController { ) ?? false)) { await matrixState.client.setPushRule( - 'global', PushRuleKind.override, PangeaEventTypes.textToSpeechRule, [PushRuleAction.dontNotify],

&ocLoc9A$wPy8_q%vWMD!f8oM3 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 KHfOcV!T$$A-C67a literal 0 HcmV?d00001 diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index d4614af10..50b0c5708 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -9,7 +9,6 @@ import 'package:fluffychat/pages/chat/events/message.dart'; import 'package:fluffychat/pages/chat/seen_by_row.dart'; import 'package:fluffychat/pages/chat/typing_indicators.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; -import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -34,10 +33,6 @@ class ChatEventList extends StatelessWidget { for (var i = 0; i < events.length; i++) { thisEventsKeyMap[events[i].eventId] = i; } - - final hasWallpaper = - controller.room.client.applicationAccountConfig.wallpaperUrl != null; - return SelectionArea( child: ListView.custom( padding: EdgeInsets.only( @@ -142,8 +137,7 @@ class ChatEventList extends StatelessWidget { i > 0 && controller.readMarkerEventId == event.eventId, nextEvent: i + 1 < events.length ? events[i + 1] : null, previousEvent: i > 0 ? events[i - 1] : null, - avatarPresenceBackgroundColor: - hasWallpaper ? Colors.transparent : null, + avatarPresenceBackgroundColor: Colors.transparent, ), ); }, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 221153d27..c4240eda4 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -260,18 +260,30 @@ class ChatView extends StatelessWidget { onDragExited: controller.onDragExited, child: Stack( children: [ - if (accountConfig.wallpaperUrl != null) - Opacity( - opacity: accountConfig.wallpaperOpacity ?? 1, - child: MxcImage( - uri: accountConfig.wallpaperUrl, - fit: BoxFit.cover, - isThumbnail: true, - width: FluffyThemes.columnWidth * 4, - height: FluffyThemes.columnWidth * 4, - placeholder: (_) => Container(), - ), - ), + accountConfig.wallpaperUrl != null + ? Opacity( + opacity: accountConfig.wallpaperOpacity ?? 1, + child: MxcImage( + uri: accountConfig.wallpaperUrl, + fit: BoxFit.cover, + isThumbnail: true, + width: FluffyThemes.columnWidth * 4, + height: FluffyThemes.columnWidth * 4, + placeholder: (_) => Container(), + ), + ) + : ColorFiltered( + colorFilter: ColorFilter.mode( + theme.colorScheme.surfaceContainerHighest, + BlendMode.color, + ), + child: Image.asset( + 'assets/chat_wallpaper_${theme.brightness.name}.png', + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + ), + ), SafeArea( child: Column( children: [ @@ -308,10 +320,7 @@ class ChatView extends StatelessWidget { alignment: Alignment.center, child: Material( clipBehavior: Clip.hardEdge, - color: theme - .colorScheme - // ignore: deprecated_member_use - .surfaceVariant, + color: theme.colorScheme.surfaceBright, borderRadius: const BorderRadius.all( Radius.circular(24), ), diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 032e8967d..5d1631fec 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -84,7 +84,7 @@ class Message extends StatelessWidget { final ownMessage = event.senderId == client.userID; final alignment = ownMessage ? Alignment.topRight : Alignment.topLeft; // ignore: deprecated_member_use - var color = theme.colorScheme.surfaceVariant; + var color = theme.colorScheme.surfaceBright; final displayTime = event.type == EventTypes.RoomCreate || nextEvent == null || !event.originServerTs.sameEnvironment(nextEvent!.originServerTs); diff --git a/lib/pages/chat/events/room_creation_state_event.dart b/lib/pages/chat/events/room_creation_state_event.dart index 8960a801f..9362fd3d2 100644 --- a/lib/pages/chat/events/room_creation_state_event.dart +++ b/lib/pages/chat/events/room_creation_state_event.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; @@ -27,11 +28,27 @@ class RoomCreationStateEvent extends StatelessWidget { ), Text( roomName, - style: theme.textTheme.headlineSmall, + style: TextStyle( + fontSize: 24 * AppConfig.fontSizeFactor, + shadows: [ + Shadow( + color: theme.colorScheme.surface, + blurRadius: 3, + ), + ], + ), ), Text( '${event.originServerTs.localizedTime(context)} | ${l10n.countParticipants((event.room.summary.mJoinedMemberCount ?? 1) + (event.room.summary.mInvitedMemberCount ?? 0))}', - style: theme.textTheme.labelSmall, + style: TextStyle( + fontSize: 12 * AppConfig.fontSizeFactor, + shadows: [ + Shadow( + color: theme.colorScheme.surface, + blurRadius: 3, + ), + ], + ), ), const SizedBox(height: 48), ], diff --git a/lib/pages/chat/typing_indicators.dart b/lib/pages/chat/typing_indicators.dart index a58138a90..13949f4aa 100644 --- a/lib/pages/chat/typing_indicators.dart +++ b/lib/pages/chat/typing_indicators.dart @@ -81,9 +81,7 @@ class TypingIndicators extends StatelessWidget { ), const SizedBox(width: 8), Material( - color: - // ignore: deprecated_member_use - theme.colorScheme.surfaceVariant, + color: theme.colorScheme.surfaceBright, borderRadius: const BorderRadius.all( Radius.circular(AppConfig.borderRadius), ), From 4857684655f8409de27b03d6280c605118221704 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 21 Oct 2024 16:36:26 +0200 Subject: [PATCH 077/236] Revert "feat: Add default chat wallpaper" This reverts commit 7d8369ab3043c31cccace793e568e73d21f0b6e5. --- assets/chat_wallpaper_dark.png | Bin 47238 -> 0 bytes assets/chat_wallpaper_light.png | Bin 120529 -> 0 bytes lib/pages/chat/chat_event_list.dart | 8 +++- lib/pages/chat/chat_view.dart | 41 +++++++----------- lib/pages/chat/events/message.dart | 2 +- .../events/room_creation_state_event.dart | 21 +-------- lib/pages/chat/typing_indicators.dart | 4 +- 7 files changed, 29 insertions(+), 47 deletions(-) delete mode 100644 assets/chat_wallpaper_dark.png delete mode 100644 assets/chat_wallpaper_light.png diff --git a/assets/chat_wallpaper_dark.png b/assets/chat_wallpaper_dark.png deleted file mode 100644 index 88d1cf803675b7880db0ffd77a8dde360ca70aff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47238 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 CC|~mc diff --git a/assets/chat_wallpaper_light.png b/assets/chat_wallpaper_light.png deleted file mode 100644 index 781491c40a6a8ca1b53c32699ec4a3432fa33b80..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120529 zcmZ^L2|N?-|2S)E=pYm&NfC07LXMR#naB|;SJ>Qy;aIRFsY)bB2 z3AwLW?*B8}e9Z6r@AXo%p5y(z@9TL&bT6FU# zjumoWn3$Mv*lB3!I%u5LaB*~T(|5JBvN>hv?&@gitqW&j5`BClqD-q&m;1BMOTBvK z``h>N%Y|tC_|E2e-S!CgX&D2Sm-_JmtWJK1#PqIn8HxSdZ~Vc;HUGYW>vdO-Q(HJY zaLQuuY`r>T4cde{tNNA{aqjQ0;`i$E9hrrQ4#qr2eo86&QT*HYc5tG87V34za|yQP zrIO+4Z!?`b_f51*JULo*uEr9jVm%Wf`hiQPjY-woOr~ytNU?Cw9`Y@PGJFA zTd`kR-KDivw;H69jweQ2 zIgRtgIk^(0+uBZUQhfR>va!9*3(C8P$$1E=U~qQm*=)ZXet}gc4svf?w7|0ekru;BR)jX1BV%l`a&cN8i zSR1Kq<>Dk|Y3*{&M#{^{l`@M-#Y-7{bh7cV6!da(baq$vQWaVqp$tA#zLpjeTpi-! zpekgntt+VE;$|awR7yrlMo5iKP*70C%^Ibwcj^pnI`~ah$kxNdRasj4=FOW@H{nt) zZfI#)B_$|8zUT$}|d>snrOx#6KI zBt+S0?GF`BYpb<=TyMBJuI^%OC2iwq<7DIP;VvyJB`f{E=6Tqm{)5ojorVs87-`D? zfN3)SbDE~Bhm}0Q)M+mO5AA#aP^;B_-94)3lS;gQ9M-RqtDdQ<9aI~XfR@u?g8LcYhC24Jgvb^EwA*5#Le9gw$(%IR= z#$8a?&Bflv!9zuwI)QN^>bH#Rt4RM3P*f-gP&U!Cad&aNK_P*fEL=`y6)g-d%`6swc~QlZACBE2SIYHaY^8Aq6y5KNk< zP8fJGk90hY$*OrH*=Eo@w~|ykARX3UbQpdwBUuGN#vOqsQ!cL(IMlE98~s zzzTH%D2zi85yZp{f%-r%q&JXOmdOnh^ zKlYzpolhTj1qQQyquYEKEJ^t)c_&6RhO^$_wedYohH-|hFx#xlQn!Vv+rzRYUFSn; z<5dNuTxRnpLMb58mxRKQyKPtAsFv}dchZ3xF%uNG`x$^D*E-%>H;_Xx|4C2~O4zo8 zelB}%uiV7U>zltI8McSkaC6j0y_Y(3n)bEmN&FJ}f$`?s^c_|g!0cm#*?PXqNIOLv z_Vqyh)$asVw41U0ZAKVN!AP^yr6RQRaQx%aRqee^|Bbe43>Qbe1p1{7?Qn=jNFH+eKmL(i&a34@@lVWv)|Aouq?9C&Vtw(a*fJq#x)hL_0M!D2><%x2JtY z#b?H81VX>|JRhBdEQFb3k6mHCe2f7>uC@R_&tkiNv(nZlL9Vaxk>kCOPCj6yiv&24 z$w|Wm+H{OP%hk5*wp6$)>{<|X9CN1RNN&wG_c*}zm2py6-Yz-{sSDvRvOwj}ntD&u0WO3R2lR&x7v4vtvGj^D ztjzIML@>GfOy+_AhQRzQ7-N}9XoQYKc}&s!CJVP?_L{3iLjHS zp76_^ehP~nko`O3RgonA|1<)H>Ie+LEM)iR2>RC!tf%f0=0?tbEGnJRU=;T!L0w-W z??}?2$44UEUG_98=!lxp`IEW`1_{1?x#SIj}MW>|hr|2Y~mcm+A471o&@>Y4VNz#ed3 zdPF}$4Yy*F`qIDV++;EL#Wn3u!}hGu#Ne^NWrS}kbSyx}AQyQWu9k1zvH@B7MPS`w zo_clK_ZGsKhRl_-6&{)eFxNYZM-DAWThp`EeM!9!mbd)==hQ7Bp z;XmI0uc$wHKO?C5M!dpHI`#1$ z7~d0w$*PQ~m@w(~ELmC{pFYq@_ro|cw36Sa6Y6Ez(f7_P zg)!p!H^;xF_Z_%nt`#Ai=8jlJt(|Y^ocMrcEbxx|%5cK{a_FYw zY0n4w#2L9a<4p4SYm|Qv z5{YEdR;YDIrsEooX|QVxcgsR2?kl7FFtknGS%i}LE-OXO>z#-^y( z?@L>k#z1U;>D&3ejGIMCaeP2tZ5tUFSg@&UbH~o2V@G$b!N_53YT@(sS10_Xbj)`8 zqN~tYyoFp;YPbUr7YiuPZtcxT+Rr2Jw2ed`!vjNRCTcds3yh@fT7!mu437IX*jKR% zzF(KvYz24|=?Zs+#e7T5%NkcsIg7Vs~GV zrUOp>AG|0A(9Gb2$`+}lJ@&y$gYVpZuF;uFQ1=aGzy2u{B>uHg%OAQ{T%DTifyrGU z`t5Z$Qsf%QNS+fJ@TVyS!~uwV7Mcxv%aV~#fXkfQ+^z$azo^6&J|3&EaMkj~t^V+D zf%J84*a?}5A=sy+fRQ7q^!{DLN-lm*WogP<|AT@R4tp^TN>0K{$Xv?J~nxBIQ9~A1Max$Pcfsg1C2uCU@i~|HYL3?QltRNNEUY7`Lxhqwj<{4ZWJ^xXwACDUv+M+x8iOnyn=R zNaZny7c2XcZ=o`xdg&YNg%RGXm}Dhl8@_DTZ(`&F4$+g{K4r^4ojHkq<=L$*79!dg zShSzy)72}tC~)Z0`HV!lh}`co?$N_as^0s|uF`e0zri(OC|>96-?H>uX*yp5Xu#sX zK*C*Z{7$~`LG^xaP-S2tgCnjb9b?h(!9@5HGvP-KHsfnZFk8I6y3l5tsv9x27%M{y zFq1k{3E>-bI_LfR)VZ4rH^;HLG3ocH8tFz-Jj0Izg>$ZRj4(88;``yrdGF+f{*Quh6fP#j`f|RQ*adxip2=1 zG@w|x6Vd6#3yVs|?dKHE_>;G!D(!4hgvU}!xA{DzQ!v}|UX*Ghw|xb2fDx~Z`^h{-*VEr0!|{D7xn_uqXEbmy36 z>=a)kiVIs`erTz|mUnfi-<2Cxb6?Cvm8h<1W#}2$UzQ^pfpJ=ml7*E8s7n$6f z`n9b}HBnuCFBc&N&LXe*F^0z1`LFSV9s)d{{PuLlAqMo1cOImphAO4LJb&2G_l&-I z7!i@Is3&Sh9y-M?A79xAFT*Oav z0SP-;)*{FnNRqhOyt7=(u}`n6$VFBgu-Ebu!Dibwy@~k%g1(9M)o0W8-el ztlxuw-0LILcjVBqPE(8VEP9B@{s}Rh=J*ZM93iwKmlbDy3ePJ#k+KCN;JVfOX0v7d zO*c$6q&ONGHCTmPSZKGvdSvJ0BM_;>TRxOq_HFf@pU+QX)Gj~7IT~d~wGvW?;&SKY z$IHx{ByKh$OI(<>uT;|I5`PPFeH=R;Zb^=u3)r79uBvp4agh;2K}f9U3?bc>;!y_V zwcHF5uEhRv`uY|+{^E5oeXvCoKEgF^IQT>P1q0IG>K3|4QptXH_k9at2sR^eZrLj@ zKEyp{JNo$!ax&|G^t%PokaBAB=$af*-q)+A`hSya{|?t1kWylo8sXzTggqaSdt%q~ z&aRzbO-G-Ul-*6sF=5m=`Nare37rCE8Vp)W&8n}}xr$g^{pC0l9!zt2Fb7$G=XzFm zbUtqSc$GoBf@~FM&8IT7ntJEBQh_Jn8~qd0-qEFWS0PS;raP`UY1*@++?tjFNhlNoQP1mEnY^M|(488*3&GvPA zdhk#C5U28{mrVwydK)*<6(SpOIRAy4jH@e_>s&-kJ73$usOn&t_8j1w1fjl;w`hc! zXn6iRw#6az%zpYN`zQhB#e#pH=6pQ_(Fg=cKvhN`AdSXs+cf=kcM&2hNOC=%XC-13O!p!lll_w ze@kUI?!afX4Oz7P9Z;XBCqr05v<%-%G&)=Q-J_p{M)J+$)4icuauk#$Ax_6uIFDqxQ%9};>7bm34gr<*Eg#4W}r#_O$-bL z^5Lf8i5;fFy9!9Ewhv6$HqpSb-x=p)R}j~mUbB~3TRRD2Mc0d~SiuxQfeP$OtrK9b zJD8Cvi(@^;Bk$nv^OuGRkQUa*%b}lq#47m^8E2NUof73@u1oxgH9i6|7wWJ2X+;YO zk^E1ob=UGC@t$jy0wEcWP2Ra^*bX0n~+%XHe}7&VOV(do%fS^t3|^4Y!q;jn=Dj|t`O zBXrk;#sLev&czzNytI>|?u-|La3&Kf3Qbmk3jN#ewWhRJX%;31Vnidwf?!}Mcy8=i z7hnJ3_{vF?uk$n_!O+U}ql_G&LE!+-`o*8)rwi0~^2je-(TCGU$gxRy&%Ltst)7L; z*)p1OShfhq2g3MxfV5ML;CjblLMY;8ajNtk&ic|IU&d-z@a}VsaP7}X{i6j{Ls(;5 z=DHsBF(#T_mBOS}My9rp$4B}z&=8Sqjp_2;IlZ#nwNhe4Iweyu?kduB0--;Z{;h>J zR~ss%0&*+-;kGY_AHP&if9bS9XMXBh2q$0wuI4pYxTdk%&U$xYXN!N&^*`%)K0qgF z4a?MBcn8Nvtn+SP2HVjc2e~K{35n;pUvXpc$n#dEsufQ)^#-~-6uR>(4^jSUxe@7I zVQWU8s|@tQO*{ zTh9*F(tR+R$Mr*~e!I>0>6T9Cw-sXi&)h_EqStA%U=UeeaWh+*QT=0=~%Foq^FlRUponzSFD$Mib%yJv??CZfA#Mj0>Y- zDKa8X!P`rKC1Z$zp8xJJWyH-(P(lj9}kkGQEKnjgt3$DNG_K z)=!?n#R4~Ha@Nw5)pmQq50Ud!wOPpW+((4mTjYZZQF9A7@nxLfoq#xg-%fqS)2wUZ zqAFMTK_k+qMHcyI-LwoS|9Qcz%P|2dY)_l(QG)?RjJ_Atjg^7{_rms{H$|kvJ?r<9 z3OQKhCAaEO85Xm|E@9X>wda$>^}vLsys)*}+$w_bO_0&(n`0J=ovN7p+(}h@c%LTt-An>CMO=A>8|pcwdr4yS{1;yX}z4 zk%KfY5n(%J{F>tL5LJTVQcg#&KgOM*nIkG8U$UlHh&aWLDbCwvp<~7}b7xuPZ{);& zqp8q?EHP~8YrK=qrw6A{;^;F{p_L4>Y;gm_{WWFu@+RAl*!IgEh7I9CY4NFYv{I2p zHUgwG#jk!zTp9c~HiWgApO@c0z5xX;5U~i&c9*IFzl!&Ttxl6#8GZcImz`K`o1Qe$ zysrE#1XPr>K6}#m^?CH8$zjG6`K)Nf*pbLL?Sx66vd&8ET^HAax*sUAcrsQygQh!@ zPhlDc7rXkZV%Y5BJ@3MWbITd3-QDjXVp}!aC544>lYc-x?LSM9VbJZSBz<%$S1TdM zFteZ~MV>MAZe&kEh&a7X)D3;{o*X)aEiVGI*L-{fF;}&>mDt5b_{Hd_xwu{as$?T! zEMsX7@p%;idTGDDqq>YvBBndOa?*6HFyvzOlaG<9_i$M@@#Rxn5L%C}lrRWDzBC4A z9e<@H%IMk7X!sGMJwkI}#CQJLjxJxpD|$ZorlTSRJ5Q>5)8oaUPz7Z!37*6{ z-NwrQ8n19DDKvS#=MB$#D3TFSVTU`ync-;_)8hWSzB-*_M8k80(g#AmW|bF^qJr~h zS#6)ti?X^;5nOSl+ZPth|CT1o{w*59KGO@M+Zw74&4d)O!0i&V&`&;1t5^CimPt~F6@Sj=xNY=@qlyPb6M+%`s8y0lAFDb5ZO7ZA~ZbNs9#*V&_u@TCE- zbWnq8aOvGgTT2iF(6YX~|$x+VCwKOQxLFms+@u{q`Zc7D%4Xw5^ z5qG%M`wl$aw~sKl?6OjhVyp?(!cxv-3r*8vf7o*|sjdhlNQY{c6~4_vx9w?2WVcOw zuk?_*yYUIZyYM3(Gt!|UYSOW_drXHIG3O~(e@D3THWsFrws^POU38kCD8cSq*10^U zTHq+X;9`m>SW!5tv5G<6RK#)I+@eN*)c$Kpn(?zQ>_x`hd%vSd%4_i-)xzjvU^e%* zz%v_VE1K2J^+u-0YcrQh+uvokj(XO%|2c7mtDfJZN1N(wM@iIgN2`Uq6L!2xy6kfY z{_X9C;xg|+8Ob=l@0IR?@ZX`S;UTUlKUmSTrz*rOyaEPawPOJDg=#N_)qLhea1y)V9z zf*3pI>QY})idutbJz^iDKNid#$3^&Q^;Eqevxg9aLHfQL;48c`wP=VyOpE`bl1DKA z0X{t_WhJ=T8H3hj40^3Q8IpPakW&S`@a6e7l6sPfo8Ywa z)UcuAwEYu6DUtNFH`MW%aLDz+*D$Sl0e_dzJM#R;XNsa{)2HviH9s+W?`AAt4C}V; zuzH>p55c~vo$}3sX;Vqz755;t_*~ZK?DpRpIk%WKiTaj`;_GbR_g9Nfw68V%UXf9W~ zG^&!+!6QWn6h3*b9hE!k+V3mcfa}7&Z(cFWYu^2$j93`z&1zd?yyXQ|p_lBmm9lGEK5?_= z@0TQL9QM};Kh-2qeaew5-S6%Dy>ll!->UQfbT2|)>5bH1P$W%*rpzf5`w%K|-m#xL z_Z=$Vr8FN1+eP$ic85|ALW`XIK>v!e-*$A^PQoD#5bf_Ac4CnqHW{YQ9xK~ z!whplb47TrPue~r%ywvnAIs27(a-ibc-t+kT6=q48G7C^=BISBiJnpWD(1QcDW&hL z0Jx>J(G%~qBWVVTdIDu=8WesuJH|T|L}W&fX8<*u3Yxf9580B6pYtQi=S@b#ISHp$ zUd=FsQq2WEX2#wlOBP z`(dZ>_5OL-v05xEDzV6rq~e>sbRJRg=G`*Ynp}WR-Gf&iu@LGGDNp}f!R6DyxO)i# zmR2e@YQ9Z_7z^Ec{cvJ{m@xfaLWcOT!jO zrD}KJc|zlPhNRL2$5&@r>UIsyQ8rzuI^DHG`u%#-+B>M48r?HO!z6mKM?-yIAK7NkG;1yFc&@r;F?OXf4s_lBPT5u zT>IU2pzqyPDq%Xt${-j24h!Pu&ORQkF0p%)rXYV2fq-)o?N+z2$b&o%aWpB~`9XJ) zi#RgFg)&>e^*@OTfkP_dXoEVxHw%BqMm>Qe%4&O?43*!TJWSQLQQ{F}b8SU-eW?bo3!f_IQU)JtHN>!JFs^XN7{i9W|4UXg+C~|?qr@i_vdR3Xq zM1~-C2{~Kf55%Fo;_V{(9^QRI;l+V;6c0zeb>9!(Re8$L7c^3AP+b@&KL%YHlIXSK zE@kmBL5@}=rM7iLso>3*zXMt5w024Vv>=q7Br6}Fan_FFZG@w$<) z;mFRlq9vdx-kBxw$Z>&a&@JJoC6!!_#+C>(Ydc>YT@7O~=b?IvPaNKX!plI+ixqCd zC-=2v_9}piG5CYH5R0_AGF_QSGTgPyYI94^^a#2MkEjN zH~!|UO-Mz!VLL;@I>QKnqH^Bm4!kydpwb6BTNHm5frzZSc!a8^Y%xD$<+URBaS`RC z;ju{@`h)PVpv90)Jp{KTO#7%$h4s&auq(NTB}ts1jrE(m6qT$9z%bRj!%7DvI-HIz z9iw2MO-<(Bdfy#Gpw#yvw{0r5yl4AvFk8ifql@mYv ztM0(BE5T#s6ON4etrw0TViPcQ{N&3a%@P>@{AaCBS zZF$U&{_;R6alNvy;=X@A%nRhvE3}G#HBBXLYJ}JB9T+)*h}74=Ld6CX=HD~&lHRr9 zyKwCwu2}h!pDTTXZ<#J>Fe+aRgk&gJ=dXJQzG)IA&;*qqQ-w!fFKS?se|d43+Q%s& zJz=I0nMd;MLoQk@2|yGUt_5EGo;|$e_u|{mRlL!cj1r%K*#;QaWxqR@dCl&#)C~oF zAo5)GgH({XG0UzZfjnBrvo)t(=64eh?Vy)LR?o(-4XmgL?xI4$=eC!Lttb7>lGe4F zpN2{7BbIKFBR$jkyWbGx92n!Eu)`nWV6K%Kc{MG1jtDW_J$K0q!TU0MOGd=`w z{XSu38!K8Wwl+8-*kMmeVP5kn3%}WL?{+4*%viiEPUhrU<^7ujb*E-y5kXx z5J!mmd7tgNZMQEU#)Z`IZIJvl#BpBTvR57GZlE*^o-QvYvdB+wE226HmpE;Wvi=6c zr-P?5M|m|2>OQG&oDLMn+|u~a_9~ASpQbuxM^&~Z1|c!lOx{QI8*?+8A^0jb6gH2j zFTLv`p#8_!=V&a(Az~CtE}aLI4A|Bm2Kd0Jo~oTKEHTZl{i>%lH+|(Js z8|#tiEsj){$`7P8hb;c>*l}ZJb1GrNvn10@+(s4^s)2xSB4w^}l!YcHSH}9G>lfj= zn5#DQfS$^1l-`mIoS0SFcb_QPJul+K!A%hH0hLt|%uhz#ROB+0oLf0RsPxy{fOtwK zQXIVtmzB0YXhbBtV$SkLtla$mG2QI15E`AGbCh-nY90o}F3 zJY3giq@@_pwC2;BKnW{fH)=Y7E*d{XBR=v)!~3$$E=RhZu|A{^Ty_0E9JMb=j@^$< zBk^|BkIBClBA3rOEqA&n{kZ99`dwnKZ9P`ITOeXE4r0`_lCG*g@$zrpkaY}(#BfKH z9*sO1)x1PL*XQ4SUp3tG#rZTP*)Oi|uP)zJI2i9P${?*HNQ9z(|1CD#nyayyFLu@= zqum8o2YfwLznf@nznp^wN!EzN^WlM@70cyt3uBfgcqa$ZvFzvdkm=d|sH1rYag!~v zpj+2`*zPEW0=igyp=;WbM%z2qn|I+-c1MZz^q!*CO{*{Fwm8k)L%2GU+tu6d6yRIoKeFK7Lnx^hn1$KS=0E6Pl`2EnR(A9? z@#z3t2dmP1aIvV6oDC@u7+B;j>BPKh*-N(m_V?3WMyl|x{2#lZ@~OF@*D3qqbD{U{ zz>^NEl>786;ND*)l54R4j+N(-tfr~9X^oM@1#yFy-@~lGj}ygkA-$mag?I&(1hU*$ zwQLO$`X9x4SKA3jE?FRXzb7T6>}LyxtG4-+<67e;w~p7*(t9*(qWc2z^ZOwdd4(|b zuDFt0vAO0PanFm1~2culXm ztx^=+3Luj`&x#(rZycyF6!&%rJEP9$Fe{pDqJPDl!V;jcbiD@&mCaMrpDf`v#sIWuz1|LFqTmp&r zN&8GlI_td)>YT(-){rmcKU>$ES&j_DZsQNgY(0?744wCU$aXkf75J~SrN#ZL6^WOu z$ZZ;W`BN(O*AM8}_j>pM8#3gTShXS426eT&aJ!PcxzijCH=-ZRqcVNc$`umit})kf zD-2LQ#~6H+Oo~$au(VeiNc^pgDYzFK)#0ujoTlF_p{ih*3REa6WP$ke*m1uU%~U5- z{ZNGvbv)7m6c=-65|l49*BK}ctnx$;3kc~jO$MbNOL#>UoThr=XI|n!zZ*)w%KhJ6 zC~ZCJ?wer1jRRNb;vL^dVt?U?AkCJp14?s?ecB;~~0k_imgV!9)MmyM`2CD%T4_8n2GpHc&@DK?+*#mye+NusV zDXU_BA73i-duB^@&j%z@ea2r+ee9S%>>*>%Edm2sfT7I-W6ky}8@?@_#m%OV4lh`l zzf9yia)~?%i7##@{h_E{i_;kHFy!d#_k|`4BR!zD=={@4q{HP)8LI6G- zaP$K*H^0ySbouy2)#(mfowIz!u|bdJ-NfI# zI)BF)RoTMs3^M!5%tx%`ChY8C-5HQT;01}DDr0m3#V5zGLf+qs7z1ev87_?rJaD_? zmU}jIZERvo8N&Lc1(YKCZ#WHYYcNCqn$ZPjTUV@Vm6Fe(Zg=1vACNNJ+0kueY_)8n zhGcRzgKozIi<#NWzLI(y`)}VOIXy}oTOMw6xuBdp+LaCzYtpd&B?`u6Gz8D`elPqs zqtQ=x6(KqW!{KSa=rbX4?8fhdXuOXL#FEFm{?|hs^BMar06Xdz-{?OEos|x0?aG*D{HdJoX z_xiF$+L!a#T%EKbtW>ns2uLbhJ*`^}?tGwbo8jL+|8wmJcOaM_ZP2E1LJ^R7u>q;T z3+15YCCVh=x5Hmuvy3-ivS&CUkUdG8D6lK0AV6OtN7X$z=hsh)-)AG(Ex%LHw*i-BJ<9ao#&nx8|m8>VVR! zaQ*Up-7*yZ$b{G!5+y*S>FAy{A8?-#UQ9v>)VO zt}o90yaU$?9bm`2W$-vG&O&_I(TURLZ^kV|)b3@WJ^u!Va-|C;V_^gGlv8DBS%k<~ zJ1TO;%DF2Q6ssLOvpx%e(PPn5EbwPJlPK7ygcCDV9e6kOu8;rmn>$USv=T*xwMAOmOj5zCXFdxU}BEw4ax=9`3Ti9tPHACLvtnjP%O+uFt=J$?gc z@~oM^J7|!>hk+t3>XxIeVnc46iJQuCvordP1+MLwcBfit^1v4O*=;t;G2e4<1I@dA znOVjkY&LSyB2&iN7Bm6Yy~DK%s9)({pOwbsu)N8^VRsW@sCXUH8|L<=B#WiDzyQ$h zJS1Pnh^Qz@9$kLcL+EpRZ`1;sric$8on~wsaz$d4E3plv@8s%`u2u)Oz?fOZ{*@a) zlpL6~pAMsbQ23#Kr*YObksGpyaMzUzQf$_vlNq7~OL0YYNR!U8K>DO$5}6-8Y5w`hq0ea9JHAh5WCZZ#TD)f4;!nIcg7x@ z7K{Mta9WRU7N@QF<(Lwj3BvDXe25H43mV);)32aV4Xf@`d~pv!Uf=`a=G#3@lM533 zkM%+1ON;gL-{JQdig$hFi-lkD!`@h9@YN41tl0~x%b-R1UvG6+*4WDHzAeK(WzMZ- zYF@hW;5*prArVr@|-wv*EdY=<+i= zvYT{s^L8-Po8lCs5Nv0f;{LvH11^lxQsGNf6YoQ{^)d%;8-ZMRH1A#lZj1+RMKbZ+ ztWY@C30KJeK5xan#-hBU`J3dQrqSO5xD;J;FfIh6`Vrd_sCJA8&ROrZK>&-OF+0#- zf+e)2;LD{&g!7~6tcCBQi`A}#X(0+y%gZT3maK1)kcH(v&jq}F?v-C|s6gKY<|S3~ z#`|=I_}9RYvV^N&;6J+C5MGHZ4u3GjclvC{(X(Agl9lEU;}^klx)JzzVDT0nm#75D zXvfOw%j(x}snlJ6SXV(1g2)^0et$SI3n(D>g>YTVmdT_m$NNiavwLMF$%+olbpl3> zI4}u#%WWT!6QyQql7<9eN@`%+7`qrfDLQYlfluJI$}Q~T3&qP1YjXy^9U?24GuNp# zH)d1Tc!YP*ef=9>B93~#VxUmaZybY5f5V_(k}qIv$_e2X{nz{ME87h1D`JcHX?A|} z7^I4In^v`KDQot=dlV-&PP6?ja~q?bgkIvlFFn<3CS0om`lK@AtEDTD-7$f<%ReuG z6ywvfM6j9bIn3xSY`63BwG30)f?2Qa@BfG}-gY%-nKHWUx5MU|?k#Md7JM$K5AwEJ z!|j?Sh}b{w`s50Zl8?p|RYuH~IXxf$*L(-g`9pNPa~fUub3J->!|~&)hwPU7_@?tM zMCNxAe{+sI2nYUYYK~kg0?A{0Q>9)|@<0efv_%V+XPn!6LLXqyt4T1t+<@7{cIw;{ zX;DsM)D&m2SzhYa&x&ljfMAp=-UI$2rj12_2R>}@QvMk-Lt*LkN^EUko?E|cOq<~& z#w_@(P=q*o%HT4H1g7qu@HM$>)BhN)9m|(+sTO3Iq6&7M0d@e>0dwTMSGWKG@p}5E z1f#&osCA=(bTq|BxuW*jkrPd-ymyqXMH!fr(6$5Bmo@CkgL){3%wfj1u6|IFMBg(Y z;+DtU;Wr7y{UXUesJO@%0Rf=u#zIkZGFOuQD`Hz8J+c)9VWnZMJP-UEUOCoaudG4v zdN%|B83>tVSzZ)>c;ExFxS}rKYGqq}WHqF&cq(zLS3Vbu{FW=O4IuxGaK=b}#2P3} z^Alh;Zh1Mg>xqM~;KL}0O8{pYTeQ$*ug^C#Dmu<(yH*60xh!ayn{Ar;-QdJ5a&@T$ z1A@UEvz+aEbn+WpEbspJ5&J}4?Nzw|^b5FR{l!*^QqDMaF7UrhUCMwfm#mDDtmGrdWsdb;!&4Burf%yG zg1`*ZT0Pd))qVkWZO^&_hWJjeZKz+;*Y!@<>NVKThiYM;j`=@bG5zkCxm?I-aiC2c zaRS6sjf8=h#u+#hE0zlK^)fcw@*_&W{;S7smzvl^kmu)&lVIoS(OE3HDizqCTA6Hv zezA79nQAyDz+&~M_IAV`9X>hArS8W!kL&rl3b#GKjF^#+9*c#c?;(+1X<^*AtMV zeM8w8p$Lzw!R!P2W;=HY1rKNpn2maR45xVjxD#_P zi0(utJc^P4UY$pwc9y6`Av`#{rk4wQUp$6lej90;MSQmo=M{ zcnZd~qNm7pg)uuP=z56Rkf+MTiMj52lTRiV7}F<>l>7kO&ObZQQ-*aZwQ;GgX~~0o z*=Qj-t?SPCG>Bg-E8dOgCSFu3sBx}1z_({hk(*9D98rNa1|u(C*nxo zz6>4)NgJy-13HoKDFF=4e+S@mU@SpeT83*BC>&9jKyU-7J%HnkZW(x$VP06Cva1KX z1P?KVLqyz0uLxYSP~FMsM+D)QS=xZq7mEl1suG4;=3Qh|c(9*G%mY@I{nK=nrd!j)Zss*R?AxSJ^9y*0pQRr& z1C3^ZLj)>vjfU<(3*89`l(IVNypNyJ4pnj5W{J&L#zx$?Dh@>7G%4uy(<4OTDXna+ zl@x#2;7C1Lqq9&~g!jUVlx2)KI_tA6<2x&Y`4QkQ%bch8>+-Jlil=Yu^A(O?6a>Aw zP&clQTz zjip*3hYE==%gdkR6wG(PfM6y~MGB`GE35Sk>8QG1^H8)U38F20`sTkW#exO@9)dhY{lKM_Bd$jRf1WW0dNl24ku2qMY$>H8`rU>g6s1Y(QqE`0=EFF64^1f9 z4lWCI+0OEE>Vh6d-%jyz@xU|f&lC~9 zscZK$cH6qM0AJ<|H(ZCfA8^8L)f}4PD}+*=VRm~EX$wY7XU1WA3g=+S}r9YkMrJc zS$rn$7GJFF@b_1Y9XjB-lz&%Mr!(R;Rlz;&oionrTij-dwO`+Yz|65?^BD>W4`W{S zAlYob3lpvlZ}7Wdzjy!RC;lGSd1wR6NEEZ$H>=1-_Z>4Xb*thA!5uD(w0$-51GD?h zB_Ci`YXxg605}h7TjWWNaB(!7NnvZHmsxXbom~(@3$)jLoS>wquE0uzC8wH7 zMvB{9OWF`}3+88obgfC3-jxdQV8Yd3**_XH1Fu5j=bP)df~eFu`S=y@?N{||kWjhI zYiC6DC!g=7^|R1)hk;oAitv)d-P4O^E5&&=VtYNZQ}jfWZnFT1W&dSQNizCF#y=3; zMI(v8o~e&&VN;hj@Kq4r2Lf)Jkxn3|O_!(3XU=4%3j=pRE5jZR!ZB*=}6IUW;-u^;007H!~4oi^Vd+6 zuT<=`|K63vt}fLG)(sBQNze*J;#Ov@j0ZOENHP{Wkn#bn@xfp_$QI$fFl1bxl9whq zI>bo@3h#Ziu{$w_dqQ~WNpats9Q&-FMXe2ALDufFw)+7@GA~94_*@<)ZEQ9VOpelT z;?PB12XZ~+CE53rdY4uI(o(ueX@)^^CN9fk;-2XvAXn7>=kvSo>xu-3`ht5}_E>`h zpmS(B%leL7YF^qTgRtYju@p-?iQtrK22QfnUA*Me?hPXTndalnkmH=^o*@gIZed}T zzs&Ez#gaJ*Bi0|Z870(Hx|@BzL;CtSw50NVXJy$5i`T%dkbBC-$^kAgv)Kr2*A_e& z<$d34HOu-R1?;*1EZ+j9qs5}Fl~p`v``^ArvR}75z60U_{4;70Qo`kR2YwRUA(_j{ z2yzdL0ZC$pQ&b&iV&C5;TrVt`JgB!SXsQrxnTAD~Z$EYV2Lk3^*LHoAGH<$#I<_VO zs~F0fjfrQ6gfN+ zC3Y=j>xTQ6)?EMo_LYv07Rcsx>SjCmRdG4Qy$L@7vliHum;;`=p{BbH4%9>CWPhaT z0crUBn(q)JC~=bYpjE`YsiHiwgl6-`&Ycnmgb8hmS^B$^+hK zNChnvV^#nmn1JwmweoLD?H0C5RbODC;70m0Bt$!Z_Ge4f%#i)Sd&4}~Z_u{zd*;U; zW(assODp>GQP6U1ez!r^8mQoP&MU&=7Gk<@(V>ViJj}Z7xbRE-lLIwRoY1H1TbzN2 zm6)6dZ+lsUBG@e2o`E}n6bTt zpNzb0M0^(v2z*Zzymf;aVu@dF6jnG)>U2J<~J>xQWk-PSn84 zyYwKR7+bY3_KfU$Mo1^&_rRO0WP#eKx_Yxf#1)2tS^ZjhpC;4q7Q%+@mqP_J$!Kmo zTm;@-Z~|r@#z&MtP%Zdw>z2Fzi4Upp4?4MrEVjb_uZ-9Rj3pmwx)a1y92{qVv!Y zs2pobZA_|DR-C4#cAB$1V*@Bif$4V7J)CpOI_CLx{%9(C;O*OE9T%rsj^6z?_Y3Hg z&mka}gWm};wM4ePJr&rtzCDD3=-I;@=7gL6O6Dg|R|yoBIBMaHBzq;k5##!SHte5M zN?^k9K)>HO*I-ktcNf`~D+Aqdj~R$Mx7YGDHAB)T;qvd(`!4uQik<#b)1L-BcT*`75yhcTA zaDdc;lw-`XCQuSvVNJoO9*GLk_`ZN=V13FeT5P2OVk?&oUs-p}yeZ4G5NYa}an~Jh z=73`qBNxb(NbR`c~g zUYtuNtGq3~^y}RVo_hY9qjfkC0El^L#!6^eA`2ev_xY)X;`Ca3&x;P&ZI(83!pp?L zYwh69ZL`Ga!LTtxyBj2)Dz88Ob&* z^B-cl0&xJiY}(blAM*ejk4V$B5%SZ-Ac4PB8=YF8_S|mcvpRy{(l5+Pz`W*VVn{f? zve#?>{ez?>ua~A{p)B%!xsM)m8XF3-NM78>wX#F}u54YI)pPrP&L7#zGEW)wK84p` z!wRKccJ-W!IdjD6w-*n5^lov7^Jr`$R|_R@;wuYdb(F5(ZfbVK`kKe!(&$-x=-NSO zAHWNP;SWO2p?>YS7#>s4e>@6H9=v`OV*_Nq_MLq+;xR9@BI*Lso)YCt7_1m!op z%LFXTspNgS{KMZ-S*T*QTUGO0Jy=_eKs25f6)m$J!o?yK8nTkUvqwv%b9|qB`S{Wj zdAaMv@HkSnSYt+Ed? z{|_lPv#ETUme-Kq%9@%{SM*$fFtimdfQ z^;}fbz72b~d7Z^YI-8!$`jcsx*Au(MrtxUS?Sk`sMYxJw2 zgzju{xc{oz`_bi}tXtd@g8HLA1B>*KYuEzZE3>eZGHkjdD;A{L9_Oy?>@t09Lyj42 zJ_wn#@tZ#xAPq!JEww#XSuG$nm+-1eI9X|SKEXd2u^=EjIqUFeYF>F?!?!3?hSRTF z$i1)>>D~s<+Xq04vGnxU2Xe1v?;ZTWUrtHy@SvVXiPjsu`=tHrLSW@^F@NivU-3%O zXHr+0)vv%$hM%L}J^8Y}-m;2bFW9G>ioSDl0n*c6*~FtHMd#k!W=aq`ekKgK`1>+I z&%Kk1&dRm;NtNX-$M-diI54#>#H%iENMQl`=*A=F<yyR^9fb@e`U9CQp}FRYR+Qg383EeK99jUcQt2z{h_x0s&|IlY)EP-K|FVHSr{2J=0xP%c?;M>} zF6R#%$@6dflB6$~W%_G{IgLrUsKemH&#qGS@4o)+xmL>;M&@+v(+8YN{*Sakw;!(e z%j4_PZ=9O&AD%j5*f3z}y-hVrWTFD3&5orNI<17CRME*ZPpZ-!ENdPJ+%`{gh6-&B zDZ23NtJPlpk&-SgC_LPK=NaW=Wp6e*YZOt^!0(<^;wCqrSvWqjR7VW8-|bbh<^S>Z z-0@WQZ|8KB(I6v4lxWDv3WXz;vQqX|%9e3L$T%fhMx}7b$j;uI6Ghpw$x3!sve$c^ z>UnzbMUT&|TUQWm(voIE1|+KQE6r%~CBS3zZo-7sA>n)u^a` zX^sO&oBJ+b&^NAMo<#QB#X3o=b)S`#G>tKhgbI8`K~1E&zEj=C){URvCUl_2Cz)F( zoaNoJo?xp(WF=lYd`vL3IKX!-obk-d;zFz9p-VU^u^DRS%yr-XRY#iocbb!v(b=Ir z&fhFU-#ec0r2X-(`^V{QbIE5i0?FkZ-f4_-&OK%0C%laGzk6dt!`8LmWnF8$xn6d)dqa`nFFBSPkGWGvzfLrPl%8Ye+y`gAvi4z?tJddcT4$8Q8)hX_ zb(g=aw62b~jt|ABWb&3%*C_|uT8+3~=$J257%INmsDrn^68FhN+Q!1E#HHKMW!z9t zI@n%&W0k}&u_^Klb7yxz@#Dvq4}GsxsJ>RMJz-N9AD_g5R@-G#e(3-yjcMo22y^}A z)#4#(VC)XpRmt877vCw4*;WZ2abL??nZZHZT?)S{OaC#tewpcNG#7gMqQ}ixc2C4r z*PBdDaf$OyT>L5pbJ@OZ#dB;?)_9if;&Q8~r)aY1{+!MD(46UfJq*r6Oy}mbST2=!lu8|6+Y2)M>eUTjEgTHLJ4orbG57xaUeL=?8uTM2q~U#FC1cHHnS;5f zMk`xS!ib*#jsE!O1iyarnN)V^U~yC$^J>{uZl5*sIO8>#j8~_{xM{lZuRFM*N+ff% zFGe5Z@P03I7Sk`F^w^yOuccNg@;B?lNogKbJ$Uh*Z;bE! zBr1_ijK{%PWpeZ}V_A*+a|}0gw3nlY(2wYqq?CVrXJGzCdB3pslOy03mPEtm%?ryi z>K6jzs?#A`a7RJhA2qApki-7{OPGhn83=QsEoUCMn*@i>F6m;d=b&qNfGqc&rl|SV zLz@LI@ppHq9vDO|=IEX|7_yj(*;m35(texdmZTQcgW{`y_k(Q&rvSTe314(k=os?2@SwrLO^vd@iIqOL>tJ|0 zXOyPnoBUulwhKhXtw5?$Z&8`Dv5N3Fy?KI;?P)El)X=$N`C*tjBMY;1$3lG9Dh&fW z4AFb?(hvz}ZD-kv-SwB{ZueDl%892bU+g$55^H$`LflmP2)Yemv{&O);6rN5h9L)X znIGh2jbP95YQhBX@1qR+kkIA^6a6eX9?o*vso^#Hx~dc0spII13q4yQ`Y>`mZDU{1 zFI?lUpj%eH>vEKhR-HxopCdA40BX#GbA~4+OYZjbgBSxF@FBmv6GMCykD&=U&hiObN;8{} z4kXS_TvDlg;%&%_;<3s8V-EG7qyh83dao~9pP!WZ} z(6r_Am)^)|(H;{O&s(o%79Y9^HC63Sq(3%>o_{d}A(-%tjz_w~zCAIAU^_Y^XXwjqFy20ReWYY=gAzbg& zUTEj22>2lyQy|~_-0(U_CUr}!_xA73y9c_<0%0oAHgkoOdi-5=kS4F@Z2A*J{|W#{ zQfT>dtjMlt$tE-IZe37j$dIwa9%KujW<)$K11|2-1KCF*0V!8Z8}zzqpCm#kd(~d` zW2Vf>!8|*b$bq&!U|(}2KiV)q;U87x2caToe!ZH5 z_~+qYqQI71kWMhZ(oio!G{Jh%`>Tj??3Nid zlg!(V&6=@E?JlG|8%VDzst}j0!ZstzeaD$@I{!Ql>wQny2m!dnpiRUhgXoubmMN;C zk77><%!@L@?XG|j^3Ufl|7U;0A5emt&Aj4UbIatl}+>|$4X1vt#- z5HLXa%reabB@UU?XOZ5kBeky@*!_b#pUsd{-3CeBURElA@C)=YdaBy5+J)u(cZ0r{ zGR}v6%&FDtP{WYcWUo1G8i(`~&AmKSI6{c~joLi=aBixxsTt+UfvWsc2JuI?VMb zw|-mQBAeiZ_I-Y7RFG%}mIwEYNEj(TX@5DYPy^_WK_4%U(|3-D-F^)#W0o{TA?C&l zBCjYh%JI+W=Wp7GQ^L$vcRbEk;A8jKbbd9feh*IMsmgyI0br--1J?#tg|C)2~mmK^UCQB{@M8;Wlw+3}nx}^T)m+d=UG<`9q66L{lIzZBA425L2fHc>mL=;SEhf zzopM*(mU;;4g+OaZw%-8BBLYE)i0W(EB{xNvRT2v_#8uNd|CzT_FEuqk6=SCJr7ET z1g&O;Chxxt{>LAMWXz{59?JREl(bfVQWfC1&tI`tSh&LK8&E(68MbST3Y+5RM8IQo z!T()u1@D7$&Gn(p##x&UXUaQ))FPh~;$_hQlZL>+!o z&h+3Pm-&Y%MyVP03LjS&y5iMl8z4Ya-84e2Bzcf|4wejVhw~Qg#)7{Ai#iiT+-+FpY>+ONI{; zdPG79^%VF>75ON~!Q4bmw1D#t<7W+hYrgM29N=i}t9ydkHq#q8=C)`c(UyZe#&Zc~ zoHlpbLT#heSv%(GaY+!J)BZSy;7YXQxN?raFUP1mr|I zn(I|*ikb>h8fwsGk?@@!ygeu0RMqfU7+?-$P)sM?d_BY5sXJ z+~kO|!A+C2u0%!lf>75I#t8Y7c12;YCKlV|!nwom#w7KL{!IoXsCLpXa&yJJ9m{jZ ztncpb6vi^>TW0k72`ac_NKwa3j%gf{-kb!1Zz|;wLdzbHs0yUT95a@1NuOWITNJdK zWPz0?SpOHomk{o?kL;}7$MKfcbIy=Y?wVmLJs!B2{BF45Df=irT2|im&OG(-z^(?G@B*S3vOoE->BnG_y+hL@(#vd!~IjYoqGzYl4FY6?@_o0V8j; z`L{x`&~M9WYCXe&*TQ?$i~(c$YJJ_SPVEx=^P|!Jw&Y(+zl-iSr#z&|YM<@6^Is@8 zvGeVJmq>Z?yRVZ=38_F;!b#VO5UA!g;GN*qq{bABNzVqjXc>Cg>+x>lQs{kdC85x(;2*S zDPA)RBG`i2Wh>zP?`{A1W9mE``h?y66J;bxlw^q^jAF^F#rb2sNi1=zipSZ)MQx^c zzWUo7C4`~^-qsk6l3Tp=5;zbD-OQ(gM@7{?dAC2Uh1La8{TlCoRtf!!6NpC~K-np# z^X@j@^|pzO1XV-6VtVuJw!cEAL{(;wyDB2EBH_GU=2YtGkTf$R+E35XWYtuuca63^ zQ$T<&RUj`_X`mP?Xf4n~z{NYnckGmdt(j?o5ri{qnj1G3!TmzXeTpWbk31nqe+Yi! zMk#>dkLO8wi(n&&=!L_iCk1`omTwDK*~e%tDDuwKP&Gsl8Ge@y7@T&L)U{>B#_$Hj z#&U*H)yjWAx4=gBcf__p=F1kYHkwUO|I z=T0|?m!3xZm@>F|`4RLPXxq5dbZCa4L5x zV&dX_MVjE1@Wis=u-&T_^NJ@*v~M>RgK4+Qv}t?6Ph!JKqdn)tj#{O=oTgT&SgLcWI0H9G#w-Cb%6m)^Cv@lWJ-@4eLB?ICOCiyJm=fcmBaG4xCj^{xL_8hq!&RY9}b!a@n z+DLzWu^78_xz z;Xj=J2fO)c_O@9Y-4x-oI@MSG?D^rqTCA>ZQ)-gfak>nbag((yWJ^O)m!jk3wD*165VCkTMBqYn{_MY68V4XU;J02fbMf;SJ*TkRF&Ayv{`$UY^ z4mB*#38>_MYmN|?#+*V`T$-96Pb?fZKkmppYOgbX4f^_dM&d2WGmn5kf2=CRi9kHt zDc-psaurV<>4_Ok3?bh~vCWmvwZ>wq_)tI(+S) zuf@2EAYYVl7&7DUq=J+8+?`F7NQ}Po6wkMJTtC)xXQ*C{WSK^T*IKtEK^zH~Z1v>K z;%{AVIomahq6L;cL2O@ytp=TW^VDw=b~(uzXb~vzrmQJQxa2$FclgMn&cP40jzxzda0dmgh(vh%Nsl9bw~Yek>|j3c@q0C7$BAN` z@)IS!+LchC1C!=9A(Ru;aPyjrj-y>{&vj719Uxk{zhAk*Znd`*OOk+WB!- zY^~#tR*H6C3=;@`CK(x|petJj!SU{W7nlO#y8Ptu%t)Lg%8K;!y+Q7$JCX3pKqGQ< zK;Fc~X^*6SE+1z`EB38H$5qQJ$5kHl0!vAhDrjV7yTw~bjv^y^j+|rkX{XhSC zesWM~>#s(Rkf?pqY?d7ZI=k* z(0ORLxy0+I-L;1zldjgn%k-^bNw`%(3OiMsHlV2JKFh{-mY+Ap7^0RH-n8aXT-S6| zev(#O6~v8A7sAa!z-7F(jSLB=doQqllHb0UJCXjP!^I5Kgs<=IF(|Y>(NoaXU_o%s zF$Jgt(C&WIrc6ro?d9=r>S^Ojv-`fqj(vcV>D0S1EtMmis`!ZF2)6?TYAozsU-j6Q z$Tfp6vu%SfQ(W)mv1hU1un>BwRkcD4y=Y<#_yUw*P*9~ky7ox#9%JLg{1Zs(^?a}0 zgUeP_Kb+*T?PDy5;O$X=~W^8!(~tEgfjjTG8C z$EF*Y%H7n)4^2+VtTJL%ne4XEx9UR!`0r25LNK4h%}5fAmj<-Kn(_StyUK4Sw? zCv6d+`n^vePbny z#mZYI(<-2*{ip$H&C`yJt0#V))tUm=H;~Xm%DfogGOsQ2yu`@~6MhTq3fUCDH3V1S z2!<^&Tr>O8pi8Xw&J$WJt;>%(?rys_PXJKc~!pWu~)P>>K}n>NK*MEf*`h4*p!Uo4upu#HEB+`SgdEB?_~hL^F*x{ zf(}+nBjCwrg_fJ7RE$2TcZM48JNT)vo#T7@h2i4%*))q;dB@dD34JDLvab*Z z{%X^@aYfxPC+HZOC%{^Ho@%o0oG-(AAY2^FYpUW71aZ!^RUqZs<-eSv6SQ%#YK zBLT-;hL>dAF*a~lTJ=3FNY+CF0lVdj;)m(aIY$RZRL@cPoS=Zu$<#Ld*8BDHQ@B7xs*>Apr6Y z%3KfZy!?q&a82yPnce{d^oD2eGyWgH7pY z*`n<5<~9+cZr+AABNI0pHryIpYDrcaBs^+D>kK+$?wOs=Uq~fE?Idf$J`}i$5seMj z@>ix5KdK4Vi)^w_F@i|@aAb-n#BqMfMmLO=OhY`wSoI9cVC%KIP!VAq6JR(#LUswk zGYrSzss_@OqrF(8=O-^`yqrYyXQ{|aR)ISg^Z)3AQS|rwtTU*)cUBXU{VByx*SWqW zv6Ssq0dfv`^Lg9f1#cLGV!{uOYWdo_01HdGEUTo~<sec)bNxB=tR zB*~y-tXYi>Stbn5HZ3YG|7TU*A6~J+eZr5?AiC}H#g9|jL`SN&#-!2v7oq1$FA`mW zsQV{>4&V6W2Sig(83oPC{dN~-M6SB6yjwP0R_+%mwo)#NpUzrrnLO`sD*6sgKY+T+ zAAF|~qN+{{<5ZRf1b)9t~=Z|*ms+~z1%8c8O^|!!sf;2v*oaK#RqQi(N&Zcgc;nay8n9UfNgbFTf*yKq(ZY@<8)hUj(UqeeZ2OsYZp*yTc~Egq9tg z^lr0-XevL9r411IXBL3LsXB+6hH+_k=+mPUtm6zt5O$c}i{aXr8%X!t{a{SH}9A5I+U4l#k(|4wLAK_R#>P<8AF{fGZ_}{!pKAU+Mbz%Sk61=| zSmeeI@92!au*>NhsMm*&t%ebpL63apdYHExE6?@GFIPm97Kb@m)?gXOh_{gys}P^+DrHCAAUe19E2X)tuz8;g(%eLz3182h zFC36Dt(i-)smkILM$Lfi2pYP+aR$mSyv9;9z;=;d4u!UWrT~6LiavQpyLw7g6;M%C zp~c3A(w}z=cf`rLpM#elc2a>NuFlpsf4x7E)+?4dt6)Qc0%hK^`$NV)hoh9>&g16}ZQGH~uIK=-^+L z+8@Ugi6^wVn9N0A*n9dL$N`y4hO~DXVbG_TSuh47=r){x1P%ZF@zkMueW{G~>ubxs zSJx#@Q`7!9JLPC47+W@EAHsfp-+$fe82XF|mOSUx;xaXB@MqtHcCM-=Vq-C_;}oEz z3bKSY?NburPqJu7>bFdvX=yXQt(nJg1 zr#*A>577UM43Pp1-g@xcqw-7P^bow@e(?z@uf+&PTXI9B`gP_my=@&*&r5?y9$1kJ z5`(72@3tY`{w!`BclKeVR(EV~YV(&b&y1@2#--Q_2w~sE>;E<#i2w2fU7O<+b8_MS z?o$a#Bz2|dEfw7{dqCVg;}e5S$!5C7y+qH2PW3D=WHnt2zQ>mEQf%xnj#wF%``8#E zf)Jt+UNBu<*Msi;z}EXA`#Zgc$n#eHG4yvL1NX^POR$z75u2ix1Zm_04#5(*p#kTI?CV8>B7BzO(J?LLrtcR&TpXU8gg2 zsJ#RS+!Wb6Hn@-3VdT2EKCx|mmM5i*T(jOhYl$g(3{=LH5L34q1^|^ew8ImY z$~Rh1IxN|VWG&Y3g2d4auqISAgvV zBQfDJ0*hWa=*PpR!FuHoMlf?8Ux-*Ha6_#3QN z(6H>GVY3F2{+atUf(?l2<(z8N&TnweeQR_qF3`zR`gyy9u4J;{U^~ezz`#lP%t?$M z!bPlwaaC(oMr%*A8PAg$uhF$_MWOW|kFEFVHbQENph?=YeTOlf+=&U{vNyE+K2mUL z5`|zpp<{m$76~c=o$uEhxLlgacp;H8rz2Ctj-uls!4$m~VidLQ_o{#m{7cZrS=(i- ziJNp!OxfSF!@ch;qtAJ}WJ6*6h%06nwa8h(pLC!QlFPmed}C&t-AoZ@9VD#d`iIW& zl7Bk@6%EqOClIJu<(TzuW7nHprrEo$&5&f<_lB>wdSeSFrOFz?dMF@PC|tGsy{+hX z3iTIBXHn|FugqA>s8{v()-pE;SVDd~+uga|UzfEA!C~|}XRz#IQcIMZpi;F8atn-2s?nglqA9m3gw`3>9q9% zVr+sf)s?j7wx3OQ>KBXtS!J+9|NoS=2~qF3KCForF%QHzIA#oxyl+dZ{T^aHc>IO1B5DMb)5vj_ z-^&#K6u=l7lw^r%86M2>vJ`<_Dn)ViPXcJNDEfuxOYS}h?ulJsz@A(K!oO4L5+BzJ1>ygg~=sA6gCi+9pN} z%Sb$IX^4SiD~stNunwm85UQ#)=y^B;Cp5x>NV0!tw_$+LKcd5oN@IVmhPbk{?(9=c zJIbwcBD5Pppt30y6R?B^Gxvtk+)BxcAAT5TxMilgGCP7mO}*G_oA-InZI`YvOJ>`v zI{w;gl;MVGMJ^~vupHUBsTG=d5<)JaZj%2g)u)AFN)7SG)~P@kkp!%pbaxIR2U{PA z$+;VoCgr^g7BpU_zqE(V`T^wjsEoY-xz!)E6ekm|EhUgsQg=Xxg-aoHSzPFgSh=7O ziWrZOJ62 z2G9rF*oqMxJZpQL+5|f`rGLdb=9}M$#6j?y$KagPo~)8|yi)PGW9G8v&>TqtaY))LWL^J~!5@FH zMH39&{l9q z0K$*|i}}w%&l}+YvFgSEQLdyRwOM~P>Qxv4L2TR^?^i_-ys`9gTv0H{wOOE1LXQK} zOPpGt)~$sgd6m4OGQhPvipfe(!X#0xjv1Wc^=6%G-)t>!Sj#hA4C`v>u4cMSCN}tqEsv zH|^v(MDMFZ2bdlU(0pV6L==`cc-Sa0uX1g0@o}51np~E7t{h(`m`=v4c z$2Xkdx2I`@v<`xMgR%lpj0nPnC!SoigCn+(S*eJVY`R!-b>JpW+V6{*M2r8eG zt(#{!5Ve~by41xezbfQC&!z2$Y!Y0G2Q8e#s6`=FG6YG-kes>m2#FsE)Vh!=kXgwt zr8>n#nGI8tpe8}UOnMX|Jlxv^g^3jSQ{`#656jI0zFaO7t3QY89A z;#^2eDe#@^YpyHKRAC?1nn#1aaS($FYM}4b!rSlv{TnE7;0X|UVIox3Ze~>o6H?fz zdQ|5{^COY00I+c!xlEY03d1rmMIwV=jM$09;Pg1_pp4V3lWO0-j;in9nY4$k-UqCj zS6WQu2#3}sO}`;X6S-uw6;I9rjp4O6UjPGoq+q6NDlAy9PK#`Tkv-@uKnYjO#OM*u zX++AyI$sq+u88N(z)`bV2ZG4rGuw$kGq5-3d#cG`U-0pJw(CL1zcFz@iXSA#ff#9o zr%g1$W%;Eun4y14R7=NsGh*R1$LMmm3(D*u+-UMqYS~7DS_FDo{Fa48Sz@>fxmT*K zE-YY|VNQVTxh5OIj_}G#`eC(tkL9|rMcciX>-=3phgnDM`ja&n^vXtL`dg0H`8{W+;{qw2tQpnWJ!%Wr(% zYW(?q1~3-HHse!_oDePrlb0mJKT^Nckb0iQ6v*Eb)FA3sN?S?uNk{Moa3?YFR3Uv4XWT&o?o)Mwhig)2TIp=gk(IQ8K znFZ7=0R=Bd9S=t0m8t2I;fr}LUDAeTq%~u$bHfke6BvU_@QYCBw*QM>!6L!wehM;(~=3Qb%_>U!Iee3DSix`d#N_sy`Xc$Q}8 zae#g=bh*x0d;#tMx1cd1XLbk@;4(3ydv_kH3P9h-$7$pvgmuDcaTEN;lF_$hh*sRM zwzy{_8~3;W#5ArDHSb&>r6iJgrXq+nGot_)PY z*K_l~#*hH*t)fnZi(Wf4A&K=!7Y5S-3?yu3?#5xbzrZWVW-uWCi}BNeuhYMqBe;2( zBvb&b6K`mG6GhyDoNa;5L@FH+p#0{t8!ynw1pjFXI=>@1r2zryZK`mS z38@UOw0ksc^)RX=rlFJxN%;1ovpoeRBXw69-JEV|gLjPy7Y3Ad-(!(R_W)Jhcq|K`YZMFd7Yq6)Vjk!YQtdii~Uh}MrF01tEJ_8~lbftv~@53wfS z$o^UG{U3HfI2&%9%Y=gINiyKNmklUIsveJ!IIVw!(UHniD!zWW)deq*>f8)KyZ2`JjnFA1Dcepgm-E z)uAx)Z-Qn%M75QPOMe%t6~eF0Kmc!eBS`*!jOvIqaQ%%R=b=1_7 z7WiFMy~rD(P@Ld{+DlU7lfMhPp|s(3*`ua{2=7a$$gS2?bM>ZSQ+f&)cztaIsk-MA zz$#k(!a9Top-oCzmg5IPj~HevYC@_JL%kEX1$rVt0`i#?Lm`_!N^nFcw~meL31Taj z1stS1fv>`k0y8KN2r_Jb7Jj`*ny)H|rNQTQ4zLF72OO|PgYJxuF+|`;HfwG1j6=85 z?v@{gFsmq>c_^{qGJ?O=rfT4NA>&YWdsfvhu(WZ~M0ss%PSjnSPK#NFdQPY*&NO_}ec$h|`U=L?v@ibAwwZN&9)Y}#i2>3eH z12{9yCk@Q=2m|~fOs_StX5UvP1XI&`7fz^8-Y@O$AoGyd;5g?vkpE8e{{5C4C^S7VC;jtwgxt^;Tpp_H z7~X-)WWRpSU8CgWafpQxr(|Gfe;`!XV){@sD12C{Oa#$>{f~GIBs7!N8~wz0?FRE@ zimUfk!^4d@`gKo2GHO!2;k4eDZ~cY~tI1thVsPL(#I3GhZQZz6?I%>A3|T(qHKX1a zi-py^#3wJe;IJU|YcCr|BB0Dq!C-^}&$=j|tz`;f-G1P;3Qco_P&>ht^vnf;dBe9c z*xjZ?>Z1jLd-VS)i4@0C)ywM-CKjF8M|8aZBNEQ`5S-``4%2ceqTKsAO!mqM(xviA zU+N4dPCOkIhZlAOs(yNle=*q>92%w8a0qjY428x6hy;~ow<1uLQa0fh279z%#tD)H zxOYG#@4|N>`P1-*S`U&)?CT-YccQ!0>q(6lhu`tRom0giRq1kZ$GE+PDOb5E7dR$R#=!@l)mHZaD*^CVf}>A zcB=0Lw>g3Xy9K9A;ZYURMgQri_KpXRj^!zNi@NC3Z7=Fliq<+xRxejg=u7^u=c9xr= z1T&CAW@4uhj1#t0H7GwGkP0XfAJ)|(fx?3NcJbaC@zk`tMvoI+$RW1{GQjEnk4*!&iPytKg0Qz?#h-s{t^pIr zA#PLnGnoDNYn{p8IX8T>yHf$T!VrhzT;YoRxwCE)?I^MVj-;OMe7Xg98SZ}M@nHm# zp%OVmxKaDdG5(KOj>uEO%rSh`!}C^gXtEI4?tpEn+=tZca0VT^6@Nx7W*?lp?J_`y zJy{xoqS&VNdTq7&uti!zGkTTufJX3{Oylw8NNAMBRdI*sxq7@1Ec1OD>lwU2>`#sI8#nK|Pyw&wgOqv@VHzO?Ow3oDPI)bwY{?A@xYq=L-|(daA-Ig` zjmD6fPOx_Md&N%hIuQ-SH^7Ce0hfjl&YQ_W#eN?`Vi{eiOuN{ z&8kopY%hp{NG??y*i9Lf3HOj`-@t;@yDB89Ig^zH6-^B1B|ULPFmOcTF_6ICZMrlc zC3^J1Rd71MBaQi(&V&>NcOnJZ+ubn&)kQOKqnRX&i+}s=?gS|?ioKm1m_3L=^>k1= z>8a)jjW;mrizLA$=2jt8q*KDJ>ED>_K)??Z4MV~1n(-Dv;vT2^Cvdu;D@;gbKG|qE zU6K>iP7;(ayiR5}(NOy9d`b@p{l|coqlvV90qFB0dV~+*vt&`wET=F}T@qteMh$rL zlLql6ts9K=X!Rbl0MP}`+DE+9OqwTCRhB;R*g7XPx`_&D&WGFx%RE9{T+2=Nkk%rxpMU5$9u(c%IzD73<9 z!U;6xiHG^i2xSW(@P&g|(s@x2yE~2oE{_VaJpSu3c!@3=XpXAs&Yj%qb{@hmS$9Go zyteG45JD-JJK+kh?-)LMxs%As(U2dKO?a^OKPmq|Z15ek)<1{UmN7g~6ZaGZSR7PF zC|^qYfj}jMc%r$^gS8)hPUs~UBnp(cKu;uras$T`zG&h2p!~c$;UUp!J(eATz#DP8 z;cmQ40ip>-_@zs|pW!yx)}EicA2-2Cg#*0XDaQgz1=ZCSO;8{^08Y}>P^R=ZM3piL zvrcrMXmt;~V#gRjaM#G^nI)2@I|35~1Y6B>?v~%EeRTA(4}>X{fQ_lD2NBQ@OBZr= z^%tLhsKQ%&h&V)dWm?h#RkN_}Ma5qN#9D9_F7}#%N zz@)|a5oo6R`;Bg-UvAX86Ag&}h*zN3h}tl>sa0(D%BxP=f}--0v7QB~h@ zbzLj&wHK_DfMV=$9dM2EvKkO)Oc{~AQx%ky6cElPM&AD`>1*%`v%^GZ(Bf5C5twpd zj(qG-MoKzD)=(#<)pu%y-Bh`i@tzt05_Nyr4M>Zm|;5mgRa=Yh4gyx>=_a)rO^kTuaPim$So z`WybxqeC_y@zdL5LKkW`6AP36re}=7OHA&&AU`Ae7tjLq0m{xl>TIb&LcE_M5Oi2r zSjqhBiw-I>MA7F22yjxCL3%A)B&f#HSfV-R4=+8MC$@xOj=2-AfoAyWgh8Jr3cq6* z9L~RiWT20;W@Sn+nG>?Vg@P#uFpO@o9ii=I3z+2)u0F%W3g9@RN;swR|BNU>3zHK` z$y4yAOvb|ou>HP)xM?SExr(5-sXlNS&?)G-K@gXZIOT0_8X?L7I}a!UF9j;6QN*Gl zRRB!F3n3l^wm`e`+R*dZ(W^BQ&34R1xb?F)5s2fi4(%NV*G4jND@X!^=Rg!RVXu%5 zctn>_^s6;bB$g0{-PIXK{+ZO()perB;`S{`d6DjcY>RBS)QP-h0ZG>R@v@Ao%YDN0 zNv#s2OH=hvv%J#5a@a^eu=A3t{*LQfUQNZyojY!B>Zf+Gz_qyHTH3P~u7YEw{#O~& zLrmXvQQbZ5$=$bLXsJjlWwRV_`EtIXq~g<9qN0`Lj_@UJ$G^gJF!Z##4@v{cO)3+- z_fv~N5PvZ71+UTP6M+o&!_6~g<4Zj~W!^)Cs1n_d3YqMz=pp3;jcqmoH(7nAk2NuB ze-xPetRZ>&FX!#T0jfdnGZ>oUQr4cjY-&?OF=YMl~!NNAF@R!$KYUb~Z*lAGdo zmNi;-iKoS+Etcn9{&thHR{ia#>H>_Nv&N?ai~ouc?;tHLJVRz$TwLtQyGT@cUf_-H zI-FAQHtpe@`t$Go?A~wXPHM9H>{YvROW(__h)*J-B(Z2(@3wGD#EysRLM+Z}su+Q} zl{V~+CmO_f-=8FoV@6#~?wy&Lnddh)oY^J16r}*MIii1NH6C_eV%GOllezEqQO>p4 zoXNWZQ@gM2t-aNAX>z9R(PTEo=@j`Oi7y)Ajm_iqXsUg?eCX}X&6AmIRD)aARu)$lde%!>?-qv|n_NvS2Q;ik&~f9L@u6SxyCeYX1EtyP&bCnH(mAzYxvZ? zzc`vC{kS67fq6Z!1}kQp;Hq%#QR;Ce4f|cU=J$x%dN^kaF=H1bJOs*y3J5Wjh53G* zOkR52*lt5+pUviRAw5}1tA5I~o?uGO48-BlbKu8(i!YdNDYv*CR^joG*NF}0E#?)f)m-D;jHC9s z8P%aJKk&pYCs57rZ!OZkFJEX*r(>d<*I{-oceR;eGAH}J9;!_2`0lIM>gFU%uE`B8 zi2Y!8Wxa=A5Q;l3vF0+!tZ{zR;VG!dN&>w62A7w|gmkm!IcicNZFk zEIZuk5d|-8Q1Z-nQU{a^L<$RLY8Fu6zj7Sag46xRzsU@r>P@UuD)w4D}}31 zuLG7B@RLBvsj(m>?@zR$hug5c_C2d3qAuj?V5r}muQfAZqmmAq`mthQq#?Kdmd`+Z zF{f{3IE?)OVT|V~eTn^$)($Jh+8djSV0xuMXY3cb1Ho=7;J%Xj)rU0`@wRFeKkDue zxz6dk+$p@?St0HcpvR_gv`VV5(Y{yMfKXbipjJ zv=^Dgco)iod4Ox&Cb&BFdR;`^<`H}O#g^6z3T%p+O5xncnMX>Tz6EL(=%q>Qw9O8a z($q$O(o{*9=RXL5C zr=!Gn=v|opBxW;VN8;ceo3M)$U`L+unz{}qKiNPDH z^U~4y9VSc9y;m!4G?Yx#gh*(gnCalH<63RzpK<1kZJF^kl0$WpUVG3bd9utceC}Rw zz3dLF?3`h;jkEy+KcM3&{(dNx$UV#~iGtgn6SNi!E(+&D8t3_rGG%96i}9$OlV(h~ zAsctW+a+)YTXJ1zbxPRtSdK$^iI;|bXu195q-dATHIbPGAx{w--c7*|j0>vIK{vr< zG$D9DO`K|YzB}id-8@U-TI!cBw540=Y*)6!Uf((D;o6*=*PPm~=X8uKKehGgxW6~A ztkHGZa5BKW@ETR)xU*ZG_t34|;&(jfmOAHHQ%qYWHr@mpu!n|b`-;*wt2Z$CaDRCjv(<<+ku=3okQr_6DTvxvf4VoTRnCSv=gt(SJ2f zJgiJ|x0L+tq7sF@Z60*qy4Ey49El^3H`O}apwy8ljjn}KAoYurWSMMAI+(_=uaty3 zHxfO`W$P#4nj3i@bc7K}qnrUrfgiOWGAD)cJ_$_nOQ>ih&l?X9;8 zJ46FkPuGkGK-m6IU5lXrtA=QLm|igvgG!PV^dn$n8kdO3vPpfr;?SXj7kekm=?;q) zJgjj#-|BR!|Lgb9HpK}upZnV;OmGaxn7eMSj=!HzbUh*<{AMA;X1Ce33pCQfX+@i@ z5ud$eB?X>D5nAvzud-hpy1@v!m>Bm#n}dRi9o`OUEye{(oIlpOqHZu>4f*<{XIaY^ z6Fg)szBcBxv>=f=!7zts=UZ7M>zTh-S3x+hAyCmIya79@B_M)yMlIZoxv4}l2v>p7 z?%up`nV1~2H; z7MpD}qHB**xgOpL?#1F_aZ;4y$vOu`_DD{QK``Mai9(1qW!IQwcS~3Le5hotwPx5< z%*{_Cy5}ajUcO|(cDc@&7bTt?>T=R$(Q}Ni;|fX-^R=VY?_agZPaU*34z8NbnL!1o zo5wl)c@GnX@Z}=G6~(3Ij`}BFV85fEZ4%#}%OpXGPkku{c)ZvK*O5egc(+||N( z^$T?yOy*uP+*E-vsEDC?S3#M~_?=*Ev8NslXHk1L*H@_F|^8$ zzUkUe*C~;g%;Ve?9b#zzwpzS?0%S^&;2N`6%5+9Z=-9@>uFOMJu9G|FUoWn$-89vJ za3}_Phfw(Cs9{I7k2}omPBIjKUvRm-b$6#djkj;gV)7kao*j?dq46I|3LWtS_^0od z;JvF^to0rheeABfjh-%>Xjq?Z#fK5abU;D>Jye@sA*tW!+{sW?x4kQr)C|O8l@=I^jBo=5W!M z)@R#o&-REgM}*|;6sjGQw9pmzJXjpI=fRNNO2=-~Fa7bWMVnkXD5mrqv@K|AU$yW0x4DfiPp>hy`&*17VnNRchXp-;d=ppV^CaC$615()|abd+k@s3yr!YFunw*Zuy$K1<3S znnh4}H=$Ye)%7D3Zd7a2lWnMmeLnOO!zaBFS`l;%Fz)(ru2Gh&#s zbkbK}Uth>h8x+$K;$Tm%2QWXhw({cynK7p$g;PVFs?_`X7tN>>i4P;D_);}p3^t~1 zFwLmG`LfhUKP}80jaem_5r%t$irr-*&o(4BQ(B@`rAGMdEaoOTTa>^udnC{yfa3I< zbK_JZ=h=$GZVfH0elpfIpjqNuFB%ElzbQ91M^8plGs`p~!yeV%*)dI?dCa=fol`Nh zh|4VSc5uOXfuUm&Z|e1f0&C=@&s-88jheGg=dNSFq@4GmYCW%rvo7|kn~;56mTE*0 zs~-Mm7C^hRYORgQMroN%?vYn!9iK5dIoXN(!rYQ=*a3U+u;d{;;Ukw0U3!tPP1@^i zS81}|`g(nZye@CHt7LM>1UD}_YT@6H(q&*QW0I=LeP;Y8w(7Gndq3$|fdHBF>b4B% z7$*C6(E7DIr~K=hlRD|+E0G#vaB|AcM_-7pF-nWHMWzC7=IwIGt%<9vlOdZE@uxmM;XDcCF{bGt-(`R8i}&32Pv(o`0_XS5&Z29Lmy@#1{apovzjm2mf%3^ zr07m)Kq+h$W;6e7Bm>D=6%PW~xx)}zMwXQwubvx{v$x-`t>ib>^PkC%L(6g=Z7qS+4HwT~iI>51kL4I~GNb z^WNyn#woL{D9$e~E*3l*>^B^Bs1U1&g+(7xdrgkO046g|@o4tiy${wg+`$iMnXC8e z6>#)t*^16Tmb@gooSHNq9pUtW?~BB=%}U4Q#oH4LzE{O#PEp^(*Vj$4exKG8nqKZ- zPC54{BB|OA*00`lSY23J>ab;in6&jCoDm8yqUrD~U-eWqA7i@$Uh$IFuI)!e7#>Bf zhFWRW8TttCK>3*3nQ}Q7o}Bk(WUWY9UGo-ibz5!j&wQ(x*tk4jIE7u9rR!#Qa}9V` zv8Jg?!*S=&mL$v~n7Fx;Zfh%g-D018%Za03V$e)vCW#?I$E+`=*!5K}U#xTuBi-)e zExvzwv0d6O%&`37nu|~IV4OJ&b+u&4t4px4q`aBGblptJ9dSe)qh{O|$Cq^t5k)BF~%lV!JN| zDU;ce0o3Kyj&5DTwfS?Q-vjq`-|E~sc51(lS6IUW<2`)zWTYMW7<4`T?;BdwWC5wg z6sB57V{IU<8b0c7G=ZM^JZ^u>e)vAhESt)rZ}86EyDaI8)}Pk=&a*NFQLyb7{!#Os zz*XS*wkCDX7pJh`xHIgk>7A9`ccf~R$xr{;mIPhtrIS_=nq6O;N{*`?0m^<)rn0X- z(12%ZcaxQ?bdDi%ob(GNW*X z>D`5ldEVk(M&0InBN%*B5|O~pCRF72BkZRIhP8MU>#-j_TkqMSFt#WZ!nj{pxuF&c zY2MYOk{JDh@;%MCe*;5n+Z4DRXr!ZiAGt}Pt^t}2w_I!az(53JPs$q86>CT={SSrj1MkXiM&2|}!WG`SlYagrPg=X0w;T7Q z>i>2{pZMqkglPj+2_zCv)@S|0LX7-c8FPJ-k)7Z1Hi*p0;t zdQ^V>44v9pv%RZX)wznr{_vPDrRy^p&765K8$u~_y%kI@R^T73+|*T;+X#ZuKyDre zF(tVuFH6kCD! z-sa{eQQvr!r>klGZ!XQkiQLMvvexTc`5jP4ahGo?!9+bvhRnC)1Ej+VPLBR!T>+DjHU=^< zP#&~GE*b`-iQ#q~eJ`s-@FKYGe zFa>j4|F~D`B_2AxZct7EMP5U^q9x?rXsotF?_j)*PcM<4P+J!J>Z<3%$S;g@>FJ~O zw%2d1lYH5}J64l^Y@ISqSd%VLp?DVEJClh3VU+XEJpPMHCJ~5cnLn{~CEA-Am#h(V zab@CDNdxPv)nZSlzwxd_Kws0%54dLaM688Ggf7r z>iizhAc%k zR+^}9oIIqt{nCU(3K}0Q`dVw90EXHZ*m#D%?o*K?tZsSZtF`Z^8xPLmSf1lo@7>h& z@~aQ_?KK%qM=zJ0rPMPYp?Nx9D6gOp7&!g{6Uf$AW<@SHIvc>IJ9u2*o)Q;MF>HC| zWvL(-su*}OK|TfC1h2!_cF8b9X0{pIgIz8`Opb|Fo<8u1L~R>B8s8)*nZLJOqz5Rdvyuf-b4)#0;z z@idIxO-X&?kLvWGHR+LbJ#JrrHosuS+vMLFyhd?0Ku-m^lz-ud)J8dc!V77t51dZl zu|OBQ+q2t@77j0M%k@@V6e1$oaTxWp?e_RcF1l+WFan;0#q(gBh(?63N}hozSL)L| z6t4^Ip?)gW1JcuR@1CD+&cI(-8(UO9wwiBQ?^(uO{_2uRyP__vlBs92&@2*e>B;m# zLrt^o0#N7G6@J976gFH}EnUAqTAo_|SH1BlGc`tg^aje!mHpWX{YvT-BZ~w>Mn>#r zif&VqZ26jK%on*m6*bhsEgHdpepQq!XrB_Q3JVKy?y;Et{P#lvcYquD!|no=c#VP& z?%~3g>~m$c3<#1<4}AXQu|UJKyf+h;bdk21rgyT%p8ZH{6}fwb(PvtS|E{KybZ2~E z!Y*O`Xxn_?cuY63*iEq`5R+@Y_h@zOD^R%oNvWShR&OI#HqYGjcW~#-U&W!~Qd>bs$J~&5n2(G|CkEv+$3U1>Fz*KZdKS}%P%2JSg zZtS25L2VmfFlHiXrSRumfPSz`bOC|8>$B^G1?WtiZ9!H8qnA*WTWX5ri@~mk^(B5K zw;U?*!@}!BnB=6ga%SDhP&FvO^scohnV)u#l zONOTPgT|D<^PRrROo0?_%wzyj0 zTMj44S6bMMwwHSn%$S-(_XkbnzKlAKXZV^l6W!-5!HGGjlbn%%Rz^`~1X`uZqfmh4 zcj{RcS5x~H$+~N9Ov}&|`?twk$hQfva_*JjZ#6Q9icR=f+J34^!>N_1L_I<@OEcC1IxRWNk6SAMB|_|Ee{)X8bsz8;@E zGi#TxyIp+@AAL`}V=$p#%#LlD;x~Y%|DKlko=dW?PldT1mu!0D%wwz1(2W=etp1BN zy#o3tWLcD(!Jvm2X=Ws&CbZ*00D?3bW4c%^e(WVSs!cO%I$yaZt}IFMgSsiQL^X^uG5wB$X+W2;y=a}mFbH&5%RQQ_2S6mgbXRi zDYW=TPaBuXH>2|kXK~O<;xp$~6-VSf`rz@|mGO!#^HfXIDtbqB&lfM^^tS_E5s`N^ z=FP(FlfC4O?2k!|P)*_0CQpulDUY`>jSG)w`oG^HanG`Y+h~kQI}WRh+R#FU2m*T3 z14q!%ZC)mQ^LQ#Wd#e&Ja>Dd3 z0@|4e0;C;%LPZxdL}*M^x^JY$YuM)4vAH{xCNzhR|Hxe8ZB4)Ds0F)-U}>|~06qLg*D^#x&9OPf z+$lMTJ3qGiu&u+x$N0PxI1sjPe0I8*R?RGYJH{%D{a$*pw>V!2!0f4R;#VD)y7`w? ziL*CriqN&f9R$2E$DrLc#~RCQHcTdE6?ftq@^sJB@uYS*+bm!K|Pe6)#KUgb_H z@_9q-eNPt!LkJ^uJx?(#>y)S$DN4I>gO!jewVq_LBu)z$>Ugc|!lMmVEhW3;F}At* z&COQt2DBB9Z;u%J$R3^5@Xe@f&iQEnM0wc#!t~yW0F&qzlq8Fkm-uYeh-M#f0V`{zGdA9D4@r%$0{# z4eIU-4I)lce=BkO+LclA{k+r4JlnNM!G$3L#55(?-RF4dGLf~cqSXOmY3IO!8=JSn z$b`2Iw0J~9FUZet&29cjf4Pmut@)OQoQDWpo)7p`5jdE-Wk`46!4{dFt`uI{vcKR% zF~kX30lffclq8ind^ELNq?;27va;q=hpgqNsOD#b-7PJ6%O0=3Na43p?X>d=AvoF#(>tTSKkVcqsbgchmVuvr}y<7u^*I zGBYFwiOM#sg9n;>$?^N9>Nkjs1O3u}`4QTYh&NKH5emH_d_Crx(F}(7D8s2*N;;e@hUnt9>3P-INrOr$CPrJ*hKZdbGaESiX!}M}) zBm{9&7HL9tZkoi!s7I2vNKf#_B@z9E3hP)@&0Bj&i{;sMtgE~J$+y!S7Fp)T#{C(E zUG}yVf~VK{0>ZK$XpY7hQ3Q^EkiN)kh7OoOCcG(l?y}d8e$ys=?4l>Pit7D3XS^@?Y)4Ley@$ zsmsEc&=UzmR3?$=XNh)})sR32u^TdPLJG_;-!&7*OE;x^YW%p1E3unCjX#B1NnEO2 z$t-u2vEis+sn&;7vJDgtByXWkYp$1W`hs7zHz=9e!GzenFWnA%WQrn)6oCTm@2zsD zu430@bLo4rOO>YKq8He#?ljqT{zc5O=?k6tg}Ab4(aO{ke}Z*A-Sd|Awa-bSA6>mdk)a44rZPij;vBPU5S4^K%; z6nypci_AAP!)dxM4DO)xUk(;_EOyRSFC<8Rn!Ttk1#Lar?s7@_jEg3!>gF3z>HCw9 zAX$Lzm*SRrwc+DIuOt`@ZcJdA5O#N(0R}+^hOT0IoElnVFG*N5b^{C z4oP1MK5?QHGP-Gf|Gg+2h5dp%iyVX~(6F*G7Dlgm_aCvAFSf4c#mdMK->G(P=%_8$ zt(a)ph1Z{YS^TKttImY~n;DmUOD4@Q^s4Dx@vmO3|Kb04Di*b)4i|JR&%-!54&guf z-50UbbQ24o{=U_z`DiA!OSVurArYSXEk<-*NljpfMpmIB9aA_}df>c0j*Ua?l&b~9yyc_k8tTDTM=cDSeA@;wtom#RWKE1O}sfgNe{ImG8&TKNMMc3rC%LA2Z4vFXJ2 zHJKkvoA+vv03YvL*S6n)y`Ov<&e)q~Ncm7;o09!Q`#rLvn|-X7%NNxfsZkfEHY>U2 z@0Rn7n$61wYR+!DCaa_-6%Oo^SaIVQBKHGEZ1$`>&SUFW-m%|~aR+yU{M_9R+)H~2 z4Hv+Ed0Ea%5$U_kIm5GDD=I^7s@(I+=+{cl~^X|tf= zi`}wYtCx`4M>jOO_Ll3mAMh;aVjg#9?d#cSTEVBfNo9nF@9kW@xLNehSiBoCk(eLc z1}%VK8O2x4>H-y%V|bN&TIwl|+aX9f%?Pgz@F*2?w%Txr6QfvgO+K4eCSo80R!hX- z^Uf`B!hfnQIU#=LmJ=Jc>ej2BXa=~tf*aQfUr4^94BH|x$}#Bh>Fa`jQus37r~;MD zgUa$xc1~L4XDXli&QvhyPM#)B+-j?zC_jZq3oN7-Wi{Y}s%?XDfbX;(mnRaOKq{=^~`c{LTpY`qJ ze86doR`YSJv%5ICweLNSe+f%B(WZtRbB3oxmifDYq#FOxUD>DN3r=W zySbvs9x85YwaAQJ#zfPJVF9VKxwh1l8|pbOq$_OrU1O#DGayjVXq@ueYp7{(s^!*{ z98H2AxH<9sZ1j|?&^TL-7YyHLXk;UAN?6qF; zjk8n*_+{0eb0>e_e)Zapn*l_0Z&%+C)-gEVQHi->ISCH0+H=>!^D!6PxXJ zd%kJ&PQ9z~foe5OJW}h%AIG8cg3bytyjyJe)fIpC^gxkOLvB0pb$?dzonl5UdxxXt z`%>07ImdaZ_rM#PY6VM%IplYB3>Mew(JoagAy?Z{zkk4cMON(nC70 zly{-rw%j%+6hcw-ABX>HX&<6d?RjvDf%(0`gmw^m@o|@El~#L*CiBDZO-1~xtTUUA z4xf|8udn+#?k_|4>p21$Ig8MZB!oGkA#(OUx8}(Id&|Z<2yrEyvubozV29?;e3gd8 z99gWwEmmbU`5)o=C^p*7L=&1M;8Vc^U;s>X z8Id~tM5>t}IKto+y-u2sw~fv=&AaU;t+ZnX)5D+DQ>0K=Ffg^AC6C&#?q6qx%|oF^ zr?!O^Mctq(i_Ys6Jqt&@4jQ149CGR-mpv+n@Pq^%u^6%H zR@M41Oxp<=tF0GB{cK5E6Ah(Hqr>i)F&-#4qE%%__UkRPw(r~2-(o;5!W1IWe%Bb6>ZjHw-On|G|EDV-Y;~%Ttm)nymDbjAad_P*11my zFP8?F&$`rl^bJ_NISvMMGe0K;B%H&?h!u}I5`Ng1Has4n5N>kHAXR&o2({@xsah(7 zgB_QzU%Z2Q4Ar!u*Bv*5C}TJ7`_$gXg;IS^syyVP-ltma57vM^Ze)c-XAPMK_k`82tg^aSXhW)L5~u86Jy$wXlw-NuebP0? zc`19>HotZBR$Kaj9QmwpoXun~^Pwsdq=qt$2nyA+qa zE+17ou;R9Uqp;#8O&Y1pefc}L0WE1ipaSC)o1YhPI}yPT83IOU(YG&D9|F^}gV z>?D=+0=*iJnagI%Ui7cXtHuP-y33SdXt(ZcIaOGOp$ue8o0AdQXP5|gmp2HX`aQ@MNgDEVC zcb%*8h!N%=ml}7UJ{NCdO`AS)UN1&ezhjqOQ$c<~^mFx1%=BHYCN%M!RMN_&*=F_8 z-(LS3z66BZihjP*xu933S!s+MW^WU5 z@y*Tm&oAezZ|n6RTaLuWu1m#;p=G|x+_t6292*6Zi8J<ze&PvC%vSa5v2WX$ko#NzF z)P+UeiEg|NQFpJqe0O~Q$Nox6s@WR`H3ojkeeni;CtR6U32($G&hkR~gAQ>x<0j7O z{)>P9J^&1=7|RQa0NYPgKZbU=Cf_v|xtJw<;4o`6?qF*Y*-U(=C0etr5r7f4`_6Sm zXhprU*`=R`;9?*jx=u7Ol)TrQK2a&gbZ&AiVv2zp7}A%y1BqbCxR7T zS#7AT-?RyJmqr>_V*LX01CQO5nYJ(87MX52j;9>Lr^fU=w&Q}%bXF-tW_D*f-Ibsj z#xGmZ;f)CC%@UiH-WO#*uH?ry2<2vnXq*?;c)7_C?zVU7_3*2mc8`6fy=9m3&UdJ| zED%ou`Yw5@3Xmsg{?`VPRL~0|Ao&bXlV^|Ak~eNNh<*r;OzgeA6ds%~e@fGQ#dJ}+ zv9aB%4c8d`tn7U@_!FG**?sTwM}`NR5#@1Nt4`)-ZON1|@uwMQ_#61(TW`!WaWJvL z0;(xfJa8ML0Ks-j7e*qCLi}a4cvJ$6!e(u&EgxR~yz{j>yrYiGuBVtVL@zD?x8&~F zYkR>!fg}y|COlB3r{Q2LGZ?P~@z!#kci(-WUz~UG`>`qN5X&Dsicp1DNPx+sbVO$F!PWrc>|&m<^Vw2dOGg>Xf8Rg@&U$b{>tzZUhWp!@Zr8z3EB3Yh z_%&~T-}bw(Agjqw$ChD9g}v63!2$sfkuIsd4(H6L?H&J>-thOW&2Lku$%^Zlj`J<3`2@}$H+_2``jG?2lc?~vP|9z zJqG2rW;t>FL9h_?5nWWu{HbRWDZ7Sw7(KPtEFyN$+NX^)yJ#OdO?A7jE$~E^Qb$Rf zeIHg8wdztqS!`Q8niVa!t2r3{q<;21r(&$ixd7YAJW^OtwENv)&%JQGeLL&@tuHR9 zR5vB${Pw=T!kc%8l9iS?Mh`RH>;D1`;5gZwe5%b^IwHhr6hX^nv&(N7LUBq)ptpmp zfKy4qr5U^R&GteV#AsirS6zDY)&0!intJL0{Y9zRX8Nf6i7N^Dy$(nAN_=no9EVIt zCR|HPkV4l=r%JjYH4h{Poa1c3jNtzrKEQe^g2N{aXBDL@XZFsoXO7CK?oZq)#>OWp z2EH&x$J&LWF6++x+^UTBI=%AfY`R34qj5^_^mEf1f3#UnCP~z{G=Y_>)!!;>+HZKa z-wCVQ-r|Oc5yL#(qv(Z)tw3zJK??evU0~Drwgu+F<{w5OJ8N3VO3qE^pGeRE1VHyw{eiqa#+R;-pGV_ z`5{{?;iE@}1q8}#GB{bG%lthXI_0`5njTJXF0y~eWSNg9Okril+sO@%ng(1&j@Amr ziRZi(1WU1I;j+>2)Vp5R=c09p%X+4-PS9?5sL1oFCt^-o@htUz@M;A?Wbo^OBx1vJ6%UixyNr{ zY-|A7tra~H>!y79_NE5y=Iaz2i=%l14sABbe&M1o>D#hZ$KDHVALndbPMXnS zkMO5mROaNdGt<8|rqVm69wS!xte#>bBrFXQofxVkW*Wnbm445RtOjO36LY8YKEzTv z>XKd({1eoc&!-5>5;^r>Mnig`KGiOfl7R&#)T06C+C^E)fjoCn2 zhYPN|5dE6sBKBbxRW6(RUqoq$P*22OU@&9E2K%DT+HVZwSa4QDQ$nYK0R4 zblh-dEgBen>|RT2a&kDM_!VwNwI_6dZDpEm!jaZX0gvi^uYKNz|%c*XXG4T*$al8v|qM4x40a_D}wC zZAI{5{V-K9kL|9VQqYW-?8bN_iejhfKEXKAQaC&Tq}#{dXC8%GF-B!1h2K_$Qnw3V zSz7vuBQ4Hh`V$vdOG3|xBnxVN$$@8>zp?t}D;l1a^BHbT0p{MBbH<3x>jm1C7cn2A zLFrw{PklynOkHE{Tf<4=NZ&~BeO3>Rtx*J~bGG5FVI-^`LXscct=G2P?Q-PqC^owy z^ykZ98EVMHg-gmk!fKr_tm$F4D|(x3o!87Tx+WBHa~va`3a^6z;}Vw_$kfz9@9xvIZ9b0WbYE>2+c z+AVwvRS$7t2OsUlf!TMAQSMSYV{YDw`z$rKK+!7FSP!dV*W4=*(e17e$9$pxsqXjI zV5iG~rlDjM-C13O62B~REBm8SG~1~sh;rs+#MLxDz`xeDrD3pRnDptk_DScBCst|B z{1l`i@+3{mT+->Ai~yblObv%zw3LJ@9|bwh)6$_H2wo;GzTiXl*Mu_KRTr1QF_E2l z{`dyWPmWVhRD=RY0i{ZdrRThIb7IGLh2$#}Bi1&p7-OaOk3GfJd5vvUJ#Gk`1GkMC zmhMMEQp`?BlDn^MqVTG_yZ`hav# zI4hS;@Jn0}Mer4;>QO}{uC-s!5Idx@tU)ijdyn^1;73`!3CdGJ?JIo3y=^iRC9TDt za|{gXPTIVo7M8dzd+vgjDda1)?17)(zTsJ%Qp}8-^krRvw|_;j^T@cYd9;jZjMaIo zkCy@L7G+%M>OLvDtS8b>EsMY6+t)J7<`g-wbq8 z!1cjLwv`-ly_Ix?ejf*Fx;eopw9HFi|8oESyvhpTp^wr#d+Z#Y^GK?0T<>f`Fb9sU zZrjshg?Z?T=_22{y;8S80bUm2w<5p4v>YR*0!P_wR8T0*EU3td4QAYF3I!4y9_l6a zcMs0Ei@7-#T!UM5@EEE|Maoc{_nTdvc%_v-7@Vvic=uA;XRkDv8E6Ib_d9Ip$G%58 z!Fa&Z=7w;YN%BrvfARJ&)M_-bB^fq!$jsQ=-5;3nre((A4nRCz8azL?cSw}p;@j$$ zJtCr+e9WQ>VLs#gntfT=XGA4UZtMH==p$8uLgRYx&-?~A;2#nPsi2lEZ+V?l|B!A< z2SZX{v55LT%~SU`H0&lh_Q&9CVfk=PCntlGgn%-05wEN%Daa3M*}#(rq_F4QnkgV~LQFKMMgM1=YQcfS@Mjsm4Y^$G9nsX}H_^>f(v%MTaIj`I9; zLC)f5!7{G+01k=cWGoxS&R@~tEVx+;bYzlmtnRx=@Lj0|J9H_lq^R_YgyE<0h$8)C z?(PvM?TCV>3hNoDXXgjp*?bG@>Xb8tkZt&7A~wu`576qv<{9+1fC%a#Yj&O->Pf&LdsRg3`J2^ETt>_oE=RW7D+-6C7;K?4@Apm0H? zSJQpKGy$D9U`N>}R}Sh29RV2081$CKsc-T$sLzVE_(m`N6j5z+n&YK@cVh-0vCz%_ z$Yg9?zjm-(X4f0kj}Hp29mV>uq?yu;4O-f{10*!0;+MR$>C}GxPuf1 ztiT>xjk`Om{a!uMV`S8$dEu(z;{;;CR2%&p_ z%)Lu~xE0ju`goiVhI|i(7zW<}P;*0D?47!*9fc|J-z68R?$?y6elwuX6XH}X^?)vs zf4h`=#4{s8Xstol!(O0*3zl&hNyI)OViQ$++y=9%0~>-p?EawbLlAdbrTe^^p1g2f z&pdVORH!D6&h54tsuh{fYCleoh3c!+D$o~BRz%u*n)V+rB_oFmXGNf4+O`<6UtX3 zo8ZNs6R|1eu-0oLe^W2-wJ8O>g;`Vqe9HwF9qD9Q2QZ->k&{IOnJG?xtlxremRaFG zwhQ&BvP-|65AKElL_Z;2Zd4UToHcX8Q(__N@8_dp#A-)7#ckH8;jZOT88(-{v(_~< z7HM4yeX+?Ek2oI8rV=^q9H_UKDNgRfMdkvEymeEQU#sNWxf2P|UW2m*#@>{ZD3Enj zK$($geE}q&KW%_t(0+;IK+ZseJ-^LQ`+7zllO&m5m>$2d4|KX!Fh3Uebd?yf=1qG_ z5M&04&H(wInTXA62?jACCBw5|nuIFs5&DgV?NQ_fstX@{)4wmvCrlZMf!ap2*v;U( zZ~xeom@ple4X6;0I%;|j>~;T?`iBOEHC!y(&Twz2WgVyv7(W#6l$$naZ{VAmvIfXb z%U!`{IVZwSrS9g|U2=cX6^)7g^<7j}s1j&hr*Kd_M@O_gSwP5+XNk%%G&0x$R4q#t zerd7ZkV{ZYy;CeGD+yF?3?}wniF>YB)xzF{CjY+&oQCV(@B5Np3e~JG)?NPs&QiUa z{Zi4>>pRP)SCyMDar8gMb1{>HW=JC*n*bOt29OZ6zy6!3r6a@78UR0esLtI}U$|JC z-MG;?nlNNiZ+OnyAgSMLJci9c2C+%o?KKfuQt6tW@!oTHC|V+sd;o8j zj^l*~v%E`UvS~D!0HUL|c7>3r(C%e~aqSa!C+rr>ErmmBX77aDi;(I!F$Ka@?Fjdyv`GGzODP5%PpWrxB)Flms>qEpy|%ioBFW~0m!&NHXX;`#bdLToVcP9 zgKnV)wS5{C&1yHe7ZHmFGp@WiOwiaCTD-O+H01rW{7DUQh$*ZIGB8DL_i}Gv(zh!X0VB9kQGe%fHNlBvpPZtlG?r}OoG+0Xl~HA z;m?sov01VQYM}bAFKuRQ(s*QWs)b|tL(%v4kc)ReIiiQWPck!D^@AD6>5V&x?So0@ z?KAKve;W*Kiz;eaLb+!tKubxmT4hFDW0VCz+h( zw&pdgrS9OCKdXP?%B613;wy4X$Fk2}1e0B4p%)YxCI6;bPXQ{_5m8gxGrc{(tKQ|O zYBIBNqs!w)1~u{?JRipk?;Nr90+yQtSv8DD2g;>t#fZHW z1tFd)^#0WZWd-D0NvU^`V4CnH=LT+DiG=yi2!&I98(ip#Tkk1vcvJvp&luQ3V>AC< zOcu&c14wVxhtabdAr;iQ*Y#OW$J<}owe+j{7G7BjXtSN8Zs`s{@UV{MXH_s+3x5R= z)c+JAoInks>B&N{(p$g}Ore0gRyIvm;O|mEzW#Ur$vr~{eNtoy?xM7WBe-`P468$K zhrMjkKrH}6uAsL2(@uqypt>LY7&HoWc+}c4!g77d+3)=l&>yW!#eJrg2Z65nH4gCy zHK1oy`wae=0Z_$00nw05%A(y1UTY3tDFkPvE=`TAE9&vn8wVDVr3;-G@t9L~0MKPT zK#IkH)OJ4%EdY+wU%fNjAZ~0CNe{U@2HX%dA4!2A4LQ*90#X~7)PeNEY&k9FD?V25 zGsu@dcD(>c79fnC#D%<9iFSOyqXcr73fMNcjYq9NDZIZ)5j^8bRi1{$bJzhi4%$jL z7&8!VnQ`C}Wn}eTBgR?#G0YBP!=wo^H@oFQGYa!wU?w|K0$L_l@=LQZucGKkJ1DFk zQ}s0gR6>6QRYYn<~Xlu^{i%IeGpQALyL9ubt3i+BR|-_t50jMtU$B zdAHuk2UGvGC)}i+0O6VgDhNn93T#pIxN%V5Qxhamml!1CD*$93jyQOqFInl=R(KjB@@7~R^YiZ-z{QRlmWDKHH}m}GB{;4I zIen_hmH5(06UJmMnk3Hf)d~zG=Ai>eqD!?$jM*dnk1OhW3JXgM1>`YCGzJ~|{J;UA zVsl391OAM4>E?`gJ^vzIG7;y-Px8x=Z%B|qUWU6$r>>-J{w2Ov`(><^qN9fM$a98) zG^l82iR2p@GcPT|7vH7d5Kp)|tS3|J{zK%^Mo@doJjBB*bbE9=!#Y^W+kBnHsQ z$t#zTV$PrrFYrk`@}DGVubm~LaqIrYM{l=cPE0fCFH13dU&{p-;uzATNM^?_c0XWd zcttU;+d={<1_T(kQ^jus05aRPOmU#~Z+&nU6~Y{Q%lFgOh0m((s-0nPO2L%za9X$Y z^a7RM?Ryyj=KL?_yk`LlzyQnvP{VL*_J3#)(+WawQQL7Klc3yvX^IhN<<|4WeyO4x z?T|R`@Yo|DJ)~@^dqh<;58|(YWDv>rvSKgxvk{bqp8wyBvtR!=3*aHsvlB%-P3CIt zvv$QX;Fp0EVXxLrI#r@=+GW4#73IKGGwrZz~-+Pz3>;ll9f3;7fwi1 zFA!-7R5eM+TSt76@R^O-i6S4V=B!7OW_AhNm76wcl8Lb1q>!cC{zb;QRhT;&jATzx}0UWTm%BbX=c#7 zA`z6Y`Il1wrndY~4O^eGT-WJFC@ zMXjoI?HKt-Ei{pi?@N2h9+%s0An&C=g{Og8*dPlFm%+g%oz*Pe7>mQiflYj1cn0