diff --git a/README.md b/README.md index bb61124..25d03db 100644 --- a/README.md +++ b/README.md @@ -94,9 +94,9 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [x] Editing - [x] Deleting - [x] Reactions -- [ ] Pins - - [ ] Displaying - - [ ] Creating +- [x] Pins + - [x] Displaying + - [x] Creating - [ ] Threads - [x] Profile popouts - [x] Working actions diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index fb57735..8ce4826 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -28,6 +28,7 @@ import "package:nexus/models/requests/report_request.dart"; import "package:nexus/models/requests/send_event_request.dart"; import "package:nexus/models/requests/send_message_request.dart"; import "package:nexus/models/requests/set_membership_request.dart"; +import "package:nexus/models/requests/set_state_request.dart"; import "package:nexus/models/room.dart"; import "package:nexus/models/sync_data.dart"; import "package:nexus/src/third_party/gomuks.g.dart"; @@ -172,6 +173,9 @@ class ClientController extends AsyncNotifier { Future sendEvent(SendEventRequest request) async => Event.fromJson(await _sendCommand("send_event", request.toJson())); + Future setState(SetStateRequest request) async => + Event.fromJson(await _sendCommand("set_state", request.toJson())); + Future verify(String recoveryKey) async { try { await _sendCommand("verify", {"recovery_key": recoveryKey}); diff --git a/lib/controllers/pinned_events_controller.dart b/lib/controllers/pinned_events_controller.dart index 54d6196..53081ff 100644 --- a/lib/controllers/pinned_events_controller.dart +++ b/lib/controllers/pinned_events_controller.dart @@ -41,22 +41,49 @@ class PinnedEventsController extends AsyncNotifier> { return .new(); } - Future togglePin(Event event) async { + Future addPin(Event event) async { final client = ref.read(ClientController.provider.notifier); final current = await future; - final updatedPinnedIds = current.any((e) => e.eventId == event.eventId) - ? current - .where((e) => e.eventId != event.eventId) - .map((e) => e.eventId) - .toList() - : [...current.map((e) => e.eventId), event.eventId]; + if (current.any((ev) => ev.eventId == event.eventId)) { + return; + } - await client.sendEvent( + final updatedPinnedIds = [ + ...current.map((ev) => ev.eventId), + event.eventId, + ]; + + await client.setState( .new( roomId: roomId, type: EventType.pinnedEvents.type, - stateKey: "", + stateKey: + "", //The empty string stands for m.room.pinned_events according to matrix spec. + content: {"pinned": updatedPinnedIds}, + ), + ); + } + + Future removePin(Event event) async { + final client = ref.read(ClientController.provider.notifier); + final current = await future; + + if (!current.any((ev) => ev.eventId == event.eventId)) { + return; + } + + final updatedPinnedIds = [ + ...current + .where((ev) => ev.eventId != event.eventId) + .map((ev) => ev.eventId), + ]; + + await client.setState( + .new( + roomId: roomId, + type: EventType.pinnedEvents.type, + stateKey: "", //The empty string stands for m.room.pinned_events according to matrix spec. content: {"pinned": updatedPinnedIds}, ), ); diff --git a/lib/models/requests/send_event_request.dart b/lib/models/requests/send_event_request.dart index e117df2..da5de32 100644 --- a/lib/models/requests/send_event_request.dart +++ b/lib/models/requests/send_event_request.dart @@ -8,7 +8,6 @@ abstract class SendEventRequest with _$SendEventRequest { required String roomId, required String type, required Map content, - @Default(null) String? stateKey, @Default(false) bool synchronous, @Default(false) bool disableEncryption, }) = _SendEventRequest; diff --git a/lib/models/requests/set_state_request.dart b/lib/models/requests/set_state_request.dart new file mode 100644 index 0000000..30f62c3 --- /dev/null +++ b/lib/models/requests/set_state_request.dart @@ -0,0 +1,17 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "set_state_request.freezed.dart"; +part "set_state_request.g.dart"; + +@freezed +abstract class SetStateRequest with _$SetStateRequest { + const factory SetStateRequest({ + required String roomId, + required String type, + required String stateKey, + required Map content, + @Default(0) int delayMs, + }) = _SetStateRequest; + + factory SetStateRequest.fromJson(Map json) => + _$SetStateRequestFromJson(json); +} \ No newline at end of file diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart index 8f08ca9..a5dfc0f 100644 --- a/lib/widgets/room_chat.dart +++ b/lib/widgets/room_chat.dart @@ -7,6 +7,7 @@ import "package:measure_size/measure_size.dart"; import "package:nexus/controllers/account_data_controller.dart"; import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_state_controller.dart"; +import "package:nexus/controllers/pinned_events_controller.dart"; import "package:nexus/controllers/power_level_controller.dart"; import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/controllers/room_chat_controller.dart"; @@ -249,6 +250,39 @@ class RoomChat extends HookConsumerWidget { }, child: ListTile(leading: Icon(Icons.edit), title: Text("Edit")), ), + if (ref.watch( + PowerLevelController.provider( + PowerLevelConfig(eventType: .pinnedEvents, roomId: roomId), + ), + )) + () { + final isPinned = ref + .watch(PinnedEventsController.provider(roomId)) + .maybeWhen( + data: (pinnedEvents) => + pinnedEvents.any((ev) => ev.eventId == event.eventId), + orElse: () => false, + ); + + return PopupMenuItem( + onTap: () async { + final notifier = ref.read( + PinnedEventsController.provider(roomId).notifier, + ); + if (isPinned) { + await notifier.removePin(event); + } else { + await notifier.addPin(event); + } + }, + child: ListTile( + leading: Icon( + isPinned ? Icons.push_pin_outlined : Icons.push_pin, + ), + title: Text(isPinned ? "Unpin Event" : "Pin Event"), + ), + ); + }(), PopupMenuItem( onTap: () async { final room = ref.watch(