fix: Follow up for spaces hierarchy

- fix endless spinner
- fix rooms shon twice
- fix spaces accidentally opened as rooms
- add missing spaces header to spaces view

Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
This commit is contained in:
TheOneWithTheBraid 2022-07-24 19:02:14 +02:00 committed by Krille Fear
parent 82cb094b44
commit a522a90e35
10 changed files with 234 additions and 50 deletions

View File

@ -22,6 +22,7 @@ import 'config/themes.dart';
import 'utils/background_push.dart';
import 'utils/custom_scroll_behaviour.dart';
import 'utils/localized_exception_extension.dart';
import 'utils/space_navigator.dart';
import 'widgets/lock_screen.dart';
import 'widgets/matrix.dart';
@ -132,6 +133,9 @@ class _FluffyChatAppState extends State<FluffyChatApp> {
localizationsDelegates: const [
...L10n.localizationsDelegates,
],
navigatorObservers: [
SpaceNavigator.routeObserver,
],
supportedLocales: L10n.supportedLocales,
initialUrl: _initialUrl ?? '/',
routes: AppRoutes(columnMode ?? false).routes,

View File

@ -18,6 +18,7 @@ import 'package:fluffychat/pages/chat_list/spaces_entry.dart';
import 'package:fluffychat/utils/famedlysdk_store.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/space_navigator.dart';
import '../../../utils/account_bundles.dart';
import '../../main.dart';
import '../../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
@ -47,7 +48,8 @@ class ChatList extends StatefulWidget {
ChatListController createState() => ChatListController();
}
class ChatListController extends State<ChatList> with TickerProviderStateMixin {
class ChatListController extends State<ChatList>
with TickerProviderStateMixin, RouteAware {
StreamSubscription? _intentDataStreamSubscription;
StreamSubscription? _intentFileStreamSubscription;
@ -66,6 +68,8 @@ class ChatListController extends State<ChatList> with TickerProviderStateMixin {
bool isSearching = false;
static const String _serverStoreNamespace = 'im.fluffychat.search.server';
StreamSubscription<String?>? _spacesSubscription;
void setServer() async {
final newServer = await showTextInputDialog(
useRootNavigator: false,
@ -177,11 +181,6 @@ class ChatListController extends State<ChatList> with TickerProviderStateMixin {
}
}
void setActiveSpacesEntry(BuildContext context, SpacesEntry? spaceId) {
Scaffold.of(context).closeDrawer();
setState(() => _activeSpacesEntry = spaceId);
}
void editSpace(BuildContext context, String spaceId) async {
await Matrix.of(context).client.getRoomById(spaceId)!.postLoad();
VRouter.of(context).toSegments(['spaces', spaceId]);
@ -310,6 +309,8 @@ class ChatListController extends State<ChatList> with TickerProviderStateMixin {
_checkTorBrowser();
_subscribeSpaceChanges();
super.initState();
}
@ -336,6 +337,7 @@ class ChatListController extends State<ChatList> with TickerProviderStateMixin {
_intentDataStreamSubscription?.cancel();
_intentFileStreamSubscription?.cancel();
_intentUriStreamSubscription?.cancel();
_spacesSubscription?.cancel();
scrollController.removeListener(_onScroll);
super.dispose();
}
@ -671,6 +673,27 @@ class ChatListController extends State<ChatList> with TickerProviderStateMixin {
Future<void> dehydrate() =>
SettingsAccountController.dehydrateDevice(context);
_adjustSpaceQuery(String? spaceId) {
cancelSearch();
setState(() {
if (spaceId != null) {
final matching =
spacesEntries.where((element) => element.routeHandle == spaceId);
if (matching.isNotEmpty) {
_activeSpacesEntry = matching.first;
} else {
_activeSpacesEntry = defaultSpacesEntry;
}
} else {
_activeSpacesEntry = defaultSpacesEntry;
}
});
}
void _subscribeSpaceChanges() {
_spacesSubscription = SpaceNavigator.stream.listen(_adjustSpaceQuery);
}
}
enum EditBundleAction { addToBundle, removeFromBundle }

View File

@ -5,12 +5,14 @@ import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix_link_text/link_text.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
import 'package:fluffychat/pages/chat_list/search_title.dart';
import 'package:fluffychat/pages/chat_list/spaces_entry.dart';
import 'package:fluffychat/pages/chat_list/stories_header.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/connection_status_header.dart';
import 'package:fluffychat/widgets/profile_bottom_sheet.dart';
@ -75,6 +77,7 @@ class _ChatListViewBodyState extends State<ChatListViewBody> {
mainAxisSize: MainAxisSize.min,
children: [
const ConnectionStatusHeader(),
SpaceRoomListTopBar(widget.controller),
if (roomSearchResult != null) ...[
SearchTitle(
title: L10n.of(context)!.publicRooms,
@ -205,7 +208,7 @@ class _ChatListViewBodyState extends State<ChatListViewBody> {
title: L10n.of(context)!.chats,
icon: const Icon(Icons.chat_outlined),
),
if (rooms.isEmpty)
if (rooms.isEmpty && !widget.controller.isSearchMode)
Column(
key: const ValueKey(null),
mainAxisAlignment: MainAxisAlignment.center,
@ -366,6 +369,63 @@ class _ChatListViewBodyState extends State<ChatListViewBody> {
_lastSpace = widget.controller.activeSpacesEntry;
return reversed;
}
@override
void didUpdateWidget(covariant ChatListViewBody oldWidget) {
setState(() {});
super.didUpdateWidget(oldWidget);
}
}
class SpaceRoomListTopBar extends StatefulWidget {
final ChatListController controller;
const SpaceRoomListTopBar(this.controller, {Key? key}) : super(key: key);
@override
State<SpaceRoomListTopBar> createState() => _SpaceRoomListTopBarState();
}
class _SpaceRoomListTopBarState extends State<SpaceRoomListTopBar> {
bool _limitSize = true;
@override
Widget build(BuildContext context) {
if (widget.controller.activeSpacesEntry is SpaceSpacesEntry &&
!widget.controller.isSearchMode &&
(widget.controller.activeSpacesEntry as SpaceSpacesEntry)
.space
.topic
.isNotEmpty) {
return GestureDetector(
onTap: () => setState(() {
_limitSize = !_limitSize;
}),
child: Column(
children: [
Padding(
child: LinkText(
text: (widget.controller.activeSpacesEntry as SpaceSpacesEntry)
.space
.topic,
maxLines: _limitSize ? 3 : null,
linkStyle: const TextStyle(color: Colors.blueAccent),
textStyle: TextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyText2!.color,
),
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
),
padding: const EdgeInsets.all(8),
),
const Divider(),
],
),
);
} else {
return Container();
}
}
}
class _SearchItem extends StatelessWidget {

View File

@ -9,9 +9,13 @@ import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
class RecommendedRoomListItem extends StatelessWidget {
final SpaceRoomsChunk room;
final VoidCallback onRoomJoined;
const RecommendedRoomListItem({Key? key, required this.room})
: super(key: key);
const RecommendedRoomListItem({
Key? key,
required this.room,
required this.onRoomJoined,
}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -59,28 +63,30 @@ class RecommendedRoomListItem extends StatelessWidget {
),
],
);
final subtitle = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: Text(
room.topic ?? 'topic',
softWrap: false,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context).textTheme.bodyText2!.color,
),
),
),
],
);
final subtitle = room.topic != null
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: Text(
room.topic!,
softWrap: false,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context).textTheme.bodyText2!.color,
),
),
),
],
)
: null;
void handler() => showModalBottomSheet(
context: context,
builder: (c) => PublicRoomBottomSheet(
roomAlias: room.canonicalAlias!,
outerContext: context,
chunk: room,
onRoomJoined: onRoomJoined,
),
);
if (room.roomType == 'm.space') {

View File

@ -4,6 +4,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_list/spaces_drawer.dart';
import 'package:fluffychat/utils/space_navigator.dart';
import 'package:fluffychat/widgets/avatar.dart';
class SpacesDrawerEntry extends StatelessWidget {
@ -46,10 +47,11 @@ class SpacesDrawerEntry extends StatelessWidget {
overflow: TextOverflow.fade,
),
);
void onTap() => controller.setActiveSpacesEntry(
context,
space,
);
void onTap() {
SpaceNavigator.navigateToSpace(space.routeHandle);
Scaffold.of(context).closeDrawer();
}
final trailing = room != null
? SizedBox(
width: 32,

View File

@ -19,18 +19,25 @@ abstract class SpacesEntry {
// Gets the (translated) name of this entry.
String getName(BuildContext context);
// Gets an icon for this entry (avoided if a space is given)
Icon getIcon(bool active) => active
? const Icon(CupertinoIcons.chat_bubble_2_fill)
: const Icon(CupertinoIcons.chat_bubble_2);
// If this is a specific Room, returns the space Room for various purposes.
Room? getSpace(BuildContext context) => null;
// Gets a list of rooms - this is done as part of _ChatListViewBodyState to get the full list of rooms visible from this SpacesEntry.
List<Room> getRooms(BuildContext context);
// Checks that this entry is still valid.
bool stillValid(BuildContext context) => true;
// Returns true if the Stories header should be shown.
bool shouldShowStoriesHeader(BuildContext context) => false;
String? get routeHandle;
}
// Common room validity checks
@ -58,7 +65,9 @@ bool _roomInsideSpace(Room room, Room space) {
// "All rooms" entry.
class AllRoomsSpacesEntry extends SpacesEntry {
static final AllRoomsSpacesEntry _value = AllRoomsSpacesEntry._();
AllRoomsSpacesEntry._();
factory AllRoomsSpacesEntry() {
return _value;
}
@ -75,6 +84,9 @@ class AllRoomsSpacesEntry extends SpacesEntry {
.toList();
}
@override
final String? routeHandle = null;
@override
bool shouldShowStoriesHeader(BuildContext context) => true;
@ -90,7 +102,9 @@ class AllRoomsSpacesEntry extends SpacesEntry {
// "Direct Chats" entry.
class DirectChatsSpacesEntry extends SpacesEntry {
static final DirectChatsSpacesEntry _value = DirectChatsSpacesEntry._();
DirectChatsSpacesEntry._();
factory DirectChatsSpacesEntry() {
return _value;
}
@ -107,6 +121,9 @@ class DirectChatsSpacesEntry extends SpacesEntry {
.toList();
}
@override
final String? routeHandle = null;
@override
bool shouldShowStoriesHeader(BuildContext context) => true;
@ -122,7 +139,9 @@ class DirectChatsSpacesEntry extends SpacesEntry {
// "Groups" entry.
class GroupsSpacesEntry extends SpacesEntry {
static final GroupsSpacesEntry _value = GroupsSpacesEntry._();
GroupsSpacesEntry._();
factory GroupsSpacesEntry() {
return _value;
}
@ -147,6 +166,9 @@ class GroupsSpacesEntry extends SpacesEntry {
.toList();
}
@override
final String? routeHandle = 'groups';
bool separatedGroup(Room room, List<Room> spaces) {
return !spaces.any((space) => _roomInsideSpace(room, space));
}
@ -163,6 +185,7 @@ class GroupsSpacesEntry extends SpacesEntry {
// All rooms associated with a specific space.
class SpaceSpacesEntry extends SpacesEntry {
final Room space;
const SpaceSpacesEntry(this.space);
@override
@ -204,6 +227,9 @@ class SpaceSpacesEntry extends SpacesEntry {
bool stillValid(BuildContext context) =>
Matrix.of(context).client.getRoomById(space.id) != null;
@override
String? get routeHandle => space.id;
@override
bool operator ==(Object other) {
return hashCode == other.hashCode;

View File

@ -94,14 +94,12 @@ class _SpacesHierarchyProposalsState extends State<SpacesHierarchyProposals> {
strokeWidth: 1,
),
),
onTap: () => setState(
() => SpacesHierarchyProposals._cache[widget.space!]!
.invalidate(),
),
onTap: _refreshRooms,
),
...rooms.map(
(e) => RecommendedRoomListItem(
room: e,
onRoomJoined: _refreshRooms,
),
),
],
@ -109,9 +107,9 @@ class _SpacesHierarchyProposalsState extends State<SpacesHierarchyProposals> {
} else {
child = Column(
key: const ValueKey(null),
children: const [
LinearProgressIndicator(),
ListTile(),
children: [
if (!snapshot.hasError) const LinearProgressIndicator(),
const ListTile(),
],
);
}
@ -143,4 +141,8 @@ class _SpacesHierarchyProposalsState extends State<SpacesHierarchyProposals> {
return Container();
}
}
void _refreshRooms() => setState(
() => SpacesHierarchyProposals._cache[widget.space!]!.invalidate(),
);
}

View File

@ -0,0 +1,23 @@
import 'dart:async';
import 'package:flutter/material.dart';
/// this is a workaround to allow navigation of spaces out from any widget.
/// Reason is that we have no reliable way to listen on *query* changes of
/// VRouter.
///
/// Time wasted: 3h
abstract class SpaceNavigator {
const SpaceNavigator._();
// TODO(TheOneWithTheBraid): adjust routing table in order to represent spaces
// ... in any present path
static final routeObserver = RouteObserver();
static final StreamController<String?> _controller =
StreamController.broadcast();
static Stream<String?> get stream => _controller.stream;
static void navigateToSpace(String? spaceId) => _controller.add(spaceId);
}

View File

@ -10,6 +10,7 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/space_navigator.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/profile_bottom_sheet.dart';
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
@ -18,6 +19,7 @@ import 'platform_infos.dart';
class UrlLauncher {
final String? url;
final BuildContext context;
const UrlLauncher(this.context, this.url);
void launchUrl() {
@ -130,6 +132,11 @@ class UrlLauncher {
}
servers.addAll(identityParts.via);
if (room != null) {
if (room.isSpace) {
SpaceNavigator.navigateToSpace(room.id);
VRouter.of(context).toSegments(['rooms']);
return;
}
// we have the room, so....just open it
if (event != null) {
VRouter.of(context).toSegments(['rooms', room.id],

View File

@ -3,39 +3,48 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_matrix_html/flutter_html.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix_link_text/link_text.dart';
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/content_banner.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../utils/localized_exception_extension.dart';
class PublicRoomBottomSheet extends StatelessWidget {
final String roomAlias;
final String? roomAlias;
final BuildContext outerContext;
final PublicRoomsChunk? chunk;
const PublicRoomBottomSheet({
required this.roomAlias,
final VoidCallback? onRoomJoined;
PublicRoomBottomSheet({
this.roomAlias,
required this.outerContext,
this.chunk,
this.onRoomJoined,
Key? key,
}) : super(key: key);
}) : super(key: key) {
assert(roomAlias != null || chunk != null);
}
void _joinRoom(BuildContext context) async {
final client = Matrix.of(context).client;
final result = await showFutureLoadingDialog<String>(
context: context,
future: () => client.joinRoom(roomAlias),
future: () => client.joinRoom(roomAlias ?? chunk!.roomId),
);
if (result.error == null) {
if (client.getRoomById(result.result!) == null) {
await client.onSync.stream.firstWhere(
(sync) => sync.rooms?.join?.containsKey(result.result) ?? false);
}
VRouter.of(context).toSegments(['rooms', result.result!]);
// don't open the room if the joined room is a space
if (!client.getRoomById(result.result!)!.isSpace) {
VRouter.of(context).toSegments(['rooms', result.result!]);
}
Navigator.of(context, rootNavigator: false).pop();
return;
}
@ -47,7 +56,7 @@ class PublicRoomBottomSheet extends StatelessWidget {
final chunk = this.chunk;
if (chunk != null) return chunk;
final query = await Matrix.of(context).client.queryPublicRooms(
server: roomAlias.domain,
server: roomAlias!.domain,
filter: PublicRoomQueryFilter(
genericSearchTerm: roomAlias,
),
@ -76,7 +85,7 @@ class PublicRoomBottomSheet extends StatelessWidget {
backgroundColor:
Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5),
title: Text(
roomAlias,
roomAlias ?? chunk!.name ?? chunk!.roomId,
overflow: TextOverflow.fade,
),
leading: IconButton(
@ -118,15 +127,37 @@ class PublicRoomBottomSheet extends StatelessWidget {
client: Matrix.of(context).client,
),
ListTile(
title:
Text(profile?.name ?? roomAlias.localpart ?? ''),
title: Text(profile?.name ??
roomAlias?.localpart ??
chunk!.roomId.localpart ??
''),
subtitle: Text(
'${L10n.of(context)!.participant}: ${profile?.numJoinedMembers ?? 0}'),
'${L10n.of(context)!.participant}: ${profile?.numJoinedMembers ?? 0}',
),
trailing: const Icon(Icons.account_box_outlined),
),
if (profile?.topic?.isNotEmpty ?? false)
ListTile(
subtitle: Html(data: profile!.topic!),
title: Text(
L10n.of(context)!.groupDescription,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
),
subtitle: LinkText(
text: profile!.topic!,
linkStyle:
const TextStyle(color: Colors.blueAccent),
textStyle: TextStyle(
fontSize: 14,
color: Theme.of(context)
.textTheme
.bodyText2!
.color,
),
onLinkTap: (url) =>
UrlLauncher(context, url).launchUrl(),
),
),
],
);