treewide sweepfixes

This commit is contained in:
Henry Hiles 2026-10-14 17:01:39 -04:00
commit 012ecd9249
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
41 changed files with 270 additions and 386 deletions

View file

@ -23,7 +23,7 @@ class AttachmentController(final String roomId)
); );
final content = await ref final content = await ref
.watch(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.uploadMedia(.new(path: file.path, encrypt: isEncrypted)); .uploadMedia(.new(path: file.path, encrypt: isEncrypted));
state = (filename, content); state = (filename, content);

View file

@ -8,7 +8,7 @@ class AuthUrlController(final Uri homeserver)
extends AsyncNotifier<OAuthAuthCodeResponse> { extends AsyncNotifier<OAuthAuthCodeResponse> {
@override @override
Future<OAuthAuthCodeResponse> build() async => ref Future<OAuthAuthCodeResponse> build() async => ref
.watch(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.getAuthUrl( .getAuthUrl(
.new( .new(
homeserverUrl: homeserver, homeserverUrl: homeserver,

View file

@ -4,7 +4,7 @@ import "package:nexus/controllers/client.dart";
class ClientIdController(final Uri homeserver) extends AsyncNotifier<String> { class ClientIdController(final Uri homeserver) extends AsyncNotifier<String> {
@override @override
Future<String> build() => ref Future<String> build() => ref
.watch(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.registerClient( .registerClient(
.new( .new(
clientName: "Nexus", clientName: "Nexus",

View file

@ -19,13 +19,13 @@ class EventController(final GetEventRequest request)
if (event != null) return event; if (event != null) return event;
final fetchedEvent = await ref final fetchedEvent = await ref
.watch(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.getEvent(request) .getEvent(request)
.onError((_, _) => null); .onError((_, _) => null);
if (fetchedEvent != null) { if (fetchedEvent != null) {
ref ref
.watch(RoomsController.provider.notifier) .read(RoomsController.provider.notifier)
.update( .update(
.new({ .new({
request.roomId: .new( request.roomId: .new(

View file

@ -40,28 +40,23 @@ class GomuksListenerController extends AsyncNotifier<void> {
switch (muksEventType) { switch (muksEventType) {
case "client_state": case "client_state":
ref ref
.watch(ClientStateController.provider.notifier) .read(ClientStateController.provider.notifier)
.set(.fromJson(decodedMuksEvent)); .set(.fromJson(decodedMuksEvent));
break;
case "sync_status": case "sync_status":
ref ref
.watch(SyncStatusController.provider.notifier) .read(SyncStatusController.provider.notifier)
.set(.fromJson(decodedMuksEvent)); .set(.fromJson(decodedMuksEvent));
break;
case "init_complete": case "init_complete":
ref.watch(InitCompleteController.provider.notifier).complete(); ref.read(InitCompleteController.provider.notifier).complete();
break;
case "send_complete": case "send_complete":
final event = Event.fromJson(decodedMuksEvent["event"]); final event = Event.fromJson(decodedMuksEvent["event"]);
ref ref
.watch(RoomsController.provider.notifier) .read(RoomsController.provider.notifier)
.update( .update(
.new({ .new({
event.roomId: .new(events: .new({event.rowId: event})), event.roomId: .new(events: .new({event.rowId: event})),
}), }),
); );
break;
case "sync_complete": case "sync_complete":
final syncData = SyncData.fromJson(decodedMuksEvent); final syncData = SyncData.fromJson(decodedMuksEvent);
final roomProvider = RoomsController.provider; final roomProvider = RoomsController.provider;
@ -73,28 +68,27 @@ class GomuksListenerController extends AsyncNotifier<void> {
} }
ref ref
.watch(roomProvider.notifier) .read(roomProvider.notifier)
.update(syncData.rooms, syncData.leftRooms); .update(syncData.rooms, syncData.leftRooms);
ref ref
.watch(accountDataProvider.notifier) .read(accountDataProvider.notifier)
.update(syncData.accountData); .update(syncData.accountData);
if (syncData.topLevelSpaces != null) { if (syncData.topLevelSpaces case final topLevelSpaces?) {
ref ref
.watch(TopLevelSpacesController.provider.notifier) .read(TopLevelSpacesController.provider.notifier)
.set(syncData.topLevelSpaces!); .set(topLevelSpaces);
} }
if (syncData.spaceEdges != null) { if (syncData.spaceEdges case final spaceEdges?) {
ref ref
.watch(SpaceEdgesController.provider.notifier) .read(SpaceEdgesController.provider.notifier)
.set(syncData.spaceEdges!); .set(spaceEdges);
} }
// ref // ref
// .watch(SyncStatusController.provider.notifier) // .read(SyncStatusController.provider.notifier)
// .set(SyncStatus.fromJson(decodedMuksEvent)); // .set(SyncStatus.fromJson(decodedMuksEvent));
break;
default: default:
debugPrint("Unhandled event: $muksEventType"); debugPrint("Unhandled event: $muksEventType");
} }

View file

@ -11,7 +11,7 @@ class KeyController(final String key) extends AsyncNotifier<String?> {
ref.watch(SharedPrefsController.provider).getString(key); ref.watch(SharedPrefsController.provider).getString(key);
Future<void> set(String? value) async { Future<void> set(String? value) async {
final prefs = ref.watch(SharedPrefsController.provider); final prefs = ref.read(SharedPrefsController.provider);
state = .data(value); state = .data(value);
if (value == null) { if (value == null) {

View file

@ -10,7 +10,7 @@ class MemberListOpenedController extends AsyncNotifier<bool> {
Future<void> set(bool value) async { Future<void> set(bool value) async {
state = .data(value); state = .data(value);
await ref.watch(SharedPrefsController.provider).setBool(key, value); await ref.read(SharedPrefsController.provider).setBool(key, value);
} }
static final provider = static final provider =

View file

@ -18,7 +18,7 @@ class MembersController(final String roomId)
if (!room.hasFetchedMembers) { if (!room.hasFetchedMembers) {
final fetchedState = await ref final fetchedState = await ref
.watch(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.getRoomState( .getRoomState(
GetRoomStateRequest( GetRoomStateRequest(
roomId: roomId, roomId: roomId,

View file

@ -55,7 +55,7 @@ class NotificationsController(final NotificationsRequest request)
final lastNotification = currentNotifications.lastOrNull; final lastNotification = currentNotifications.lastOrNull;
final lastTs = lastNotification == null final lastTs = lastNotification == null
? null ? null
: ref.watch( : ref.read(
RoomsController.provider.select( RoomsController.provider.select(
(rooms) => rooms[lastNotification.$2] (rooms) => rooms[lastNotification.$2]
?.events[lastNotification.$1] ?.events[lastNotification.$1]

View file

@ -13,8 +13,8 @@ class PushKeyController(final String instance) extends AsyncNotifier<String?> {
Future<void> set(String? value) async { Future<void> set(String? value) async {
final provider = KeyController.provider(KeyController.pushKeyKey); final provider = KeyController.provider(KeyController.pushKeyKey);
final notifier = ref.watch(provider.notifier); final notifier = ref.read(provider.notifier);
final prefs = IMap(json.decode(await ref.watch(provider.future) ?? "{}")); final prefs = IMap(json.decode(await ref.read(provider.future) ?? "{}"));
state = .data(value); state = .data(value);
notifier.set( notifier.set(

View file

@ -18,7 +18,7 @@ class ReactionsController(final ReactionsConfig config)
final reactionEvents = eventInfo?.$2.isNotEmpty == true final reactionEvents = eventInfo?.$2.isNotEmpty == true
? await ref ? await ref
.watch(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.getRelatedEvents( .getRelatedEvents(
.new( .new(
roomId: config.roomId, roomId: config.roomId,

View file

@ -27,7 +27,7 @@ class RecentEmojiController extends Notifier<IList<RecentEmoji>> {
} }
Future<void> add(String emoji) => ref Future<void> add(String emoji) => ref
.watch(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.setAccountData( .setAccountData(
.new( .new(
type: AccountData.recentEmojiKey, type: AccountData.recentEmojiKey,

View file

@ -88,7 +88,7 @@ class RoomChatController(final ChatParams info)
} }
Future<void> deleteMessage(Event event, {String? reason}) => ref Future<void> deleteMessage(Event event, {String? reason}) => ref
.watch(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.redactEvent( .redactEvent(
RedactEventRequest( RedactEventRequest(
eventId: event.eventId, eventId: event.eventId,
@ -146,7 +146,7 @@ class RoomChatController(final ChatParams info)
); );
if (response.events.isEmpty) { if (response.events.isEmpty) {
state = .data(state.value); state = .data(chat);
} }
ref ref
@ -234,7 +234,7 @@ class RoomChatController(final ChatParams info)
Content? baseContent; Content? baseContent;
final relatedEvent = relatedEventRowId == null final relatedEvent = relatedEventRowId == null
? null ? null
: ref.watch( : ref.read(
RoomsController.provider.select( RoomsController.provider.select(
(rooms) => rooms[info.roomId]?.events[relatedEventRowId], (rooms) => rooms[info.roomId]?.events[relatedEventRowId],
), ),
@ -267,9 +267,7 @@ class RoomChatController(final ChatParams info)
baseContent: baseContent, baseContent: baseContent,
mentions: Mentions( mentions: Mentions(
userIds: [ userIds: [
if (shouldMention == true && if (shouldMention && relatedEvent != null && relationType == .reply)
relatedEvent != null &&
relationType == RelationType.reply)
relatedEvent.sender, relatedEvent.sender,
].toIList(), ].toIList(),
room: taggedMessage.contains("@room"), room: taggedMessage.contains("@room"),
@ -307,24 +305,20 @@ class RoomChatController(final ChatParams info)
), ),
); );
final reactionEvents = allReactionEvents final reactionEvent = allReactionEvents?.firstWhereOrNull(
?.where((event) => event.redactedBy == null) (event) =>
.toIList(); event.redactedBy == null &&
switch (event.content) {
final reactionEvent = reactionEvents?.firstWhereOrNull( ReactionContent(:final key) =>
(event) => switch (event.content) { key == reaction && event.sender == userId,
ReactionContent(:final key) => _ => false,
key == reaction && event.sender == userId, },
_ => false,
},
); );
if (reactionEvent != null) { if (reactionEvent != null) {
await ref await client.redactEvent(
.watch(ClientController.provider.notifier) .new(eventId: reactionEvent.eventId, roomId: info.roomId),
.redactEvent( );
.new(eventId: reactionEvent.eventId, roomId: info.roomId),
);
} }
} }

View file

@ -48,7 +48,7 @@ class RoomsController extends Notifier<IMap<String, Room>> {
hasMore: incoming.hasMore, hasMore: incoming.hasMore,
clientSticky: incoming.resetClientSticky clientSticky: incoming.resetClientSticky
? incoming.clientSticky ? incoming.clientSticky
: (incoming.clientSticky.isEmpty == true : (incoming.clientSticky.isEmpty
? existing.clientSticky ? existing.clientSticky
: existing.clientSticky.addAll( : existing.clientSticky.addAll(
incoming.clientSticky, incoming.clientSticky,
@ -73,9 +73,9 @@ class RoomsController extends Notifier<IMap<String, Room>> {
incoming.hasFetchedMembers || existing.hasFetchedMembers, incoming.hasFetchedMembers || existing.hasFetchedMembers,
hasFetchedState: hasFetchedState:
incoming.hasFetchedState || existing.hasFetchedState, incoming.hasFetchedState || existing.hasFetchedState,
timeline: (incoming.reset timeline: incoming.reset
? incoming.timeline ? incoming.timeline
: existing.timeline.addAll(incoming.timeline)), : existing.timeline.addAll(incoming.timeline),
receipts: incoming.receipts.entries.fold( receipts: incoming.receipts.entries.fold(
existing.receipts, existing.receipts,
(receiptAcc, event) => receiptAcc.add( (receiptAcc, event) => receiptAcc.add(
@ -88,12 +88,7 @@ class RoomsController extends Notifier<IMap<String, Room>> {
); );
}); });
final prunedList = leftRooms.fold( state = leftRooms.fold(merged, (acc, roomId) => acc.remove(roomId));
merged,
(acc, roomId) => acc.remove(roomId),
);
state = prunedList;
} }
static final provider = NotifierProvider<RoomsController, IMap<String, Room>>( static final provider = NotifierProvider<RoomsController, IMap<String, Room>>(

View file

@ -1,4 +1,5 @@
import "dart:convert"; import "dart:convert";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/settings_file.dart"; import "package:nexus/controllers/settings_file.dart";
import "package:nexus/models/settings.dart"; import "package:nexus/models/settings.dart";
@ -17,7 +18,7 @@ class SettingsController extends AsyncNotifier<Settings> {
Future<void> set(Settings settings) async { Future<void> set(Settings settings) async {
state = .data(settings); state = .data(settings);
final file = await ref.watch(SettingsFileController.provider.future); final file = await ref.read(SettingsFileController.provider.future);
await file.writeAsString(json.encode(settings.toJson())); await file.writeAsString(json.encode(settings.toJson()));
} }

View file

@ -1,4 +1,5 @@
import "dart:io"; import "dart:io";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:path/path.dart"; import "package:path/path.dart";
import "package:path_provider/path_provider.dart"; import "package:path_provider/path_provider.dart";

View file

@ -47,7 +47,7 @@ class SettingsSectionsController
options: ThemeMode.values, options: ThemeMode.values,
getName: (option) => toBeginningOfSentenceCase(option.name), getName: (option) => toBeginningOfSentenceCase(option.name),
onChanged: (value) => ref onChanged: (value) => ref
.watch(SettingsController.provider.notifier) .read(SettingsController.provider.notifier)
.set(settings.copyWith(theme: value)) .set(settings.copyWith(theme: value))
.onError(showError), .onError(showError),
), ),
@ -67,7 +67,7 @@ class SettingsSectionsController
Platform.isMacOS || Platform.isMacOS ||
Platform.isWindows) Platform.isWindows)
? (value) => ref ? (value) => ref
.watch(SettingsController.provider.notifier) .read(SettingsController.provider.notifier)
.set(settings.copyWith(useDynamicTheming: value)) .set(settings.copyWith(useDynamicTheming: value))
.onError(showError) .onError(showError)
: null, : null,
@ -89,7 +89,7 @@ class SettingsSectionsController
secondary: Icon(icon), secondary: Icon(icon),
value: settings.showAllInHome, value: settings.showAllInHome,
onChanged: (value) => ref onChanged: (value) => ref
.watch(SettingsController.provider.notifier) .read(SettingsController.provider.notifier)
.set(settings.copyWith(showAllInHome: value)) .set(settings.copyWith(showAllInHome: value))
.onError(showError), .onError(showError),
), ),
@ -106,7 +106,7 @@ class SettingsSectionsController
secondary: Icon(icon), secondary: Icon(icon),
value: settings.showMinimizeButton, value: settings.showMinimizeButton,
onChanged: (value) => ref onChanged: (value) => ref
.watch(SettingsController.provider.notifier) .read(SettingsController.provider.notifier)
.set(settings.copyWith(showMinimizeButton: value)) .set(settings.copyWith(showMinimizeButton: value))
.onError(showError), .onError(showError),
), ),
@ -122,7 +122,7 @@ class SettingsSectionsController
value: settings.linuxMobileMode, value: settings.linuxMobileMode,
onChanged: Platform.isLinux onChanged: Platform.isLinux
? (value) => ref ? (value) => ref
.watch(SettingsController.provider.notifier) .read(SettingsController.provider.notifier)
.set(settings.copyWith(linuxMobileMode: value)) .set(settings.copyWith(linuxMobileMode: value))
.onError(showError) .onError(showError)
: null, : null,
@ -190,14 +190,14 @@ class SettingsSectionsController
loading.value = true; loading.value = true;
if (value != null) { if (value != null) {
if (await ref if (await ref
.watch( .read(
NotificationController NotificationController
.provider .provider
.notifier, .notifier,
) )
.requestPermissions()) { .requestPermissions()) {
await ref await ref
.watch( .read(
UnifiedPushController UnifiedPushController
.provider .provider
.notifier, .notifier,
@ -208,7 +208,7 @@ class SettingsSectionsController
} }
} else { } else {
await ref await ref
.watch( .read(
UnifiedPushController UnifiedPushController
.provider .provider
.notifier, .notifier,
@ -268,7 +268,7 @@ class SettingsSectionsController
onChanged: specVersionsResponse.maybeWhen( onChanged: specVersionsResponse.maybeWhen(
data: (_) => data: (_) =>
(value) => ref (value) => ref
.watch(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.setAccountData( .setAccountData(
.new( .new(
type: AccountData.invitePermissionConfigKey, type: AccountData.invitePermissionConfigKey,

View file

@ -19,10 +19,10 @@ class SpacesController extends AsyncNotifier<IList<Space>> {
final spaceEdges = ref.watch(SpaceEdgesController.provider); final spaceEdges = ref.watch(SpaceEdgesController.provider);
final accountData = ref.watch(AccountDataController.provider); final accountData = ref.watch(AccountDataController.provider);
final childrenById = { final childrenById = spaceEdges.map(
for (final entry in spaceEdges.entries) (spaceId, edges) =>
entry.key: entry.value.map((e) => e.childId).toList(), MapEntry(spaceId, edges.map((edge) => edge.childId).toList()),
}; );
Set<String> collectDescendants(String startId) { Set<String> collectDescendants(String startId) {
final visited = <String>{}; final visited = <String>{};
@ -78,21 +78,22 @@ class SpacesController extends AsyncNotifier<IList<Space>> {
final spaces = topLevelSpaceIds.map(buildSpace).toIList(); final spaces = topLevelSpaceIds.map(buildSpace).toIList();
final usedRooms = { final usedRooms = spaces
for (final space in spaces) ...[ .expand(
...space.children, (space) => [
...space.subSpaces.expand((s) => s.children), ...space.children,
], ...space.subSpaces.expand((s) => s.children),
}.nonNulls.toISet(); ],
)
.toISet();
final usedRoomIds = usedRooms.map((room) => room.metadata?.id).toSet();
final directMessages = accountData.directMessages.values.flattened; final directMessages = accountData.directMessages.values.flattened.toSet();
final otherRooms = rooms.entries final otherRooms = rooms.entries
.where( .where(
(e) => (e) =>
!usedRooms !usedRoomIds.contains(e.key) &&
.map((element) => element.metadata?.id)
.contains(e.key) &&
!topLevelSpaceIds.contains(e.key) && !topLevelSpaceIds.contains(e.key) &&
!childrenById.containsKey(e.key), !childrenById.containsKey(e.key),
) )

View file

@ -20,7 +20,7 @@ class UnifiedPushAllowedController extends AsyncNotifier<String?> {
} }
final capabilities = await ref final capabilities = await ref
.watch(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.getCapabilities(); .getCapabilities();
if (capabilities.webpush?.vapid == null) { if (capabilities.webpush?.vapid == null) {

View file

@ -11,7 +11,7 @@ class UrlPreviewController(final Uri url)
try { try {
return await ref return await ref
.watch(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.getUrlPreview(url); .getUrlPreview(url);
} catch (error, stackTrace) { } catch (error, stackTrace) {
debugPrintStack(label: error.toString(), stackTrace: stackTrace); debugPrintStack(label: error.toString(), stackTrace: stackTrace);

View file

@ -1,4 +1,5 @@
import "dart:async"; import "dart:async";
import "package:collection/collection.dart"; import "package:collection/collection.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/members.dart"; import "package:nexus/controllers/members.dart";

View file

@ -34,7 +34,7 @@ extension BuildEventOptions on Event {
)).notifier; )).notifier;
final isPinned = ref final isPinned = ref
.watch(PinnedIdsController.provider(roomId)) .read(PinnedIdsController.provider(roomId))
.contains(eventId); .contains(eventId);
void showReasonDialog({ void showReasonDialog({
@ -86,7 +86,7 @@ extension BuildEventOptions on Event {
} }
return .new([ return .new([
if (ref.watch( if (ref.read(
PowerLevelController.provider( PowerLevelController.provider(
.new(eventType: .reaction, roomId: roomId), .new(eventType: .reaction, roomId: roomId),
), ),
@ -98,30 +98,33 @@ extension BuildEventOptions on Event {
child: Row( child: Row(
mainAxisAlignment: .center, mainAxisAlignment: .center,
children: [ children: [
for (final emoji in { ...{
...ref ...ref
.watch(RecentEmojiController.provider) .read(RecentEmojiController.provider)
.map((entry) => entry.emoji), .map((entry) => entry.emoji),
"👍", "👍",
"🤣", "🤣",
"😭", "😭",
"🤔", "🤔",
}.take(3)) }
IconButton( .take(3)
icon: Text(emoji), .map(
onPressed: () async { (emoji) => IconButton(
Navigator.of(context).pop(); icon: Text(emoji),
await ref onPressed: () async {
.watch(notifier) Navigator.of(context).pop();
.sendReaction(emoji, this) await ref
.onError(showError); .read(notifier)
.sendReaction(emoji, this)
.onError(showError);
await ref await ref
.read(RecentEmojiController.provider.notifier) .read(RecentEmojiController.provider.notifier)
.add(emoji) .add(emoji)
.onError(showError); .onError(showError);
}, },
), ),
),
IconButton( IconButton(
icon: const Icon(Icons.emoji_emotions), icon: const Icon(Icons.emoji_emotions),
onPressed: () { onPressed: () {
@ -135,7 +138,7 @@ extension BuildEventOptions on Event {
onSelection: (emoji) { onSelection: (emoji) {
Navigator.of(context).pop(); Navigator.of(context).pop();
ref ref
.watch(notifier) .read(notifier)
.sendReaction(emoji, this) .sendReaction(emoji, this)
.onError(showError); .onError(showError);
}, },
@ -148,7 +151,7 @@ extension BuildEventOptions on Event {
), ),
), ),
if (ref.watch( if (ref.read(
PowerLevelController.provider( PowerLevelController.provider(
.new(eventType: .message, roomId: roomId), .new(eventType: .message, roomId: roomId),
), ),
@ -167,7 +170,7 @@ extension BuildEventOptions on Event {
child: const ListTile(leading: Icon(Icons.edit), title: Text("Edit")), child: const ListTile(leading: Icon(Icons.edit), title: Text("Edit")),
), ),
if (ref.watch( if (ref.read(
PowerLevelController.provider( PowerLevelController.provider(
.state(eventType: .pinnedEvents, roomId: roomId), .state(eventType: .pinnedEvents, roomId: roomId),
), ),
@ -225,7 +228,7 @@ extension BuildEventOptions on Event {
), ),
), ),
if (ref.watch( if (ref.read(
PowerLevelController.provider( PowerLevelController.provider(
.redaction(targetUser: sender, roomId: roomId), .redaction(targetUser: sender, roomId: roomId),
), ),
@ -239,7 +242,7 @@ extension BuildEventOptions on Event {
"This cannot be reversed.", "This cannot be reversed.",
action: "Delete", action: "Delete",
onConfirm: (reason) => onConfirm: (reason) =>
ref.watch(notifier).deleteMessage(this, reason: reason), ref.read(notifier).deleteMessage(this, reason: reason),
), ),
child: ListTile( child: ListTile(
leading: Icon(Icons.delete, color: danger), leading: Icon(Icons.delete, color: danger),
@ -249,7 +252,7 @@ extension BuildEventOptions on Event {
if (eventId.isFake) if (eventId.isFake)
PopupMenuItem( PopupMenuItem(
onTap: () => ref.watch(notifier).hideClientSticky(rowId), onTap: () => ref.read(notifier).hideClientSticky(rowId),
child: ListTile( child: ListTile(
leading: Icon(Icons.delete, color: danger), leading: Icon(Icons.delete, color: danger),
title: Text("Hide", style: .new(color: danger)), title: Text("Hide", style: .new(color: danger)),
@ -260,7 +263,7 @@ extension BuildEventOptions on Event {
onTap: () => showReasonDialog( onTap: () => showReasonDialog(
title: "Report", title: "Report",
description: description:
"Report this this to your server administrators, " "Report this event to your server administrators, "
"who can take action like banning this server or room.", "who can take action like banning this server or room.",
action: "Report", action: "Report",
onConfirm: (reason) => ref onConfirm: (reason) => ref

View file

@ -0,0 +1,23 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:material_ui/material_ui.dart";
import "package:navigation_rail_m3e/navigation_rail_m3e.dart";
import "package:nexus/models/room.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
extension RoomDestinations on IList<Room> {
List<NavigationRailM3EDestination> toDestinations({required bool isDm}) =>
map(
(room) => NavigationRailM3EDestination(
label: room.metadata?.name ?? "Unnamed Room",
badgeCount: switch (room.metadata?.unreadNotifications) {
0 || null => room.metadata?.unreadMessages == 0 ? null : 0,
int unread => unread,
},
icon: AvatarOrHash(
room.metadata?.avatar,
room.metadata?.name ?? "Unnamed Room",
fallback: isDm ? null : const Icon(Icons.numbers),
),
),
).toList();
}

View file

@ -51,7 +51,7 @@ extension ShowAboutDialog on BuildContext {
), ),
M3ECardColumn( M3ECardColumn(
onTap: (index) => onTap: (index) =>
ref.watch(LaunchHelper.provider).launchUrl(switch (index) { ref.read(LaunchHelper.provider).launchUrl(switch (index) {
0 => .https("git.federated.nexus", "nexus/nexus"), 0 => .https("git.federated.nexus", "nexus/nexus"),
_ => .https("liberapay.com", "QuadRadical"), _ => .https("liberapay.com", "QuadRadical"),
}), }),

View file

@ -11,6 +11,7 @@ import "package:fluttertagger/fluttertagger.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/attachment.dart"; import "package:nexus/controllers/attachment.dart";
import "package:nexus/controllers/image_picker.dart"; import "package:nexus/controllers/image_picker.dart";
import "package:image_picker/image_picker.dart";
import "package:nexus/controllers/power_level.dart"; import "package:nexus/controllers/power_level.dart";
import "package:nexus/models/content/message.dart"; import "package:nexus/models/content/message.dart";
import "package:nexus/models/relation_type.dart"; import "package:nexus/models/relation_type.dart";
@ -89,6 +90,14 @@ class const Composer(
final attachment = ref.watch(AttachmentController.provider(roomId)); final attachment = ref.watch(AttachmentController.provider(roomId));
Future<void> attach(Future<XFile?> pick) async {
if (await pick case final file?) {
await ref
.read(AttachmentController.provider(roomId).notifier)
.add(file);
}
}
void send() { void send() {
if (controller.value.text.isEmpty && attachment == null || if (controller.value.text.isEmpty && attachment == null ||
attachment != null && attachment.$2 == null) { attachment != null && attachment.$2 == null) {
@ -173,53 +182,33 @@ class const Composer(
tooltip: "Add media", tooltip: "Add media",
enabled: attachment == null, enabled: attachment == null,
itemBuilder: (context) => [ itemBuilder: (context) => [
if (Platform.isAndroid || Platform.isIOS) for (final (source, title, icon) in [
if (Platform.isAndroid || Platform.isIOS)
(
ImageSource.camera,
"Camera",
Icons.add_a_photo,
),
(
ImageSource.gallery,
"Gallery",
Icons.add_photo_alternate,
),
])
PopupMenuItem( PopupMenuItem(
child: ListTile( child: ListTile(
title: Text("Camera"), title: Text(title),
leading: Icon(Icons.add_a_photo), leading: Icon(icon),
), ),
onTap: () async => ref onTap: () => attach(
.read( ref
AttachmentController.provider(roomId) .read(ImagePickerController.provider)
.notifier, .pickImage(source: source),
) ).onError(showError),
.add(
(await ref
.watch(
ImagePickerController.provider,
)
.pickImage(source: .camera))!,
)
.onError(showError),
), ),
PopupMenuItem( PopupMenuItem(
child: ListTile( onTap: () =>
title: Text("Gallery"), attach(openFile()).onError(showError),
leading: Icon(Icons.add_photo_alternate),
),
onTap: () async => ref
.read(
AttachmentController.provider(roomId)
.notifier,
)
.add(
(await ref
.watch(
ImagePickerController.provider,
)
.pickImage(source: .gallery))!,
)
.onError(showError),
),
PopupMenuItem(
onTap: () async => ref
.watch(
AttachmentController.provider(roomId)
.notifier,
)
.add((await openFile())!)
.onError(showError),
child: ListTile( child: ListTile(
title: Text("Files"), title: Text("Files"),
leading: Icon(Icons.attachment), leading: Icon(Icons.attachment),

View file

@ -26,7 +26,7 @@ class const EmojiPicker({
if (!isFreeText) { if (!isFreeText) {
await ref await ref
.watch(RecentEmojiController.provider.notifier) .read(RecentEmojiController.provider.notifier)
.add(emoji) .add(emoji)
.onError(showError); .onError(showError);
} }

View file

@ -149,6 +149,6 @@ class const Html(
.nonNulls, .nonNulls,
), ),
}, },
onTapUrl: (url) => ref.watch(LaunchHelper.provider).launchUrl(.parse(url)), onTapUrl: (url) => ref.read(LaunchHelper.provider).launchUrl(.parse(url)),
); );
} }

View file

@ -16,7 +16,7 @@ final class const LinkifiedText(
style: style, style: style,
options: .new(humanize: false), options: .new(humanize: false),
onOpen: (link) => onOpen: (link) =>
ref.watch(LaunchHelper.provider).launchUrl(.parse(link.url)), ref.read(LaunchHelper.provider).launchUrl(.parse(link.url)),
linkStyle: .new(color: Theme.of(context).colorScheme.primary), linkStyle: .new(color: Theme.of(context).colorScheme.primary),
overflow: maxLines == null ? null : .ellipsis, overflow: maxLines == null ? null : .ellipsis,
); );

View file

@ -79,9 +79,11 @@ class const MemberList(final String roomId, {super.key})
) )
: Expanded( : Expanded(
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: value.expand((group) {
for (final MapEntry(key: powerLevel, value: members) final MapEntry(key: powerLevel, value: members) =
in value) ...[ group;
return [
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: .symmetric(horizontal: 16), padding: .symmetric(horizontal: 16),
@ -151,8 +153,8 @@ class const MemberList(final String roomId, {super.key})
} }
}, },
), ),
], ];
], }).toList(),
), ),
), ),
AsyncLoading _ => Loading(), AsyncLoading _ => Loading(),

View file

@ -18,7 +18,7 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final launch = ref.watch(LaunchHelper.provider).launchUrl; final launch = ref.read(LaunchHelper.provider).launchUrl;
final isLoading = useState(false); final isLoading = useState(false);
final homeserverUrl = useTextEditingController(); final homeserverUrl = useTextEditingController();
@ -52,7 +52,7 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget {
AuthUrlController.provider(url).future, AuthUrlController.provider(url).future,
); );
await ref.watch(LaunchHelper.provider).launchUrl(codeResponse.url); await ref.read(LaunchHelper.provider).launchUrl(codeResponse.url);
AppLinks().uriLinkStream.listen((encodedUri) async { AppLinks().uriLinkStream.listen((encodedUri) async {
final state = encodedUri.queryParameters["state"]; final state = encodedUri.queryParameters["state"];
@ -64,7 +64,7 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget {
if (code != null) { if (code != null) {
await ref await ref
.watch(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.exchangeToken( .exchangeToken(
.new( .new(
homeserverUrl: url, homeserverUrl: url,
@ -74,7 +74,7 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget {
path: "/", path: "/",
), ),
code: code, code: code,
clientId: await ref.watch( clientId: await ref.read(
ClientIdController.provider(url).future, ClientIdController.provider(url).future,
), ),
), ),

View file

@ -30,7 +30,7 @@ class const SettingsPage({super.key}) extends ConsumerWidget {
barHintText: "Search...", barHintText: "Search...",
isFullScreen: categoriesArePages, isFullScreen: categoriesArePages,
suggestionsBuilder: (suggestionsContext, controller) async { suggestionsBuilder: (suggestionsContext, controller) async {
final categories = await ref.watch( final categories = await ref.read(
SettingsSectionsController.provider.future, SettingsSectionsController.provider.future,
); );
final query = controller.text.toLowerCase(); final query = controller.text.toLowerCase();

View file

@ -23,7 +23,7 @@ class const VerifyPage({super.key}) extends HookConsumerWidget {
} }
inputError.value = await ref inputError.value = await ref
.watch(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.verify(passphraseController.text); .verify(passphraseController.text);
} finally { } finally {
isLoading.value = false; isLoading.value = false;

View file

@ -27,7 +27,7 @@ class const AudioPlayer(
player.platform?.state = player.platform!.state.copyWith(buffering: true); player.platform?.state = player.platform!.state.copyWith(buffering: true);
scheduleMicrotask(() async { scheduleMicrotask(() async {
final video = await ref final video = await ref
.watch(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.downloadMedia(.new(mxc: uri, encrypted: encrypted)); .downloadMedia(.new(mxc: uri, encrypted: encrypted));
await player.open(Media(video.path), play: false); await player.open(Media(video.path), play: false);

View file

@ -25,7 +25,7 @@ class const VideoPlayer(
player.platform?.state = player.platform!.state.copyWith(buffering: true); player.platform?.state = player.platform!.state.copyWith(buffering: true);
scheduleMicrotask(() async { scheduleMicrotask(() async {
final video = await ref final video = await ref
.watch(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.downloadMedia(.new(mxc: uri, encrypted: encrypted)); .downloadMedia(.new(mxc: uri, encrypted: encrypted));
await player.open(Media(video.path), play: false); await player.open(Media(video.path), play: false);
}); });

View file

@ -81,6 +81,15 @@ class const EventRenderer(
event.sender == previousEvent?.sender && event.sender == previousEvent?.sender &&
event.pmp?.id == previousEvent?.pmp?.id; event.pmp?.id == previousEvent?.pmp?.id;
GenericEventRenderer generic(IconData icon, String text) =>
.new(icon, maxLines: maxLines, [
WidgetSpan(
alignment: .middle,
child: MessageDisplayname(event, shouldWrap: maxLines == null),
),
TextSpan(text: text),
]);
final child = event.redactedBy != null || event.relationType == "m.replace" final child = event.redactedBy != null || event.relationType == "m.replace"
? null ? null
: switch (event.content) { : switch (event.content) {
@ -122,169 +131,67 @@ class const EventRenderer(
_ => MembershipRenderer(event, maxLines: maxLines), _ => MembershipRenderer(event, maxLines: maxLines),
}, },
AvatarContent() => GenericEventRenderer( AvatarContent() => generic(
Icons.interests, Icons.interests,
maxLines: maxLines, "changed the room avatar",
[
WidgetSpan(
alignment: .middle,
child: MessageDisplayname(
event,
shouldWrap: maxLines == null,
),
),
TextSpan(text: "changed the room avatar"),
],
), ),
CreateContent() => generic(Icons.add, "created the room"),
CreateContent() => GenericEventRenderer( PowerLevelsContent() => generic(
Icons.add,
maxLines: maxLines,
[
WidgetSpan(
alignment: .middle,
child: MessageDisplayname(
event,
shouldWrap: maxLines == null,
),
),
TextSpan(text: "created the room"),
],
),
PowerLevelsContent() => GenericEventRenderer(
Icons.power, Icons.power,
maxLines: maxLines, "changed the room's power levels",
[
WidgetSpan(
alignment: .middle,
child: MessageDisplayname(
event,
shouldWrap: maxLines == null,
),
),
TextSpan(text: "changed the room's power levels"),
],
), ),
JoinRulesContent() => generic(
JoinRulesContent() => GenericEventRenderer(
Icons.rule, Icons.rule,
maxLines: maxLines, "changed the room's join rules",
[
WidgetSpan(
alignment: .middle,
child: MessageDisplayname(
event,
shouldWrap: maxLines == null,
),
),
TextSpan(text: "changed the room's join rules"),
],
), ),
TopicContent() => generic(
TopicContent() => GenericEventRenderer(
Icons.description, Icons.description,
maxLines: maxLines, "updated the room topic",
[
WidgetSpan(
alignment: .middle,
child: MessageDisplayname(
event,
shouldWrap: maxLines == null,
),
),
TextSpan(text: "updated the room topic"),
],
), ),
HistoryVisibilityContent(:final historyVisibility) => generic(
HistoryVisibilityContent(:final historyVisibility) => Icons.history,
GenericEventRenderer(Icons.history, maxLines: maxLines, [ "changed the room's history visibility to ${switch (historyVisibility) {
WidgetSpan( .invited => "since invited",
alignment: .middle, .joined => "since joined",
child: MessageDisplayname( .shared => "all history visible (shared)",
event, .worldReadable => "all history visible (world readable)",
shouldWrap: maxLines == null, }}",
), ),
), PinnedEventsContent() => generic(
TextSpan(
text:
"changed the room's history visibility to ${switch (historyVisibility) {
.invited => "since invited",
.joined => "since joined",
.shared => "all history visible (shared)",
.worldReadable => "all history visible (world readable)",
}}",
),
]),
PinnedEventsContent() => GenericEventRenderer(
Icons.push_pin, Icons.push_pin,
maxLines: maxLines, "pinned/unpinned some events",
[
WidgetSpan(
alignment: .middle,
child: MessageDisplayname(
event,
shouldWrap: maxLines == null,
),
),
TextSpan(text: "pinned/unpinned some events"),
],
), ),
ServerACLContent() => generic(
ServerACLContent() => GenericEventRenderer(
Icons.list, Icons.list,
maxLines: maxLines, "updated the server ban list",
[
WidgetSpan(
alignment: .middle,
child: MessageDisplayname(
event,
shouldWrap: maxLines == null,
),
),
TextSpan(text: "updated the server ban list"),
],
), ),
CanonicalAliasContent(:final alias, :final altAliases) => generic(
Icons.numbers,
switch ([
if (event.previousContent case CanonicalAliasContent(
alias: final prevAlias,
altAliases: final prevAltAliases,
)) ...[
if (prevAlias != alias)
if (alias == null)
"removed the room's canonical alias"
else
"changed the room's canonical alias to $alias",
CanonicalAliasContent(:final alias, :final altAliases) => if (prevAltAliases
GenericEventRenderer(Icons.numbers, maxLines: maxLines, [ .remove(alias ?? "")
WidgetSpan( .remove(prevAlias ?? "") !=
alignment: .middle, altAliases.remove(alias ?? "").remove(prevAlias ?? ""))
child: MessageDisplayname( "changed the room's aliases",
event, ] else ...[
shouldWrap: maxLines == null, if (alias != null) "set the room's canonical alias",
), if (altAliases.isNotEmpty) "set the room's aliases",
), ],
TextSpan( ]) {
text: switch ([ [] => "did something related to room aliases",
if (event.previousContent case CanonicalAliasContent( List prev => prev.join(" and "),
alias: final prevAlias, },
altAliases: final prevAltAliases, ),
)) ...[
if (prevAlias != alias)
if (alias == null)
"removed the room's canonical alias"
else
"changed the room's canonical alias to $alias",
if (prevAltAliases
.remove(alias ?? "")
.remove(prevAlias ?? "") !=
altAliases
.remove(alias ?? "")
.remove(prevAlias ?? ""))
"changed the room's aliases",
] else ...[
if (alias != null) "set the room's canonical alias",
if (altAliases.isNotEmpty) "set the room's aliases",
],
]) {
[] => "did something related to room aliases",
List prev => prev.join(" and "),
},
),
]),
_ => null, _ => null,
}; };

View file

@ -1,3 +1,4 @@
import "package:collection/collection.dart";
import "package:material_ui/material_ui.dart"; import "package:material_ui/material_ui.dart";
class const GenericEventRenderer( class const GenericEventRenderer(
@ -17,12 +18,14 @@ class const GenericEventRenderer(
Flexible( Flexible(
child: Text.rich( child: Text.rich(
TextSpan( TextSpan(
children: [ children: children
for (final (index, child) in children.indexed) ...[ .expandIndexed(
if (index > 0) TextSpan(text: " "), (index, child) => [
child, if (index > 0) TextSpan(text: " "),
], child,
], ],
)
.toList(),
), ),
maxLines: maxLines ?? 999, maxLines: maxLines ?? 999,
overflow: .ellipsis, overflow: .ellipsis,

View file

@ -271,8 +271,7 @@ final class const RoomChat({
), ),
), ),
if (memberListOpened == true && showMembersByDefault) if (memberListOpened && showMembersByDefault) MemberList(roomId),
MemberList(roomId),
], ],
), ),
), ),

View file

@ -4,6 +4,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:material_ui/material_ui.dart"; import "package:material_ui/material_ui.dart";
import "package:navigation_rail_m3e/navigation_rail_m3e.dart"; import "package:navigation_rail_m3e/navigation_rail_m3e.dart";
import "package:nexus/helpers/extensions/get_all_child_rooms.dart"; import "package:nexus/helpers/extensions/get_all_child_rooms.dart";
import "package:nexus/helpers/extensions/room_destinations.dart";
import "package:nexus/models/nav_page.dart"; import "package:nexus/models/nav_page.dart";
import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/divider_widget.dart"; import "package:nexus/widgets/divider_widget.dart";
@ -13,32 +14,11 @@ import "package:nexus/widgets/join_dialog.dart";
import "package:nexus/widgets/room_menu_button.dart"; import "package:nexus/widgets/room_menu_button.dart";
import "package:nexus/controllers/key.dart"; import "package:nexus/controllers/key.dart";
import "package:nexus/controllers/spaces.dart"; import "package:nexus/controllers/spaces.dart";
import "package:nexus/models/room.dart";
import "package:collection/collection.dart"; import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
// Needed for navigation_rail_m3e (#65). // Needed for navigation_rail_m3e (#65).
import "package:flutter/material.dart" as old_mat; import "package:flutter/material.dart" as old_mat;
List<NavigationRailM3EDestination> roomDestinations(
IList<Room> rooms, {
required bool isDm,
}) => [
for (final room in rooms)
.new(
label: room.metadata?.name ?? "Unnamed Room",
badgeCount: switch (room.metadata?.unreadNotifications) {
0 || null => room.metadata?.unreadMessages == 0 ? null : 0,
int unread => unread,
},
icon: AvatarOrHash(
room.metadata?.avatar,
room.metadata?.name ?? "Unnamed Room",
fallback: isDm ? null : const Icon(Icons.numbers),
),
),
];
class const RoomList({super.key}) extends ConsumerWidget implements NavPage { class const RoomList({super.key}) extends ConsumerWidget implements NavPage {
@override @override
String get title => "Rooms"; String get title => "Rooms";
@ -265,13 +245,12 @@ class const RoomsPane({super.key}) extends ConsumerWidget {
header: selectedSpace.room == null header: selectedSpace.room == null
? null ? null
: const DividerWidget(Text("Rooms")), : const DividerWidget(Text("Rooms")),
destinations: roomDestinations( destinations: selectedSpace.children.toDestinations(
selectedSpace.children,
isDm: isDm, isDm: isDm,
), ),
), ),
for (final subSpace in selectedSpace.subSpaces) ...selectedSpace.subSpaces.map(
.new( (subSpace) => NavigationRailM3ESection(
header: DividerWidget( header: DividerWidget(
Row( Row(
mainAxisSize: .min, mainAxisSize: .min,
@ -293,11 +272,9 @@ class const RoomsPane({super.key}) extends ConsumerWidget {
], ],
), ),
), ),
destinations: roomDestinations( destinations: subSpace.children.toDestinations(isDm: isDm),
subSpace.children,
isDm: isDm,
),
), ),
),
], ],
onDestinationSelected: (value) { onDestinationSelected: (value) {
ref ref

View file

@ -33,7 +33,7 @@ final class const RoomMenuButton(
if (room != null) ...[ if (room != null) ...[
PopupMenuItem( PopupMenuItem(
onTap: () async { onTap: () async {
final vias = ref.watch(ViaController.provider(room!)); final vias = ref.read(ViaController.provider(room!));
await Clipboard.setData( await Clipboard.setData(
.new( .new(
@ -126,8 +126,10 @@ final class const RoomMenuButton(
duration: Duration(days: 1), duration: Duration(days: 1),
), ),
); );
await client.leaveRoom(room!); await client
snackbar.close(); .leaveRoom(room!)
.onError(showError)
.whenComplete(snackbar.close);
}, },
child: Text("Leave"), child: Text("Leave"),
), ),

View file

@ -15,7 +15,7 @@ class const UrlPreview(final Uri link, {super.key}) extends ConsumerWidget {
data: (preview) => preview == null data: (preview) => preview == null
? SizedBox.shrink() ? SizedBox.shrink()
: InkWell( : InkWell(
onTap: () => ref.watch(LaunchHelper.provider).launchUrl(link), onTap: () => ref.read(LaunchHelper.provider).launchUrl(link),
child: Card( child: Card(
margin: .symmetric(vertical: 4), margin: .symmetric(vertical: 4),
color: Theme.of(context) color: Theme.of(context)

View file

@ -26,6 +26,9 @@ final class const UserBottomSheet(
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final textTheme = theme.textTheme; final textTheme = theme.textTheme;
final subtitleStyle = textTheme.titleSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
);
final client = ref.read(ClientController.provider.notifier); final client = ref.read(ClientController.provider.notifier);
void showMembershipDialog(MembershipAction action) => showDialog( void showMembershipDialog(MembershipAction action) => showDialog(
@ -112,13 +115,7 @@ final class const UserBottomSheet(
style: textTheme.headlineLarge, style: textTheme.headlineLarge,
textAlign: .center, textAlign: .center,
), ),
SelectableText( SelectableText(userId, textAlign: .center, style: subtitleStyle),
userId,
textAlign: .center,
style: textTheme.titleSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
ref ref
.watch(ProfileController.provider(userId)) .watch(ProfileController.provider(userId))
@ -151,9 +148,7 @@ final class const UserBottomSheet(
Text( Text(
pronoun.summary, pronoun.summary,
textAlign: .center, textAlign: .center,
style: textTheme.titleSmall?.copyWith( style: subtitleStyle,
color: theme.colorScheme.onSurfaceVariant,
),
), ),
], ],
) )
@ -172,9 +167,7 @@ final class const UserBottomSheet(
Text( Text(
profileResponse.profile.timezone!, profileResponse.profile.timezone!,
textAlign: .center, textAlign: .center,
style: textTheme.titleSmall?.copyWith( style: subtitleStyle,
color: theme.colorScheme.onSurfaceVariant,
),
), ),
], ],
], ],
@ -212,7 +205,6 @@ final class const UserBottomSheet(
Padding( Padding(
padding: .only(top: 4), padding: .only(top: 4),
child: Row( child: Row(
mainAxisSize: MainAxisSize.max,
spacing: 8, spacing: 8,
children: [ children: [
if (member.status == .join || member.status == .invite) if (member.status == .join || member.status == .invite)