diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 0bfb8fa..70e6715 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -1780,6 +1780,26 @@ "type": "String", "placeholders": {} }, + "recoverMessage": "Recover message", + "@recoverMessage": { + "type": "String", + "placeholders": {} + }, + "recoveredMessage": "Recovered message", + "@recoveredMessage": { + "type": "String", + "placeholders": {} + }, + "errorRecoveringMessage": "An error has occured while recovering a message.", + "@errorRecoveringMessage": { + "type": "String", + "placeholders": {} + }, + "errorRecoveringMessageNoAdmin": "This feature is available on Synapse homeservers only for adminstrators.", + "@errorRecoveringMessageNoAdmin": { + "type": "String", + "placeholders": {} + }, "requestPermission": "Request permission", "@requestPermission": { "type": "String", @@ -1800,6 +1820,11 @@ "type": "String", "placeholders": {} }, + "retry": "Retry", + "@retry": { + "type": "String", + "placeholders": {} + }, "search": "Search", "@search": { "type": "String", @@ -2710,7 +2735,7 @@ "@openLinkInBrowser": {}, "reportErrorDescription": "😭 Oh no. Something went wrong. If you want, you can report this bug to the developers.", "@reportErrorDescription": {}, - "report": "report", + "report": "Report", "@report": {}, "signInWithPassword": "Sign in with password", "@signInWithPassword": {}, diff --git a/assets/l10n/intl_ru.arb b/assets/l10n/intl_ru.arb index 24e5da2..a25b0e5 100644 --- a/assets/l10n/intl_ru.arb +++ b/assets/l10n/intl_ru.arb @@ -1529,11 +1529,21 @@ "type": "String", "placeholders": {} }, - "reportMessage": "Сообщить о сообщении", + "reportMessage": "Пожаловаться", "@reportMessage": { "type": "String", "placeholders": {} }, + "retry": "Повторить", + "@retry": { + "type": "String", + "placeholders": {} + }, + "recoverMessage": "Восстановить", + "@recoverMessage": { + "type": "String", + "placeholders": {} + }, "requestPermission": "Запросить разрешение", "@requestPermission": { "type": "String", @@ -2631,7 +2641,7 @@ "@makeAdminDescription": {}, "archiveRoomDescription": "Чат переместится в архив. Другим пользователям будет видно, что вы вышли из чата.", "@archiveRoomDescription": {}, - "hasKnocked": "🚪 {user} постучался", + "hasKnocked": "🚪 {user} подал заявку на вступление", "@hasKnocked": { "placeholders": { "user": { @@ -2869,11 +2879,11 @@ "@customEmojisAndStickersBody": {}, "hideMemberChangesInPublicChatsBody": "Для улучшения читаемости не показывать на временной шкале входы и выходы из чата.", "@hideMemberChangesInPublicChatsBody": {}, - "knocking": "Стучаться", + "knocking": "Подали заявку", "@knocking": {}, "accessAndVisibility": "Доступность и видимость", "@accessAndVisibility": {}, - "publicChatAddresses": "Адресы публичного чата", + "publicChatAddresses": "Адреса публичного чата", "@publicChatAddresses": {}, "accessAndVisibilityDescription": "Кому разрешено войти в этот чат и как этот чат может быть обнаружен.", "@accessAndVisibilityDescription": {}, @@ -2881,7 +2891,7 @@ "@userRole": {}, "noDatabaseEncryption": "Шифрование базы данных не поддерживается на этой платформе", "@noDatabaseEncryption": {}, - "appLockDescription": "Заблокировать приложение когда не используется пин код", + "appLockDescription": "Заблокировать приложение, когда не используется пин-код", "@appLockDescription": {}, "calls": "Звонки", "@calls": {}, @@ -2889,7 +2899,7 @@ "@customEmojisAndStickers": {}, "hideRedactedMessages": "Скрыть редактированные сообщения", "@hideRedactedMessages": {}, - "hideInvalidOrUnknownMessageFormats": "Скрыть неправильные или неизвестные форматы сообщения", + "hideInvalidOrUnknownMessageFormats": "Скрыть неверные или неизвестные форматы сообщения", "@hideInvalidOrUnknownMessageFormats": {}, "hideRedactedMessagesBody": "Если кто-то редактирует сообщение, оно будет скрыто в чате.", "@hideRedactedMessagesBody": {}, @@ -2909,9 +2919,9 @@ } } }, - "knock": "Постучаться", + "knock": "Подать заявку", "@knock": {}, - "usersMustKnock": "Пользователи должны постучаться", + "usersMustKnock": "По заявке на вступление", "@usersMustKnock": {}, "noOneCanJoin": "Никто не может присоединиться", "@noOneCanJoin": {}, @@ -2928,7 +2938,7 @@ }, "createNewAddress": "Создать новый адрес", "@createNewAddress": {}, - "minimumPowerLevel": "{level} является минимальным уровнем.", + "minimumPowerLevel": "{level} является минимальным уровнем прав.", "@minimumPowerLevel": { "type": "String", "placeholders": { @@ -2937,9 +2947,9 @@ } } }, - "commandHint_ignore": "Игнорировать данный matrix ID", + "commandHint_ignore": "Игнорировать данный Matrix ID", "@commandHint_ignore": {}, - "commandHint_unignore": "Не игнорировать данный matrix ID", + "commandHint_unignore": "Перестать игнорировать данный Matrix ID", "@commandHint_unignore": {}, "unreadChatsInApp": "{appname}: {unread} непрочитанные чаты", "@unreadChatsInApp": { diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 94c1ed4..8dd5d1c 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:io'; +import 'package:fluffychat/pages/chat/recovered_event_dialog.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/synapse_admin_extension.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -271,6 +273,7 @@ class ChatController extends State files: files, room: room, outerContext: context, + replyEvent: replyEvent ), ); } @@ -552,8 +555,10 @@ class ChatController extends State files: files, room: room, outerContext: context, + replyEvent: replyEvent, ), ); + replyEvent = null; } void sendImageFromClipBoard(Uint8List? image) async { @@ -712,6 +717,38 @@ class ChatController extends State }); } + void recoverEventAction() async { + final mx = Matrix.of(context); + if (!await mx.client.isSynapseAdministrator()) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(L10n.of(context).errorRecoveringMessageNoAdmin)), + ); + return; + } + final event = selectedEvents.single; + await mx.client.reportEvent(roomId, event.eventId, + reason: "Extera (Next) Redacted Event Recover"); + + final reports = await mx.client.getEventReports(); + final report = reports.firstWhere( + (rep) => rep['room_id'] == roomId && rep['event_id'] == event.eventId); + final recoveredEvent = await mx.client.getReportedEvent(report['id']); + + if (recoveredEvent == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(L10n.of(context).errorRecoveringMessage)), + ); + return; + } + + Navigator.of(context).push(new MaterialPageRoute( + builder: (BuildContext ctx) { + return RecoveredEventDialog(event: recoveredEvent!, timeline: timeline!); + }, + fullscreenDialog: true + )); + } + void reportEventAction() async { final event = selectedEvents.single; final score = await showModalActionPopup( @@ -757,9 +794,6 @@ class ChatController extends State showEmojiPicker = false; selectedEvents.clear(); }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context).contentHasBeenReported)), - ); } void deleteErrorEventsAction() async { @@ -1087,20 +1121,18 @@ class ChatController extends State } void onSelectMessage(Event event) { - if (!event.redacted) { - if (selectedEvents.contains(event)) { - setState( - () => selectedEvents.remove(event), - ); - } else { - setState( - () => selectedEvents.add(event), - ); - } - selectedEvents.sort( - (a, b) => a.originServerTs.compareTo(b.originServerTs), + if (selectedEvents.contains(event)) { + setState( + () => selectedEvents.remove(event), + ); + } else { + setState( + () => selectedEvents.add(event), ); } + selectedEvents.sort( + (a, b) => a.originServerTs.compareTo(b.originServerTs), + ); } int? findChildIndexCallback(Key key, Map thisEventsKeyMap) { diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index b95811e..01feeae 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -35,7 +35,7 @@ class ChatEventList extends StatelessWidget { ]; final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; - + final events = timeline.events.filterByVisibleInGui(); final animateInEventIndex = controller.animateInEventIndex; @@ -43,6 +43,7 @@ class ChatEventList extends StatelessWidget { // ListView's findChildIndexCallback final thisEventsKeyMap = {}; for (var i = 0; i < events.length; i++) { + //print('${events[i].roomId} ${events[i].senderId} ${events[i].body}'); thisEventsKeyMap[events[i].eventId] = i; } diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 3580a69..0f8e315 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -28,7 +28,7 @@ import '../../utils/stream_extension.dart'; import 'chat_emoji_picker.dart'; import 'chat_input_row.dart'; -enum _EventContextAction { info, report } +enum _EventContextAction { info, recover, report } class ChatView extends StatelessWidget { final ChatController controller; @@ -81,6 +81,9 @@ class ChatView extends StatelessWidget { case _EventContextAction.report: controller.reportEventAction(); break; + case _EventContextAction.recover: + controller.recoverEventAction(); + break; } }, itemBuilder: (context) => [ @@ -95,6 +98,15 @@ class ChatView extends StatelessWidget { ], ), ), + if (controller.selectedEvents.single.redacted) + PopupMenuItem( + value: _EventContextAction.recover, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.redo), + const SizedBox(width: 12), + Text(L10n.of(context).recoverMessage), + ]), + ), if (controller.selectedEvents.single.status.isSent) PopupMenuItem( value: _EventContextAction.report, diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index c85c85e..194eea1 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -38,7 +38,7 @@ class Message extends StatelessWidget { final bool animateIn; final void Function()? resetAnimateIn; final bool wallpaperMode; - final ScrollController scrollController; + final ScrollController? scrollController; final List colors; const Message( @@ -58,7 +58,7 @@ class Message extends StatelessWidget { this.resetAnimateIn, this.wallpaperMode = false, required this.onMention, - required this.scrollController, + this.scrollController, required this.colors, super.key, }); @@ -597,13 +597,13 @@ class Message extends StatelessWidget { class BubbleBackground extends StatelessWidget { const BubbleBackground({ super.key, - required this.scrollController, required this.colors, required this.ignore, required this.child, + this.scrollController, }); - final ScrollController scrollController; + final ScrollController? scrollController; final List colors; final bool ignore; final Widget child; @@ -635,6 +635,7 @@ class BubblePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { + if (!(context.widget is ScrollView || context.widget is SingleChildScrollView)) return; final scrollable = _scrollable ??= Scrollable.of(context); final scrollableBox = scrollable.context.findRenderObject() as RenderBox; final scrollableRect = Offset.zero & scrollableBox.size; diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 116aaf4..e305c93 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -113,7 +113,7 @@ class MessageContent extends StatelessWidget { case MessageTypes.Image: case MessageTypes.Sticker: if (event.redacted) continue textmessage; - const maxSize = 256.0; + final maxSize = event.messageType == MessageTypes.Image ? 512.0 : 256.0; final w = event.content .tryGetMap('info') ?.tryGet('w'); diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index 466d249..9237cd3 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/pages/chat/events/html_message.dart'; import 'package:flutter/material.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; @@ -79,7 +80,7 @@ class MessageDownloadContent extends StatelessWidget { ), ), ), - if (fileDescription != null) ...[ + if (fileDescription != null && !event.isRichFileDescription) ...[ Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, @@ -103,6 +104,27 @@ class MessageDownloadContent extends StatelessWidget { ), ), ], + if (fileDescription != null && event.isRichFileDescription) ...[ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: HtmlMessage( + html: fileDescription, + textColor: textColor, + room: event.room, + fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, + linkStyle: TextStyle( + color: linkColor, + fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, + decoration: TextDecoration.underline, + decorationColor: linkColor, + ), + onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), + ), + ), + ], ], ); } diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 7be7a3c..c2089da 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -152,7 +152,7 @@ class InputBar extends StatelessWidget { } } } - final userMatch = RegExp(r'(?:\s|^)@([-\w]+)$').firstMatch(searchText); + final userMatch = RegExp(r'(?:\s|^)@([^ \[\]]+)$', unicode: true).firstMatch(searchText); if (userMatch != null) { final userSearch = userMatch[1]!.toLowerCase(); for (final user in room.getParticipants()) { diff --git a/lib/pages/chat/recovered_event_dialog.dart b/lib/pages/chat/recovered_event_dialog.dart new file mode 100644 index 0000000..e49248f --- /dev/null +++ b/lib/pages/chat/recovered_event_dialog.dart @@ -0,0 +1,67 @@ +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat/events/message.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class RecoveredEventDialog extends StatefulWidget { + final Event event; + final Timeline timeline; + + const RecoveredEventDialog({ + required this.event, + required this.timeline, + super.key, + }); + + @override + RecoveredEventDialogState createState() => + RecoveredEventDialogState(event, timeline); +} + +class RecoveredEventDialogState extends State { + final Event event; + final Timeline timeline; + RecoveredEventDialogState(this.event, this.timeline); + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final colors = [ + theme.secondaryBubbleColor, + theme.bubbleColor, + ]; + + final message = Message( + event, + colors: colors, + onInfoTab: (Event ev) => {}, + onMention: () => {}, + onSelect: (Event ev) => {}, + onSwipe: () => {}, + scrollToEventId: (String p0) => {}, + timeline: timeline, + animateIn: false, + displayReadMarker: false, + highlightMarker: false, + longPressSelect: false, + selected: false, + wallpaperMode: false, + ); + + return Scaffold( + appBar: AppBar( + title: Text(L10n.of(context).recoveredMessage) + ), + body: Container( + child: Column( + children: [ + message, + ], + ), + ), + ); + } +} diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index cba1d79..101555c 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -15,16 +16,20 @@ import 'package:fluffychat/utils/size_string.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/dialog_text_field.dart'; import '../../utils/resize_video.dart'; +import 'package:matrix/src/utils/markdown.dart'; +import 'package:html_unescape/html_unescape.dart'; class SendFileDialog extends StatefulWidget { final Room room; final List files; final BuildContext outerContext; + final Event? replyEvent; const SendFileDialog({ required this.room, required this.files, required this.outerContext, + this.replyEvent, super.key, }); @@ -97,13 +102,40 @@ class SendFileDialogState extends State { } final label = _labelTextController.text.trim(); + final extraContent = {}; + + if (label.isNotEmpty) { + extraContent['body'] = label; + final html = markdown( + label, + getEmotePacks: () => widget.room.getImagePacksFlat(ImagePackUsage.emoticon), + getMention: widget.room.getMention, + convertLinebreaks: Matrix.of(context).client.convertLinebreaksInFormatting, + ); + + // if the decoded html is the same as the body, there is no need in sending a formatted message + if (HtmlUnescape() + .convert(html.replaceAll(RegExp(r'
\n?'), '\n')) != + label) { + extraContent['format'] = 'org.matrix.custom.html'; + extraContent['formatted_body'] = html; + } + } + + if (widget.replyEvent != null) { + extraContent['m.relates_to'] = { + 'm.in_reply_to': { + 'event_id': widget.replyEvent!.eventId, + }, + }; + } try { await widget.room.sendFileEvent( file, thumbnail: thumbnail, shrinkImageMaxDimension: compress ? 1600 : null, - extraContent: label.isEmpty ? null : {'body': label}, + extraContent: extraContent, ); } on MatrixException catch (e) { final retryAfterMs = e.retryAfterMs; @@ -128,12 +160,13 @@ class SendFileDialogState extends State { file, thumbnail: thumbnail, shrinkImageMaxDimension: compress ? 1600 : null, - extraContent: label.isEmpty ? null : {'body': label}, + extraContent: extraContent, ); } } scaffoldMessenger.clearSnackBars(); } catch (e) { + print('error: ${e.toString()}'); scaffoldMessenger.clearSnackBars(); final theme = Theme.of(context); scaffoldMessenger.showSnackBar( diff --git a/lib/pages/chat/send_location_dialog.dart b/lib/pages/chat/send_location_dialog.dart index 58c44db..b13c0b8 100644 --- a/lib/pages/chat/send_location_dialog.dart +++ b/lib/pages/chat/send_location_dialog.dart @@ -37,6 +37,7 @@ class SendLocationDialogState extends State { } Future requestLocation() async { + error = null; if (!(await Geolocator.isLocationServiceEnabled())) { setState(() => disabled = true); return; @@ -58,15 +59,15 @@ class SendLocationDialogState extends State { try { position = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( - accuracy: LocationAccuracy.best, - timeLimit: Duration(seconds: 30), + accuracy: LocationAccuracy.high, + timeLimit: Duration(seconds: 5), ), ); } on TimeoutException { position = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.medium, - timeLimit: Duration(seconds: 30), + timeLimit: Duration(seconds: 5), ), ); } @@ -123,6 +124,8 @@ class SendLocationDialogState extends State { onPressed: Navigator.of(context, rootNavigator: false).pop, child: Text(L10n.of(context).cancel), ), + if (error != null) + AdaptiveDialogAction(onPressed: requestLocation, child: Text(L10n.of(context).retry)), if (position != null) AdaptiveDialogAction( onPressed: isSending ? null : sendAction, diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index 326f7d7..27b32d6 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -27,6 +27,7 @@ class NewGroupController extends State { bool publicGroup = false; bool groupCanBeFound = false; + bool enableEncryption = false; Uint8List? avatar; @@ -49,6 +50,8 @@ class NewGroupController extends State { void setGroupCanBeFound(bool b) => setState(() => groupCanBeFound = b); + void setEnableEncryption(bool b) => setState(() => enableEncryption = b); + void selectPhoto() async { final photo = await selectFiles( context, @@ -68,9 +71,7 @@ class NewGroupController extends State { final roomId = await Matrix.of(context).client.createGroupChat( visibility: groupCanBeFound ? sdk.Visibility.public : sdk.Visibility.private, - preset: publicGroup - ? sdk.CreateRoomPreset.publicChat - : sdk.CreateRoomPreset.privateChat, + enableEncryption: enableEncryption, groupName: nameController.text.isNotEmpty ? nameController.text : null, initialState: [ if (avatar != null) @@ -78,8 +79,16 @@ class NewGroupController extends State { type: sdk.EventTypes.RoomAvatar, content: {'url': avatarUrl.toString()}, ), + sdk.StateEvent( + type: sdk.EventTypes.RoomJoinRules, + content: {'join_rule': publicGroup ? 'public' : 'invite'} + ) ], ); + final room = Matrix.of(context).client.getRoomById(roomId); + // if (room != null) { + // room.setJoinRules(publicGroup ? JoinRules.public : JoinRules.invite); + // } if (!mounted) return; context.go('/rooms/$roomId/invite'); } diff --git a/lib/pages/new_group/new_group_view.dart b/lib/pages/new_group/new_group_view.dart index f4cfe34..5cf1b9a 100644 --- a/lib/pages/new_group/new_group_view.dart +++ b/lib/pages/new_group/new_group_view.dart @@ -134,8 +134,10 @@ class NewGroupView extends StatelessWidget { color: theme.colorScheme.onSurface, ), ), - value: !controller.publicGroup, - onChanged: null, + value: controller.enableEncryption, + onChanged: controller.loading + ? null + : controller.setEnableEncryption, ), ), AnimatedSize( diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index 982e8ba..fc97935 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -226,7 +226,7 @@ class SettingsStyleView extends StatelessWidget { vertical: 8, ), child: Text( - 'What is Extera Next?', + 'погнали в роблокс?', style: TextStyle( color: theme.onBubbleColor, fontSize: AppConfig.messageFontSize * @@ -259,7 +259,7 @@ class SettingsStyleView extends StatelessWidget { vertical: 8, ), child: Text( - 'It is another attempt at making Extera, this time it is a fork of FluffyChat, that eliminates a lot of cons of Cinny-based (Web) Extera, like: slow chats loading, laggy animations.', + 'Го, в toilet tower defense', style: TextStyle( color: theme.colorScheme.onSurface, fontSize: AppConfig.messageFontSize * diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 3401606..d611198 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -18,7 +18,6 @@ import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/utils/custom_http_client.dart'; import 'package:fluffychat/utils/custom_image_resizer.dart'; import 'package:fluffychat/utils/init_with_restore.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart'; @@ -119,7 +118,7 @@ abstract class ClientManager { }, logLevel: kReleaseMode ? Level.warning : Level.verbose, databaseBuilder: flutterMatrixSdkDatabaseBuilder, - legacyDatabaseBuilder: FlutterHiveCollectionsDatabase.databaseBuilder, + //legacyDatabaseBuilder: FlutterHiveCollectionsDatabase.databaseBuilder, supportedLoginTypes: { AuthenticationTypes.password, AuthenticationTypes.sso, diff --git a/lib/utils/date_time_extension.dart b/lib/utils/date_time_extension.dart index 92fbee7..437482f 100644 --- a/lib/utils/date_time_extension.dart +++ b/lib/utils/date_time_extension.dart @@ -28,7 +28,8 @@ extension DateTimeExtension on DateTime { /// Returns a simple time String. String localizedTimeOfDay(BuildContext context) => - L10n.of(context).alwaysUse24HourFormat == 'true' + (MediaQuery.alwaysUse24HourFormatOf(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); diff --git a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart index cd9d223..76655a6 100644 --- a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart +++ b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart @@ -55,9 +55,5 @@ extension IsStateExtension on Event { content.tryGet('membership') == 'ban' || stateKey != senderId); - bool get isState => !{ - EventTypes.Message, - EventTypes.Sticker, - EventTypes.Encrypted, - }.contains(type); + bool get isState => this.stateKey != null; } diff --git a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.txt similarity index 100% rename from lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart rename to lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.txt 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 0919b60..71b8482 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 @@ -11,7 +11,6 @@ import 'package:universal_html/html.dart' as html; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/client_manager.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'cipher.dart'; @@ -55,7 +54,9 @@ Future flutterMatrixSdkDatabaseBuilder(Client client) async { Logs().e('Unable to send error notification', e, s); } - return FlutterHiveCollectionsDatabase.databaseBuilder(client); + database = await _constructDatabase(client); + await database.open(); + return database; } } diff --git a/lib/utils/matrix_sdk_extensions/matrix_locals.dart b/lib/utils/matrix_sdk_extensions/matrix_locals.dart index 165130c..c16eaf9 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_locals.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_locals.dart @@ -7,6 +7,11 @@ class MatrixLocals extends MatrixLocalizations { MatrixLocals(this.l10n); + @override + String voiceMessage(String senderName, Duration? duration) { + return l10n.voiceMessage; + } + @override String acceptedTheInvitation(String targetName) { return l10n.acceptedTheInvitation(targetName); diff --git a/lib/utils/matrix_sdk_extensions/synapse_admin_extension.dart b/lib/utils/matrix_sdk_extensions/synapse_admin_extension.dart new file mode 100644 index 0000000..65c5c19 --- /dev/null +++ b/lib/utils/matrix_sdk_extensions/synapse_admin_extension.dart @@ -0,0 +1,75 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:matrix/matrix.dart' as matrix; + +extension SynapseAdmin on matrix.Client { + Future> getEventReports({int from = 0, int limit = 10}) async { + final requestUri = Uri( + path: '/_synapse/admin/v1/event_reports', + query: 'from=$from&limit=$limit&order_by=received_ts&dir=b' + ); + + if (baseUri == null) return []; + print(baseUri!.resolveUri(requestUri).toString()); + final request = Request('GET', baseUri!.resolveUri(requestUri)); + request.headers['authorization'] = 'Bearer $accessToken'; + final response = await httpClient.send(request); + final responseBody = await response.stream.toBytes(); + if (response.statusCode != 200) unexpectedResponse(response, responseBody); + final responseString = utf8.decode(responseBody); + final json = jsonDecode(responseString); + return json['event_reports']; + } + + Future getReportedEvent(int id) async { + final requestUri = Uri( + path: '/_synapse/admin/v1/event_reports/$id', + ); + + if (baseUri == null) return null; + final request = Request('GET', baseUri!.resolveUri(requestUri)); + request.headers['authorization'] = 'Bearer $accessToken'; + final response = await httpClient.send(request); + final responseBody = await response.stream.toBytes(); + if (response.statusCode != 200) unexpectedResponse(response, responseBody); + final responseString = utf8.decode(responseBody); + final json = jsonDecode(responseString); + + final room = getRoomById(json['room_id']); + + if (room == null) return null; + + print('Event content: ${jsonEncode(json['event_json'])}'); + + return new matrix.Event( + content: json['event_json']['content'], + type: json['event_json']['type'], + eventId: json['event_id'], + senderId: json['sender'], + originServerTs: DateTime.fromMillisecondsSinceEpoch(json['event_json']['origin_server_ts']), + room: room, + ); + } + + Future isSynapseAdministrator() async { + print('Checking if I am admin...'); + print('User ID: $userID'); + if (userID == null) return false; + final requestUri = Uri( + path: '/_synapse/admin/v1/users/$userID/admin', + ); + + print('Base URL: ${baseUri.toString()}'); + if (baseUri == null) return false; + final request = Request('GET', baseUri!.resolveUri(requestUri)); + request.headers['authorization'] = 'Bearer $accessToken'; + final response = await httpClient.send(request); + final responseBody = await response.stream.toBytes(); + if (response.statusCode != 200) unexpectedResponse(response, responseBody); + final responseString = utf8.decode(responseBody); + final json = jsonDecode(responseString); + print('Response from endpoint: $responseString'); + return json['admin']; + } +} diff --git a/linux/my_application.cc b/linux/my_application.cc index 986be4e..446ceea 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -53,11 +53,11 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "FluffyChat"); + gtk_header_bar_set_title(header_bar, "Extera Next"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "FluffyChat"); + gtk_window_set_title(window, "Extera Next"); } gtk_window_set_default_size(window, 864, 720); diff --git a/pubspec.lock b/pubspec.lock index c12db7a..9bcaf90 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1159,10 +1159,10 @@ packages: dependency: "direct main" description: name: matrix - sha256: "829f225f74b2a8c83b6b1f960004ecf742215d9ee9d8cc9e0f1974ff8e281f0f" + sha256: "7d15fdbc760be7e40c58bb65e03baa8241b1e31db2bc67dab61883aabc083a85" url: "https://pub.dev" source: hosted - version: "0.39.1" + version: "0.40.0" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 789384c..97524ce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,7 +62,7 @@ dependencies: latlong2: ^0.9.1 linkify: ^5.0.0 material: ^1.0.0+2 - matrix: ^0.39.1 + matrix: 0.40.0 mime: ^1.0.6 native_imaging: ^0.2.0 opus_caf_converter_dart: ^1.0.1 diff --git a/test/utils/test_client.dart b/test/utils/test_client.dart.txt similarity index 100% rename from test/utils/test_client.dart rename to test/utils/test_client.dart.txt diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 3c2f3ab..dabb4a3 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.CreateAndShow(L"FluffyChat", origin, size)) { + if (!window.CreateAndShow(L"Extera Next", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true);