Remove flutter chat #26

Manually merged
Henry-Hiles merged 108 commits from remove-flutter-chat into main 2026-05-22 15:26:28 -04:00
19 changed files with 153 additions and 158 deletions
Showing only changes of commit df5040e06c - Show all commits

remove selected room/space controllers

Henry Hiles 2026-05-20 12:33:14 -04:00
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs

View file

@ -12,14 +12,14 @@ class KeyController extends Notifier<String?> {
String? build() =>
ref.watch(SharedPrefsController.provider).requireValue.getString(key);
Future<void> set(String? id) async {
Future<void> set(String? value) async {
final prefs = ref.watch(SharedPrefsController.provider).requireValue;
state = id;
state = value;
if (id == null) {
if (value == null) {
prefs.remove(key);
} else {
prefs.setString(key, id);
prefs.setString(key, value);
}
}

View file

@ -1,21 +1,21 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/members_controller.dart";
import "package:nexus/models/configs/members_by_status_config.dart";
import "package:nexus/models/content/membership.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/membership_status.dart";
class MembersByTypeController extends AsyncNotifier<IList<Event>> {
final MembershipStatus filterStatus;
MembersByTypeController(this.filterStatus);
class MembersByStatusController extends AsyncNotifier<IList<Event>> {
final MembersByStatusConfig config;
MembersByStatusController(this.config);
@override
Future<IList<Event>> build() => ref.watch(
MembersController.provider.selectAsync(
MembersController.provider(config.roomId).selectAsync(
(members) => members
.where(
(membership) => switch (membership.content) {
MembershipContent(:final status) => filterStatus == status,
MembershipContent(:final status) => config.status == status,
_ => false,
},
)
@ -25,8 +25,8 @@ class MembersByTypeController extends AsyncNotifier<IList<Event>> {
static final provider =
AsyncNotifierProvider.family<
MembersByTypeController,
MembersByStatusController,
IList<Event>,
MembershipStatus
>(MembersByTypeController.new);
MembersByStatusConfig
>(MembersByStatusController.new);
}

View file

@ -3,48 +3,39 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/models/content/content.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/requests/get_room_state_request.dart";
class MembersController extends AsyncNotifier<IList<Event>> {
final String roomId;
MembersController(this.roomId);
@override
Future<IList<Event>> build() async {
final data = ref.watch(
SelectedRoomController.provider.select(
(value) => value?.metadata == null
? null
: (
value!.metadata!.id,
value.metadata!.hasMemberList,
value.hasFetchedMembers,
),
),
final room = ref.watch(
RoomsController.provider.select((value) => value[roomId]),
);
if (data == null) return const IList.empty();
if (!data.$3) {
if (room == null) return const IList.empty();
if (!room.hasFetchedMembers) {
final fetchedState = await ref
.watch(ClientController.provider.notifier)
.getRoomState(
GetRoomStateRequest(
roomId: data.$1,
fetchMembers: data.$2 == false,
roomId: roomId,
fetchMembers: room.metadata?.hasMemberList ?? true,
includeMembers: true,
),
);
ref
.read(RoomsController.provider.notifier)
.addState(data.$1, fetchedState, isMembers: true);
.addState(roomId, fetchedState, isMembers: true);
}
final room = ref.watch(
RoomsController.provider.select((value) => value[data.$1]),
);
return room?.state[EventType.membership.type]?.values
return room.state[EventType.membership.type]?.values
.map(
(rowId) =>
room.events.firstWhereOrNull((event) => event.rowId == rowId),
@ -54,8 +45,6 @@ class MembersController extends AsyncNotifier<IList<Event>> {
const IList.empty();
}
static final provider =
AsyncNotifierProvider.autoDispose<MembersController, IList<Event>>(
MembersController.new,
);
static final provider = AsyncNotifierProvider.autoDispose
.family<MembersController, IList<Event>, String>(MembersController.new);
}

View file

@ -1,7 +1,7 @@
import "package:collection/collection.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/models/configs/power_level_config.dart";
import "package:nexus/models/content/content.dart";
import "package:nexus/models/content/power_levels.dart";
@ -20,7 +20,10 @@ class PowerLevelController extends Notifier<bool> {
);
}
final room = ref.watch(SelectedRoomController.provider);
final room = ref.watch(
RoomsController.provider.select((value) => value[config.roomId]),
);
final event = room?.events.firstWhereOrNull(
(event) => event.rowId == room.state[EventType.powerLevels.type]?[""],
);

View file

@ -12,6 +12,8 @@ class ProfileController extends AsyncNotifier<Profile> {
return client.getProfile(userId);
}
static final provider = AsyncNotifierProvider.autoDispose
.family<ProfileController, Profile, String>(ProfileController.new);
static final provider =
AsyncNotifierProvider.family<ProfileController, Profile, String>(
ProfileController.new,
);
}

View file

@ -1,24 +0,0 @@
import "package:collection/collection.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/key_controller.dart";
import "package:nexus/controllers/selected_space_controller.dart";
import "package:nexus/models/room.dart";
class SelectedRoomController extends Notifier<Room?> {
@override
Room? build() {
final space = ref.watch(SelectedSpaceController.provider);
final selectedRoomId = ref.watch(
KeyController.provider(KeyController.roomKey),
);
return space.children.firstWhereOrNull(
(room) => room.metadata?.id == selectedRoomId,
) ??
space.children.firstOrNull;
}
static final provider = NotifierProvider<SelectedRoomController, Room?>(
SelectedRoomController.new,
);
}

View file

@ -1,22 +0,0 @@
import "package:collection/collection.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/key_controller.dart";
import "package:nexus/controllers/spaces_controller.dart";
import "package:nexus/models/space.dart";
class SelectedSpaceController extends Notifier<Space> {
@override
Space build() {
final spaces = ref.watch(SpacesController.provider);
final selectedSpaceId = ref.watch(
KeyController.provider(KeyController.spaceKey),
);
return spaces.firstWhereOrNull((space) => space.id == selectedSpaceId) ??
spaces.first;
}
static final provider = NotifierProvider<SelectedSpaceController, Space>(
SelectedSpaceController.new,
);
}

View file

@ -13,18 +13,6 @@ class UserController extends AsyncNotifier<MembershipContent> {
@override
Future<MembershipContent> build() async {
final member = await ref.watch(
MembersController.provider.selectAsync(
(value) => value.firstWhereOrNull(
(membership) => membership.stateKey == userId,
),
),
);
if (member?.content case final MembershipContent content) {
return content;
}
final profile = await ref.watch(ProfileController.provider(userId).future);
return MembershipContent(
status: MembershipStatus.leave,

View file

@ -0,0 +1,15 @@
import "package:freezed_annotation/freezed_annotation.dart";
import "package:nexus/models/membership_status.dart";
part "members_by_status_config.freezed.dart";
part "members_by_status_config.g.dart";
@freezed
abstract class MembersByStatusConfig with _$MembersByStatusConfig {
const factory MembersByStatusConfig({
required String roomId,
required MembershipStatus status,
}) = _MembersByStatusConfig;
factory MembersByStatusConfig.fromJson(Map<String, Object?> json) =>
_$MembersByStatusConfigFromJson(json);
}

View file

@ -5,17 +5,24 @@ part "power_level_config.freezed.dart";
@freezed
sealed class PowerLevelConfig with _$PowerLevelConfig {
const factory PowerLevelConfig({required EventType eventType}) =
EventPowerLevelConfig;
const factory PowerLevelConfig({
required EventType eventType,
required String roomId,
}) = EventPowerLevelConfig;
const factory PowerLevelConfig.membershipAction({
required MembershipAction action,
required String targetUser,
required String roomId,
}) = MembershipActionPowerLevelConfig;
const factory PowerLevelConfig.state({required EventType eventType}) =
StatePowerLevelConfig;
const factory PowerLevelConfig.state({
required EventType eventType,
required String roomId,
}) = StatePowerLevelConfig;
const factory PowerLevelConfig.redaction({required String targetUser}) =
RedactionPowerLevelConfig;
const factory PowerLevelConfig.redaction({
required String targetUser,
required String roomId,
}) = RedactionPowerLevelConfig;
}

View file

@ -1,6 +1,7 @@
import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/init_complete_controller.dart";
import "package:nexus/controllers/key_controller.dart";
import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/sidebar.dart";
import "package:nexus/widgets/room_chat.dart";
@ -15,22 +16,22 @@ class ChatPage extends ConsumerWidget {
final isDesktop = constraints.maxWidth > 650;
final showMembersByDefault = constraints.maxWidth > 1000;
final initComplete = ref.watch(InitCompleteController.provider);
final roomId = ref.watch(KeyController.provider(KeyController.roomKey));
return Scaffold(
appBar: initComplete ? null : Appbar(),
body: initComplete
? Builder(
builder: (context) => Row(
? Row(
children: [
if (isDesktop) Sidebar(isDesktop: isDesktop),
Expanded(
child: RoomChat(
roomId: roomId,
isDesktop: isDesktop,
showMembersByDefault: showMembersByDefault,
),
),
],
),
)
: Center(
child: Column(

View file

@ -13,6 +13,7 @@ import "package:nexus/widgets/composer/relation_preview.dart";
import "package:nexus/widgets/emoji_picker_button.dart";
class ChatBox extends HookConsumerWidget {
final String roomId;
final Event? relatedEvent;
final RelationType relationType;
final VoidCallback onDismiss;
@ -23,7 +24,8 @@ class ChatBox extends HookConsumerWidget {
required IList<Tag> tags,
})
onSend;
const ChatBox({
const ChatBox(
this.roomId, {
required this.relatedEvent,
required this.relationType,
required this.onDismiss,
@ -88,7 +90,10 @@ class ChatBox extends HookConsumerWidget {
children:
ref.watch(
PowerLevelController.provider(
PowerLevelConfig(eventType: EventType.message),
PowerLevelConfig(
eventType: EventType.message,
roomId: roomId,
),
),
)
? [
@ -125,6 +130,7 @@ class ChatBox extends HookConsumerWidget {
child: FlutterTagger(
triggerStrategy: TriggerStrategy.eager,
overlay: MentionOverlay(
roomId,
query: query.value,
triggerCharacter: triggerCharacter.value,
addTag: ({required id, required name}) {

View file

@ -1,10 +1,11 @@
import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/members_by_type_controller.dart";
import "package:nexus/controllers/members_by_status_controller.dart";
import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/controllers/via_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/helpers/extensions/get_localpart.dart";
import "package:nexus/models/configs/members_by_status_config.dart";
import "package:nexus/models/content/membership.dart";
import "package:nexus/models/membership_status.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
@ -13,8 +14,10 @@ import "package:nexus/widgets/loading.dart";
class MentionOverlay extends ConsumerWidget {
final String? triggerCharacter;
final String query;
final String roomId;
final void Function({required String id, required String name}) addTag;
const MentionOverlay({
const MentionOverlay(
this.roomId, {
required this.query,
required this.addTag,
required this.triggerCharacter,
@ -36,7 +39,12 @@ class MentionOverlay extends ConsumerWidget {
"@" =>
ref
.watch(
MembersByTypeController.provider(MembershipStatus.join),
MembersByStatusController.provider(
MembersByStatusConfig(
roomId: roomId,
status: MembershipStatus.join,
),
),
)
.betterWhen(
data: (members) => ListView(

View file

@ -1,22 +1,26 @@
import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/members_by_type_controller.dart";
import "package:nexus/controllers/members_by_status_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/helpers/extensions/get_localpart.dart";
import "package:nexus/helpers/extensions/show_user_popover.dart";
import "package:nexus/models/configs/members_by_status_config.dart";
import "package:nexus/models/content/membership.dart";
import "package:nexus/models/membership_status.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
class MemberList extends HookConsumerWidget {
const MemberList({super.key});
final String roomId;
const MemberList(this.roomId, {super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final status = useState(MembershipStatus.join);
final membersProvider = ref.watch(
MembersByTypeController.provider(status.value),
MembersByStatusController.provider(
MembersByStatusConfig(roomId: roomId, status: status.value),
),
);
return Drawer(

View file

@ -2,7 +2,7 @@ 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/client_state_controller.dart";
import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/helpers/extensions/mxc_to_https.dart";
import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
@ -13,7 +13,9 @@ class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget {
final bool isDesktop;
final void Function(BuildContext context)? onOpenMemberList;
final void Function(BuildContext context) onOpenDrawer;
final String? roomId;
const RoomAppbar({
required this.roomId,
required this.isDesktop,
required this.onOpenDrawer,
this.onOpenMemberList,
@ -25,7 +27,9 @@ class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final room = ref.watch(SelectedRoomController.provider);
final room = roomId == null
? null
: ref.watch(RoomsController.provider.select((value) => value[roomId!]));
return Appbar(
leading: isDesktop
? room == null

View file

@ -8,7 +8,6 @@ import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/controllers/power_level_controller.dart";
import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/controllers/room_chat_controller.dart";
import "package:nexus/controllers/via_controller.dart";
import "package:nexus/models/configs/power_level_config.dart";
@ -32,7 +31,9 @@ import "package:super_sliver_list/super_sliver_list.dart";
class RoomChat extends HookConsumerWidget {
final bool isDesktop;
final bool showMembersByDefault;
final String? roomId;
const RoomChat({
required this.roomId,
required this.isDesktop,
required this.showMembersByDefault,
super.key,
@ -47,15 +48,12 @@ class RoomChat extends HookConsumerWidget {
final memberListOpened = useState<bool>(showMembersByDefault);
final userId = ref.watch(ClientStateController.provider)?.userId;
final roomId = ref.watch(
SelectedRoomController.provider.select((value) => value?.metadata?.id),
);
final theme = Theme.of(context);
if (roomId == null || userId == null) {
if (userId == null || this.roomId == null) {
return Scaffold(
appBar: RoomAppbar(
roomId: this.roomId,
isDesktop: isDesktop,
onOpenDrawer: (_) => Scaffold.of(context).openDrawer(),
onOpenMemberList: null,
@ -69,6 +67,8 @@ class RoomChat extends HookConsumerWidget {
);
}
final roomId = this.roomId!;
final controllerProvider = RoomChatController.provider(roomId);
final notifier = ref.watch(controllerProvider.notifier);
@ -76,10 +76,13 @@ class RoomChat extends HookConsumerWidget {
final listController = useRef(ListController());
final scrollController = useScrollController();
scrollController.addListener(() async {
useEffect(() {
Future<void> listener() async {
if (!scrollController.position.atEdge) return;
if (scrollController.position.pixels == 0) {
context.mounted;
final room = ref.watch(
RoomsController.provider.select((value) => value[roomId]),
);
@ -87,7 +90,11 @@ class RoomChat extends HookConsumerWidget {
} else {
await notifier.loadOlder();
}
});
}
scrollController.addListener(listener);
return () => scrollController.removeListener(listener);
}, [roomId]);
final composerNode = useFocusNode(
onKeyEvent: (_, event) {
@ -107,7 +114,7 @@ class RoomChat extends HookConsumerWidget {
return [
if (ref.watch(
PowerLevelController.provider(
PowerLevelConfig(eventType: EventType.reaction),
PowerLevelConfig(eventType: EventType.reaction, roomId: roomId),
),
))
PopupMenuItem(
@ -156,7 +163,7 @@ class RoomChat extends HookConsumerWidget {
),
if (ref.watch(
PowerLevelController.provider(
PowerLevelConfig(eventType: EventType.message),
PowerLevelConfig(eventType: EventType.message, roomId: roomId),
),
))
PopupMenuItem(
@ -178,7 +185,9 @@ class RoomChat extends HookConsumerWidget {
),
PopupMenuItem(
onTap: () async {
final room = ref.watch(SelectedRoomController.provider);
final room = ref.watch(
RoomsController.provider.select((value) => value[roomId]),
);
if (room == null) return;
final vias = ref.watch(ViaController.provider(room));
@ -194,7 +203,10 @@ class RoomChat extends HookConsumerWidget {
),
if (ref.watch(
PowerLevelController.provider(
PowerLevelConfig.redaction(targetUser: event.sender),
PowerLevelConfig.redaction(
targetUser: event.sender,
roomId: roomId,
),
),
))
PopupMenuItem(
@ -308,6 +320,7 @@ class RoomChat extends HookConsumerWidget {
return Scaffold(
appBar: RoomAppbar(
roomId: roomId,
isDesktop: isDesktop,
onOpenDrawer: (_) => Scaffold.of(context).openDrawer(),
onOpenMemberList: (thisContext) {
@ -387,6 +400,7 @@ class RoomChat extends HookConsumerWidget {
),
),
ChatBox(
roomId,
node: composerNode,
onSend: (text, {required shouldMention, required tags}) =>
notifier
@ -407,11 +421,11 @@ class RoomChat extends HookConsumerWidget {
),
if (memberListOpened.value == true && showMembersByDefault)
MemberList(),
MemberList(roomId),
],
),
endDrawer: showMembersByDefault ? null : MemberList(),
endDrawer: showMembersByDefault ? null : MemberList(roomId),
);
}
}

View file

@ -1,7 +1,7 @@
import "package:collection/collection.dart";
import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/key_controller.dart";
import "package:nexus/controllers/selected_space_controller.dart";
import "package:nexus/controllers/spaces_controller.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/join_dialog.dart";
@ -31,7 +31,9 @@ class Sidebar extends HookConsumerWidget {
);
final selectedIndex = indexOfSelected == -1 ? 0 : indexOfSelected;
final selectedSpace = ref.watch(SelectedSpaceController.provider);
final selectedSpace =
spaces.firstWhereOrNull((space) => space.id == selectedSpaceId) ??
spaces.first;
final indexOfSelectedRoom = selectedSpace.children.indexWhere(
(room) => room.metadata?.id == selectedRoomId,

View file

@ -6,7 +6,6 @@ import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/controllers/power_level_controller.dart";
import "package:nexus/controllers/profile_controller.dart";
import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/helpers/extensions/get_localpart.dart";
import "package:nexus/helpers/extensions/mxc_to_https.dart";
@ -23,16 +22,14 @@ import "package:nexus/widgets/form_text_input.dart";
class UserPopover extends ConsumerWidget {
final MembershipContent member;
final String userId;
const UserPopover(this.member, this.userId, {super.key});
final String? roomId;
const UserPopover(this.member, this.userId, {this.roomId, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final client = ref.watch(ClientController.provider.notifier);
final roomId = ref.watch(
SelectedRoomController.provider.select((room) => room?.metadata?.id),
);
void showMembershipDialog(MembershipAction action) => showDialog(
context: context,
@ -164,6 +161,7 @@ class UserPopover extends ConsumerWidget {
PowerLevelController.provider(
PowerLevelConfig.membershipAction(
action: MembershipAction.kick,
roomId: roomId!,
targetUser: userId,
),
),
@ -185,6 +183,7 @@ class UserPopover extends ConsumerWidget {
if (ref.watch(
PowerLevelController.provider(
PowerLevelConfig.membershipAction(
roomId: roomId!,
action: MembershipAction.ban,
targetUser: userId,
),

View file

@ -5,7 +5,6 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/controllers/cross_cache_controller.dart";
import "package:nexus/controllers/room_chat_controller.dart";
import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/helpers/extensions/get_headers.dart";
import "package:nexus/helpers/extensions/mxc_to_https.dart";
import "package:nexus/main.dart";