From 5e85ecc3036692b3611c43e2a9b8640d2bcb8580 Mon Sep 17 00:00:00 2001 From: Grafcube Date: Mon, 23 Feb 2026 21:05:10 +0530 Subject: [PATCH 1/6] Implement private read receipts and typing indicators Includes options for room specific overrides. Fixes #456 Fixes #645 --- .../matrix_typing_indicators_component.dart | 27 ++- commet/lib/client/matrix/matrix_client.dart | 71 ++++++ commet/lib/client/matrix/matrix_room.dart | 12 +- commet/lib/client/matrix/matrix_timeline.dart | 15 +- .../matrix/matrix_room_chat_privacy.dart | 214 ++++++++++++++++++ .../security/matrix/matrix_security_tab.dart | 140 +++++++++++- .../general/room_general_settings_page.dart | 13 +- 7 files changed, 483 insertions(+), 9 deletions(-) create mode 100644 commet/lib/ui/pages/matrix/matrix_room_chat_privacy.dart diff --git a/commet/lib/client/matrix/components/typing_indicators/matrix_typing_indicators_component.dart b/commet/lib/client/matrix/components/typing_indicators/matrix_typing_indicators_component.dart index de7871618..5f1ad5eca 100644 --- a/commet/lib/client/matrix/components/typing_indicators/matrix_typing_indicators_component.dart +++ b/commet/lib/client/matrix/components/typing_indicators/matrix_typing_indicators_component.dart @@ -6,6 +6,8 @@ import 'package:commet/client/matrix/matrix_client.dart'; import 'package:commet/client/matrix/matrix_member.dart'; import 'package:commet/client/matrix/matrix_room.dart'; import 'package:commet/client/member.dart'; +import 'package:commet/debug/log.dart'; +import 'package:matrix/matrix_api_lite/model/matrix_exception.dart'; import 'package:matrix/matrix_api_lite/model/sync_update.dart'; class MatrixTypingIndicatorsComponent @@ -44,7 +46,28 @@ class MatrixTypingIndicatorsComponent .toList(); @override - Future setTypingStatus(bool status) { - return room.matrixRoom.setTyping(status, timeout: 2000); + Future setTypingStatus(bool status) async { + var pti = await client.matrixClient + .getAccountData( + client.matrixClient.userID!, MatrixClient.privateTypingIndicatorKey) + .catchError((e) { + if (!(e is MatrixException && e.error == MatrixError.M_NOT_FOUND)) + Log.e(e); + return {"enabled": false}; + }); + + var rpti = await client + .getRoomAccountData(client.matrixClient.userID!, room.matrixRoom.id, + MatrixClient.privateTypingIndicatorKey) + .catchError((e) { + if (!(e is MatrixException && e.error == MatrixError.M_NOT_FOUND)) + Log.e(e); + return {"enabled": false}; + }); + var rdisabled = rpti["enabled"] is bool ? !(rpti["enabled"] as bool) : null; + var enabled = pti["enabled"] is bool ? pti["enabled"] as bool : false; + + if (rdisabled ?? !enabled) + return room.matrixRoom.setTyping(status, timeout: 2000); } } diff --git a/commet/lib/client/matrix/matrix_client.dart b/commet/lib/client/matrix/matrix_client.dart index 2a86b28bd..71228774b 100644 --- a/commet/lib/client/matrix/matrix_client.dart +++ b/commet/lib/client/matrix/matrix_client.dart @@ -29,10 +29,12 @@ import 'package:crypto/crypto.dart'; import 'dart:convert'; import 'package:commet/client/client.dart'; import 'package:flutter/material.dart'; +import 'package:http/http.dart' show Request; import 'package:intl/intl.dart'; import 'package:matrix/matrix.dart' as matrix; import 'package:matrix/encryption.dart'; import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vodozemac; +import 'package:matrix/matrix_api_lite/generated/internal.dart' show ignore; import '../../ui/atoms/code_block.dart'; import 'matrix_room.dart'; @@ -139,6 +141,12 @@ class MatrixClient extends Client { StoredStreamController connectionStatusChanged = StoredStreamController(); + static const String privateReadReceiptsKey = + "chat.commet.private_read_receipts"; + + static const String privateTypingIndicatorKey = + "chat.commet.private_typing_indicator"; + static String get matrixClientOlmMissingMessage => Intl.message( "libolm is not installed or was not found. End to End Encryption will not be available until this is resolved", name: "matrixClientOlmMissingMessage", @@ -207,6 +215,21 @@ class MatrixClient extends Client { } await Future.wait(futures); + + for (final client in manager.clients) { + if (client is MatrixClient) { + var prr = await client.matrixClient + .getAccountData(client.matrixClient.userID!, + MatrixClient.privateReadReceiptsKey) + .catchError((e) { + if (!(e is matrix.MatrixException && + e.error == matrix.MatrixError.M_NOT_FOUND)) Log.e(e); + return {"enabled": false}; + }); + if (prr["enabled"] is bool ? prr["enabled"] as bool : false) + client.matrixClient.receiptsPublicByDefault = false; + } + } }); } @@ -636,6 +659,54 @@ class MatrixClient extends Client { return _spaces.any((element) => element.identifier == identifier); } + Future> getRoomAccountData( + String userId, + String roomId, + String type, + ) async { + final requestUri = Uri( + path: + "_matrix/client/v3/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/account_data/${Uri.encodeComponent(type)}", + ); + final request = + Request('GET', matrixClient.baseUri!.resolveUri(requestUri)); + request.headers['authorization'] = 'Bearer ${matrixClient.bearerToken!}'; + final response = await matrixClient.httpClient.send(request); + final responseBody = await response.stream.toBytes(); + if (response.statusCode == 404) + return {}; + else if (response.statusCode != 200) + matrixClient.unexpectedResponse(response, responseBody); + final responseString = utf8.decode(responseBody); + final json = jsonDecode(responseString); + print(matrixClient.receiptsPublicByDefault); + return json as Map; + } + + Future setRoomAccountData( + String userId, + String roomId, + String type, + Map body, + ) async { + final requestUri = Uri( + path: + "_matrix/client/v3/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/account_data/${Uri.encodeComponent(type)}", + ); + final request = + Request('PUT', matrixClient.baseUri!.resolveUri(requestUri)); + request.headers['authorization'] = 'Bearer ${matrixClient.bearerToken!}'; + request.headers['content-type'] = 'application/json'; + request.bodyBytes = utf8.encode(jsonEncode(body)); + final response = await matrixClient.httpClient.send(request); + final responseBody = await response.stream.toBytes(); + if (response.statusCode != 200) + matrixClient.unexpectedResponse(response, responseBody); + final responseString = utf8.decode(responseBody); + final json = jsonDecode(responseString); + return ignore(json); + } + (String, List?)? parseAddressToIdAndVia(String address) { String id = address; List? via; diff --git a/commet/lib/client/matrix/matrix_room.dart b/commet/lib/client/matrix/matrix_room.dart index 0b1c3028e..c1d5523c1 100644 --- a/commet/lib/client/matrix/matrix_room.dart +++ b/commet/lib/client/matrix/matrix_room.dart @@ -845,7 +845,17 @@ class MatrixRoom extends Room { Future markAsRead() async { var tl = await matrixRoom.getTimeline(); - await tl.setReadMarker(); + var rprr = await _client + .getRoomAccountData(_client.matrixClient.userID!, matrixRoom.id, + MatrixClient.privateReadReceiptsKey) + .catchError((e) { + if (!(e is matrix.MatrixException && + e.error == matrix.MatrixError.M_NOT_FOUND)) Log.e(e); + return {"enabled": false}; + }); + + await tl.setReadMarker( + public: rprr["enabled"] is bool ? !(rprr["enabled"] as bool) : null); } @override diff --git a/commet/lib/client/matrix/matrix_timeline.dart b/commet/lib/client/matrix/matrix_timeline.dart index 6556b8001..4f10ee777 100644 --- a/commet/lib/client/matrix/matrix_timeline.dart +++ b/commet/lib/client/matrix/matrix_timeline.dart @@ -7,6 +7,7 @@ import 'package:commet/client/matrix/timeline_events/matrix_timeline_event.dart' import 'package:commet/client/timeline_events/timeline_event.dart'; import 'package:commet/client/timeline_events/timeline_event_message.dart'; import 'package:commet/client/timeline_events/timeline_event_sticker.dart'; +import 'package:commet/debug/log.dart'; import '../client.dart'; import 'package:matrix/matrix.dart' as matrix; @@ -126,9 +127,21 @@ class MatrixTimeline extends Timeline { @override void markAsRead(TimelineEvent event) async { + var rprr = await (client as MatrixClient) + .getRoomAccountData( + (client as MatrixClient).matrixClient.userID!, + (room as MatrixRoom).matrixRoom.id, + MatrixClient.privateReadReceiptsKey) + .catchError((e) { + if (!(e is matrix.MatrixException && + e.error == matrix.MatrixError.M_NOT_FOUND)) Log.e(e); + return {"enabled": false}; + }); + if (event.status == TimelineEventStatus.synced || event.status == TimelineEventStatus.sent) { - await _matrixTimeline?.setReadMarker(); + await _matrixTimeline?.setReadMarker( + public: rprr["enabled"] is bool ? !(rprr["enabled"] as bool) : null); var receipts = room.getComponent(); receipts?.handleEvent(event.eventId, room.client.self!.identifier); diff --git a/commet/lib/ui/pages/matrix/matrix_room_chat_privacy.dart b/commet/lib/ui/pages/matrix/matrix_room_chat_privacy.dart new file mode 100644 index 000000000..013f9b8c1 --- /dev/null +++ b/commet/lib/ui/pages/matrix/matrix_room_chat_privacy.dart @@ -0,0 +1,214 @@ +import 'dart:async'; + +import 'package:commet/client/matrix/matrix_client.dart'; +import 'package:commet/client/matrix/matrix_room.dart'; +import 'package:commet/debug/log.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' as material; +import 'package:intl/intl.dart'; +import 'package:matrix/matrix.dart'; +import 'package:tiamat/tiamat.dart' as tiamat; + +class MatrixRoomChatPrivacySettings extends StatefulWidget { + const MatrixRoomChatPrivacySettings(this.room); + + final MatrixRoom room; + + @override + State createState() => + _MatrixRoomChatPrivacySettingsState(); +} + +class _MatrixRoomChatPrivacySettingsState + extends State { + bool? globalPrivateReadReceipts; + bool? globalPrivateTypingIndicator; + bool? privateReadReceipts; + bool? privateTypingIndicator; + + String get labelEnabledOption => Intl.message("Enabled", + desc: "Label for an enabled value", name: "labelEnabledOption"); + + String get labelDisabledOption => Intl.message("Disabled", + desc: "Label for an disabled value", name: "labelDisabledOption"); + + String labelDefaultOption(pref) => Intl.message("Use global preference $pref", + desc: "Label to use the global preference", + name: "labelOptionDefault", + args: [pref]); + + String get labelPrivateReadReceiptsTitle => Intl.message( + "Private read receipts", + desc: + "Label for the toggle for enabling and disabling private read receipts", + name: "labelPrivateReadReceiptsToggle"); + + String get labelPrivateTypingIndicatorTitle => Intl.message( + "Private typing indicator", + desc: + "Label for the toggle for enabling and disabling private typing indicator", + name: "labelPrivateTypingIndicatorTitle"); + + @override + void initState() { + getChatPrivacy(); + super.initState(); + } + + Future getChatPrivacy() async { + var client = widget.room.client as MatrixClient; + var prr = await client.matrixClient + .getAccountData( + client.matrixClient.userID!, MatrixClient.privateReadReceiptsKey) + .catchError((e) { + if (!(e is MatrixException && e.error == MatrixError.M_NOT_FOUND)) + Log.e(e); + return {"enabled": null}; + }); + var pti = await client.matrixClient + .getAccountData( + client.matrixClient.userID!, MatrixClient.privateTypingIndicatorKey) + .catchError((e) { + if (!(e is MatrixException && e.error == MatrixError.M_NOT_FOUND)) + Log.e(e); + return {"enabled": null}; + }); + var rprr = await client + .getRoomAccountData(client.matrixClient.userID!, + widget.room.matrixRoom.id, MatrixClient.privateReadReceiptsKey) + .catchError((e) { + if (!(e is MatrixException && e.error == MatrixError.M_NOT_FOUND)) + Log.e(e); + return {"enabled": null}; + }); + var rpti = await client + .getRoomAccountData(client.matrixClient.userID!, + widget.room.matrixRoom.id, MatrixClient.privateTypingIndicatorKey) + .catchError((e) { + if (!(e is MatrixException && e.error == MatrixError.M_NOT_FOUND)) + Log.e(e); + return {"enabled": null}; + }); + + setState(() { + globalPrivateReadReceipts = + prr["enabled"] is bool ? prr["enabled"] as bool : false; + globalPrivateTypingIndicator = + pti["enabled"] is bool ? pti["enabled"] as bool : false; + privateReadReceipts = + rprr["enabled"] is bool ? rprr["enabled"] as bool : null; + privateTypingIndicator = + rpti["enabled"] is bool ? rpti["enabled"] as bool : null; + }); + } + + @override + Widget build(BuildContext context) { + return Column(children: [ + privateReadReceiptsSettings(), + const SizedBox( + height: 10, + ), + privateTypingIndicatorSettings(), + ]); + } + + Widget privateReadReceiptsSettings() { + return tiamat.Panel( + mode: tiamat.TileType.surfaceContainerLow, + header: labelPrivateReadReceiptsTitle, + child: material.Material( + color: material.Colors.transparent, + child: Column( + children: [ + tiamat.RadioButton( + groupValue: privateReadReceipts, + value: null, + icon: material.Icons.remove_outlined, + text: labelDefaultOption(globalPrivateReadReceipts is bool + ? (globalPrivateReadReceipts as bool + ? "($labelEnabledOption)" + : "($labelDisabledOption)") + : ""), + onChanged: onPRRChanged, + ), + tiamat.RadioButton( + groupValue: privateReadReceipts, + value: false, + icon: material.Icons.close, + text: labelDisabledOption, + onChanged: onPRRChanged, + ), + tiamat.RadioButton( + groupValue: privateReadReceipts, + value: true, + icon: material.Icons.check, + text: labelEnabledOption, + onChanged: onPRRChanged, + ), + ], + ), + )); + } + + Widget privateTypingIndicatorSettings() { + return tiamat.Panel( + mode: tiamat.TileType.surfaceContainerLow, + header: labelPrivateTypingIndicatorTitle, + child: material.Material( + color: material.Colors.transparent, + child: Column( + children: [ + tiamat.RadioButton( + groupValue: privateTypingIndicator, + value: null, + icon: material.Icons.remove_outlined, + text: labelDefaultOption(globalPrivateTypingIndicator is bool + ? (globalPrivateTypingIndicator as bool + ? "($labelEnabledOption)" + : "($labelDisabledOption)") + : ""), + onChanged: onPTIChanged, + ), + tiamat.RadioButton( + groupValue: privateTypingIndicator, + value: false, + icon: material.Icons.close, + text: labelDisabledOption, + onChanged: onPTIChanged, + ), + tiamat.RadioButton( + groupValue: privateTypingIndicator, + value: true, + icon: material.Icons.check, + text: labelEnabledOption, + onChanged: onPTIChanged, + ), + ], + ), + )); + } + + Future onPRRChanged(bool? value) async { + var client = widget.room.client as MatrixClient; + client.setRoomAccountData( + client.matrixClient.userID!, + widget.room.identifier, + MatrixClient.privateReadReceiptsKey, + {"enabled": value}, + ); + setState(() => privateReadReceipts = value); + } + + Future onPTIChanged(bool? value) async { + var client = widget.room.client as MatrixClient; + client.setRoomAccountData( + client.matrixClient.userID!, + widget.room.identifier, + MatrixClient.privateTypingIndicatorKey, + {"enabled": value}, + ); + + setState(() => privateTypingIndicator = value); + } +} diff --git a/commet/lib/ui/pages/settings/categories/account/security/matrix/matrix_security_tab.dart b/commet/lib/ui/pages/settings/categories/account/security/matrix/matrix_security_tab.dart index 77c02aab1..529cb01a3 100644 --- a/commet/lib/ui/pages/settings/categories/account/security/matrix/matrix_security_tab.dart +++ b/commet/lib/ui/pages/settings/categories/account/security/matrix/matrix_security_tab.dart @@ -1,4 +1,5 @@ import 'package:commet/client/matrix/matrix_client.dart'; +import 'package:commet/debug/log.dart'; import 'package:commet/ui/navigation/adaptive_dialog.dart'; import 'package:commet/ui/pages/settings/categories/account/security/matrix/session/matrix_session.dart'; import 'package:commet/utils/common_strings.dart'; @@ -20,8 +21,38 @@ class MatrixSecurityTab extends StatefulWidget { class _MatrixSecurityTabState extends State { bool crossSigningEnabled = false; bool? messageBackupEnabled; + bool privateReadReceipts = false; + bool privateTypingIndicator = false; List? devices; + String get labelChatPrivacyTitle => Intl.message("Chat Privacy", + desc: "Header for the chat privacy section in settings", + name: "labelChatPrivacyTitle"); + + String get labelPrivateReadReceiptsToggle => Intl.message( + "Private read receipts", + desc: + "Label for the toggle for enabling and disabling sending read receipts", + name: "labelPrivateReadReceiptsToggle"); + + String get labelPrivateReadReceiptsDescription => Intl.message( + "Prevent other members of a room from knowing when you have read their messages.", + desc: + "description for the toggle for enabling and disabling sending read receipts", + name: "labelPrivateReadReceiptsDescriptionn"); + + String get labelPrivateTypingIndicatorToggle => Intl.message( + "Private typing indicator", + desc: + "Label for the toggle for enabling and disabling sending typing indicator", + name: "labelPrivateTypingIndicatorToggle"); + + String get labelPrivateTypingIndicatorDescription => Intl.message( + "Prevent other members of a room from knowing when you are typing a message.", + desc: + "description for the toggle for enabling and disabling sending typing indicator", + name: "labelPrivateTypingIndicatorDescription"); + String get labelMatrixCrossSigning => Intl.message("Cross signing", desc: "Title label for matrix cross signing", name: "labelMatrixCrossSigning"); @@ -66,6 +97,7 @@ class _MatrixSecurityTabState extends State { void initState() { checkState(); getDevices(); + getChatPrivacy(); super.initState(); } @@ -89,6 +121,32 @@ class _MatrixSecurityTabState extends State { }); } + void getChatPrivacy() async { + var prr = await widget.client.matrixClient + .getAccountData(widget.client.matrixClient.userID!, + MatrixClient.privateReadReceiptsKey) + .catchError((e) { + if (!(e is MatrixException && e.error == MatrixError.M_NOT_FOUND)) + Log.e(e); + return {"enabled": false}; + }); + var pti = await widget.client.matrixClient + .getAccountData(widget.client.matrixClient.userID!, + MatrixClient.privateTypingIndicatorKey) + .catchError((e) { + if (!(e is MatrixException && e.error == MatrixError.M_NOT_FOUND)) + Log.e(e); + return {"enabled": false}; + }); + + setState(() { + privateReadReceipts = + prr["enabled"] is bool ? prr["enabled"] as bool : false; + privateTypingIndicator = + pti["enabled"] is bool ? pti["enabled"] as bool : false; + }); + } + @override Widget build(BuildContext context) { return Column( @@ -96,9 +154,16 @@ class _MatrixSecurityTabState extends State { mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ - Padding( - padding: const EdgeInsets.fromLTRB(0, 4, 0, 4), - child: crossSigningPanel(), + const SizedBox( + height: 4, + ), + chatPrivacyPanel(), + const SizedBox( + height: 4, + ), + crossSigningPanel(), + const SizedBox( + height: 4, ), sessionsPanel() ], @@ -235,4 +300,73 @@ class _MatrixSecurityTabState extends State { ], ); } + + Widget chatPrivacyPanel() { + return Panel( + header: labelChatPrivacyTitle, + mode: TileType.surfaceContainerLow, + child: Column(children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + tiamat.Text.labelEmphasised(labelPrivateReadReceiptsToggle), + tiamat.Text.labelLow(labelPrivateReadReceiptsDescription) + ]), + tiamat.Switch( + state: privateReadReceipts, + onChanged: (value) { + setState(() { + privateReadReceipts = value; + widget.client.matrixClient.setAccountData( + widget.client.matrixClient.userID!, + MatrixClient.privateReadReceiptsKey, + {"enabled": privateReadReceipts}); + widget.client.matrixClient.receiptsPublicByDefault = + !privateReadReceipts; + }); + }, + ) + ], + ), + ), + const SizedBox( + height: 10, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + tiamat.Text.labelEmphasised( + labelPrivateTypingIndicatorToggle), + tiamat.Text.labelLow(labelPrivateTypingIndicatorDescription) + ]), + tiamat.Switch( + state: privateTypingIndicator, + onChanged: (value) { + setState(() { + privateTypingIndicator = value; + widget.client.matrixClient.setAccountData( + widget.client.matrixClient.userID!, + MatrixClient.privateTypingIndicatorKey, + {"enabled": privateTypingIndicator}); + }); + }, + ) + ], + ), + ), + ]), + ); + } } diff --git a/commet/lib/ui/pages/settings/categories/room/general/room_general_settings_page.dart b/commet/lib/ui/pages/settings/categories/room/general/room_general_settings_page.dart index f8c62a369..60f74c844 100644 --- a/commet/lib/ui/pages/settings/categories/room/general/room_general_settings_page.dart +++ b/commet/lib/ui/pages/settings/categories/room/general/room_general_settings_page.dart @@ -1,8 +1,12 @@ +import 'package:commet/client/matrix/matrix_client.dart'; import 'package:commet/client/matrix/matrix_room.dart'; import 'package:commet/client/room.dart'; +import 'package:commet/debug/log.dart'; +import 'package:commet/ui/pages/matrix/matrix_room_chat_privacy.dart'; import 'package:commet/ui/pages/matrix/room_address_settings/matrix_room_address_settings.dart'; import 'package:commet/ui/pages/settings/categories/room/general/room_general_settings_view.dart'; import 'package:flutter/widgets.dart'; +import 'package:matrix/matrix_api_lite/model/matrix_exception.dart'; class RoomGeneralSettingsPage extends StatefulWidget { const RoomGeneralSettingsPage({super.key, required this.room}); @@ -32,8 +36,13 @@ class _RoomGeneralSettingsPageState extends State { const SizedBox( height: 10, ), - if (widget.room is MatrixRoom) - MatrixRoomAddressSettings((widget.room as MatrixRoom).matrixRoom) + if (widget.room is MatrixRoom) ...[ + MatrixRoomChatPrivacySettings(widget.room as MatrixRoom), + const SizedBox( + height: 10, + ), + MatrixRoomAddressSettings((widget.room as MatrixRoom).matrixRoom), + ] ], ); } From 17cd0eb51bae9421394e8e3d6b4113c2bca6dc05 Mon Sep 17 00:00:00 2001 From: Grafcube Date: Wed, 25 Feb 2026 20:31:26 +0530 Subject: [PATCH 2/6] Implement function in components --- .../read_receipts/read_receipt_component.dart | 2 + .../typing_indicator_component.dart | 3 + .../user_presence_component.dart | 6 + .../matrix_read_receipt_component.dart | 24 ++ .../matrix_typing_indicators_component.dart | 49 ++-- .../user_presence/matrix_user_presence.dart | 38 ++++ commet/lib/client/matrix/matrix_client.dart | 71 ------ commet/lib/client/matrix/matrix_room.dart | 14 +- commet/lib/client/matrix/matrix_timeline.dart | 16 +- .../matrix/matrix_room_chat_privacy.dart | 214 ------------------ .../preferences/preferences_chat_privacy.dart | 116 ++++++++++ .../account/preferences/preferences_tab.dart | 44 ++++ .../security/matrix/matrix_security_tab.dart | 131 ----------- .../account/settings_category_account.dart | 13 ++ .../general/room_general_chat_privacy.dart | 165 ++++++++++++++ .../general/room_general_settings_page.dart | 5 +- 16 files changed, 445 insertions(+), 466 deletions(-) delete mode 100644 commet/lib/ui/pages/matrix/matrix_room_chat_privacy.dart create mode 100644 commet/lib/ui/pages/settings/categories/account/preferences/preferences_chat_privacy.dart create mode 100644 commet/lib/ui/pages/settings/categories/account/preferences/preferences_tab.dart create mode 100644 commet/lib/ui/pages/settings/categories/room/general/room_general_chat_privacy.dart diff --git a/commet/lib/client/components/read_receipts/read_receipt_component.dart b/commet/lib/client/components/read_receipts/read_receipt_component.dart index 0ea26b956..319c95e54 100644 --- a/commet/lib/client/components/read_receipts/read_receipt_component.dart +++ b/commet/lib/client/components/read_receipts/read_receipt_component.dart @@ -5,6 +5,8 @@ import 'package:commet/client/timeline_events/timeline_event.dart'; abstract class ReadReceiptComponent implements RoomComponent { Stream get onReadReceiptsUpdated; + bool? get usePublicReadReceiptsForRoom; + Future setUsePublicReadReceiptsForRoom(bool? value); List? getReceipts(TimelineEvent event); } diff --git a/commet/lib/client/components/typing_indicators/typing_indicator_component.dart b/commet/lib/client/components/typing_indicators/typing_indicator_component.dart index 4a8a7f35f..4cc05be64 100644 --- a/commet/lib/client/components/typing_indicators/typing_indicator_component.dart +++ b/commet/lib/client/components/typing_indicators/typing_indicator_component.dart @@ -6,6 +6,9 @@ abstract class TypingIndicatorComponent implements RoomComponent { Stream get onTypingUsersUpdated; + bool? get typingIndicatorEnabledForRoom; + Future setTypingIndicatorEnabledForRoom(bool? value); + List get typingUsers; Future setTypingStatus(bool status); diff --git a/commet/lib/client/components/user_presence/user_presence_component.dart b/commet/lib/client/components/user_presence/user_presence_component.dart index 55bd86f8d..7837e5397 100644 --- a/commet/lib/client/components/user_presence/user_presence_component.dart +++ b/commet/lib/client/components/user_presence/user_presence_component.dart @@ -29,6 +29,12 @@ class UserPresence { abstract class UserPresenceComponent implements Component { Stream<(String, UserPresence)> get onPresenceChanged; + bool get usePublicReadReceipts; + Future setUsePublicReadReceipts(bool value); + + bool get typingIndicatorEnabled; + Future setTypingIndicatorEnabled(bool value); + Future getUserPresence(String userId); Future setStatus(UserPresenceStatus status, diff --git a/commet/lib/client/matrix/components/read_receipts/matrix_read_receipt_component.dart b/commet/lib/client/matrix/components/read_receipts/matrix_read_receipt_component.dart index b235adb21..10361eaf5 100644 --- a/commet/lib/client/matrix/components/read_receipts/matrix_read_receipt_component.dart +++ b/commet/lib/client/matrix/components/read_receipts/matrix_read_receipt_component.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:commet/client/components/read_receipts/read_receipt_component.dart'; import 'package:commet/client/matrix/components/matrix_sync_listener.dart'; +import 'package:commet/client/matrix/components/user_presence/matrix_user_presence.dart'; import 'package:commet/client/matrix/matrix_client.dart'; import 'package:commet/client/matrix/matrix_room.dart'; import 'package:commet/client/matrix/matrix_timeline.dart'; @@ -18,6 +19,9 @@ class MatrixReadReceiptComponent @override MatrixRoom room; + static const String publicReadReceiptsKey = + "chat.commet.public_read_receipts"; + final StreamController _controller = StreamController.broadcast(); @@ -30,6 +34,22 @@ class MatrixReadReceiptComponent Map userToPreviousReceipt = {}; + @override + bool? get usePublicReadReceiptsForRoom { + var publicReadReceiptsForRoom = room + .matrixRoom.roomAccountData[publicReadReceiptsKey]?.content["enabled"]; + return publicReadReceiptsForRoom is bool ? publicReadReceiptsForRoom : null; + } + + @override + Future setUsePublicReadReceiptsForRoom(bool? value) async => + await client.matrixClient.setAccountDataPerRoom( + client.matrixClient.userID!, + room.matrixRoom.id, + publicReadReceiptsKey, + {"enabled": value}, + ); + @override onSync(JoinedRoomUpdate update) { final ephemeral = update.ephemeral; @@ -59,6 +79,10 @@ class MatrixReadReceiptComponent } } } + + client.matrixClient.receiptsPublicByDefault = client + .getComponent()! + .usePublicReadReceipts; } void handleEvent(String eventId, String userId) { diff --git a/commet/lib/client/matrix/components/typing_indicators/matrix_typing_indicators_component.dart b/commet/lib/client/matrix/components/typing_indicators/matrix_typing_indicators_component.dart index 5f1ad5eca..8e192deb9 100644 --- a/commet/lib/client/matrix/components/typing_indicators/matrix_typing_indicators_component.dart +++ b/commet/lib/client/matrix/components/typing_indicators/matrix_typing_indicators_component.dart @@ -2,12 +2,11 @@ import 'dart:async'; import 'package:commet/client/components/typing_indicators/typing_indicator_component.dart'; import 'package:commet/client/matrix/components/matrix_sync_listener.dart'; +import 'package:commet/client/matrix/components/user_presence/matrix_user_presence.dart'; import 'package:commet/client/matrix/matrix_client.dart'; import 'package:commet/client/matrix/matrix_member.dart'; import 'package:commet/client/matrix/matrix_room.dart'; import 'package:commet/client/member.dart'; -import 'package:commet/debug/log.dart'; -import 'package:matrix/matrix_api_lite/model/matrix_exception.dart'; import 'package:matrix/matrix_api_lite/model/sync_update.dart'; class MatrixTypingIndicatorsComponent @@ -21,8 +20,29 @@ class MatrixTypingIndicatorsComponent MatrixTypingIndicatorsComponent(this.client, this.room); + static const String publicTypingIndicatorKey = + "chat.commet.private_typing_indicator"; + final StreamController _controller = StreamController.broadcast(); + @override + bool? get typingIndicatorEnabledForRoom { + var publicTypingIndicatorForRoom = room.matrixRoom + .roomAccountData[publicTypingIndicatorKey]?.content["enabled"]; + return publicTypingIndicatorForRoom is bool + ? publicTypingIndicatorForRoom + : null; + } + + @override + Future setTypingIndicatorEnabledForRoom(bool? value) async => + await client.matrixClient.setAccountDataPerRoom( + client.matrixClient.userID!, + room.matrixRoom.id, + publicTypingIndicatorKey, + {"enabled": value}, + ); + @override onSync(JoinedRoomUpdate update) { final ephemeral = update.ephemeral; @@ -47,27 +67,10 @@ class MatrixTypingIndicatorsComponent @override Future setTypingStatus(bool status) async { - var pti = await client.matrixClient - .getAccountData( - client.matrixClient.userID!, MatrixClient.privateTypingIndicatorKey) - .catchError((e) { - if (!(e is MatrixException && e.error == MatrixError.M_NOT_FOUND)) - Log.e(e); - return {"enabled": false}; - }); - - var rpti = await client - .getRoomAccountData(client.matrixClient.userID!, room.matrixRoom.id, - MatrixClient.privateTypingIndicatorKey) - .catchError((e) { - if (!(e is MatrixException && e.error == MatrixError.M_NOT_FOUND)) - Log.e(e); - return {"enabled": false}; - }); - var rdisabled = rpti["enabled"] is bool ? !(rpti["enabled"] as bool) : null; - var enabled = pti["enabled"] is bool ? pti["enabled"] as bool : false; - - if (rdisabled ?? !enabled) + var typingIndicatorEnabled = client + .getComponent()! + .typingIndicatorEnabled; + if (typingIndicatorEnabledForRoom ?? typingIndicatorEnabled) return room.matrixRoom.setTyping(status, timeout: 2000); } } diff --git a/commet/lib/client/matrix/components/user_presence/matrix_user_presence.dart b/commet/lib/client/matrix/components/user_presence/matrix_user_presence.dart index a1f1f670b..ff775ee71 100644 --- a/commet/lib/client/matrix/components/user_presence/matrix_user_presence.dart +++ b/commet/lib/client/matrix/components/user_presence/matrix_user_presence.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:commet/client/components/user_presence/user_presence_component.dart'; import 'package:commet/client/components/user_presence/user_presence_lifecycle_watcher.dart'; +import 'package:commet/client/matrix/components/read_receipts/matrix_read_receipt_component.dart'; +import 'package:commet/client/matrix/components/typing_indicators/matrix_typing_indicators_component.dart'; import 'package:commet/client/matrix/matrix_client.dart'; import 'package:commet/utils/in_memory_cache.dart'; import 'package:matrix/matrix.dart'; @@ -28,6 +30,42 @@ class MatrixUserPresenceComponent UserPresenceLifecycleWatcher().init(); } + @override + bool get usePublicReadReceipts { + var publicReadReceipts = client + .matrixClient + .accountData[MatrixReadReceiptComponent.publicReadReceiptsKey] + ?.content["enabled"]; + return publicReadReceipts is bool ? publicReadReceipts : true; + } + + @override + Future setUsePublicReadReceipts(bool value) async { + await client.matrixClient.setAccountData( + client.matrixClient.userID!, + MatrixReadReceiptComponent.publicReadReceiptsKey, + {"enabled": value}, + ); + client.matrixClient.receiptsPublicByDefault = value; + } + + @override + bool get typingIndicatorEnabled { + var publicTypingIndicator = client + .matrixClient + .accountData[MatrixTypingIndicatorsComponent.publicTypingIndicatorKey] + ?.content["enabled"]; + return publicTypingIndicator is bool ? publicTypingIndicator : true; + } + + @override + Future setTypingIndicatorEnabled(bool value) async => + await client.matrixClient.setAccountData( + client.matrixClient.userID!, + MatrixTypingIndicatorsComponent.publicTypingIndicatorKey, + {"enabled": value}, + ); + @override Future getUserPresence(String userId) async { final presence = await client.matrixClient.fetchCurrentPresence(userId); diff --git a/commet/lib/client/matrix/matrix_client.dart b/commet/lib/client/matrix/matrix_client.dart index 71228774b..2a86b28bd 100644 --- a/commet/lib/client/matrix/matrix_client.dart +++ b/commet/lib/client/matrix/matrix_client.dart @@ -29,12 +29,10 @@ import 'package:crypto/crypto.dart'; import 'dart:convert'; import 'package:commet/client/client.dart'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' show Request; import 'package:intl/intl.dart'; import 'package:matrix/matrix.dart' as matrix; import 'package:matrix/encryption.dart'; import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vodozemac; -import 'package:matrix/matrix_api_lite/generated/internal.dart' show ignore; import '../../ui/atoms/code_block.dart'; import 'matrix_room.dart'; @@ -141,12 +139,6 @@ class MatrixClient extends Client { StoredStreamController connectionStatusChanged = StoredStreamController(); - static const String privateReadReceiptsKey = - "chat.commet.private_read_receipts"; - - static const String privateTypingIndicatorKey = - "chat.commet.private_typing_indicator"; - static String get matrixClientOlmMissingMessage => Intl.message( "libolm is not installed or was not found. End to End Encryption will not be available until this is resolved", name: "matrixClientOlmMissingMessage", @@ -215,21 +207,6 @@ class MatrixClient extends Client { } await Future.wait(futures); - - for (final client in manager.clients) { - if (client is MatrixClient) { - var prr = await client.matrixClient - .getAccountData(client.matrixClient.userID!, - MatrixClient.privateReadReceiptsKey) - .catchError((e) { - if (!(e is matrix.MatrixException && - e.error == matrix.MatrixError.M_NOT_FOUND)) Log.e(e); - return {"enabled": false}; - }); - if (prr["enabled"] is bool ? prr["enabled"] as bool : false) - client.matrixClient.receiptsPublicByDefault = false; - } - } }); } @@ -659,54 +636,6 @@ class MatrixClient extends Client { return _spaces.any((element) => element.identifier == identifier); } - Future> getRoomAccountData( - String userId, - String roomId, - String type, - ) async { - final requestUri = Uri( - path: - "_matrix/client/v3/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/account_data/${Uri.encodeComponent(type)}", - ); - final request = - Request('GET', matrixClient.baseUri!.resolveUri(requestUri)); - request.headers['authorization'] = 'Bearer ${matrixClient.bearerToken!}'; - final response = await matrixClient.httpClient.send(request); - final responseBody = await response.stream.toBytes(); - if (response.statusCode == 404) - return {}; - else if (response.statusCode != 200) - matrixClient.unexpectedResponse(response, responseBody); - final responseString = utf8.decode(responseBody); - final json = jsonDecode(responseString); - print(matrixClient.receiptsPublicByDefault); - return json as Map; - } - - Future setRoomAccountData( - String userId, - String roomId, - String type, - Map body, - ) async { - final requestUri = Uri( - path: - "_matrix/client/v3/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/account_data/${Uri.encodeComponent(type)}", - ); - final request = - Request('PUT', matrixClient.baseUri!.resolveUri(requestUri)); - request.headers['authorization'] = 'Bearer ${matrixClient.bearerToken!}'; - request.headers['content-type'] = 'application/json'; - request.bodyBytes = utf8.encode(jsonEncode(body)); - final response = await matrixClient.httpClient.send(request); - final responseBody = await response.stream.toBytes(); - if (response.statusCode != 200) - matrixClient.unexpectedResponse(response, responseBody); - final responseString = utf8.decode(responseBody); - final json = jsonDecode(responseString); - return ignore(json); - } - (String, List?)? parseAddressToIdAndVia(String address) { String id = address; List? via; diff --git a/commet/lib/client/matrix/matrix_room.dart b/commet/lib/client/matrix/matrix_room.dart index c1d5523c1..f547e4fdb 100644 --- a/commet/lib/client/matrix/matrix_room.dart +++ b/commet/lib/client/matrix/matrix_room.dart @@ -13,6 +13,7 @@ import 'package:commet/client/components/room_component.dart'; import 'package:commet/client/components/user_color/user_color_component.dart'; import 'package:commet/client/matrix/components/calendar_room_component/matrix_calendar_room_component.dart'; import 'package:commet/client/matrix/components/emoticon/matrix_room_emoticon_component.dart'; +import 'package:commet/client/matrix/components/read_receipts/matrix_read_receipt_component.dart'; import 'package:commet/client/matrix/matrix_attachment.dart'; import 'package:commet/client/matrix/matrix_client.dart'; import 'package:commet/client/matrix/matrix_member.dart'; @@ -844,18 +845,9 @@ class MatrixRoom extends Room { @override Future markAsRead() async { var tl = await matrixRoom.getTimeline(); - - var rprr = await _client - .getRoomAccountData(_client.matrixClient.userID!, matrixRoom.id, - MatrixClient.privateReadReceiptsKey) - .catchError((e) { - if (!(e is matrix.MatrixException && - e.error == matrix.MatrixError.M_NOT_FOUND)) Log.e(e); - return {"enabled": false}; - }); - + var readReceiptComponent = getComponent(); await tl.setReadMarker( - public: rprr["enabled"] is bool ? !(rprr["enabled"] as bool) : null); + public: readReceiptComponent?.usePublicReadReceiptsForRoom); } @override diff --git a/commet/lib/client/matrix/matrix_timeline.dart b/commet/lib/client/matrix/matrix_timeline.dart index 4f10ee777..1e853a78f 100644 --- a/commet/lib/client/matrix/matrix_timeline.dart +++ b/commet/lib/client/matrix/matrix_timeline.dart @@ -7,7 +7,6 @@ import 'package:commet/client/matrix/timeline_events/matrix_timeline_event.dart' import 'package:commet/client/timeline_events/timeline_event.dart'; import 'package:commet/client/timeline_events/timeline_event_message.dart'; import 'package:commet/client/timeline_events/timeline_event_sticker.dart'; -import 'package:commet/debug/log.dart'; import '../client.dart'; import 'package:matrix/matrix.dart' as matrix; @@ -127,23 +126,12 @@ class MatrixTimeline extends Timeline { @override void markAsRead(TimelineEvent event) async { - var rprr = await (client as MatrixClient) - .getRoomAccountData( - (client as MatrixClient).matrixClient.userID!, - (room as MatrixRoom).matrixRoom.id, - MatrixClient.privateReadReceiptsKey) - .catchError((e) { - if (!(e is matrix.MatrixException && - e.error == matrix.MatrixError.M_NOT_FOUND)) Log.e(e); - return {"enabled": false}; - }); - + var receipts = room.getComponent(); if (event.status == TimelineEventStatus.synced || event.status == TimelineEventStatus.sent) { await _matrixTimeline?.setReadMarker( - public: rprr["enabled"] is bool ? !(rprr["enabled"] as bool) : null); + public: receipts?.usePublicReadReceiptsForRoom); - var receipts = room.getComponent(); receipts?.handleEvent(event.eventId, room.client.self!.identifier); } } diff --git a/commet/lib/ui/pages/matrix/matrix_room_chat_privacy.dart b/commet/lib/ui/pages/matrix/matrix_room_chat_privacy.dart deleted file mode 100644 index 013f9b8c1..000000000 --- a/commet/lib/ui/pages/matrix/matrix_room_chat_privacy.dart +++ /dev/null @@ -1,214 +0,0 @@ -import 'dart:async'; - -import 'package:commet/client/matrix/matrix_client.dart'; -import 'package:commet/client/matrix/matrix_room.dart'; -import 'package:commet/debug/log.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/material.dart' as material; -import 'package:intl/intl.dart'; -import 'package:matrix/matrix.dart'; -import 'package:tiamat/tiamat.dart' as tiamat; - -class MatrixRoomChatPrivacySettings extends StatefulWidget { - const MatrixRoomChatPrivacySettings(this.room); - - final MatrixRoom room; - - @override - State createState() => - _MatrixRoomChatPrivacySettingsState(); -} - -class _MatrixRoomChatPrivacySettingsState - extends State { - bool? globalPrivateReadReceipts; - bool? globalPrivateTypingIndicator; - bool? privateReadReceipts; - bool? privateTypingIndicator; - - String get labelEnabledOption => Intl.message("Enabled", - desc: "Label for an enabled value", name: "labelEnabledOption"); - - String get labelDisabledOption => Intl.message("Disabled", - desc: "Label for an disabled value", name: "labelDisabledOption"); - - String labelDefaultOption(pref) => Intl.message("Use global preference $pref", - desc: "Label to use the global preference", - name: "labelOptionDefault", - args: [pref]); - - String get labelPrivateReadReceiptsTitle => Intl.message( - "Private read receipts", - desc: - "Label for the toggle for enabling and disabling private read receipts", - name: "labelPrivateReadReceiptsToggle"); - - String get labelPrivateTypingIndicatorTitle => Intl.message( - "Private typing indicator", - desc: - "Label for the toggle for enabling and disabling private typing indicator", - name: "labelPrivateTypingIndicatorTitle"); - - @override - void initState() { - getChatPrivacy(); - super.initState(); - } - - Future getChatPrivacy() async { - var client = widget.room.client as MatrixClient; - var prr = await client.matrixClient - .getAccountData( - client.matrixClient.userID!, MatrixClient.privateReadReceiptsKey) - .catchError((e) { - if (!(e is MatrixException && e.error == MatrixError.M_NOT_FOUND)) - Log.e(e); - return {"enabled": null}; - }); - var pti = await client.matrixClient - .getAccountData( - client.matrixClient.userID!, MatrixClient.privateTypingIndicatorKey) - .catchError((e) { - if (!(e is MatrixException && e.error == MatrixError.M_NOT_FOUND)) - Log.e(e); - return {"enabled": null}; - }); - var rprr = await client - .getRoomAccountData(client.matrixClient.userID!, - widget.room.matrixRoom.id, MatrixClient.privateReadReceiptsKey) - .catchError((e) { - if (!(e is MatrixException && e.error == MatrixError.M_NOT_FOUND)) - Log.e(e); - return {"enabled": null}; - }); - var rpti = await client - .getRoomAccountData(client.matrixClient.userID!, - widget.room.matrixRoom.id, MatrixClient.privateTypingIndicatorKey) - .catchError((e) { - if (!(e is MatrixException && e.error == MatrixError.M_NOT_FOUND)) - Log.e(e); - return {"enabled": null}; - }); - - setState(() { - globalPrivateReadReceipts = - prr["enabled"] is bool ? prr["enabled"] as bool : false; - globalPrivateTypingIndicator = - pti["enabled"] is bool ? pti["enabled"] as bool : false; - privateReadReceipts = - rprr["enabled"] is bool ? rprr["enabled"] as bool : null; - privateTypingIndicator = - rpti["enabled"] is bool ? rpti["enabled"] as bool : null; - }); - } - - @override - Widget build(BuildContext context) { - return Column(children: [ - privateReadReceiptsSettings(), - const SizedBox( - height: 10, - ), - privateTypingIndicatorSettings(), - ]); - } - - Widget privateReadReceiptsSettings() { - return tiamat.Panel( - mode: tiamat.TileType.surfaceContainerLow, - header: labelPrivateReadReceiptsTitle, - child: material.Material( - color: material.Colors.transparent, - child: Column( - children: [ - tiamat.RadioButton( - groupValue: privateReadReceipts, - value: null, - icon: material.Icons.remove_outlined, - text: labelDefaultOption(globalPrivateReadReceipts is bool - ? (globalPrivateReadReceipts as bool - ? "($labelEnabledOption)" - : "($labelDisabledOption)") - : ""), - onChanged: onPRRChanged, - ), - tiamat.RadioButton( - groupValue: privateReadReceipts, - value: false, - icon: material.Icons.close, - text: labelDisabledOption, - onChanged: onPRRChanged, - ), - tiamat.RadioButton( - groupValue: privateReadReceipts, - value: true, - icon: material.Icons.check, - text: labelEnabledOption, - onChanged: onPRRChanged, - ), - ], - ), - )); - } - - Widget privateTypingIndicatorSettings() { - return tiamat.Panel( - mode: tiamat.TileType.surfaceContainerLow, - header: labelPrivateTypingIndicatorTitle, - child: material.Material( - color: material.Colors.transparent, - child: Column( - children: [ - tiamat.RadioButton( - groupValue: privateTypingIndicator, - value: null, - icon: material.Icons.remove_outlined, - text: labelDefaultOption(globalPrivateTypingIndicator is bool - ? (globalPrivateTypingIndicator as bool - ? "($labelEnabledOption)" - : "($labelDisabledOption)") - : ""), - onChanged: onPTIChanged, - ), - tiamat.RadioButton( - groupValue: privateTypingIndicator, - value: false, - icon: material.Icons.close, - text: labelDisabledOption, - onChanged: onPTIChanged, - ), - tiamat.RadioButton( - groupValue: privateTypingIndicator, - value: true, - icon: material.Icons.check, - text: labelEnabledOption, - onChanged: onPTIChanged, - ), - ], - ), - )); - } - - Future onPRRChanged(bool? value) async { - var client = widget.room.client as MatrixClient; - client.setRoomAccountData( - client.matrixClient.userID!, - widget.room.identifier, - MatrixClient.privateReadReceiptsKey, - {"enabled": value}, - ); - setState(() => privateReadReceipts = value); - } - - Future onPTIChanged(bool? value) async { - var client = widget.room.client as MatrixClient; - client.setRoomAccountData( - client.matrixClient.userID!, - widget.room.identifier, - MatrixClient.privateTypingIndicatorKey, - {"enabled": value}, - ); - - setState(() => privateTypingIndicator = value); - } -} diff --git a/commet/lib/ui/pages/settings/categories/account/preferences/preferences_chat_privacy.dart b/commet/lib/ui/pages/settings/categories/account/preferences/preferences_chat_privacy.dart new file mode 100644 index 000000000..b6ab6ed67 --- /dev/null +++ b/commet/lib/ui/pages/settings/categories/account/preferences/preferences_chat_privacy.dart @@ -0,0 +1,116 @@ +import 'package:commet/client/client.dart'; +import 'package:commet/client/components/user_presence/user_presence_component.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:tiamat/tiamat.dart' as tiamat; + +class ChatPrivacyPreferences extends StatefulWidget { + const ChatPrivacyPreferences({required this.client}); + final T client; + + @override + State createState() => _ChatPrivacyPreferences(); +} + +class _ChatPrivacyPreferences extends State { + bool publicReadReceipts = true; + bool publicTypingIndicator = true; + + UserPresenceComponent get userPresenceComponent => + widget.client.getComponent()!; + + String get labelChatPrivacyTitle => Intl.message("Chat Privacy", + desc: "Header for the chat privacy section in settings", + name: "labelChatPrivacyTitle"); + + String get labelPublicReadReceiptsToggle => Intl.message( + "Public read receipts", + desc: + "Label for the toggle for enabling and disabling sending read receipts", + name: "labelPublicReadReceiptsToggle"); + + String get labelPublicReadReceiptsDescription => Intl.message( + "Let other members of a room know when you have read their messages.", + desc: + "description for the toggle for enabling and disabling sending read receipts", + name: "labelPublicReadReceiptsDescriptionn"); + + String get labelPrivateTypingIndicatorToggle => Intl.message( + "Public typing indicator", + desc: + "Label for the toggle for enabling and disabling sending typing indicator", + name: "labelPublicTypingIndicatorToggle"); + + String get labelPublicTypingIndicatorDescription => Intl.message( + "Let other members of a room know when you are typing a message.", + desc: + "description for the toggle for enabling and disabling sending typing indicator", + name: "labelPublicTypingIndicatorDescription"); + + @override + void initState() { + setState(() { + publicReadReceipts = userPresenceComponent.usePublicReadReceipts; + publicTypingIndicator = userPresenceComponent.typingIndicatorEnabled; + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return tiamat.Panel( + header: labelChatPrivacyTitle, + mode: tiamat.TileType.surfaceContainerLow, + child: Column(children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + tiamat.Text.labelEmphasised(labelPublicReadReceiptsToggle), + tiamat.Text.labelLow(labelPublicReadReceiptsDescription) + ]), + tiamat.Switch( + state: publicReadReceipts, + onChanged: (value) async { + print(widget.client.getAllComponents()); + await userPresenceComponent.setUsePublicReadReceipts(value); + setState(() => publicReadReceipts = value); + }) + ], + ), + ), + const SizedBox( + height: 10, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + tiamat.Text.labelEmphasised( + labelPrivateTypingIndicatorToggle), + tiamat.Text.labelLow(labelPublicTypingIndicatorDescription) + ]), + tiamat.Switch( + state: publicTypingIndicator, + onChanged: (value) async { + await userPresenceComponent + .setTypingIndicatorEnabled(value); + setState(() => publicTypingIndicator = value); + }) + ], + ), + ), + ]), + ); + } +} diff --git a/commet/lib/ui/pages/settings/categories/account/preferences/preferences_tab.dart b/commet/lib/ui/pages/settings/categories/account/preferences/preferences_tab.dart new file mode 100644 index 000000000..4da598f77 --- /dev/null +++ b/commet/lib/ui/pages/settings/categories/account/preferences/preferences_tab.dart @@ -0,0 +1,44 @@ +import 'package:commet/client/client.dart'; +import 'package:commet/client/client_manager.dart'; +import 'package:commet/ui/molecules/account_selector.dart'; +import 'package:commet/ui/pages/settings/categories/account/preferences/preferences_chat_privacy.dart'; +import 'package:flutter/material.dart'; + +class AccountSettingsTab extends StatefulWidget { + const AccountSettingsTab({required this.clientManager, super.key}); + final ClientManager clientManager; + + @override + State createState() => _AccountSettingsTabState(); +} + +class _AccountSettingsTabState extends State { + late Client selectedClient; + + @override + void initState() { + selectedClient = widget.clientManager.clients.first; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (widget.clientManager.clients.length > 1) + AccountSelector( + widget.clientManager.clients, + onClientSelected: (client) { + setState(() { + selectedClient = client; + }); + }, + ), + const SizedBox( + height: 4, + ), + ChatPrivacyPreferences(client: selectedClient), + ], + ); + } +} diff --git a/commet/lib/ui/pages/settings/categories/account/security/matrix/matrix_security_tab.dart b/commet/lib/ui/pages/settings/categories/account/security/matrix/matrix_security_tab.dart index 529cb01a3..ce48dd168 100644 --- a/commet/lib/ui/pages/settings/categories/account/security/matrix/matrix_security_tab.dart +++ b/commet/lib/ui/pages/settings/categories/account/security/matrix/matrix_security_tab.dart @@ -1,5 +1,4 @@ import 'package:commet/client/matrix/matrix_client.dart'; -import 'package:commet/debug/log.dart'; import 'package:commet/ui/navigation/adaptive_dialog.dart'; import 'package:commet/ui/pages/settings/categories/account/security/matrix/session/matrix_session.dart'; import 'package:commet/utils/common_strings.dart'; @@ -21,38 +20,8 @@ class MatrixSecurityTab extends StatefulWidget { class _MatrixSecurityTabState extends State { bool crossSigningEnabled = false; bool? messageBackupEnabled; - bool privateReadReceipts = false; - bool privateTypingIndicator = false; List? devices; - String get labelChatPrivacyTitle => Intl.message("Chat Privacy", - desc: "Header for the chat privacy section in settings", - name: "labelChatPrivacyTitle"); - - String get labelPrivateReadReceiptsToggle => Intl.message( - "Private read receipts", - desc: - "Label for the toggle for enabling and disabling sending read receipts", - name: "labelPrivateReadReceiptsToggle"); - - String get labelPrivateReadReceiptsDescription => Intl.message( - "Prevent other members of a room from knowing when you have read their messages.", - desc: - "description for the toggle for enabling and disabling sending read receipts", - name: "labelPrivateReadReceiptsDescriptionn"); - - String get labelPrivateTypingIndicatorToggle => Intl.message( - "Private typing indicator", - desc: - "Label for the toggle for enabling and disabling sending typing indicator", - name: "labelPrivateTypingIndicatorToggle"); - - String get labelPrivateTypingIndicatorDescription => Intl.message( - "Prevent other members of a room from knowing when you are typing a message.", - desc: - "description for the toggle for enabling and disabling sending typing indicator", - name: "labelPrivateTypingIndicatorDescription"); - String get labelMatrixCrossSigning => Intl.message("Cross signing", desc: "Title label for matrix cross signing", name: "labelMatrixCrossSigning"); @@ -97,7 +66,6 @@ class _MatrixSecurityTabState extends State { void initState() { checkState(); getDevices(); - getChatPrivacy(); super.initState(); } @@ -121,32 +89,6 @@ class _MatrixSecurityTabState extends State { }); } - void getChatPrivacy() async { - var prr = await widget.client.matrixClient - .getAccountData(widget.client.matrixClient.userID!, - MatrixClient.privateReadReceiptsKey) - .catchError((e) { - if (!(e is MatrixException && e.error == MatrixError.M_NOT_FOUND)) - Log.e(e); - return {"enabled": false}; - }); - var pti = await widget.client.matrixClient - .getAccountData(widget.client.matrixClient.userID!, - MatrixClient.privateTypingIndicatorKey) - .catchError((e) { - if (!(e is MatrixException && e.error == MatrixError.M_NOT_FOUND)) - Log.e(e); - return {"enabled": false}; - }); - - setState(() { - privateReadReceipts = - prr["enabled"] is bool ? prr["enabled"] as bool : false; - privateTypingIndicator = - pti["enabled"] is bool ? pti["enabled"] as bool : false; - }); - } - @override Widget build(BuildContext context) { return Column( @@ -154,10 +96,6 @@ class _MatrixSecurityTabState extends State { mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ - const SizedBox( - height: 4, - ), - chatPrivacyPanel(), const SizedBox( height: 4, ), @@ -300,73 +238,4 @@ class _MatrixSecurityTabState extends State { ], ); } - - Widget chatPrivacyPanel() { - return Panel( - header: labelChatPrivacyTitle, - mode: TileType.surfaceContainerLow, - child: Column(children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - tiamat.Text.labelEmphasised(labelPrivateReadReceiptsToggle), - tiamat.Text.labelLow(labelPrivateReadReceiptsDescription) - ]), - tiamat.Switch( - state: privateReadReceipts, - onChanged: (value) { - setState(() { - privateReadReceipts = value; - widget.client.matrixClient.setAccountData( - widget.client.matrixClient.userID!, - MatrixClient.privateReadReceiptsKey, - {"enabled": privateReadReceipts}); - widget.client.matrixClient.receiptsPublicByDefault = - !privateReadReceipts; - }); - }, - ) - ], - ), - ), - const SizedBox( - height: 10, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - tiamat.Text.labelEmphasised( - labelPrivateTypingIndicatorToggle), - tiamat.Text.labelLow(labelPrivateTypingIndicatorDescription) - ]), - tiamat.Switch( - state: privateTypingIndicator, - onChanged: (value) { - setState(() { - privateTypingIndicator = value; - widget.client.matrixClient.setAccountData( - widget.client.matrixClient.userID!, - MatrixClient.privateTypingIndicatorKey, - {"enabled": privateTypingIndicator}); - }); - }, - ) - ], - ), - ), - ]), - ); - } } diff --git a/commet/lib/ui/pages/settings/categories/account/settings_category_account.dart b/commet/lib/ui/pages/settings/categories/account/settings_category_account.dart index 142d73e4d..453f52fbe 100644 --- a/commet/lib/ui/pages/settings/categories/account/settings_category_account.dart +++ b/commet/lib/ui/pages/settings/categories/account/settings_category_account.dart @@ -2,6 +2,7 @@ import 'package:commet/client/client_manager.dart'; import 'package:commet/main.dart'; import 'package:commet/ui/pages/settings/categories/account/account_emoji/account_emoji_tab.dart'; import 'package:commet/ui/pages/settings/categories/account/account_state/account_state_tab.dart'; +import 'package:commet/ui/pages/settings/categories/account/preferences/preferences_tab.dart'; import 'package:commet/ui/pages/settings/categories/account/profile/profile_edit_tab.dart'; import 'package:commet/ui/pages/settings/settings_category.dart'; import 'package:commet/ui/pages/settings/settings_tab.dart'; @@ -21,6 +22,10 @@ class SettingsCategoryAccount implements SettingsCategory { name: "labelSettingsTabProfile", desc: "Label for the Profile settings page"); + String get labelSettingsTabPreferences => Intl.message("Preferences", + name: "labelSettingsTabPreferences", + desc: "Label for the Preferences settings page"); + String get labelSettingsTabSecurity => Intl.message("Security", name: "labelSettingsTabSecurity", desc: "Label for the Security settings page"); @@ -58,6 +63,14 @@ class SettingsCategoryAccount implements SettingsCategory { clientManager: Provider.of(context), ); }), + SettingsTab( + label: labelSettingsTabPreferences, + icon: Icons.settings, + pageBuilder: (context) { + return AccountSettingsTab( + clientManager: Provider.of(context), + ); + }), SettingsTab( label: labelSettingsTabSecurity, icon: Icons.security, diff --git a/commet/lib/ui/pages/settings/categories/room/general/room_general_chat_privacy.dart b/commet/lib/ui/pages/settings/categories/room/general/room_general_chat_privacy.dart new file mode 100644 index 000000000..ba3cff45a --- /dev/null +++ b/commet/lib/ui/pages/settings/categories/room/general/room_general_chat_privacy.dart @@ -0,0 +1,165 @@ +import 'dart:async'; + +import 'package:commet/client/components/read_receipts/read_receipt_component.dart'; +import 'package:commet/client/components/typing_indicators/typing_indicator_component.dart'; +import 'package:commet/client/components/user_presence/user_presence_component.dart'; +import 'package:commet/client/room.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' as material; +import 'package:intl/intl.dart'; +import 'package:tiamat/tiamat.dart' as tiamat; + +class RoomGeneralChatPrivacySettings extends StatefulWidget { + const RoomGeneralChatPrivacySettings(this.room); + + final Room room; + + @override + State createState() => + _RoomGeneralChatPrivacySettings(); +} + +class _RoomGeneralChatPrivacySettings + extends State { + bool? publicReadReceiptsForRoom; + bool? typingIndicatorEnabledForRoom; + + String get labelEnabledOption => Intl.message("Enabled", + desc: "Label for an enabled value", name: "labelEnabledOption"); + + String get labelDisabledOption => Intl.message("Disabled", + desc: "Label for an disabled value", name: "labelDisabledOption"); + + String labelDefaultOption(pref) => Intl.message("Use global preference $pref", + desc: "Label to use the global preference", + name: "labelOptionDefault", + args: [pref]); + + String get labelPublicReadReceiptsTitle => Intl.message( + "Public read receipts", + desc: + "Label for the toggle for enabling and disabling public read receipts", + name: "labelPublicReadReceiptsTitle"); + + String get labelPublicTypingIndicatorTitle => Intl.message( + "Public typing indicator", + desc: + "Label for the toggle for enabling and disabling public typing indicator", + name: "labelPublicTypingIndicatorTitle"); + + @override + void initState() { + setState(() { + publicReadReceiptsForRoom = widget.room + .getComponent()! + .usePublicReadReceiptsForRoom; + typingIndicatorEnabledForRoom = widget.room + .getComponent()! + .typingIndicatorEnabledForRoom; + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column(children: [ + publicReadReceiptsSettings(), + const SizedBox( + height: 10, + ), + publicTypingIndicatorSettings(), + ]); + } + + Widget publicReadReceiptsSettings() { + var usePublicReadReceipts = widget.room.client + .getComponent()! + .usePublicReadReceipts; + return tiamat.Panel( + mode: tiamat.TileType.surfaceContainerLow, + header: labelPublicReadReceiptsTitle, + child: material.Material( + color: material.Colors.transparent, + child: Column( + children: [ + tiamat.RadioButton( + groupValue: publicReadReceiptsForRoom, + value: null, + icon: material.Icons.remove_outlined, + text: labelDefaultOption(usePublicReadReceipts + ? "($labelEnabledOption)" + : "($labelDisabledOption)"), + onChanged: onReadReceiptPrefChanged, + ), + tiamat.RadioButton( + groupValue: publicReadReceiptsForRoom, + value: false, + icon: material.Icons.close, + text: labelDisabledOption, + onChanged: onReadReceiptPrefChanged, + ), + tiamat.RadioButton( + groupValue: publicReadReceiptsForRoom, + value: true, + icon: material.Icons.check, + text: labelEnabledOption, + onChanged: onReadReceiptPrefChanged, + ), + ], + ), + )); + } + + Widget publicTypingIndicatorSettings() { + var typingIndicatorEnabled = widget.room.client + .getComponent()! + .typingIndicatorEnabled; + return tiamat.Panel( + mode: tiamat.TileType.surfaceContainerLow, + header: labelPublicTypingIndicatorTitle, + child: material.Material( + color: material.Colors.transparent, + child: Column( + children: [ + tiamat.RadioButton( + groupValue: typingIndicatorEnabledForRoom, + value: null, + icon: material.Icons.remove_outlined, + text: labelDefaultOption(typingIndicatorEnabled + ? "($labelEnabledOption)" + : "($labelDisabledOption)"), + onChanged: onTypingIndicatorPrefChanged, + ), + tiamat.RadioButton( + groupValue: typingIndicatorEnabledForRoom, + value: false, + icon: material.Icons.close, + text: labelDisabledOption, + onChanged: onTypingIndicatorPrefChanged, + ), + tiamat.RadioButton( + groupValue: typingIndicatorEnabledForRoom, + value: true, + icon: material.Icons.check, + text: labelEnabledOption, + onChanged: onTypingIndicatorPrefChanged, + ), + ], + ), + )); + } + + Future onReadReceiptPrefChanged(bool? value) async { + widget.room + .getComponent()! + .setUsePublicReadReceiptsForRoom(value); + setState(() => publicReadReceiptsForRoom = value); + } + + Future onTypingIndicatorPrefChanged(bool? value) async { + widget.room + .getComponent()! + .setTypingIndicatorEnabledForRoom(value); + setState(() => typingIndicatorEnabledForRoom = value); + } +} diff --git a/commet/lib/ui/pages/settings/categories/room/general/room_general_settings_page.dart b/commet/lib/ui/pages/settings/categories/room/general/room_general_settings_page.dart index 60f74c844..b2f3b904d 100644 --- a/commet/lib/ui/pages/settings/categories/room/general/room_general_settings_page.dart +++ b/commet/lib/ui/pages/settings/categories/room/general/room_general_settings_page.dart @@ -1,8 +1,9 @@ +import 'package:commet/client/components/read_receipts/read_receipt_component.dart'; import 'package:commet/client/matrix/matrix_client.dart'; import 'package:commet/client/matrix/matrix_room.dart'; import 'package:commet/client/room.dart'; import 'package:commet/debug/log.dart'; -import 'package:commet/ui/pages/matrix/matrix_room_chat_privacy.dart'; +import 'package:commet/ui/pages/settings/categories/room/general/room_general_chat_privacy.dart'; import 'package:commet/ui/pages/matrix/room_address_settings/matrix_room_address_settings.dart'; import 'package:commet/ui/pages/settings/categories/room/general/room_general_settings_view.dart'; import 'package:flutter/widgets.dart'; @@ -36,8 +37,8 @@ class _RoomGeneralSettingsPageState extends State { const SizedBox( height: 10, ), + RoomGeneralChatPrivacySettings(widget.room), if (widget.room is MatrixRoom) ...[ - MatrixRoomChatPrivacySettings(widget.room as MatrixRoom), const SizedBox( height: 10, ), From f3970d4e15d514665e97bda93856f86ffdecd9ee Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:25:00 +1030 Subject: [PATCH 3/6] minor ui tweaks + fixes --- .../preferences/preferences_chat_privacy.dart | 63 ++++++++++++------- .../account/preferences/preferences_tab.dart | 5 +- .../account/settings_category_account.dart | 10 +-- .../general/room_general_chat_privacy.dart | 61 +++++++++--------- commet/lib/utils/common_strings.dart | 12 ++++ 5 files changed, 91 insertions(+), 60 deletions(-) diff --git a/commet/lib/ui/pages/settings/categories/account/preferences/preferences_chat_privacy.dart b/commet/lib/ui/pages/settings/categories/account/preferences/preferences_chat_privacy.dart index b6ab6ed67..cda188721 100644 --- a/commet/lib/ui/pages/settings/categories/account/preferences/preferences_chat_privacy.dart +++ b/commet/lib/ui/pages/settings/categories/account/preferences/preferences_chat_privacy.dart @@ -1,11 +1,12 @@ import 'package:commet/client/client.dart'; import 'package:commet/client/components/user_presence/user_presence_component.dart'; +import 'package:commet/utils/error_utils.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tiamat/tiamat.dart' as tiamat; class ChatPrivacyPreferences extends StatefulWidget { - const ChatPrivacyPreferences({required this.client}); + const ChatPrivacyPreferences({required this.client, super.key}); final T client; @override @@ -23,8 +24,7 @@ class _ChatPrivacyPreferences extends State { desc: "Header for the chat privacy section in settings", name: "labelChatPrivacyTitle"); - String get labelPublicReadReceiptsToggle => Intl.message( - "Public read receipts", + String get labelPublicReadReceiptsToggle => Intl.message("Read receipts", desc: "Label for the toggle for enabling and disabling sending read receipts", name: "labelPublicReadReceiptsToggle"); @@ -35,11 +35,10 @@ class _ChatPrivacyPreferences extends State { "description for the toggle for enabling and disabling sending read receipts", name: "labelPublicReadReceiptsDescriptionn"); - String get labelPrivateTypingIndicatorToggle => Intl.message( - "Public typing indicator", + String get labelTypingIndicatorsToggle => Intl.message("Typing indicator", desc: "Label for the toggle for enabling and disabling sending typing indicator", - name: "labelPublicTypingIndicatorToggle"); + name: "labelTypingIndicatorsToggle"); String get labelPublicTypingIndicatorDescription => Intl.message( "Let other members of a room know when you are typing a message.", @@ -67,18 +66,27 @@ class _ChatPrivacyPreferences extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - tiamat.Text.labelEmphasised(labelPublicReadReceiptsToggle), - tiamat.Text.labelLow(labelPublicReadReceiptsDescription) - ]), + Flexible( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + tiamat.Text.labelEmphasised( + labelPublicReadReceiptsToggle), + tiamat.Text.labelLow(labelPublicReadReceiptsDescription) + ]), + ), tiamat.Switch( state: publicReadReceipts, onChanged: (value) async { + setState(() { + publicReadReceipts = value; + }); print(widget.client.getAllComponents()); - await userPresenceComponent.setUsePublicReadReceipts(value); + await ErrorUtils.tryRun(context, () async { + await userPresenceComponent + .setUsePublicReadReceipts(value); + }); setState(() => publicReadReceipts = value); }) ], @@ -92,19 +100,26 @@ class _ChatPrivacyPreferences extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - tiamat.Text.labelEmphasised( - labelPrivateTypingIndicatorToggle), - tiamat.Text.labelLow(labelPublicTypingIndicatorDescription) - ]), + Flexible( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + tiamat.Text.labelEmphasised(labelTypingIndicatorsToggle), + tiamat.Text.labelLow( + labelPublicTypingIndicatorDescription) + ]), + ), tiamat.Switch( state: publicTypingIndicator, onChanged: (value) async { - await userPresenceComponent - .setTypingIndicatorEnabled(value); + setState(() { + publicTypingIndicator = value; + }); + await ErrorUtils.tryRun(context, () async { + await userPresenceComponent + .setTypingIndicatorEnabled(value); + }); setState(() => publicTypingIndicator = value); }) ], diff --git a/commet/lib/ui/pages/settings/categories/account/preferences/preferences_tab.dart b/commet/lib/ui/pages/settings/categories/account/preferences/preferences_tab.dart index 4da598f77..a2a18137d 100644 --- a/commet/lib/ui/pages/settings/categories/account/preferences/preferences_tab.dart +++ b/commet/lib/ui/pages/settings/categories/account/preferences/preferences_tab.dart @@ -37,7 +37,10 @@ class _AccountSettingsTabState extends State { const SizedBox( height: 4, ), - ChatPrivacyPreferences(client: selectedClient), + ChatPrivacyPreferences( + client: selectedClient, + key: ValueKey( + "chat-privacy-preferences_${selectedClient.identifier}")), ], ); } diff --git a/commet/lib/ui/pages/settings/categories/account/settings_category_account.dart b/commet/lib/ui/pages/settings/categories/account/settings_category_account.dart index 453f52fbe..a94f92d4c 100644 --- a/commet/lib/ui/pages/settings/categories/account/settings_category_account.dart +++ b/commet/lib/ui/pages/settings/categories/account/settings_category_account.dart @@ -22,9 +22,9 @@ class SettingsCategoryAccount implements SettingsCategory { name: "labelSettingsTabProfile", desc: "Label for the Profile settings page"); - String get labelSettingsTabPreferences => Intl.message("Preferences", - name: "labelSettingsTabPreferences", - desc: "Label for the Preferences settings page"); + String get labelSettingsTabPrivacy => Intl.message("Privacy", + name: "labelSettingsTabPrivacy", + desc: "Label for the Privacy settings page"); String get labelSettingsTabSecurity => Intl.message("Security", name: "labelSettingsTabSecurity", @@ -64,8 +64,8 @@ class SettingsCategoryAccount implements SettingsCategory { ); }), SettingsTab( - label: labelSettingsTabPreferences, - icon: Icons.settings, + label: labelSettingsTabPrivacy, + icon: Icons.public, pageBuilder: (context) { return AccountSettingsTab( clientManager: Provider.of(context), diff --git a/commet/lib/ui/pages/settings/categories/room/general/room_general_chat_privacy.dart b/commet/lib/ui/pages/settings/categories/room/general/room_general_chat_privacy.dart index ba3cff45a..a38e1d4fc 100644 --- a/commet/lib/ui/pages/settings/categories/room/general/room_general_chat_privacy.dart +++ b/commet/lib/ui/pages/settings/categories/room/general/room_general_chat_privacy.dart @@ -4,6 +4,7 @@ import 'package:commet/client/components/read_receipts/read_receipt_component.da import 'package:commet/client/components/typing_indicators/typing_indicator_component.dart'; import 'package:commet/client/components/user_presence/user_presence_component.dart'; import 'package:commet/client/room.dart'; +import 'package:commet/utils/common_strings.dart'; import 'package:flutter/material.dart'; import 'package:flutter/material.dart' as material; import 'package:intl/intl.dart'; @@ -24,28 +25,28 @@ class _RoomGeneralChatPrivacySettings bool? publicReadReceiptsForRoom; bool? typingIndicatorEnabledForRoom; - String get labelEnabledOption => Intl.message("Enabled", - desc: "Label for an enabled value", name: "labelEnabledOption"); - - String get labelDisabledOption => Intl.message("Disabled", - desc: "Label for an disabled value", name: "labelDisabledOption"); + String labelDefaultReadReceiptsOption(pref) => Intl.message( + "Use global preference ($pref)", + desc: + "Label to use the global preference, showing the current global preference value", + name: "labelDefaultReadReceiptsOption", + args: [pref]); - String labelDefaultOption(pref) => Intl.message("Use global preference $pref", - desc: "Label to use the global preference", - name: "labelOptionDefault", + String labelDefaultTypingIndicatorsOption(pref) => Intl.message( + "Use global preference ($pref)", + desc: + "Label to use the global preference, showing the current global preference value", + name: "labelDefaultTypingIndicatorsOption", args: [pref]); - String get labelPublicReadReceiptsTitle => Intl.message( - "Public read receipts", + String get labelReadReceiptsTitle => Intl.message("Read receipts", desc: "Label for the toggle for enabling and disabling public read receipts", - name: "labelPublicReadReceiptsTitle"); + name: "labelReadReceiptsTitle"); - String get labelPublicTypingIndicatorTitle => Intl.message( - "Public typing indicator", - desc: - "Label for the toggle for enabling and disabling public typing indicator", - name: "labelPublicTypingIndicatorTitle"); + String get labelTypingIndicatorTitle => Intl.message("Typing indicators", + desc: "Label for the toggle for enabling and disabling typing indicators", + name: "labelTypingIndicatorTitle"); @override void initState() { @@ -77,7 +78,7 @@ class _RoomGeneralChatPrivacySettings .usePublicReadReceipts; return tiamat.Panel( mode: tiamat.TileType.surfaceContainerLow, - header: labelPublicReadReceiptsTitle, + header: labelReadReceiptsTitle, child: material.Material( color: material.Colors.transparent, child: Column( @@ -86,23 +87,23 @@ class _RoomGeneralChatPrivacySettings groupValue: publicReadReceiptsForRoom, value: null, icon: material.Icons.remove_outlined, - text: labelDefaultOption(usePublicReadReceipts - ? "($labelEnabledOption)" - : "($labelDisabledOption)"), + text: labelDefaultReadReceiptsOption(usePublicReadReceipts + ? CommonStrings.labelPublic + : CommonStrings.labelPrivate), onChanged: onReadReceiptPrefChanged, ), tiamat.RadioButton( groupValue: publicReadReceiptsForRoom, value: false, - icon: material.Icons.close, - text: labelDisabledOption, + icon: material.Icons.hide_source, + text: CommonStrings.labelPrivate, onChanged: onReadReceiptPrefChanged, ), tiamat.RadioButton( groupValue: publicReadReceiptsForRoom, value: true, - icon: material.Icons.check, - text: labelEnabledOption, + icon: material.Icons.public, + text: CommonStrings.labelPublic, onChanged: onReadReceiptPrefChanged, ), ], @@ -116,7 +117,7 @@ class _RoomGeneralChatPrivacySettings .typingIndicatorEnabled; return tiamat.Panel( mode: tiamat.TileType.surfaceContainerLow, - header: labelPublicTypingIndicatorTitle, + header: labelTypingIndicatorTitle, child: material.Material( color: material.Colors.transparent, child: Column( @@ -125,23 +126,23 @@ class _RoomGeneralChatPrivacySettings groupValue: typingIndicatorEnabledForRoom, value: null, icon: material.Icons.remove_outlined, - text: labelDefaultOption(typingIndicatorEnabled - ? "($labelEnabledOption)" - : "($labelDisabledOption)"), + text: labelDefaultTypingIndicatorsOption(typingIndicatorEnabled + ? CommonStrings.labelEnabled + : CommonStrings.labelDisabled), onChanged: onTypingIndicatorPrefChanged, ), tiamat.RadioButton( groupValue: typingIndicatorEnabledForRoom, value: false, icon: material.Icons.close, - text: labelDisabledOption, + text: CommonStrings.labelDisabled, onChanged: onTypingIndicatorPrefChanged, ), tiamat.RadioButton( groupValue: typingIndicatorEnabledForRoom, value: true, icon: material.Icons.check, - text: labelEnabledOption, + text: CommonStrings.labelEnabled, onChanged: onTypingIndicatorPrefChanged, ), ], diff --git a/commet/lib/utils/common_strings.dart b/commet/lib/utils/common_strings.dart index bc37eee05..32f821fe9 100644 --- a/commet/lib/utils/common_strings.dart +++ b/commet/lib/utils/common_strings.dart @@ -87,6 +87,18 @@ class CommonStrings { static String get promptCopy => Intl.message("Copy", desc: "Prompt to copy text", name: "promptCopy"); + static String get labelPublic => + Intl.message("Public", desc: "Label for public", name: "labelPublic"); + + static String get labelPrivate => + Intl.message("Private", desc: "Label for private", name: "labelPrivate"); + + static String get labelEnabled => + Intl.message("Enabled", desc: "Label for enabled", name: "labelEnabled"); + + static String get labelDisabled => Intl.message("Disabled", + desc: "Label for disabled", name: "labelDisabled"); + static String get promptCopyComplete => Intl.message("Copied!", desc: "Prompt text for after a copy has been completed", name: "promptCopyComplete"); From 80de048031c3deafb9e4381df662e1f4efc4c5c1 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:25:22 +1030 Subject: [PATCH 4/6] tweak logic for resolving read receipt visibility --- commet/lib/client/matrix/matrix_room.dart | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/commet/lib/client/matrix/matrix_room.dart b/commet/lib/client/matrix/matrix_room.dart index f547e4fdb..6af321e96 100644 --- a/commet/lib/client/matrix/matrix_room.dart +++ b/commet/lib/client/matrix/matrix_room.dart @@ -14,6 +14,7 @@ import 'package:commet/client/components/user_color/user_color_component.dart'; import 'package:commet/client/matrix/components/calendar_room_component/matrix_calendar_room_component.dart'; import 'package:commet/client/matrix/components/emoticon/matrix_room_emoticon_component.dart'; import 'package:commet/client/matrix/components/read_receipts/matrix_read_receipt_component.dart'; +import 'package:commet/client/matrix/components/user_presence/matrix_user_presence.dart'; import 'package:commet/client/matrix/matrix_attachment.dart'; import 'package:commet/client/matrix/matrix_client.dart'; import 'package:commet/client/matrix/matrix_member.dart'; @@ -846,8 +847,19 @@ class MatrixRoom extends Room { Future markAsRead() async { var tl = await matrixRoom.getTimeline(); var readReceiptComponent = getComponent(); - await tl.setReadMarker( - public: readReceiptComponent?.usePublicReadReceiptsForRoom); + + bool public = true; + var presenceComp = client.getComponent(); + + if (presenceComp?.usePublicReadReceipts != null) { + public = presenceComp!.usePublicReadReceipts; + } + + if (readReceiptComponent?.usePublicReadReceiptsForRoom != null) { + public = readReceiptComponent!.usePublicReadReceiptsForRoom!; + } + + await tl.setReadMarker(public: public); } @override From 790ea3369263e836e147a69a17a055b519705f6f Mon Sep 17 00:00:00 2001 From: Grafcube Date: Fri, 27 Feb 2026 16:36:20 +0530 Subject: [PATCH 5/6] Update commet/lib/ui/pages/settings/categories/account/preferences/preferences_chat_privacy.dart --- .../categories/account/preferences/preferences_chat_privacy.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/commet/lib/ui/pages/settings/categories/account/preferences/preferences_chat_privacy.dart b/commet/lib/ui/pages/settings/categories/account/preferences/preferences_chat_privacy.dart index cda188721..b0b6f6b23 100644 --- a/commet/lib/ui/pages/settings/categories/account/preferences/preferences_chat_privacy.dart +++ b/commet/lib/ui/pages/settings/categories/account/preferences/preferences_chat_privacy.dart @@ -82,7 +82,6 @@ class _ChatPrivacyPreferences extends State { setState(() { publicReadReceipts = value; }); - print(widget.client.getAllComponents()); await ErrorUtils.tryRun(context, () async { await userPresenceComponent .setUsePublicReadReceipts(value); From ff1f7bc3325bdcfb54534a75288756e29583c206 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Fri, 27 Feb 2026 23:11:04 +1030 Subject: [PATCH 6/6] Update room_general_settings_page.dart --- .../categories/room/general/room_general_settings_page.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/commet/lib/ui/pages/settings/categories/room/general/room_general_settings_page.dart b/commet/lib/ui/pages/settings/categories/room/general/room_general_settings_page.dart index b2f3b904d..9f0dadc12 100644 --- a/commet/lib/ui/pages/settings/categories/room/general/room_general_settings_page.dart +++ b/commet/lib/ui/pages/settings/categories/room/general/room_general_settings_page.dart @@ -1,13 +1,9 @@ -import 'package:commet/client/components/read_receipts/read_receipt_component.dart'; -import 'package:commet/client/matrix/matrix_client.dart'; import 'package:commet/client/matrix/matrix_room.dart'; import 'package:commet/client/room.dart'; -import 'package:commet/debug/log.dart'; import 'package:commet/ui/pages/settings/categories/room/general/room_general_chat_privacy.dart'; import 'package:commet/ui/pages/matrix/room_address_settings/matrix_room_address_settings.dart'; import 'package:commet/ui/pages/settings/categories/room/general/room_general_settings_view.dart'; import 'package:flutter/widgets.dart'; -import 'package:matrix/matrix_api_lite/model/matrix_exception.dart'; class RoomGeneralSettingsPage extends StatefulWidget { const RoomGeneralSettingsPage({super.key, required this.room});