optimise mxc_image

fixed mx-reply tags
added russian translations to newly added strings
This commit is contained in:
OfficialDakari 2025-12-03 10:41:47 +05:00
parent 4dd242bc7d
commit e44af26e8d
9 changed files with 141 additions and 99 deletions

View File

@ -3482,5 +3482,14 @@
"@open": {}, "@open": {},
"@waitingForServer": {}, "@waitingForServer": {},
"@appIntroduction": {}, "@appIntroduction": {},
"@previous": {} "@previous": {},
"backToMainChat": "Вернуться в главный чат",
"saveChanges": "Сохранить",
"createSticker": "Создать стикер или эмодзи",
"newStickerPack": "Новый набор стикеров",
"stickerPackNameAlreadyExists": "Набор стикеров с таким названием уже существует",
"stickerPackName": "Название набора стикеров",
"attribution": "Авторство",
"useAsSticker": "Стикер",
"useAsEmoji": "Эмодзи"
} }

View File

@ -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 const Curve animationCurve = Curves.easeInOut;
static ThemeData buildTheme( static ThemeData buildTheme(

View File

@ -2904,30 +2904,30 @@ class L10nRu extends L10n {
'Пожалуйста, подождите когда администраторы примут Ваш запрос.'; 'Пожалуйста, подождите когда администраторы примут Ваш запрос.';
@override @override
String get backToMainChat => 'Back to main chat'; String get backToMainChat => 'Вернуться в главный чат';
@override @override
String get saveChanges => 'Save changes'; String get saveChanges => 'Сохранить';
@override @override
String get createSticker => 'Create sticker or emoji'; String get createSticker => 'Создать стикер или эмодзи';
@override @override
String get newStickerPack => 'New sticker pack'; String get newStickerPack => 'Новый набор стикеров';
@override @override
String get stickerPackNameAlreadyExists => String get stickerPackNameAlreadyExists =>
'A sticker pack with that name already exists'; 'Набор стикеров с таким названием уже существует';
@override @override
String get stickerPackName => 'Sticker pack name'; String get stickerPackName => 'Название набора стикеров';
@override @override
String get attribution => 'Attribution'; String get attribution => 'Авторство';
@override @override
String get useAsSticker => 'Sticker'; String get useAsSticker => 'Стикер';
@override @override
String get useAsEmoji => 'Emoji'; String get useAsEmoji => 'Эмодзи';
} }

View File

@ -1,6 +1,7 @@
import 'dart:isolate'; import 'dart:isolate';
import 'dart:ui'; import 'dart:ui';
import 'package:extera_next/utils/notification_background_handler.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -25,11 +26,12 @@ void main() async {
if (PlatformInfos.isAndroid) { if (PlatformInfos.isAndroid) {
final port = mainIsolateReceivePort = ReceivePort(); final port = mainIsolateReceivePort = ReceivePort();
IsolateNameServer.removePortNameMapping('main_isolate'); IsolateNameServer.removePortNameMapping(AppConfig.mainIsolatePortName);
IsolateNameServer.registerPortWithName( IsolateNameServer.registerPortWithName(
port.sendPort, port.sendPort,
'main_isolate', AppConfig.mainIsolatePortName,
); );
await waitForPushIsolateDone();
} }
// Our background push shared isolate accesses flutter-internal things very early in the startup proccess // Our background push shared isolate accesses flutter-internal things very early in the startup proccess

View File

@ -83,6 +83,10 @@ class HtmlMessage extends StatelessWidget {
'tg-forward', 'tg-forward',
}; };
static const Set<String> blockedHtmlTags = {
'mx-reply'
};
/// We add line breaks before these tags: /// We add line breaks before these tags:
static const Set<String> blockHtmlTags = { static const Set<String> blockHtmlTags = {
'p', 'p',
@ -139,6 +143,10 @@ class HtmlMessage extends StatelessWidget {
// We must not render elements nested more than 100 elements deep: // We must not render elements nested more than 100 elements deep:
if (depth >= 100) return const TextSpan(); 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: // This is a text node, so we render it as text:
if (node is! dom.Element || !allowedHtmlTags.contains(node.localName)) { if (node is! dom.Element || !allowedHtmlTags.contains(node.localName)) {
var text = node.text ?? ''; var text = node.text ?? '';

View File

@ -96,13 +96,14 @@ class ChatDetailsController extends State<ChatDetails> {
// okay, we need to test if there are any emote state events other than the default one // 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 // 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. // otherwise, we just open the normal one.
if ((room.states['im.ponies.room_emotes'] ?? <String, Event>{}) context.push('/rooms/${room.id}/details/emotes');
.keys // if ((room.states['im.ponies.room_emotes'] ?? <String, Event>{})
.any((String s) => s.isNotEmpty)) { // .keys
context.push('/rooms/${room.id}/details/multiple_emotes'); // .any((String s) => s.isNotEmpty)) {
} else { // context.push('/rooms/${room.id}/details/multiple_emotes');
context.push('/rooms/${room.id}/details/emotes'); // } else {
} // context.push('/rooms/${room.id}/details/emotes');
// }
} }
void setAvatarAction() async { void setAvatarAction() async {

View File

@ -1,38 +1,41 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:isolate';
import 'dart:ui'; import 'dart:ui';
import 'package:collection/collection.dart'; 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_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod; import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod;
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.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/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; bool _vodInitialized = false;
extension NotificationResponseJson on NotificationResponse { extension NotificationResponseJson on NotificationResponse {
String toJsonString() => jsonEncode({ String toJsonString() => jsonEncode({
'type': notificationResponseType.name, 'type': notificationResponseType.name,
'id': id, 'id': id,
'actionId': actionId, 'actionId': actionId,
'input': input, 'input': input,
'payload': payload, 'payload': payload,
'data': data, 'data': data,
}); });
static NotificationResponse fromJsonString(String jsonString) { static NotificationResponse fromJsonString(String jsonString) {
final json = jsonDecode(jsonString) as Map<String, Object?>; final json = jsonDecode(jsonString) as Map<String, Object?>;
return NotificationResponse( return NotificationResponse(
notificationResponseType: NotificationResponseType.values notificationResponseType: NotificationResponseType.values.singleWhere(
.singleWhere((t) => t.name == json['type']), (t) => t.name == json['type'],
),
id: json['id'] as int?, id: json['id'] as int?,
actionId: json['actionId'] as String?, actionId: json['actionId'] as String?,
input: json['input'] as String?, input: json['input'] as String?,
@ -42,16 +45,35 @@ extension NotificationResponseJson on NotificationResponse {
} }
} }
Future<void> 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') @pragma('vm:entry-point')
void notificationTapBackground( void notificationTapBackground(
NotificationResponse notificationResponse, NotificationResponse notificationResponse,
) async { ) async {
final sendPort = IsolateNameServer.lookupPortByName(AppConfig.mainIsolatePortName); final sendPort = IsolateNameServer.lookupPortByName(
AppConfig.mainIsolatePortName,
);
if (sendPort != null) { if (sendPort != null) {
sendPort.send(notificationResponse.toJsonString()); sendPort.send(notificationResponse.toJsonString());
Logs().i('Notification tap sent to main isolate'); Logs().i('Notification tap sent to main isolate!');
return; 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) { if (!_vodInitialized) {
await vod.init(); await vod.init();
@ -61,14 +83,13 @@ void notificationTapBackground(
final client = (await ClientManager.getClients( final client = (await ClientManager.getClients(
initialize: false, initialize: false,
store: store, store: store,
)) )).first;
.first;
await client.abortSync(); await client.abortSync();
await client.init( await client.init(
waitForFirstSync: false, waitForFirstSync: false,
waitUntilLoadCompletedLoaded: false, waitUntilLoadCompletedLoaded: false,
); );
if (!client.isLogged()) { if (!client.isLogged()) {
throw Exception('Notification tab in background but not logged in!'); throw Exception('Notification tab in background but not logged in!');
} }
@ -76,6 +97,8 @@ void notificationTapBackground(
await notificationTap(notificationResponse, client: client); await notificationTap(notificationResponse, client: client);
} finally { } finally {
await client.dispose(closeDatabase: false); await client.dispose(closeDatabase: false);
pushIsolateReceivePort.sendPort.send('DONE');
IsolateNameServer.removePortNameMapping(AppConfig.pushIsolatePortName);
} }
return; return;
} }
@ -83,15 +106,16 @@ void notificationTapBackground(
Future<void> notificationTap( Future<void> notificationTap(
NotificationResponse notificationResponse, { NotificationResponse notificationResponse, {
GoRouter? router, GoRouter? router,
L10n? l10n,
required Client client, required Client client,
L10n? l10n,
}) async { }) async {
Logs().d( Logs().d(
'Notification action handler started', 'Notification action handler started',
notificationResponse.notificationResponseType.name, notificationResponse.notificationResponseType.name,
); );
final payload = final payload = NotificationPushPayload.fromString(
NotificationPushPayload.fromString(notificationResponse.payload ?? ''); notificationResponse.payload ?? '',
);
switch (notificationResponse.notificationResponseType) { switch (notificationResponse.notificationResponseType) {
case NotificationResponseType.selectedNotification: case NotificationResponseType.selectedNotification:
final roomId = payload.roomId; final roomId = payload.roomId;
@ -121,7 +145,7 @@ Future<void> notificationTap(
if (actionType == null) { if (actionType == null) {
throw Exception('Selected notification with action but no action ID'); throw Exception('Selected notification with action but no action ID');
} }
final roomId = notificationResponse.payload; final roomId = payload.roomId;
if (roomId == null) { if (roomId == null) {
throw Exception('Selected notification with action but no payload'); throw Exception('Selected notification with action but no payload');
} }
@ -137,10 +161,9 @@ Future<void> notificationTap(
switch (actionType) { switch (actionType) {
case FluffyChatNotificationActions.markAsRead: case FluffyChatNotificationActions.markAsRead:
await room.setReadMarker( await room.setReadMarker(
payload!.eventId, payload.eventId ?? room.lastEvent!.eventId,
mRead: payload!.eventId, mRead: payload.eventId ?? room.lastEvent!.eventId,
public: public: AppConfig.sendPublicReadReceipts,
AppConfig.sendPublicReadReceipts, // TODO: Load preference here
); );
case FluffyChatNotificationActions.reply: case FluffyChatNotificationActions.reply:
final input = notificationResponse.input; final input = notificationResponse.input;
@ -149,25 +172,29 @@ Future<void> notificationTap(
'Selected notification with reply action but without input', '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) { if (PlatformInfos.isAndroid) {
final ownProfile = await room.client.fetchOwnProfile(); final ownProfile = await room.client.fetchOwnProfile();
final avatar = ownProfile.avatarUrl; final avatar = ownProfile.avatarUrl;
final avatarFile = avatar == null final avatarFile = avatar == null
? null ? null
: await client : await client
.downloadMxcCached( .downloadMxcCached(
avatar, avatar,
thumbnailMethod: ThumbnailMethod.scale, thumbnailMethod: ThumbnailMethod.crop,
width: notificationAvatarDimension, width: notificationAvatarDimension,
height: notificationAvatarDimension, height: notificationAvatarDimension,
animated: false, animated: false,
isThumbnail: true, isThumbnail: true,
rounded: true, rounded: true,
) )
.timeout(const Duration(seconds: 3)); .timeout(const Duration(seconds: 3));
final messagingStyleInformation = final messagingStyleInformation =
await AndroidFlutterLocalNotificationsPlugin() await AndroidFlutterLocalNotificationsPlugin()
.getActiveNotificationMessagingStyle(room.id.hashCode); .getActiveNotificationMessagingStyle(room.id.hashCode);
@ -233,4 +260,4 @@ Future<void> notificationTap(
} }
} }
enum FluffyChatNotificationActions { markAsRead, reply } enum FluffyChatNotificationActions { markAsRead, reply }

View File

@ -33,13 +33,7 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
void goToEmoteSettings() async { void goToEmoteSettings() async {
final room = widget.room; final room = widget.room;
if ((room.states['im.ponies.room_emotes'] ?? <String, Event>{}) context.push('/rooms/${room.id}/details/emotes');
.keys
.any((String s) => s.isNotEmpty)) {
context.push('/rooms/${room.id}/details/multiple_emotes');
} else {
context.push('/rooms/${room.id}/details/emotes');
}
} }
@override @override

View File

@ -141,36 +141,37 @@ class _MxcImageState extends State<MxcImage> {
final data = _imageData; final data = _imageData;
final hasData = data != null && data.isNotEmpty; final hasData = data != null && data.isNotEmpty;
return AnimatedSwitcher( return hasData
duration: FluffyThemes.animationDuration, ? ClipRRect(
child: hasData key: ValueKey(data), // Add key based on image data
? ClipRRect( borderRadius: widget.borderRadius,
borderRadius: widget.borderRadius, child: Image.memory(
child: Image.memory( data,
data, width: widget.width,
width: widget.width, height: widget.height,
height: widget.height, fit: widget.fit,
fit: widget.fit, filterQuality:
filterQuality: widget.isThumbnail widget.isThumbnail ? FilterQuality.low : FilterQuality.medium,
? FilterQuality.low errorBuilder: (context, e, s) {
: FilterQuality.medium, Logs().d('Unable to render mxc image', e, s);
errorBuilder: (context, e, s) { return SizedBox(
Logs().d('Unable to render mxc image', e, s); width: widget.width,
return SizedBox( height: widget.height,
width: widget.width, child: Material(
height: widget.height, color: Theme.of(context).colorScheme.surfaceContainer,
child: Material( child: Icon(
color: Theme.of(context).colorScheme.surfaceContainer, Icons.broken_image_outlined,
child: Icon( size: min(widget.height ?? 64, 64),
Icons.broken_image_outlined, color: Theme.of(context).colorScheme.onSurface,
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),
);
} }
} }