diff --git a/assets/l10n/intl_ru.arb b/assets/l10n/intl_ru.arb index 98e54be..ddf4d5e 100644 --- a/assets/l10n/intl_ru.arb +++ b/assets/l10n/intl_ru.arb @@ -3482,5 +3482,14 @@ "@open": {}, "@waitingForServer": {}, "@appIntroduction": {}, - "@previous": {} + "@previous": {}, + "backToMainChat": "Вернуться в главный чат", + "saveChanges": "Сохранить", + "createSticker": "Создать стикер или эмодзи", + "newStickerPack": "Новый набор стикеров", + "stickerPackNameAlreadyExists": "Набор стикеров с таким названием уже существует", + "stickerPackName": "Название набора стикеров", + "attribution": "Авторство", + "useAsSticker": "Стикер", + "useAsEmoji": "Эмодзи" } \ No newline at end of file diff --git a/lib/config/themes.dart b/lib/config/themes.dart index 4e1958a..3d94240 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -33,7 +33,7 @@ abstract class FluffyThemes { ); } - static const Duration animationDuration = Duration(milliseconds: 250); + static const Duration animationDuration = Duration(milliseconds: 3000); static const Curve animationCurve = Curves.easeInOut; static ThemeData buildTheme( diff --git a/lib/generated/l10n/l10n_ru.dart b/lib/generated/l10n/l10n_ru.dart index 976be5f..d537ed2 100644 --- a/lib/generated/l10n/l10n_ru.dart +++ b/lib/generated/l10n/l10n_ru.dart @@ -2904,30 +2904,30 @@ class L10nRu extends L10n { 'Пожалуйста, подождите когда администраторы примут Ваш запрос.'; @override - String get backToMainChat => 'Back to main chat'; + String get backToMainChat => 'Вернуться в главный чат'; @override - String get saveChanges => 'Save changes'; + String get saveChanges => 'Сохранить'; @override - String get createSticker => 'Create sticker or emoji'; + String get createSticker => 'Создать стикер или эмодзи'; @override - String get newStickerPack => 'New sticker pack'; + String get newStickerPack => 'Новый набор стикеров'; @override String get stickerPackNameAlreadyExists => - 'A sticker pack with that name already exists'; + 'Набор стикеров с таким названием уже существует'; @override - String get stickerPackName => 'Sticker pack name'; + String get stickerPackName => 'Название набора стикеров'; @override - String get attribution => 'Attribution'; + String get attribution => 'Авторство'; @override - String get useAsSticker => 'Sticker'; + String get useAsSticker => 'Стикер'; @override - String get useAsEmoji => 'Emoji'; + String get useAsEmoji => 'Эмодзи'; } diff --git a/lib/main.dart b/lib/main.dart index e598192..c57f6e0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:isolate'; import 'dart:ui'; +import 'package:extera_next/utils/notification_background_handler.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; @@ -25,11 +26,12 @@ void main() async { if (PlatformInfos.isAndroid) { final port = mainIsolateReceivePort = ReceivePort(); - IsolateNameServer.removePortNameMapping('main_isolate'); + IsolateNameServer.removePortNameMapping(AppConfig.mainIsolatePortName); IsolateNameServer.registerPortWithName( port.sendPort, - 'main_isolate', + AppConfig.mainIsolatePortName, ); + await waitForPushIsolateDone(); } // Our background push shared isolate accesses flutter-internal things very early in the startup proccess diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index e2e4888..de3a185 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -83,6 +83,10 @@ class HtmlMessage extends StatelessWidget { 'tg-forward', }; + static const Set blockedHtmlTags = { + 'mx-reply' + }; + /// We add line breaks before these tags: static const Set blockHtmlTags = { 'p', @@ -139,6 +143,10 @@ class HtmlMessage extends StatelessWidget { // We must not render elements nested more than 100 elements deep: if (depth >= 100) return const TextSpan(); + if (node is dom.Element && blockedHtmlTags.contains(node.localName)) { + return const TextSpan(); + } + // This is a text node, so we render it as text: if (node is! dom.Element || !allowedHtmlTags.contains(node.localName)) { var text = node.text ?? ''; diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index a0efa8c..feb7db2 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -96,13 +96,14 @@ class ChatDetailsController extends State { // okay, we need to test if there are any emote state events other than the default one // if so, we need to be directed to a selection screen for which pack we want to look at // otherwise, we just open the normal one. - if ((room.states['im.ponies.room_emotes'] ?? {}) - .keys - .any((String s) => s.isNotEmpty)) { - context.push('/rooms/${room.id}/details/multiple_emotes'); - } else { - context.push('/rooms/${room.id}/details/emotes'); - } + context.push('/rooms/${room.id}/details/emotes'); + // if ((room.states['im.ponies.room_emotes'] ?? {}) + // .keys + // .any((String s) => s.isNotEmpty)) { + // context.push('/rooms/${room.id}/details/multiple_emotes'); + // } else { + // context.push('/rooms/${room.id}/details/emotes'); + // } } void setAvatarAction() async { diff --git a/lib/utils/notification_background_handler.dart b/lib/utils/notification_background_handler.dart index 43ae6c1..b504ae3 100644 --- a/lib/utils/notification_background_handler.dart +++ b/lib/utils/notification_background_handler.dart @@ -1,38 +1,41 @@ import 'dart:convert'; +import 'dart:isolate'; import 'dart:ui'; import 'package:collection/collection.dart'; -import 'package:extera_next/config/app_config.dart'; -import 'package:extera_next/generated/l10n/l10n.dart'; -import 'package:extera_next/utils/client_download_content_extension.dart'; -import 'package:extera_next/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:extera_next/utils/platform_infos.dart'; -import 'package:extera_next/utils/push_helper.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:extera_next/generated/l10n/l10n.dart'; +import 'package:extera_next/utils/client_download_content_extension.dart'; import 'package:extera_next/utils/client_manager.dart'; +import 'package:extera_next/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:extera_next/utils/platform_infos.dart'; +import 'package:extera_next/utils/push_helper.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../config/app_config.dart'; +import '../config/setting_keys.dart'; bool _vodInitialized = false; extension NotificationResponseJson on NotificationResponse { String toJsonString() => jsonEncode({ - 'type': notificationResponseType.name, - 'id': id, - 'actionId': actionId, - 'input': input, - 'payload': payload, - 'data': data, - }); + 'type': notificationResponseType.name, + 'id': id, + 'actionId': actionId, + 'input': input, + 'payload': payload, + 'data': data, + }); static NotificationResponse fromJsonString(String jsonString) { final json = jsonDecode(jsonString) as Map; return NotificationResponse( - notificationResponseType: NotificationResponseType.values - .singleWhere((t) => t.name == json['type']), + notificationResponseType: NotificationResponseType.values.singleWhere( + (t) => t.name == json['type'], + ), id: json['id'] as int?, actionId: json['actionId'] as String?, input: json['input'] as String?, @@ -42,16 +45,35 @@ extension NotificationResponseJson on NotificationResponse { } } +Future waitForPushIsolateDone() async { + if (IsolateNameServer.lookupPortByName(AppConfig.pushIsolatePortName) != + null) { + Logs().i('Wait for Push Isolate to be done...'); + await Future.delayed(const Duration(milliseconds: 300)); + } +} + @pragma('vm:entry-point') void notificationTapBackground( NotificationResponse notificationResponse, ) async { - final sendPort = IsolateNameServer.lookupPortByName(AppConfig.mainIsolatePortName); + final sendPort = IsolateNameServer.lookupPortByName( + AppConfig.mainIsolatePortName, + ); if (sendPort != null) { sendPort.send(notificationResponse.toJsonString()); - Logs().i('Notification tap sent to main isolate'); + Logs().i('Notification tap sent to main isolate!'); return; } + Logs().i( + 'Main isolate no up - Create temporary client for notification tap intend!', + ); + + final pushIsolateReceivePort = ReceivePort(); + IsolateNameServer.registerPortWithName( + pushIsolateReceivePort.sendPort, + AppConfig.pushIsolatePortName, + ); if (!_vodInitialized) { await vod.init(); @@ -61,14 +83,13 @@ void notificationTapBackground( final client = (await ClientManager.getClients( initialize: false, store: store, - )) - .first; - + )).first; await client.abortSync(); await client.init( waitForFirstSync: false, waitUntilLoadCompletedLoaded: false, ); + if (!client.isLogged()) { throw Exception('Notification tab in background but not logged in!'); } @@ -76,6 +97,8 @@ void notificationTapBackground( await notificationTap(notificationResponse, client: client); } finally { await client.dispose(closeDatabase: false); + pushIsolateReceivePort.sendPort.send('DONE'); + IsolateNameServer.removePortNameMapping(AppConfig.pushIsolatePortName); } return; } @@ -83,15 +106,16 @@ void notificationTapBackground( Future notificationTap( NotificationResponse notificationResponse, { GoRouter? router, - L10n? l10n, required Client client, + L10n? l10n, }) async { Logs().d( 'Notification action handler started', notificationResponse.notificationResponseType.name, ); - final payload = - NotificationPushPayload.fromString(notificationResponse.payload ?? ''); + final payload = NotificationPushPayload.fromString( + notificationResponse.payload ?? '', + ); switch (notificationResponse.notificationResponseType) { case NotificationResponseType.selectedNotification: final roomId = payload.roomId; @@ -121,7 +145,7 @@ Future notificationTap( if (actionType == null) { throw Exception('Selected notification with action but no action ID'); } - final roomId = notificationResponse.payload; + final roomId = payload.roomId; if (roomId == null) { throw Exception('Selected notification with action but no payload'); } @@ -137,10 +161,9 @@ Future notificationTap( switch (actionType) { case FluffyChatNotificationActions.markAsRead: await room.setReadMarker( - payload!.eventId, - mRead: payload!.eventId, - public: - AppConfig.sendPublicReadReceipts, // TODO: Load preference here + payload.eventId ?? room.lastEvent!.eventId, + mRead: payload.eventId ?? room.lastEvent!.eventId, + public: AppConfig.sendPublicReadReceipts, ); case FluffyChatNotificationActions.reply: final input = notificationResponse.input; @@ -149,25 +172,29 @@ Future notificationTap( 'Selected notification with reply action but without input', ); } - final eventId = await room.sendTextEvent(input, parseCommands: false); + + final eventId = await room.sendTextEvent( + input, + parseCommands: false, + displayPendingEvent: false, + ); if (PlatformInfos.isAndroid) { final ownProfile = await room.client.fetchOwnProfile(); - final avatar = ownProfile.avatarUrl; final avatarFile = avatar == null ? null : await client - .downloadMxcCached( - avatar, - thumbnailMethod: ThumbnailMethod.scale, - width: notificationAvatarDimension, - height: notificationAvatarDimension, - animated: false, - isThumbnail: true, - rounded: true, - ) - .timeout(const Duration(seconds: 3)); + .downloadMxcCached( + avatar, + thumbnailMethod: ThumbnailMethod.crop, + width: notificationAvatarDimension, + height: notificationAvatarDimension, + animated: false, + isThumbnail: true, + rounded: true, + ) + .timeout(const Duration(seconds: 3)); final messagingStyleInformation = await AndroidFlutterLocalNotificationsPlugin() .getActiveNotificationMessagingStyle(room.id.hashCode); @@ -233,4 +260,4 @@ Future notificationTap( } } -enum FluffyChatNotificationActions { markAsRead, reply } +enum FluffyChatNotificationActions { markAsRead, reply } \ No newline at end of file diff --git a/lib/widgets/chat_settings_popup_menu.dart b/lib/widgets/chat_settings_popup_menu.dart index 05c5426..c480edf 100644 --- a/lib/widgets/chat_settings_popup_menu.dart +++ b/lib/widgets/chat_settings_popup_menu.dart @@ -33,13 +33,7 @@ class ChatSettingsPopupMenuState extends State { void goToEmoteSettings() async { final room = widget.room; - if ((room.states['im.ponies.room_emotes'] ?? {}) - .keys - .any((String s) => s.isNotEmpty)) { - context.push('/rooms/${room.id}/details/multiple_emotes'); - } else { - context.push('/rooms/${room.id}/details/emotes'); - } + context.push('/rooms/${room.id}/details/emotes'); } @override diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index 9737ef5..045e790 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -141,36 +141,37 @@ class _MxcImageState extends State { final data = _imageData; final hasData = data != null && data.isNotEmpty; - return AnimatedSwitcher( - duration: FluffyThemes.animationDuration, - child: hasData - ? ClipRRect( - borderRadius: widget.borderRadius, - child: Image.memory( - data, - width: widget.width, - height: widget.height, - fit: widget.fit, - filterQuality: widget.isThumbnail - ? FilterQuality.low - : FilterQuality.medium, - errorBuilder: (context, e, s) { - Logs().d('Unable to render mxc image', e, s); - return SizedBox( - width: widget.width, - height: widget.height, - child: Material( - color: Theme.of(context).colorScheme.surfaceContainer, - child: Icon( - Icons.broken_image_outlined, - size: min(widget.height ?? 64, 64), - color: Theme.of(context).colorScheme.onSurface, - ), + return hasData + ? ClipRRect( + key: ValueKey(data), // Add key based on image data + borderRadius: widget.borderRadius, + child: Image.memory( + data, + width: widget.width, + height: widget.height, + fit: widget.fit, + filterQuality: + widget.isThumbnail ? FilterQuality.low : FilterQuality.medium, + errorBuilder: (context, e, s) { + Logs().d('Unable to render mxc image', e, s); + return SizedBox( + width: widget.width, + height: widget.height, + child: Material( + color: Theme.of(context).colorScheme.surfaceContainer, + child: Icon( + Icons.broken_image_outlined, + size: min(widget.height ?? 64, 64), + color: Theme.of(context).colorScheme.onSurface, ), - ); - }, - ), - ) - : placeholder(context)); + ), + ); + }, + ), + ) + : KeyedSubtree( + key: const ValueKey('placeholder'), // Add key for placeholder + child: placeholder(context), + ); } }