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": {},
"@waitingForServer": {},
"@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 ThemeData buildTheme(

View File

@ -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 => 'Эмодзи';
}

View File

@ -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

View File

@ -83,6 +83,10 @@ class HtmlMessage extends StatelessWidget {
'tg-forward',
};
static const Set<String> blockedHtmlTags = {
'mx-reply'
};
/// We add line breaks before these tags:
static const Set<String> 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 ?? '';

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
// 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'] ?? <String, Event>{})
.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'] ?? <String, Event>{})
// .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 {

View File

@ -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<String, Object?>;
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<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')
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<void> 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<void> 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<void> 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<void> 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<void> notificationTap(
}
}
enum FluffyChatNotificationActions { markAsRead, reply }
enum FluffyChatNotificationActions { markAsRead, reply }

View File

@ -33,13 +33,7 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
void goToEmoteSettings() async {
final room = widget.room;
if ((room.states['im.ponies.room_emotes'] ?? <String, Event>{})
.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

View File

@ -141,36 +141,37 @@ class _MxcImageState extends State<MxcImage> {
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),
);
}
}