chore: Follow up spaces ui

This commit is contained in:
Krille 2024-07-15 15:19:50 +02:00
commit 650f87b1d2
No known key found for this signature in database
GPG key ID: E067ECD60F1A0652
4 changed files with 295 additions and 288 deletions

View file

@ -2710,5 +2710,6 @@
"@goToSpace": { "@goToSpace": {
"type": "text", "type": "text",
"space": {} "space": {}
} },
"markAsUnread": "Mark as unread"
} }

View file

@ -619,7 +619,7 @@ class ChatListController extends State<ChatList>
if (space != null) if (space != null)
SheetAction( SheetAction(
key: ChatContextAction.goToSpace, key: ChatContextAction.goToSpace,
icon: Icons.workspaces_outlined, icon: Icons.chevron_right_outlined,
label: L10n.of(context)!.goToSpace(space.getLocalizedDisplayname()), label: L10n.of(context)!.goToSpace(space.getLocalizedDisplayname()),
), ),
SheetAction( SheetAction(
@ -627,11 +627,13 @@ class ChatListController extends State<ChatList>
icon: room.markedUnread icon: room.markedUnread
? Icons.mark_as_unread ? Icons.mark_as_unread
: Icons.mark_as_unread_outlined, : Icons.mark_as_unread_outlined,
label: L10n.of(context)!.toggleUnread, label: room.markedUnread
? L10n.of(context)!.markAsRead
: L10n.of(context)!.markAsUnread,
), ),
SheetAction( SheetAction(
key: ChatContextAction.favorite, key: ChatContextAction.favorite,
icon: room.isFavourite ? Icons.pin : Icons.pin_outlined, icon: room.isFavourite ? Icons.push_pin : Icons.push_pin_outlined,
label: room.isFavourite label: room.isFavourite
? L10n.of(context)!.unpin ? L10n.of(context)!.unpin
: L10n.of(context)!.pin, : L10n.of(context)!.pin,
@ -640,7 +642,7 @@ class ChatListController extends State<ChatList>
key: ChatContextAction.mute, key: ChatContextAction.mute,
icon: room.pushRuleState == PushRuleState.notify icon: room.pushRuleState == PushRuleState.notify
? Icons.notifications_off_outlined ? Icons.notifications_off_outlined
: Icons.notifications, : Icons.notifications_outlined,
label: room.pushRuleState == PushRuleState.notify label: room.pushRuleState == PushRuleState.notify
? L10n.of(context)!.muteChat ? L10n.of(context)!.muteChat
: L10n.of(context)!.unmuteChat, : L10n.of(context)!.unmuteChat,
@ -657,6 +659,19 @@ class ChatListController extends State<ChatList>
if (action == null) return; if (action == null) return;
if (!mounted) return; if (!mounted) return;
if (action == ChatContextAction.leave) {
final confirmed = await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.areYouSure,
okLabel: L10n.of(context)!.yes,
cancelLabel: L10n.of(context)!.no,
message: L10n.of(context)!.archiveRoomDescription,
);
if (confirmed == OkCancelResult.cancel) return;
}
if (!mounted) return;
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
future: () async { future: () async {

View file

@ -22,14 +22,9 @@ class ChatListView extends StatelessWidget {
final selectMode = controller.selectMode; final selectMode = controller.selectMode;
return PopScope( return PopScope(
canPop: controller.selectMode == SelectMode.normal && canPop: controller.selectMode == SelectMode.normal &&
!controller.isSearchMode && !controller.isSearchMode,
controller.activeFilter == ActiveFilter.allChats, onPopInvoked: (pop) {
onPopInvoked: (pop) async {
if (pop) return; if (pop) return;
if (controller.activeSpaceId != null) {
controller.clearActiveSpace();
return;
}
final selMode = controller.selectMode; final selMode = controller.selectMode;
if (controller.isSearchMode) { if (controller.isSearchMode) {
controller.cancelSearch(); controller.cancelSearch();

View file

@ -165,316 +165,312 @@ class _SpaceViewState extends State<SpaceView> {
final room = Matrix.of(context).client.getRoomById(widget.spaceId); final room = Matrix.of(context).client.getRoomById(widget.spaceId);
final displayname = final displayname =
room?.getLocalizedDisplayname() ?? L10n.of(context)!.nothingFound; room?.getLocalizedDisplayname() ?? L10n.of(context)!.nothingFound;
return Scaffold( return PopScope(
appBar: AppBar( canPop: false,
leading: Center( onPopInvoked: (_) => widget.onBack(),
child: CloseButton( child: Scaffold(
onPressed: widget.onBack, appBar: AppBar(
leading: Center(
child: CloseButton(
onPressed: widget.onBack,
),
), ),
), titleSpacing: 0,
titleSpacing: 0, title: ListTile(
title: ListTile( contentPadding: EdgeInsets.zero,
contentPadding: EdgeInsets.zero, leading: Avatar(
leading: Avatar( mxContent: room?.avatar,
mxContent: room?.avatar, name: displayname,
name: displayname, borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), ),
), title: Text(
title: Text( displayname,
displayname, maxLines: 1,
maxLines: 1, overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis, ),
), subtitle: room == null
subtitle: room == null ? null
? null : Text(
: Text( L10n.of(context)!.countChatsAndCountParticipants(
L10n.of(context)!.countChatsAndCountParticipants( room.spaceChildren.length,
room.spaceChildren.length, room.summary.mJoinedMemberCount ?? 1,
room.summary.mJoinedMemberCount ?? 1, ),
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
actions: [
PopupMenuButton<SpaceActions>(
onSelected: _onSpaceAction,
itemBuilder: (context) => [
PopupMenuItem(
value: SpaceActions.settings,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.settings_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.settings),
],
),
),
PopupMenuItem(
value: SpaceActions.invite,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.person_add_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.invite),
],
),
),
PopupMenuItem(
value: SpaceActions.leave,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.delete_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.leave),
],
),
),
],
), ),
], actions: [
), PopupMenuButton<SpaceActions>(
body: room == null onSelected: _onSpaceAction,
? const Center( itemBuilder: (context) => [
child: Icon( PopupMenuItem(
Icons.search_outlined, value: SpaceActions.settings,
size: 80, child: Row(
), mainAxisSize: MainAxisSize.min,
) children: [
: StreamBuilder( const Icon(Icons.settings_outlined),
stream: room.client.onSync.stream const SizedBox(width: 12),
.where((s) => s.hasRoomUpdate) Text(L10n.of(context)!.settings),
.rateLimit(const Duration(seconds: 1)), ],
builder: (context, snapshot) {
final joinedRooms = room.spaceChildren
.map((child) {
final roomId = child.roomId;
if (roomId == null) return null;
return room.client.getRoomById(roomId);
})
.whereType<Room>()
.where((room) => room.membership != Membership.leave)
.toList();
// Sort rooms by last activity
joinedRooms.sort(
(b, a) => (a.lastEvent?.originServerTs ??
DateTime.fromMillisecondsSinceEpoch(0))
.compareTo(
b.lastEvent?.originServerTs ??
DateTime.fromMillisecondsSinceEpoch(0),
), ),
); ),
PopupMenuItem(
value: SpaceActions.invite,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.person_add_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.invite),
],
),
),
PopupMenuItem(
value: SpaceActions.leave,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.delete_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.leave),
],
),
),
],
),
],
),
body: room == null
? const Center(
child: Icon(
Icons.search_outlined,
size: 80,
),
)
: StreamBuilder(
stream: room.client.onSync.stream
.where((s) => s.hasRoomUpdate)
.rateLimit(const Duration(seconds: 1)),
builder: (context, snapshot) {
final childrenIds = room.spaceChildren
.map((c) => c.roomId)
.whereType<String>()
.toSet();
final joinedParents = room.spaceParents final joinedRooms = room.client.rooms
.map((parent) { .where((room) => childrenIds.remove(room.id))
final roomId = parent.roomId; .toList();
if (roomId == null) return null;
return room.client.getRoomById(roomId); final joinedParents = room.spaceParents
}) .map((parent) {
.whereType<Room>() final roomId = parent.roomId;
.toList(); if (roomId == null) return null;
final filter = _filterController.text.trim().toLowerCase(); return room.client.getRoomById(roomId);
return CustomScrollView( })
slivers: [ .whereType<Room>()
SliverAppBar( .toList();
floating: true, final filter = _filterController.text.trim().toLowerCase();
toolbarHeight: 72, return CustomScrollView(
scrolledUnderElevation: 0, slivers: [
backgroundColor: Colors.transparent, SliverAppBar(
automaticallyImplyLeading: false, floating: true,
title: TextField( toolbarHeight: 72,
controller: _filterController, scrolledUnderElevation: 0,
onChanged: (_) => setState(() {}), backgroundColor: Colors.transparent,
textInputAction: TextInputAction.search, automaticallyImplyLeading: false,
decoration: InputDecoration( title: TextField(
fillColor: controller: _filterController,
Theme.of(context).colorScheme.secondaryContainer, onChanged: (_) => setState(() {}),
border: OutlineInputBorder( textInputAction: TextInputAction.search,
borderSide: BorderSide.none, decoration: InputDecoration(
borderRadius: BorderRadius.circular(99), fillColor: Theme.of(context)
),
contentPadding: EdgeInsets.zero,
hintText: L10n.of(context)!.search,
hintStyle: TextStyle(
color: Theme.of(context)
.colorScheme .colorScheme
.onPrimaryContainer, .secondaryContainer,
fontWeight: FontWeight.normal, border: OutlineInputBorder(
), borderSide: BorderSide.none,
floatingLabelBehavior: FloatingLabelBehavior.never, borderRadius: BorderRadius.circular(99),
prefixIcon: IconButton( ),
onPressed: () {}, contentPadding: EdgeInsets.zero,
icon: Icon( hintText: L10n.of(context)!.search,
Icons.search_outlined, hintStyle: TextStyle(
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onPrimaryContainer, .onPrimaryContainer,
fontWeight: FontWeight.normal,
),
floatingLabelBehavior: FloatingLabelBehavior.never,
prefixIcon: IconButton(
onPressed: () {},
icon: Icon(
Icons.search_outlined,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
), ),
), ),
), ),
), ),
), SliverList.builder(
SliverList.builder( itemCount: joinedParents.length,
itemCount: joinedParents.length, itemBuilder: (context, i) {
itemBuilder: (context, i) { final displayname =
final displayname = joinedParents[i].getLocalizedDisplayname();
joinedParents[i].getLocalizedDisplayname(); return Padding(
return Padding( padding: const EdgeInsets.symmetric(
padding: const EdgeInsets.symmetric( horizontal: 8,
horizontal: 8, vertical: 1,
vertical: 1,
),
child: Material(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
clipBehavior: Clip.hardEdge,
child: ListTile(
minVerticalPadding: 0,
leading: Icon(
Icons.adaptive.arrow_back_outlined,
size: 16,
),
title: Row(
children: [
Avatar(
mxContent: joinedParents[i].avatar,
name: displayname,
size: Avatar.defaultSize / 2,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 4,
),
),
const SizedBox(width: 8),
Expanded(child: Text(displayname)),
],
),
onTap: () =>
widget.toParentSpace(joinedParents[i].id),
), ),
), child: Material(
); borderRadius:
}, BorderRadius.circular(AppConfig.borderRadius),
), clipBehavior: Clip.hardEdge,
SliverList.builder( child: ListTile(
itemCount: joinedRooms.length + 1, minVerticalPadding: 0,
itemBuilder: (context, i) { leading: Icon(
if (i == 0) { Icons.adaptive.arrow_back_outlined,
return SearchTitle( size: 16,
title: L10n.of(context)!.joinedChats,
icon: const Icon(Icons.chat_outlined),
);
}
i--;
final room = joinedRooms[i];
return ChatListItem(
room,
filter: filter,
onTap: () => widget.onChatTab(room),
onLongPress: () => widget.onChatContext(room),
activeChat: widget.activeChat == room.id,
);
},
),
SliverList.builder(
itemCount: _discoveredChildren.length + 2,
itemBuilder: (context, i) {
if (i == 0) {
return SearchTitle(
title: L10n.of(context)!.discover,
icon: const Icon(Icons.explore_outlined),
);
}
i--;
if (i == _discoveredChildren.length) {
if (_noMoreRooms) {
return Padding(
padding: const EdgeInsets.all(12.0),
child: Center(
child: Text(
L10n.of(context)!.noMoreChatsFound,
style: const TextStyle(fontSize: 13),
), ),
title: Row(
children: [
Avatar(
mxContent: joinedParents[i].avatar,
name: displayname,
size: Avatar.defaultSize / 2,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 4,
),
),
const SizedBox(width: 8),
Expanded(child: Text(displayname)),
],
),
onTap: () =>
widget.toParentSpace(joinedParents[i].id),
),
),
);
},
),
SliverList.builder(
itemCount: joinedRooms.length + 1,
itemBuilder: (context, i) {
if (i == 0) {
return SearchTitle(
title: L10n.of(context)!.joinedChats,
icon: const Icon(Icons.chat_outlined),
);
}
i--;
final room = joinedRooms[i];
return ChatListItem(
room,
filter: filter,
onTap: () => widget.onChatTab(room),
onLongPress: () => widget.onChatContext(room),
activeChat: widget.activeChat == room.id,
);
},
),
SliverList.builder(
itemCount: _discoveredChildren.length + 2,
itemBuilder: (context, i) {
if (i == 0) {
return SearchTitle(
title: L10n.of(context)!.discover,
icon: const Icon(Icons.explore_outlined),
);
}
i--;
if (i == _discoveredChildren.length) {
if (_noMoreRooms) {
return Padding(
padding: const EdgeInsets.all(12.0),
child: Center(
child: Text(
L10n.of(context)!.noMoreChatsFound,
style: const TextStyle(fontSize: 13),
),
),
);
}
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 2.0,
),
child: TextButton(
onPressed: _isLoading ? null : _loadHierarchy,
child: _isLoading
? LinearProgressIndicator(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
)
: Text(L10n.of(context)!.loadMore),
), ),
); );
} }
final item = _discoveredChildren[i];
final displayname = item.name ??
item.canonicalAlias ??
L10n.of(context)!.emptyChat;
if (!displayname.toLowerCase().contains(filter)) {
return const SizedBox.shrink();
}
return Padding( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12.0, horizontal: 8,
vertical: 2.0, vertical: 1,
), ),
child: TextButton( child: Material(
onPressed: _isLoading ? null : _loadHierarchy, borderRadius:
child: _isLoading BorderRadius.circular(AppConfig.borderRadius),
? LinearProgressIndicator( clipBehavior: Clip.hardEdge,
borderRadius: BorderRadius.circular( child: ListTile(
AppConfig.borderRadius, onTap: () => _joinChildRoom(item),
leading: Avatar(
mxContent: item.avatarUrl,
name: displayname,
borderRadius: item.roomType == 'm.space'
? BorderRadius.circular(
AppConfig.borderRadius / 2,
)
: null,
),
title: Row(
children: [
Expanded(
child: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
) ),
: Text(L10n.of(context)!.loadMore), const SizedBox(width: 8),
const Icon(
Icons.add_circle_outline_outlined,
),
],
),
subtitle: Text(
item.topic ??
L10n.of(context)!.countParticipants(
item.numJoinedMembers,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
), ),
); );
} },
final item = _discoveredChildren[i]; ),
final displayname = item.name ?? ],
item.canonicalAlias ?? );
L10n.of(context)!.emptyChat; },
if (!displayname.toLowerCase().contains(filter)) { ),
return const SizedBox.shrink(); ),
}
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 1,
),
child: Material(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
clipBehavior: Clip.hardEdge,
child: ListTile(
onTap: () => _joinChildRoom(item),
leading: Avatar(
mxContent: item.avatarUrl,
name: displayname,
borderRadius: item.roomType == 'm.space'
? BorderRadius.circular(
AppConfig.borderRadius / 2,
)
: null,
),
title: Row(
children: [
Expanded(
child: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
const Icon(Icons.add_circle_outline_outlined),
],
),
subtitle: Text(
item.topic ??
L10n.of(context)!.countParticipants(
item.numJoinedMembers,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
);
},
),
],
);
},
),
); );
} }
} }