WIP: Support for pinned messages #49
69
lib/controllers/pinned_events_controller.dart
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
|
import "package:nexus/controllers/event_controller.dart";
|
||||||
|
import "package:nexus/controllers/rooms_controller.dart";
|
||||||
|
import "package:nexus/models/content/content.dart";
|
||||||
|
import "package:nexus/models/content/pinned_events.dart";
|
||||||
|
import "package:nexus/models/event.dart";
|
||||||
|
|
||||||
|
class PinnedEventsController extends AsyncNotifier<IList<Event>> {
|
||||||
|
final String roomId;
|
||||||
|
PinnedEventsController(this.roomId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<IList<Event>> build() async {
|
||||||
|
final room = ref.watch(
|
||||||
|
RoomsController.provider.select((rooms) => rooms[roomId]),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (room == null) return .new();
|
||||||
|
|
||||||
|
final pinnedRowId = room.state[EventType.pinnedEvents.type]?[""];
|
||||||
|
final pinnedStateEvent = pinnedRowId == null
|
||||||
|
? null
|
||||||
|
: room.events[pinnedRowId];
|
||||||
|
|
||||||
|
if (pinnedStateEvent == null) return .new();
|
||||||
|
|
||||||
|
if (pinnedStateEvent.content case PinnedEventsContent content) {
|
||||||
|
return (await Future.wait(
|
||||||
|
content.pinnedEvents.map(
|
||||||
|
(eventId) => ref.watch(
|
||||||
|
EventController.provider(
|
||||||
|
.new(eventId: eventId, roomId: roomId),
|
||||||
|
).future,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)).nonNulls.toIList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return .new();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> togglePin(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];
|
||||||
|
|
||||||
|
await client.sendEvent(
|
||||||
|
.new(
|
||||||
|
roomId: roomId,
|
||||||
|
type: EventType.pinnedEvents.type,
|
||||||
|
stateKey: "",
|
||||||
|
content: {"pinned": updatedPinnedIds},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider = AsyncNotifierProvider.family
|
||||||
|
.autoDispose<PinnedEventsController, IList<Event>, String>(
|
||||||
|
PinnedEventsController.new,
|
||||||
|
|
|||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,9 @@ part "pinned_events.g.dart";
|
||||||
@freezed
|
@freezed
|
||||||
abstract class PinnedEventsContent extends Content with _$PinnedEventsContent {
|
abstract class PinnedEventsContent extends Content with _$PinnedEventsContent {
|
||||||
PinnedEventsContent._();
|
PinnedEventsContent._();
|
||||||
factory PinnedEventsContent({@Default(IList.empty()) IList<String> pinned}) =
|
factory PinnedEventsContent({
|
||||||
_PinnedEventsContent;
|
@Default(IList.empty()) @JsonKey(name: "pinned") IList<String> pinnedEvents,
|
||||||
|
}) = _PinnedEventsContent;
|
||||||
|
|
||||||
factory PinnedEventsContent.fromJson(Map<String, Object?> json) =>
|
factory PinnedEventsContent.fromJson(Map<String, Object?> json) =>
|
||||||
_$PinnedEventsContentFromJson(json);
|
_$PinnedEventsContentFromJson(json);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ abstract class SendEventRequest with _$SendEventRequest {
|
||||||
required String roomId,
|
required String roomId,
|
||||||
required String type,
|
required String type,
|
||||||
required Map<String, dynamic> content,
|
required Map<String, dynamic> content,
|
||||||
|
@Default(null) String? stateKey,
|
||||||
|
Henry-Hiles
commented
Not certain where you got this from... It's not valid: https://spec.mau.fi/gomuks/rpc.html#cmd-send-event Instead, Not certain where you got this from... It's not valid: https://spec.mau.fi/gomuks/rpc.html#cmd-send-event
Instead, [`set_state`](https://spec.mau.fi/gomuks/rpc.html#cmd-set-state) must be used. This will need its own function in `ClientController`.
istalri
commented
Oh then I understood something wrong. I though it was needed for this: Matrix Spec:
Oh then I understood something wrong. I though it was needed for this:
Matrix Spec:
> {
> "content": {
> "pinned": [
> "$someevent:example.org"
> ]
> },
> "event_id": "$143273582443PhrSn:example.org",
> "origin_server_ts": 1432735824653,
> "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
> "sender": "@example:example.org",
> "state_key": "",
> "type": "m.room.pinned_events",
> "unsigned": {
> "age": 1234,
> "membership": "join"
> }
> }
Henry-Hiles
commented
Ah, well eventually it gets to Matrix, but we actually use gomuks as an SDK essentially, so we use that to e.g. send events. You can reference their docs for a schema of all commands: https://spec.mau.fi/gomuks/rpc.html Ah, well eventually it gets to Matrix, but we actually use gomuks as an SDK essentially, so we use that to e.g. send events. You can reference their docs for a schema of all commands: https://spec.mau.fi/gomuks/rpc.html
istalri
commented
Okay, I see. I will look into it. Edit: Just read the gomuks spec, seems the Okay, I see. I will look into it.
Edit: Just read the gomuks spec, seems the `set_state` is straight forward. At first I was not sure why there needs to be multiple endpoints for events but I then it clicked and yeah it makes sense to separate state events from other events. Although I think it's slightly misleading to have `send_event` and `set_state` and not `send_state_event` or the other way around have also `set_reaction` etc but maybe that's just me.
|
|||||||
@Default(false) bool synchronous,
|
@Default(false) bool synchronous,
|
||||||
@Default(false) bool disableEncryption,
|
@Default(false) bool disableEncryption,
|
||||||
}) = _SendEventRequest;
|
}) = _SendEventRequest;
|
||||||
|
|
|
||||||
|
|
@ -33,152 +33,156 @@ class MemberList extends HookConsumerWidget {
|
||||||
|
|
||||||
return Drawer(
|
return Drawer(
|
||||||
shape: Border(),
|
shape: Border(),
|
||||||
child: Column(
|
child: Scaffold(
|
||||||
children: [
|
backgroundColor: Colors.transparent,
|
||||||
if (Scaffold.of(context).hasEndDrawer)
|
body: Column(
|
||||||
AppBar(
|
children: [
|
||||||
scrolledUnderElevation: 0,
|
Padding(
|
||||||
leading: Icon(Icons.people),
|
padding: .symmetric(vertical: 8),
|
||||||
title: Text("Members"),
|
child: M3EToggleButtonGroup(
|
||||||
actionsPadding: .only(right: 4),
|
selectedIndex: statusIndex.value,
|
||||||
actions: [
|
onSelectedIndexChanged: (index) =>
|
||||||
IconButton(
|
statusIndex.value = index ?? statusIndex.value,
|
||||||
onPressed: Scaffold.of(context).closeEndDrawer,
|
actions: options
|
||||||
icon: Icon(Icons.close),
|
.mapTo(
|
||||||
tooltip: "Close member list",
|
(name, value) => M3EToggleButtonGroupAction(
|
||||||
),
|
checkedLabel: Text(
|
||||||
],
|
"$name${switch (ref.watch(MembersByStatusController.provider(.new(roomId: roomId, status: value)))) {
|
||||||
),
|
AsyncData(:final value) || AsyncLoading(:final value?) => " (${value.length})",
|
||||||
Padding(
|
_ => "",
|
||||||
padding: .symmetric(vertical: 8),
|
}}",
|
||||||
child: M3EToggleButtonGroup(
|
|
||||||
selectedIndex: statusIndex.value,
|
|
||||||
onSelectedIndexChanged: (index) =>
|
|
||||||
statusIndex.value = index ?? statusIndex.value,
|
|
||||||
actions: options
|
|
||||||
.mapTo(
|
|
||||||
(name, value) => M3EToggleButtonGroupAction(
|
|
||||||
checkedLabel: Text(
|
|
||||||
"$name${switch (ref.watch(MembersByStatusController.provider(.new(roomId: roomId, status: value)))) {
|
|
||||||
AsyncData(:final value) || AsyncLoading(:final value?) => " (${value.length})",
|
|
||||||
_ => "",
|
|
||||||
}}",
|
|
||||||
),
|
|
||||||
label: Text(name),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
switch (ref.watch(
|
|
||||||
MembersGroupedController.provider(
|
|
||||||
.new(roomId: roomId, status: status),
|
|
||||||
),
|
|
||||||
)) {
|
|
||||||
AsyncError(:final error, :final stackTrace) => ErrorDialog(
|
|
||||||
error,
|
|
||||||
stackTrace,
|
|
||||||
),
|
|
||||||
AsyncData(:final value) || AsyncLoading(:final value?) =>
|
|
||||||
value.isEmpty
|
|
||||||
? Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: .symmetric(vertical: 18),
|
|
||||||
child: Text(
|
|
||||||
"No ${options.keys.toIList()[statusIndex.value]} Members",
|
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
|
||||||
),
|
),
|
||||||
|
label: Text(name),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Expanded(
|
.toList(),
|
||||||
child: CustomScrollView(
|
),
|
||||||
slivers: [
|
),
|
||||||
for (final MapEntry(key: powerLevel, value: members)
|
|
||||||
in value) ...[
|
switch (ref.watch(
|
||||||
SliverToBoxAdapter(
|
MembersGroupedController.provider(
|
||||||
child: Padding(
|
.new(roomId: roomId, status: status),
|
||||||
padding: .symmetric(horizontal: 16),
|
),
|
||||||
child: DividerText(
|
)) {
|
||||||
powerLevel == null
|
AsyncError(:final error, :final stackTrace) => ErrorDialog(
|
||||||
? "Creators"
|
error,
|
||||||
: "Power Level $powerLevel",
|
stackTrace,
|
||||||
|
),
|
||||||
|
AsyncData(:final value) || AsyncLoading(:final value?) =>
|
||||||
|
value.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: .symmetric(vertical: 18),
|
||||||
|
child: Text(
|
||||||
|
"No ${options.keys.toIList()[statusIndex.value]} Members",
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Expanded(
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
for (final MapEntry(key: powerLevel, value: members)
|
||||||
|
in value) ...[
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: .symmetric(horizontal: 16),
|
||||||
|
child: DividerText(
|
||||||
|
powerLevel == null
|
||||||
|
? "Creators"
|
||||||
|
: "Power Level $powerLevel",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
SliverM3ECardList(
|
||||||
SliverM3ECardList(
|
padding: .all(4),
|
||||||
padding: .all(4),
|
color: Theme.of(
|
||||||
color: Theme.of(
|
context,
|
||||||
context,
|
).colorScheme.surfaceContainerHigh,
|
||||||
).colorScheme.surfaceContainerHigh,
|
margin: .symmetric(horizontal: 12, vertical: 4),
|
||||||
margin: .symmetric(horizontal: 12, vertical: 4),
|
itemCount: members.length,
|
||||||
itemCount: members.length,
|
itemBuilder: (context, index) =>
|
||||||
itemBuilder: (context, index) =>
|
switch (members[index].content) {
|
||||||
switch (members[index].content) {
|
MembershipContent(
|
||||||
MembershipContent(
|
:final avatarUrl,
|
||||||
:final avatarUrl,
|
:final displayName,
|
||||||
:final displayName,
|
) =>
|
||||||
) =>
|
ListTile(
|
||||||
ListTile(
|
title: Text(
|
||||||
title: Text(
|
displayName ??
|
||||||
displayName ??
|
members[index]
|
||||||
members[index]
|
.stateKey!
|
||||||
|
.localpart,
|
||||||
|
overflow: .ellipsis,
|
||||||
|
style: .new(
|
||||||
|
color: members[index]
|
||||||
.stateKey!
|
.stateKey!
|
||||||
.localpart,
|
.colorHash,
|
||||||
overflow: .ellipsis,
|
fontWeight: .bold,
|
||||||
style: .new(
|
),
|
||||||
color: members[index]
|
),
|
||||||
.stateKey!
|
subtitle: Text(
|
||||||
.colorHash,
|
members[index].stateKey!,
|
||||||
fontWeight: .bold,
|
overflow: .ellipsis,
|
||||||
|
),
|
||||||
|
leading: AvatarOrHash(
|
||||||
|
avatarUrl,
|
||||||
|
height: 36,
|
||||||
|
displayName ??
|
||||||
|
members[index]
|
||||||
|
.stateKey!
|
||||||
|
.localpart,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
_ => throw Exception(
|
||||||
members[index].stateKey!,
|
"Member content was not MembershipContent",
|
||||||
overflow: .ellipsis,
|
),
|
||||||
),
|
},
|
||||||
leading: AvatarOrHash(
|
onTap: (index) {
|
||||||
avatarUrl,
|
final member = members[index];
|
||||||
height: 36,
|
if (member.content
|
||||||
displayName ??
|
case MembershipContent content) {
|
||||||
members[index]
|
showModalBottomSheet(
|
||||||
.stateKey!
|
constraints: BoxConstraints.loose(
|
||||||
.localpart,
|
Size(
|
||||||
|
500,
|
||||||
|
(context.size?.height ?? 1000) - 80,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_ => throw Exception(
|
isScrollControlled: true,
|
||||||
"Member content was not MembershipContent",
|
context: context,
|
||||||
),
|
builder: (context) => UserBottomSheet(
|
||||||
},
|
content,
|
||||||
onTap: (index) {
|
member.stateKey!,
|
||||||
final member = members[index];
|
roomId: roomId,
|
||||||
if (member.content
|
|
||||||
case MembershipContent content) {
|
|
||||||
showModalBottomSheet(
|
|
||||||
constraints: BoxConstraints.loose(
|
|
||||||
Size(
|
|
||||||
500,
|
|
||||||
(context.size?.height ?? 1000) - 80,
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
isScrollControlled: true,
|
}
|
||||||
context: context,
|
},
|
||||||
builder: (context) => UserBottomSheet(
|
),
|
||||||
content,
|
],
|
||||||
member.stateKey!,
|
|
||||||
roomId: roomId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
AsyncLoading _ => Loading(),
|
||||||
AsyncLoading _ => Loading(),
|
},
|
||||||
},
|
],
|
||||||
],
|
),
|
||||||
|
appBar: Scaffold.of(context).hasEndDrawer
|
||||||
|
? AppBar(
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
leading: Icon(Icons.people),
|
||||||
|
title: Text("Members"),
|
||||||
|
actionsPadding: .only(right: 4),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: Scaffold.of(context).closeEndDrawer,
|
||||||
|
icon: Icon(Icons.close),
|
||||||
|
tooltip: "Close member list",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
85
lib/widgets/pinned_events_drawer.dart
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/pinned_events_controller.dart";
|
||||||
|
import "package:nexus/models/event.dart";
|
||||||
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
|
import "package:nexus/widgets/loading.dart";
|
||||||
|
import "package:nexus/widgets/renderers/event.dart";
|
||||||
|
|
||||||
|
class PinnedEventsDrawer extends HookConsumerWidget {
|
||||||
|
final String roomId;
|
||||||
|
final IList<PopupMenuEntry> Function(Event event)? getEventOptions;
|
||||||
|
const PinnedEventsDrawer(this.roomId, {this.getEventOptions, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final pinsProvider = ref.watch(PinnedEventsController.provider(roomId));
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Drawer(
|
||||||
|
width: 600,
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
appBar: AppBar(
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
leading: Icon(Icons.push_pin),
|
||||||
|
title: Text("Pinned Messages"),
|
||||||
|
actionsPadding: .only(right: 4),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: Scaffold.of(context).closeEndDrawer,
|
||||||
|
icon: Icon(Icons.close),
|
||||||
|
tooltip: "Close pinned messages",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: switch (pinsProvider) {
|
||||||
|
AsyncData(:final value) when value.isEmpty => Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.push_pin_outlined,
|
||||||
|
size: 48,
|
||||||
|
color: theme.colorScheme.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
"No pinned messages.",
|
||||||
|
style: theme.textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Henry-Hiles marked this conversation as resolved
Outdated
Henry-Hiles
commented
I would say instead of this, a I would say instead of this, a `SuperSliverView` should be used, like we do in the main chat view, and instead of an extra card, just wrap `EventRenderer` with an `InkWell` or something. And then, you don't need to set `textOnly` on `EventRenderer`.
Henry-Hiles
commented
Okay, I take it back about Okay, I take it back about `SuperSliverView`, the performance of `ListView` seems acceptable here.
Henry-Hiles
commented
I've done my other suggestion with not using I've done my other suggestion with not using `textOnly`.
|
|||||||
|
AsyncData(:final value) ||
|
||||||
|
AsyncLoading(:final value?) => ListView.builder(
|
||||||
|
padding: .symmetric(horizontal: 8),
|
||||||
|
itemCount: value.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final event = value[index];
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
borderRadius: .circular(12),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
// TODO: // Close drawer, then scroll to the message
|
||||||
|
},
|
||||||
|
child: EventRenderer(
|
||||||
|
event,
|
||||||
|
maxLines: 2,
|
||||||
|
isGrouped: false,
|
||||||
|
getEventOptions: getEventOptions,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AsyncLoading() => Loading(),
|
||||||
|
AsyncError(:final error, :final stackTrace) => ErrorDialog(
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,13 +12,15 @@ import "package:nexus/widgets/room_menu.dart";
|
||||||
class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget {
|
class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
final bool isDesktop;
|
final bool isDesktop;
|
||||||
final void Function(BuildContext context)? onOpenMemberList;
|
final void Function(BuildContext context)? onOpenMemberList;
|
||||||
final void Function(BuildContext context) onOpenDrawer;
|
final void Function()? onOpenPinnedMessagesList;
|
||||||
|
final void Function() onOpenDrawer;
|
||||||
final String? roomId;
|
final String? roomId;
|
||||||
const RoomAppbar({
|
const RoomAppbar({
|
||||||
required this.roomId,
|
required this.roomId,
|
||||||
required this.isDesktop,
|
required this.isDesktop,
|
||||||
required this.onOpenDrawer,
|
required this.onOpenDrawer,
|
||||||
this.onOpenMemberList,
|
this.onOpenMemberList,
|
||||||
|
this.onOpenPinnedMessagesList,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -101,7 +103,7 @@ class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
height: 24,
|
height: 24,
|
||||||
fallback: Icon(Icons.numbers),
|
fallback: Icon(Icons.numbers),
|
||||||
)
|
)
|
||||||
: DrawerButton(onPressed: () => onOpenDrawer(context)),
|
: DrawerButton(onPressed: onOpenDrawer),
|
||||||
scrolledUnderElevation: 0,
|
scrolledUnderElevation: 0,
|
||||||
title: room == null
|
title: room == null
|
||||||
? null
|
? null
|
||||||
|
|
@ -128,7 +130,7 @@ class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
? .new()
|
? .new()
|
||||||
: .new([
|
: .new([
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: null,
|
onPressed: onOpenPinnedMessagesList?.call,
|
||||||
icon: Icon(Icons.push_pin),
|
icon: Icon(Icons.push_pin),
|
||||||
tooltip: "Open pinned messages",
|
tooltip: "Open pinned messages",
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
import "package:nexus/widgets/composer/composer.dart";
|
import "package:nexus/widgets/composer/composer.dart";
|
||||||
import "package:nexus/widgets/emoji_picker_button.dart";
|
import "package:nexus/widgets/emoji_picker_button.dart";
|
||||||
|
import "package:nexus/widgets/pinned_events_drawer.dart";
|
||||||
import "package:nexus/widgets/renderers/event.dart";
|
import "package:nexus/widgets/renderers/event.dart";
|
||||||
import "package:nexus/widgets/member_list.dart";
|
import "package:nexus/widgets/member_list.dart";
|
||||||
import "package:nexus/widgets/room_appbar.dart";
|
import "package:nexus/widgets/room_appbar.dart";
|
||||||
|
|
@ -61,8 +62,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
appBar: RoomAppbar(
|
appBar: RoomAppbar(
|
||||||
roomId: this.roomId,
|
roomId: this.roomId,
|
||||||
isDesktop: isDesktop,
|
isDesktop: isDesktop,
|
||||||
onOpenDrawer: (_) => Scaffold.of(context).openDrawer(),
|
onOpenDrawer: () => Scaffold.of(context).openDrawer(),
|
||||||
onOpenMemberList: null,
|
|
||||||
),
|
),
|
||||||
body: nothing,
|
body: nothing,
|
||||||
);
|
);
|
||||||
|
|
@ -384,131 +384,148 @@ class RoomChat extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: RoomAppbar(
|
endDrawer: PinnedEventsDrawer(roomId, getEventOptions: getEventOptions),
|
||||||
roomId: roomId,
|
body: Builder(
|
||||||
isDesktop: isDesktop,
|
builder: (middleContext) => Scaffold(
|
||||||
onOpenDrawer: (_) => Scaffold.of(context).openDrawer(),
|
endDrawer: showMembersByDefault ? null : MemberList(roomId),
|
||||||
onOpenMemberList: (thisContext) {
|
appBar: RoomAppbar(
|
||||||
memberListOpened.value = !memberListOpened.value;
|
roomId: roomId,
|
||||||
Scaffold.of(thisContext).openEndDrawer();
|
isDesktop: isDesktop,
|
||||||
},
|
onOpenDrawer: Scaffold.of(context).openDrawer,
|
||||||
),
|
onOpenMemberList: (thisContext) {
|
||||||
body: Row(
|
memberListOpened.value = !memberListOpened.value;
|
||||||
children: [
|
Scaffold.of(thisContext).openEndDrawer();
|
||||||
Expanded(
|
},
|
||||||
child: Stack(
|
onOpenPinnedMessagesList: () {
|
||||||
children: [
|
Scaffold.of(middleContext).openEndDrawer();
|
||||||
Positioned.fill(
|
},
|
||||||
child: Padding(
|
),
|
||||||
padding: .symmetric(horizontal: 4),
|
body: Row(
|
||||||
child: switch (controllerData) {
|
children: [
|
||||||
AsyncData(:final value?) ||
|
Expanded(
|
||||||
AsyncLoading(:final value?) => CustomScrollView(
|
child: Stack(
|
||||||
controller: scrollController,
|
children: [
|
||||||
slivers: [
|
Positioned.fill(
|
||||||
SliverToBoxAdapter(
|
child: Padding(
|
||||||
child: Padding(
|
padding: .symmetric(horizontal: 4),
|
||||||
padding: .symmetric(vertical: 36),
|
child: switch (controllerData) {
|
||||||
child: Center(
|
AsyncData(:final value?) ||
|
||||||
child: ElevatedButton(
|
AsyncLoading(:final value?) => CustomScrollView(
|
||||||
onPressed: controllerData is AsyncData
|
controller: scrollController,
|
||||||
? loadOlder
|
slivers: [
|
||||||
: null,
|
SliverToBoxAdapter(
|
||||||
child: Text("Load More"),
|
child: Padding(
|
||||||
|
padding: .symmetric(vertical: 36),
|
||||||
|
child: Center(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: controllerData is AsyncData
|
||||||
|
? loadOlder
|
||||||
|
: null,
|
||||||
|
child: Text("Load More"),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
SuperSliverList.builder(
|
SuperSliverList.builder(
|
||||||
listController: listController.value,
|
listController: listController.value,
|
||||||
itemCount: value.length,
|
itemCount: value.length,
|
||||||
itemBuilder: (_, index) {
|
itemBuilder: (_, index) {
|
||||||
final event = value[index];
|
final event = value[index];
|
||||||
final previousEvent = value.getOrNull(index - 1);
|
final previousEvent = value.getOrNull(
|
||||||
return HighlightWrapper(
|
index - 1,
|
||||||
EventRenderer(
|
);
|
||||||
event,
|
return HighlightWrapper(
|
||||||
onTapReply: () async {
|
EventRenderer(
|
||||||
final replyId = event.replyTo;
|
event,
|
||||||
listController.value.animateToItem(
|
onTapReply: () async {
|
||||||
index: value.indexWhere(
|
final replyId = event.replyTo;
|
||||||
(element) => element.eventId == replyId,
|
listController.value.animateToItem(
|
||||||
),
|
index: value.indexWhere(
|
||||||
scrollController: scrollController,
|
(element) =>
|
||||||
alignment: 0.5,
|
element.eventId == replyId,
|
||||||
duration: (_) => .new(milliseconds: 700),
|
),
|
||||||
curve: (_) => Curves.easeInOut,
|
scrollController: scrollController,
|
||||||
);
|
alignment: 0.5,
|
||||||
highlightedEvent.value = replyId;
|
duration: (_) =>
|
||||||
await Future.delayed(.new(seconds: 1), () {
|
.new(milliseconds: 700),
|
||||||
if (highlightedEvent.value == replyId) {
|
curve: (_) => Curves.easeInOut,
|
||||||
highlightedEvent.value = null;
|
);
|
||||||
}
|
highlightedEvent.value = replyId;
|
||||||
});
|
await Future.delayed(
|
||||||
},
|
.new(seconds: 1),
|
||||||
getEventOptions: getEventOptions,
|
() {
|
||||||
isGrouped:
|
if (highlightedEvent.value ==
|
||||||
previousEvent?.content
|
replyId) {
|
||||||
is MessageContent &&
|
highlightedEvent.value = null;
|
||||||
previousEvent?.redactedBy == null &&
|
}
|
||||||
previousEvent?.relationType !=
|
},
|
||||||
"m.replace" &&
|
);
|
||||||
"${event.sender}${event.pmp?.id}" ==
|
},
|
||||||
"${previousEvent?.sender}${previousEvent?.pmp?.id}",
|
getEventOptions: getEventOptions,
|
||||||
),
|
isGrouped:
|
||||||
isHighlighted:
|
previousEvent?.content
|
||||||
highlightedEvent.value == event.eventId,
|
is MessageContent &&
|
||||||
);
|
previousEvent?.redactedBy == null &&
|
||||||
},
|
previousEvent?.relationType !=
|
||||||
),
|
"m.replace" &&
|
||||||
|
"${event.sender}${event.pmp?.id}" ==
|
||||||
|
"${previousEvent?.sender}${previousEvent?.pmp?.id}",
|
||||||
|
),
|
||||||
|
isHighlighted:
|
||||||
|
highlightedEvent.value == event.eventId,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: .only(bottom: composerSize.value),
|
padding: .only(bottom: composerSize.value),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
AsyncData() => nothing,
|
||||||
|
AsyncLoading() => Loading(),
|
||||||
|
AsyncError(:final error, :final stackTrace) =>
|
||||||
|
ErrorDialog(error, stackTrace),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
AsyncData() => nothing,
|
|
||||||
AsyncLoading() => Loading(),
|
|
||||||
AsyncError(:final error, :final stackTrace) =>
|
|
||||||
ErrorDialog(error, stackTrace),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
child: MeasureSize(
|
|
||||||
onChange: (size) => composerSize.value = size.height,
|
|
||||||
child: Composer(
|
|
||||||
roomId,
|
|
||||||
node: composerNode,
|
|
||||||
onSend: (text, {required shouldMention, required tags}) =>
|
|
||||||
notifier
|
|
||||||
.send(
|
|
||||||
text,
|
|
||||||
tags: tags,
|
|
||||||
relationType: relationType.value,
|
|
||||||
shouldMention: shouldMention,
|
|
||||||
relation: relatedEvent.value,
|
|
||||||
)
|
|
||||||
.onError(showError),
|
|
||||||
relationType: relationType.value,
|
|
||||||
relatedEvent: relatedEvent.value,
|
|
||||||
onDismiss: () => relatedEvent.value = null,
|
|
||||||
),
|
),
|
||||||
),
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: MeasureSize(
|
||||||
|
onChange: (size) => composerSize.value = size.height,
|
||||||
|
child: Composer(
|
||||||
|
roomId,
|
||||||
|
node: composerNode,
|
||||||
|
onSend:
|
||||||
|
(text, {required shouldMention, required tags}) =>
|
||||||
|
notifier
|
||||||
|
.send(
|
||||||
|
text,
|
||||||
|
tags: tags,
|
||||||
|
relationType: relationType.value,
|
||||||
|
shouldMention: shouldMention,
|
||||||
|
relation: relatedEvent.value,
|
||||||
|
)
|
||||||
|
.onError(showError),
|
||||||
|
relationType: relationType.value,
|
||||||
|
relatedEvent: relatedEvent.value,
|
||||||
|
onDismiss: () => relatedEvent.value = null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
if (memberListOpened.value == true && showMembersByDefault)
|
if (memberListOpened.value == true && showMembersByDefault)
|
||||||
|
Henry-Hiles marked this conversation as resolved
Outdated
Henry-Hiles
commented
This isn't a good way to do this, the double scaffold is better. I will push a fix. This isn't a good way to do this, the double scaffold is better. I will push a fix.
Henry-Hiles
commented
Done. Done.
|
|||||||
MemberList(roomId),
|
MemberList(roomId),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
endDrawer: showMembersByDefault ? null : MemberList(roomId),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
I think I'd prefer a
removePinandaddPin, separate methods. And of course this needs to be called from the context menu. The context menu should have an option of "Pin" if the event is not pinned (not present in pinned events controller), and you have permission to pin, or "Unpin" if the event IS pinned, and you have permission to pin.Yeah, makes sense, seperation of concern is a useful standard. This would make it cleaner.