fluffychat/lib/pages/chat_list/space_view.dart

481 lines
18 KiB
Dart
Raw Normal View History

2022-08-30 18:24:36 +00:00
import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:go_router/go_router.dart';
2022-08-30 18:24:36 +00:00
import 'package:matrix/matrix.dart';
2024-07-14 14:49:46 +00:00
import 'package:fluffychat/config/app_config.dart';
2022-08-30 18:24:36 +00:00
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
import 'package:fluffychat/pages/chat_list/search_title.dart';
2024-07-14 14:49:46 +00:00
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/stream_extension.dart';
2022-08-30 18:24:36 +00:00
import 'package:fluffychat/widgets/avatar.dart';
2024-07-14 14:49:46 +00:00
import 'package:fluffychat/widgets/matrix.dart';
2022-08-30 18:24:36 +00:00
class SpaceView extends StatefulWidget {
2024-07-14 14:49:46 +00:00
final String spaceId;
final void Function() onBack;
final void Function(String spaceId) toParentSpace;
final void Function(Room room) onChatTab;
final void Function(Room room, BuildContext context) onChatContext;
2024-07-14 14:49:46 +00:00
final String? activeChat;
const SpaceView({
required this.spaceId,
required this.onBack,
required this.onChatTab,
required this.activeChat,
required this.toParentSpace,
required this.onChatContext,
super.key,
});
2022-08-30 18:24:36 +00:00
@override
State<SpaceView> createState() => _SpaceViewState();
}
class _SpaceViewState extends State<SpaceView> {
2024-07-14 14:49:46 +00:00
final List<SpaceRoomsChunk> _discoveredChildren = [];
final TextEditingController _filterController = TextEditingController();
String? _nextBatch;
bool _noMoreRooms = false;
bool _isLoading = false;
2023-12-23 14:07:35 +00:00
@override
void initState() {
2024-07-14 14:49:46 +00:00
_loadHierarchy();
2023-12-23 14:07:35 +00:00
super.initState();
}
2022-09-10 11:21:33 +00:00
2024-07-14 14:49:46 +00:00
void _loadHierarchy() async {
final room = Matrix.of(context).client.getRoomById(widget.spaceId);
if (room == null) return;
2023-12-23 14:07:35 +00:00
2022-08-30 18:24:36 +00:00
setState(() {
2024-07-14 14:49:46 +00:00
_isLoading = true;
2022-08-30 18:24:36 +00:00
});
2023-12-23 14:07:35 +00:00
try {
2024-07-14 14:49:46 +00:00
final hierarchy = await room.client.getSpaceHierarchy(
widget.spaceId,
suggestedOnly: false,
maxDepth: 2,
from: _nextBatch,
2023-12-24 07:14:13 +00:00
);
2024-07-14 14:49:46 +00:00
if (!mounted) return;
2023-12-23 14:07:35 +00:00
setState(() {
2024-07-14 14:49:46 +00:00
_nextBatch = hierarchy.nextBatch;
if (hierarchy.nextBatch == null) {
_noMoreRooms = true;
}
_discoveredChildren.addAll(
hierarchy.rooms
.where((c) => room.client.getRoomById(c.roomId) == null),
);
_isLoading = false;
2023-12-23 14:07:35 +00:00
});
2024-07-14 14:49:46 +00:00
} catch (e, s) {
Logs().w('Unable to load hierarchy', e, s);
if (!mounted) return;
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
2023-12-23 14:07:35 +00:00
setState(() {
2024-07-14 14:49:46 +00:00
_isLoading = false;
2023-12-23 14:07:35 +00:00
});
}
}
2024-07-14 14:49:46 +00:00
void _joinChildRoom(SpaceRoomsChunk item) async {
2022-08-30 18:24:36 +00:00
final client = Matrix.of(context).client;
2024-07-14 14:49:46 +00:00
final space = client.getRoomById(widget.spaceId);
2022-08-30 18:24:36 +00:00
2024-07-14 14:49:46 +00:00
final consent = await showOkCancelAlertDialog(
2022-08-30 18:24:36 +00:00
context: context,
2024-07-14 14:49:46 +00:00
title: item.name ?? item.canonicalAlias ?? L10n.of(context)!.emptyChat,
message: item.topic,
okLabel: L10n.of(context)!.joinRoom,
cancelLabel: L10n.of(context)!.cancel,
);
if (consent != OkCancelResult.ok) return;
if (!mounted) return;
await showFutureLoadingDialog(
context: context,
future: () async {
await client.joinRoom(
item.roomId,
serverName: space?.spaceChildren
.firstWhereOrNull(
(child) => child.roomId == item.roomId,
)
?.via,
);
if (client.getRoomById(item.roomId) == null) {
// Wait for room actually appears in sync
await client.waitForRoomInSync(item.roomId, join: true);
}
},
2022-08-30 18:24:36 +00:00
);
2024-07-14 14:49:46 +00:00
if (!mounted) return;
setState(() {
_discoveredChildren.remove(item);
});
}
void _onSpaceAction(SpaceActions action) async {
final space = Matrix.of(context).client.getRoomById(widget.spaceId);
2022-08-30 18:24:36 +00:00
switch (action) {
2024-07-14 14:49:46 +00:00
case SpaceActions.settings:
await space?.postLoad();
context.push('/rooms/${widget.spaceId}/details');
2022-08-30 18:24:36 +00:00
break;
2024-07-14 14:49:46 +00:00
case SpaceActions.invite:
await space?.postLoad();
context.push('/rooms/${widget.spaceId}/invite');
break;
case SpaceActions.leave:
final confirmed = await showOkCancelAlertDialog(
useRootNavigator: false,
2022-08-30 18:24:36 +00:00
context: context,
2024-07-14 14:49:46 +00:00
title: L10n.of(context)!.areYouSure,
okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel,
message: L10n.of(context)!.archiveRoomDescription,
2022-08-30 18:24:36 +00:00
);
2024-07-14 14:49:46 +00:00
if (!mounted) return;
if (confirmed != OkCancelResult.ok) return;
final success = await showFutureLoadingDialog(
2022-08-30 18:24:36 +00:00
context: context,
2024-07-14 14:49:46 +00:00
future: () async => await space?.leave(),
2022-08-30 18:24:36 +00:00
);
2024-07-14 14:49:46 +00:00
if (!mounted) return;
if (success.error != null) return;
widget.onBack();
2022-08-30 18:24:36 +00:00
}
}
@override
Widget build(BuildContext context) {
2024-07-14 14:49:46 +00:00
final room = Matrix.of(context).client.getRoomById(widget.spaceId);
final displayname =
room?.getLocalizedDisplayname() ?? L10n.of(context)!.nothingFound;
2024-07-18 14:47:58 +00:00
return Scaffold(
appBar: AppBar(
leading: Center(
child: CloseButton(
onPressed: widget.onBack,
2024-07-14 14:49:46 +00:00
),
2024-07-18 14:47:58 +00:00
),
titleSpacing: 0,
title: ListTile(
contentPadding: EdgeInsets.zero,
leading: Avatar(
mxContent: room?.avatar,
name: displayname,
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
),
title: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
2024-07-14 14:49:46 +00:00
),
2024-07-18 14:47:58 +00:00
subtitle: room == null
? null
: Text(
L10n.of(context)!.countChatsAndCountParticipants(
room.spaceChildren.length,
room.summary.mJoinedMemberCount ?? 1,
2024-07-14 14:49:46 +00:00
),
2024-07-18 14:47:58 +00:00
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
2024-07-18 14:47:58 +00:00
),
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),
],
),
2024-07-18 14:47:58 +00:00
),
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),
],
),
2024-07-18 14:47:58 +00:00
),
PopupMenuItem(
value: SpaceActions.leave,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.delete_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.leave),
],
2024-07-14 14:49:46 +00:00
),
2024-07-18 14:47:58 +00:00
),
],
),
],
),
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();
2024-07-14 14:49:46 +00:00
2024-07-18 14:47:58 +00:00
final joinedRooms = room.client.rooms
.where((room) => childrenIds.remove(room.id))
.toList();
2024-07-14 14:49:46 +00:00
2024-07-18 14:47:58 +00:00
final joinedParents = room.spaceParents
.map((parent) {
final roomId = parent.roomId;
if (roomId == null) return null;
return room.client.getRoomById(roomId);
})
.whereType<Room>()
.toList();
final filter = _filterController.text.trim().toLowerCase();
return CustomScrollView(
slivers: [
SliverAppBar(
floating: true,
toolbarHeight: 72,
scrolledUnderElevation: 0,
backgroundColor: Colors.transparent,
automaticallyImplyLeading: false,
title: TextField(
controller: _filterController,
onChanged: (_) => setState(() {}),
textInputAction: TextInputAction.search,
decoration: InputDecoration(
fillColor:
Theme.of(context).colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
),
contentPadding: EdgeInsets.zero,
hintText: L10n.of(context)!.search,
hintStyle: TextStyle(
color: Theme.of(context)
2024-07-14 14:49:46 +00:00
.colorScheme
2024-07-18 14:47:58 +00:00
.onPrimaryContainer,
fontWeight: FontWeight.normal,
),
floatingLabelBehavior: FloatingLabelBehavior.never,
prefixIcon: IconButton(
onPressed: () {},
icon: Icon(
Icons.search_outlined,
2024-07-14 14:49:46 +00:00
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
2023-12-23 14:07:35 +00:00
),
2024-07-14 14:49:46 +00:00
),
2024-07-18 14:47:58 +00:00
),
SliverList.builder(
itemCount: joinedParents.length,
itemBuilder: (context, i) {
final displayname =
joinedParents[i].getLocalizedDisplayname();
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
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,
2024-07-14 14:49:46 +00:00
),
2024-07-18 14:47:58 +00:00
),
const SizedBox(width: 8),
Expanded(child: Text(displayname)),
],
2024-07-14 14:49:46 +00:00
),
2024-07-18 14:47:58 +00:00
onTap: () =>
widget.toParentSpace(joinedParents[i].id),
2024-07-14 14:49:46 +00:00
),
2024-07-18 14:47:58 +00:00
),
);
},
),
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),
2024-07-14 14:49:46 +00:00
);
2024-07-18 14:47:58 +00:00
}
i--;
final room = joinedRooms[i];
return ChatListItem(
room,
filter: filter,
onTap: () => widget.onChatTab(room),
onLongPress: (context) => widget.onChatContext(
2024-07-15 13:19:50 +00:00
room,
2024-07-18 14:47:58 +00:00
context,
),
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),
2024-07-14 14:49:46 +00:00
);
2024-07-18 14:47:58 +00:00
}
i--;
if (i == _discoveredChildren.length) {
if (_noMoreRooms) {
2024-07-15 13:19:50 +00:00
return Padding(
2024-07-18 14:47:58 +00:00
padding: const EdgeInsets.all(12.0),
child: Center(
child: Text(
L10n.of(context)!.noMoreChatsFound,
style: const TextStyle(fontSize: 13),
),
2023-12-23 14:07:35 +00:00
),
2024-07-14 14:49:46 +00:00
);
}
return Padding(
padding: const EdgeInsets.symmetric(
2024-07-18 14:47:58 +00:00
horizontal: 12.0,
vertical: 2.0,
2023-03-19 18:59:50 +00:00
),
2024-07-18 14:47:58 +00:00
child: TextButton(
onPressed: _isLoading ? null : _loadHierarchy,
child: _isLoading
? LinearProgressIndicator(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
2024-07-14 14:49:46 +00:00
),
2024-07-18 14:47:58 +00:00
)
: 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(
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,
2024-07-14 14:49:46 +00:00
),
2024-07-18 14:47:58 +00:00
),
const SizedBox(width: 8),
const Icon(
Icons.add_circle_outline_outlined,
),
],
),
subtitle: Text(
item.topic ??
L10n.of(context)!.countParticipants(
item.numJoinedMembers,
2024-07-14 14:49:46 +00:00
),
2024-07-18 14:47:58 +00:00
maxLines: 1,
overflow: TextOverflow.ellipsis,
2023-12-23 14:07:35 +00:00
),
),
2024-07-18 14:47:58 +00:00
),
);
},
),
],
);
},
),
);
2022-08-30 18:24:36 +00:00
}
}
2024-07-14 14:49:46 +00:00
enum SpaceActions {
settings,
invite,
2022-08-30 18:24:36 +00:00
leave,
}