diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 62e118f63..61824dad2 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -201,6 +201,19 @@ "supportedVersions": {} } }, + "countChatsAndCountParticipants": "{chats} chats and {participants} participants", + "@countChatsAndCountParticipants": { + "type": "text", + "placeholders": { + "chats": {}, + "participants": {} + } + }, + "noMoreChatsFound": "No more chats found...", + "joinedChats": "Joined chats", + "unread": "Unread", + "space": "Space", + "spaces": "Spaces", "banFromChat": "Ban from chat", "@banFromChat": { "type": "text", diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index c6dae0aa7..ac9b54f36 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -71,7 +71,6 @@ abstract class AppConfig { static bool hideRedactedEvents = false; static bool hideUnknownEvents = true; static bool hideUnimportantStateEvents = true; - static bool separateChatTypes = false; static bool autoplayImages = true; static bool sendTypingNotifications = true; static bool sendPublicReadReceipts = true; diff --git a/lib/config/routes.dart b/lib/config/routes.dart index d9c7a70f7..75e01fcf0 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -136,12 +136,8 @@ abstract class AppRoutes { FluffyThemes.isColumnMode(context) && state.fullPath?.startsWith('/rooms/settings') == false ? TwoColumnLayout( - displayNavigationRail: - state.path?.startsWith('/rooms/settings') != true, mainView: ChatList( activeChat: state.pathParameters['roomid'], - displayNavigationRail: - state.path?.startsWith('/rooms/settings') != true, ), sideView: child, ) @@ -284,7 +280,6 @@ abstract class AppRoutes { ? TwoColumnLayout( mainView: const Settings(), sideView: child, - displayNavigationRail: false, ) : child, ), diff --git a/lib/config/setting_keys.dart b/lib/config/setting_keys.dart index 7c0e50df8..5b795b08e 100644 --- a/lib/config/setting_keys.dart +++ b/lib/config/setting_keys.dart @@ -4,7 +4,6 @@ abstract class SettingKeys { static const String hideUnknownEvents = 'chat.fluffy.hideUnknownEvents'; static const String hideUnimportantStateEvents = 'chat.fluffy.hideUnimportantStateEvents'; - static const String separateChatTypes = 'chat.fluffy.separateChatTypes'; static const String sentry = 'sentry'; static const String theme = 'theme'; static const String amoledEnabled = 'amoled_enabled'; diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index ec34a5b81..b5868b1ef 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -3,10 +3,9 @@ import 'dart:io'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat/send_file_dialog.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; -import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; @@ -14,6 +13,7 @@ import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/widgets/subscription/subscription_snackbar.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/tor_stub.dart' if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; @@ -24,6 +24,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_shortcuts/flutter_shortcuts.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:uni_links/uni_links.dart'; @@ -39,7 +40,6 @@ import '../../widgets/matrix.dart'; enum SelectMode { normal, share, - select, } enum PopupMenuAction { @@ -53,19 +53,32 @@ enum PopupMenuAction { enum ActiveFilter { allChats, + unread, groups, - messages, spaces, } +extension LocalizedActiveFilter on ActiveFilter { + String toLocalizedString(BuildContext context) { + switch (this) { + case ActiveFilter.allChats: + return L10n.of(context)!.all; + case ActiveFilter.unread: + return L10n.of(context)!.unread; + case ActiveFilter.groups: + return L10n.of(context)!.groups; + case ActiveFilter.spaces: + return L10n.of(context)!.spaces; + } + } +} + class ChatList extends StatefulWidget { static BuildContext? contextForVoip; - final bool displayNavigationRail; final String? activeChat; const ChatList({ super.key, - this.displayNavigationRail = false, required this.activeChat, }); @@ -81,113 +94,238 @@ class ChatListController extends State StreamSubscription? _intentUriStreamSubscription; - bool get displayNavigationBar => - !FluffyThemes.isColumnMode(context) && - (spaces.isNotEmpty || AppConfig.separateChatTypes); + void createNewSpace() { + context.push('/rooms/newspace'); + } - String? activeSpaceId; + ActiveFilter activeFilter = ActiveFilter.allChats; - void resetActiveSpaceId() { - setState(() { - selectedRoomIds.clear(); - activeSpaceId = null; - //#Pangea - context.go("/rooms"); - //Pangea# - }); - } + String? _activeSpaceId; + String? get activeSpaceId => _activeSpaceId; - void setActiveSpace(String? spaceId) { - setState(() { - selectedRoomIds.clear(); - activeSpaceId = spaceId; - activeFilter = ActiveFilter.spaces; + void setActiveSpace(String spaceId) => setState(() { + _activeSpaceId = spaceId; + }); + void clearActiveSpace() => setState(() { + _activeSpaceId = null; + }); + + void addChatAction() async { + if (activeSpaceId == null) { // #Pangea - // don't show all spaces view if in column mode - if (spaceId == null && FluffyThemes.isColumnMode(context)) { - activeFilter = ActiveFilter.allChats; - } + // context.go('/rooms/newprivatechat'); + context.go('/rooms/newgroup'); // Pangea# - }); + return; + } + + final roomType = await showConfirmationDialog( + context: context, + title: L10n.of(context)!.addChatOrSubSpace, + actions: [ + AlertDialogAction( + key: AddRoomType.subspace, + label: L10n.of(context)!.createNewSpace, + ), + AlertDialogAction( + key: AddRoomType.chat, + label: L10n.of(context)!.createGroup, + ), + ], + ); + if (roomType == null) return; + + final names = await showTextInputDialog( + context: context, + title: roomType == AddRoomType.subspace + ? L10n.of(context)!.createNewSpace + : L10n.of(context)!.createGroup, + textFields: [ + DialogTextField( + hintText: roomType == AddRoomType.subspace + ? L10n.of(context)!.spaceName + : L10n.of(context)!.groupName, + minLines: 1, + maxLines: 1, + maxLength: 64, + validator: (text) { + if (text == null || text.isEmpty) { + return L10n.of(context)!.pleaseChoose; + } + return null; + }, + ), + DialogTextField( + hintText: L10n.of(context)!.chatDescription, + minLines: 4, + maxLines: 8, + maxLength: 255, + ), + ], + okLabel: L10n.of(context)!.create, + cancelLabel: L10n.of(context)!.cancel, + ); + if (names == null) return; + final client = Matrix.of(context).client; + final result = await showFutureLoadingDialog( + context: context, + future: () async { + late final String roomId; + final activeSpace = client.getRoomById(activeSpaceId!)!; + await activeSpace.postLoad(); + + if (roomType == AddRoomType.subspace) { + roomId = await client.createSpace( + name: names.first, + topic: names.last.isEmpty ? null : names.last, + visibility: activeSpace.joinRules == JoinRules.public + ? sdk.Visibility.public + : sdk.Visibility.private, + ); + } else { + roomId = await client.createGroupChat( + groupName: names.first, + preset: activeSpace.joinRules == JoinRules.public + ? CreateRoomPreset.publicChat + : CreateRoomPreset.privateChat, + visibility: activeSpace.joinRules == JoinRules.public + ? sdk.Visibility.public + : sdk.Visibility.private, + initialState: names.length > 1 && names.last.isNotEmpty + ? [ + sdk.StateEvent( + type: sdk.EventTypes.RoomTopic, + content: {'topic': names.last}, + ), + ] + : null, + ); + } + await activeSpace.setSpaceChild(roomId); + }, + ); + if (result.error != null) return; } - void createNewSpace() async { - final spaceId = await context.push('/rooms/newspace'); - if (spaceId != null) { - setActiveSpace(spaceId); + void onChatTap(Room room, BuildContext context) async { + if (room.isSpace) { + setActiveSpace(room.id); + return; + } + if (room.membership == Membership.invite) { + final inviterId = + room.getState(EventTypes.RoomMember, room.client.userID!)?.senderId; + final inviteAction = await showModalActionSheet( + context: context, + message: room.isDirectChat + ? L10n.of(context)!.invitePrivateChat + : L10n.of(context)!.inviteGroupChat, + title: room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), + actions: [ + SheetAction( + key: InviteActions.accept, + label: L10n.of(context)!.accept, + icon: Icons.check_outlined, + isDefaultAction: true, + ), + SheetAction( + key: InviteActions.decline, + label: L10n.of(context)!.decline, + icon: Icons.close_outlined, + isDestructiveAction: true, + ), + SheetAction( + key: InviteActions.block, + label: L10n.of(context)!.block, + icon: Icons.block_outlined, + isDestructiveAction: true, + ), + ], + ); + if (inviteAction == null) return; + if (inviteAction == InviteActions.block) { + context.go('/rooms/settings/security/ignorelist', extra: inviterId); + return; + } + if (inviteAction == InviteActions.decline) { + await showFutureLoadingDialog( + context: context, + future: room.leave, + ); + return; + } + final joinResult = await showFutureLoadingDialog( + context: context, + future: () async { + final waitForRoom = room.client.waitForRoomInSync( + room.id, + join: true, + ); + await room.join(); + await waitForRoom; + }, + ); + if (joinResult.error != null) return; } - } - int get selectedIndex { - switch (activeFilter) { - case ActiveFilter.allChats: - case ActiveFilter.messages: - return 0; - case ActiveFilter.groups: - return 1; - case ActiveFilter.spaces: - return AppConfig.separateChatTypes ? 2 : 1; + if (room.membership == Membership.ban) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context)!.youHaveBeenBannedFromThisChat), + ), + ); + return; } - } - // #Pangea - bool isSelected(int i) { - if (activeFilter == ActiveFilter.spaces && activeSpaceId != null) { - return false; + if (room.membership == Membership.leave) { + context.go('/rooms/archive/${room.id}'); + return; } - return i == selectedIndex; - } - // Pangea# - ActiveFilter getActiveFilterByDestination(int? i) { - switch (i) { - case 1: - if (AppConfig.separateChatTypes) { - return ActiveFilter.groups; + // Share content into this room + final shareContent = Matrix.of(context).shareContent; + if (shareContent != null) { + final shareFile = shareContent.tryGet('file'); + if (shareContent.tryGet('msgtype') == 'chat.fluffy.shared_file' && + shareFile != null) { + await showDialog( + context: context, + useRootNavigator: false, + builder: (c) => SendFileDialog( + files: [shareFile], + room: room, + ), + ); + Matrix.of(context).shareContent = null; + } else { + final consent = await showOkCancelAlertDialog( + context: context, + title: L10n.of(context)!.forward, + message: L10n.of(context)!.forwardMessageTo( + room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), + ), + okLabel: L10n.of(context)!.forward, + cancelLabel: L10n.of(context)!.cancel, + ); + if (consent == OkCancelResult.cancel) { + Matrix.of(context).shareContent = null; + return; } - return ActiveFilter.spaces; - case 2: - return ActiveFilter.spaces; - case 0: - default: - if (AppConfig.separateChatTypes) { - return ActiveFilter.messages; + if (consent == OkCancelResult.ok) { + room.sendEvent(shareContent); + Matrix.of(context).shareContent = null; } - return ActiveFilter.allChats; - } - } - - void onDestinationSelected(int? i) { - setState(() { - // #Pangea - debugPrint('onDestinationSelected $i'); - // Pangea# - selectedRoomIds.clear(); - activeFilter = getActiveFilterByDestination(i); - // #Pangea - if (activeFilter != ActiveFilter.spaces) { - activeSpaceId = null; } - // Pangea# - }); - // #Pangea - final bool clickedAllSpaces = (!AppConfig.separateChatTypes && i == 1) || - (AppConfig.separateChatTypes && i == 2); - if (clickedAllSpaces) { - setActiveSpace(null); } - // Pangea# - } - ActiveFilter activeFilter = AppConfig.separateChatTypes - ? ActiveFilter.messages - : ActiveFilter.allChats; + context.go('/rooms/${room.id}'); + } bool Function(Room) getRoomFilterByActiveFilter(ActiveFilter activeFilter) { switch (activeFilter) { case ActiveFilter.allChats: return (room) => - !room.isSpace // #Pangea + true // #Pangea && !room.isAnalyticsRoom; // Pangea#; @@ -198,24 +336,21 @@ class ChatListController extends State && !room.isAnalyticsRoom; // Pangea#; - case ActiveFilter.messages: + case ActiveFilter.unread: return (room) => - !room.isSpace && - room.isDirectChat // #Pangea + room.isUnreadOrInvited // #Pangea && !room.isAnalyticsRoom; // Pangea#; case ActiveFilter.spaces: - return (r) => r.isSpace; + return (room) => room.isSpace; } } List get filteredRooms => Matrix.of(context) .client .rooms - .where( - getRoomFilterByActiveFilter(activeFilter), - ) + .where(getRoomFilterByActiveFilter(activeFilter)) .toList(); bool isSearchMode = false; @@ -227,9 +362,6 @@ class ChatListController extends State bool isSearching = false; static const String _serverStoreNamespace = 'im.fluffychat.search.server'; - //#Pangea - final PangeaController pangeaController = MatrixState.pangeaController; - //Pangea# void setServer() async { final newServer = await showTextInputDialog( @@ -387,15 +519,11 @@ class ChatListController extends State List get spaces => Matrix.of(context).client.rooms.where((r) => r.isSpace).toList(); - final selectedRoomIds = {}; - String? get activeChat => widget.activeChat; SelectMode get selectMode => Matrix.of(context).shareContent != null ? SelectMode.share - : selectedRoomIds.isEmpty - ? SelectMode.normal - : SelectMode.select; + : SelectMode.normal; void _processIncomingSharedFiles(List files) { if (files.isEmpty) return; @@ -444,15 +572,12 @@ class ChatListController extends State // For sharing images coming from outside the app while the app is closed ReceiveSharingIntent.getInitialMedia().then(_processIncomingSharedFiles); - // #Pangea - // dependency is outdated and prevents app from building - // // For sharing or opening urls/text coming from outside the app while the app is in the memory - // _intentDataStreamSubscription = ReceiveSharingIntent.getTextStream() - // .listen(_processIncomingSharedText, onError: print); + // For sharing or opening urls/text coming from outside the app while the app is in the memory + _intentDataStreamSubscription = ReceiveSharingIntent.getTextStream() + .listen(_processIncomingSharedText, onError: print); - // // For sharing or opening urls/text coming from outside the app while the app is closed - // ReceiveSharingIntent.getInitialText().then(_processIncomingSharedText); - // Pangea# + // For sharing or opening urls/text coming from outside the app while the app is closed + ReceiveSharingIntent.getInitialText().then(_processIncomingSharedText); // For receiving shared Uris _intentUriStreamSubscription = linkStream.listen(_processIncomingUris); @@ -504,8 +629,8 @@ class ChatListController extends State _checkTorBrowser(); //#Pangea - classStream = pangeaController.classController.stateStream.listen((event) { - // if (event["activeSpaceId"] != null && mounted) { + classStream = MatrixState.pangeaController.classController.stateStream + .listen((event) { if (mounted) { setActiveSpace(event["activeSpaceId"]); if (event["activeSpaceId"] != null) { @@ -514,26 +639,27 @@ class ChatListController extends State } }); - _invitedSpaceSubscription = pangeaController - .matrixState.client.onSync.stream + _invitedSpaceSubscription = MatrixState + .pangeaController.matrixState.client.onSync.stream .where((event) => event.rooms?.invite != null) .listen((event) async { for (final inviteEntry in event.rooms!.invite!.entries) { if (inviteEntry.value.inviteState == null) continue; - final bool isSpace = inviteEntry.value.inviteState!.any( + final isSpace = inviteEntry.value.inviteState!.any( (event) => event.type == EventTypes.RoomCreate && event.content['type'] == 'm.space', ); - final bool isAnalytics = inviteEntry.value.inviteState!.any( + final isAnalytics = inviteEntry.value.inviteState!.any( (event) => event.type == EventTypes.RoomCreate && event.content['type'] == PangeaRoomTypes.analytics, ); if (isSpace) { - final String spaceId = inviteEntry.key; - final Room? space = pangeaController.matrixState.client.getRoomById( + final spaceId = inviteEntry.key; + final space = + MatrixState.pangeaController.matrixState.client.getRoomById( spaceId, ); if (space != null) { @@ -546,8 +672,8 @@ class ChatListController extends State } if (isAnalytics) { - final Room? analyticsRoom = - pangeaController.matrixState.client.getRoomById(inviteEntry.key); + final analyticsRoom = MatrixState.pangeaController.matrixState.client + .getRoomById(inviteEntry.key); try { await analyticsRoom?.join(); } catch (err, s) { @@ -562,8 +688,8 @@ class ChatListController extends State } }); - _subscriptionStatusStream ??= pangeaController - .subscriptionController.subscriptionStream.stream + _subscriptionStatusStream ??= MatrixState + .pangeaController.subscriptionController.subscriptionStream.stream .listen((event) { if (mounted) { showSubscribedSnackbar(context); @@ -573,7 +699,7 @@ class ChatListController extends State // listen for space child updates for any space that is not the active space // so that when the user navigates to the space that was updated, it will // reload any rooms that have been added / removed - final client = pangeaController.matrixState.client; + final client = MatrixState.pangeaController.matrixState.client; _spaceChildSubscription ??= client.onRoomState.stream.where((u) { return u.state.type == EventTypes.SpaceChild && u.roomId != activeSpaceId; }).listen((update) { @@ -599,133 +725,69 @@ class ChatListController extends State super.dispose(); } - // #Pangea - final StreamController selectionsStream = - StreamController.broadcast(); - // Pangea# - - void toggleSelection(String roomId) { - // #Pangea - // setState( - // () => selectedRoomIds.contains(roomId) - // ? selectedRoomIds.remove(roomId) - // : selectedRoomIds.add(roomId), - // ); - selectedRoomIds.contains(roomId) - ? selectedRoomIds.remove(roomId) - : selectedRoomIds.add(roomId); - selectionsStream.add(roomId); - // Pangea# - } - - Future toggleUnread() async { - await showFutureLoadingDialog( + void chatContextAction(Room room) async { + final action = await showModalActionSheet( context: context, - future: () async { - final markUnread = anySelectedRoomNotMarkedUnread; - final client = Matrix.of(context).client; - for (final roomId in selectedRoomIds) { - final room = client.getRoomById(roomId)!; - if (room.markedUnread == markUnread) continue; - await client.getRoomById(roomId)!.markUnread(markUnread); - } - }, + title: room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), + actions: [ + SheetAction( + key: ChatContextAction.markUnread, + icon: room.markedUnread + ? Icons.mark_as_unread + : Icons.mark_as_unread_outlined, + label: room.markedUnread + ? L10n.of(context)!.markAsRead + : L10n.of(context)!.unread, + ), + SheetAction( + key: ChatContextAction.favorite, + icon: room.isFavourite ? Icons.pin : Icons.pin_outlined, + label: room.isFavourite + ? L10n.of(context)!.unpin + : L10n.of(context)!.pin, + ), + SheetAction( + key: ChatContextAction.mute, + icon: room.pushRuleState == PushRuleState.notify + ? Icons.notifications_off_outlined + : Icons.notifications, + label: room.pushRuleState == PushRuleState.notify + ? L10n.of(context)!.muteChat + : L10n.of(context)!.unmuteChat, + ), + SheetAction( + isDestructiveAction: true, + key: ChatContextAction.leave, + icon: Icons.delete_outlined, + label: L10n.of(context)!.leave, + ), + ], ); - cancelAction(); - } - Future toggleFavouriteRoom() async { - await showFutureLoadingDialog( - context: context, - future: () async { - final makeFavorite = anySelectedRoomNotFavorite; - final client = Matrix.of(context).client; - for (final roomId in selectedRoomIds) { - final room = client.getRoomById(roomId)!; - if (room.isFavourite == makeFavorite) continue; - await client.getRoomById(roomId)!.setFavourite(makeFavorite); - } - }, - ); - cancelAction(); - } + if (action == null) return; + if (!mounted) return; - Future toggleMuted() async { await showFutureLoadingDialog( context: context, - future: () async { - final newState = anySelectedRoomNotMuted - ? PushRuleState.mentionsOnly - : PushRuleState.notify; - final client = Matrix.of(context).client; - for (final roomId in selectedRoomIds) { - final room = client.getRoomById(roomId)!; - if (room.pushRuleState == newState) continue; - await client.getRoomById(roomId)!.setPushRuleState(newState); + future: () { + switch (action) { + case ChatContextAction.favorite: + return room.setFavourite(!room.isFavourite); + case ChatContextAction.markUnread: + return room.markUnread(!room.markedUnread); + case ChatContextAction.mute: + return room.setPushRuleState( + room.pushRuleState == PushRuleState.notify + ? PushRuleState.mentionsOnly + : PushRuleState.notify, + ); + case ChatContextAction.leave: + return room.leave(); } }, ); - cancelAction(); } - Future archiveAction() async { - final confirmed = await showOkCancelAlertDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.cancel, - message: L10n.of(context)!.archiveRoomDescription, - ) == - OkCancelResult.ok; - if (!confirmed) return; - // #Pangea - final bool archivedActiveRoom = - selectedRoomIds.contains(Matrix.of(context).activeRoomId); - // Pangea# - await showFutureLoadingDialog( - context: context, - future: () => _archiveSelectedRooms(), - ); - // #Pangea - // setState(() {}); - if (archivedActiveRoom) { - context.go('/rooms'); - } - // Pangea# - } - - // #Pangea - Future leaveAction() async { - final bool onlyAdmin = await Matrix.of(context) - .client - .getRoomById(selectedRoomIds.first) - ?.isOnlyAdmin() ?? - false; - final confirmed = await showOkCancelAlertDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.cancel, - message: onlyAdmin && selectedRoomIds.length == 1 - ? L10n.of(context)!.onlyAdminDescription - : L10n.of(context)!.leaveRoomDescription, - ) == - OkCancelResult.ok; - if (!confirmed) return; - final bool leftActiveRoom = - selectedRoomIds.contains(Matrix.of(context).activeRoomId); - await showFutureLoadingDialog( - context: context, - future: () => _leaveSelectedRooms(onlyAdmin), - ); - if (leftActiveRoom) { - context.go('/rooms'); - } - } - // Pangea# - void dismissStatusList() async { final result = await showOkCancelAlertDialog( title: L10n.of(context)!.hidePresences, @@ -770,140 +832,6 @@ class ChatListController extends State ); } - Future _archiveSelectedRooms() async { - final client = Matrix.of(context).client; - while (selectedRoomIds.isNotEmpty) { - final roomId = selectedRoomIds.first; - try { - // #Pangea - // await client.getRoomById(roomId)!.leave(); - await client.getRoomById(roomId)!.archive(); - // Pangea# - } finally { - toggleSelection(roomId); - } - } - } - - // #Pangea - Future _leaveSelectedRooms(bool onlyAdmin) async { - final client = Matrix.of(context).client; - while (selectedRoomIds.isNotEmpty) { - final roomId = selectedRoomIds.first; - try { - final room = client.getRoomById(roomId); - if (!room!.isSpace && - room.membership == Membership.join && - room.isUnread) { - await room.markUnread(false); - } - onlyAdmin ? await room.archive() : await room.leave(); - } finally { - toggleSelection(roomId); - } - } - } - // Pangea# - - Future addToSpace() async { - // #Pangea - final firstSelectedRoom = - Matrix.of(context).client.getRoomById(selectedRoomIds.toList().first); - // Pangea# - final selectedSpace = await showConfirmationDialog( - context: context, - title: L10n.of(context)!.addToSpace, - // #Pangea - // message: L10n.of(context)!.addToSpaceDescription, - message: L10n.of(context)!.addSpaceToSpaceDescription, - // Pangea# - fullyCapitalizedForMaterial: false, - actions: Matrix.of(context) - .client - .rooms - .where( - (r) => - r.isSpace - // #Pangea - && - selectedRoomIds - .map((id) => Matrix.of(context).client.getRoomById(id)) - // Only show non-recursion-causing spaces - // Performs a few other checks as well - .every((e) => r.canAddAsParentOf(e)), - //Pangea# - ) - .map( - (space) => AlertDialogAction( - key: space.id, - // #Pangea - // label: space - // .getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), - label: space.nameIncludingParents(context), - // If user is not admin of space, button is grayed out - textStyle: TextStyle( - color: (firstSelectedRoom == null) - ? Theme.of(context).colorScheme.outline - : Theme.of(context).colorScheme.surfaceTint, - ), - // Pangea# - ), - ) - .toList(), - ); - if (selectedSpace == null) return; - final result = await showFutureLoadingDialog( - context: context, - future: () async { - final space = Matrix.of(context).client.getRoomById(selectedSpace)!; - // #Pangea - if (firstSelectedRoom == null) { - throw L10n.of(context)!.nonexistentSelection; - } - - if (space.canSendDefaultStates) { - for (final roomId in selectedRoomIds) { - await space.pangeaSetSpaceChild(roomId, suggested: true); - } - } - // Pangea# - }, - ); - if (result.error == null) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - // #Pangea - // content: Text(L10n.of(context)!.chatHasBeenAddedToThisSpace), - content: Text(L10n.of(context)!.roomAddedToSpace), - // Pangea# - ), - ); - } - - // #Pangea - // setState(() => selectedRoomIds.clear()); - if (firstSelectedRoom != null) { - toggleSelection(firstSelectedRoom.id); - } - // Pangea# - } - - bool get anySelectedRoomNotMarkedUnread => selectedRoomIds.any( - (roomId) => - !Matrix.of(context).client.getRoomById(roomId)!.markedUnread, - ); - - bool get anySelectedRoomNotFavorite => selectedRoomIds.any( - (roomId) => !Matrix.of(context).client.getRoomById(roomId)!.isFavourite, - ); - - bool get anySelectedRoomNotMuted => selectedRoomIds.any( - (roomId) => - Matrix.of(context).client.getRoomById(roomId)!.pushRuleState == - PushRuleState.notify, - ); - bool waitForFirstSync = false; Future _waitForFirstSync() async { @@ -943,12 +871,13 @@ class ChatListController extends State Future _initPangeaControllers(Client client) async { if (mounted) { GoogleAnalytics.analyticsUserUpdate(client.userID); - pangeaController.startChatWithBotIfNotPresent(); - await pangeaController.subscriptionController.initialize(); - await pangeaController.myAnalytics.initialize(); - pangeaController.afterSyncAndFirstLoginInitialization(context); - await pangeaController.inviteBotToExistingSpaces(); - await pangeaController.setPangeaPushRules(); + MatrixState.pangeaController.startChatWithBotIfNotPresent(); + await MatrixState.pangeaController.subscriptionController.initialize(); + await MatrixState.pangeaController.myAnalytics.initialize(); + MatrixState.pangeaController + .afterSyncAndFirstLoginInitialization(context); + await MatrixState.pangeaController.inviteBotToExistingSpaces(); + await MatrixState.pangeaController.setPangeaPushRules(); client.migrateAnalyticsRooms(); } else { ErrorHandler.logError( @@ -961,24 +890,20 @@ class ChatListController extends State void cancelAction() { if (selectMode == SelectMode.share) { setState(() => Matrix.of(context).shareContent = null); - } else { - // #Pangea - // setState(() => selectedRoomIds.clear()); - for (final roomId in selectedRoomIds.toList()) { - toggleSelection(roomId); - } - // Pangea# } } + void setActiveFilter(ActiveFilter filter) { + setState(() { + activeFilter = filter; + }); + } + void setActiveClient(Client client) { context.go('/rooms'); setState(() { - activeFilter = AppConfig.separateChatTypes - ? ActiveFilter.messages - : ActiveFilter.allChats; - activeSpaceId = null; - selectedRoomIds.clear(); + activeFilter = ActiveFilter.allChats; + _activeSpaceId = null; Matrix.of(context).setActiveClient(client); }); _clientStream.add(client); @@ -987,7 +912,7 @@ class ChatListController extends State void setActiveBundle(String bundle) { context.go('/rooms'); setState(() { - selectedRoomIds.clear(); + _activeSpaceId = null; Matrix.of(context).activeBundle = bundle; if (!Matrix.of(context) .currentBundle! @@ -1080,3 +1005,135 @@ class ChatListController extends State } enum EditBundleAction { addToBundle, removeFromBundle } + +enum InviteActions { + accept, + decline, + block, +} + +enum AddRoomType { chat, subspace } + +enum ChatContextAction { + favorite, + markUnread, + mute, + leave, +} + + +// TODO re-integrate this logic + // // #Pangea + // Future leaveAction() async { + // final onlyAdmin = await Matrix.of(context) + // .client + // .getRoomById(selectedRoomIds.first) + // ?.isOnlyAdmin() ?? + // false; + // final confirmed = await showOkCancelAlertDialog( + // useRootNavigator: false, + // context: context, + // title: L10n.of(context)!.areYouSure, + // okLabel: L10n.of(context)!.yes, + // cancelLabel: L10n.of(context)!.cancel, + // message: onlyAdmin && selectedRoomIds.length == 1 + // ? L10n.of(context)!.onlyAdminDescription + // : L10n.of(context)!.leaveRoomDescription, + // ) == + // OkCancelResult.ok; + // if (!confirmed) return; + // final leftActiveRoom = + // selectedRoomIds.contains(Matrix.of(context).activeRoomId); + // await showFutureLoadingDialog( + // context: context, + // future: () => _leaveSelectedRooms(onlyAdmin), + // ); + // if (leftActiveRoom) { + // context.go('/rooms'); + // } + // } + // // Pangea# + + // Future addToSpace() async { + // // #Pangea + // final firstSelectedRoom = + // Matrix.of(context).client.getRoomById(selectedRoomIds.toList().first); + // // Pangea# + // final selectedSpace = await showConfirmationDialog( + // context: context, + // title: L10n.of(context)!.addToSpace, + // // #Pangea + // // message: L10n.of(context)!.addToSpaceDescription, + // message: L10n.of(context)!.addSpaceToSpaceDescription, + // // Pangea# + // fullyCapitalizedForMaterial: false, + // actions: Matrix.of(context) + // .client + // .rooms + // .where( + // (r) => + // r.isSpace + // // #Pangea + // && + // selectedRoomIds + // .map((id) => Matrix.of(context).client.getRoomById(id)) + // // Only show non-recursion-causing spaces + // // Performs a few other checks as well + // .every((e) => r.canAddAsParentOf(e)), + // //Pangea# + // ) + // .map( + // (space) => AlertDialogAction( + // key: space.id, + // // #Pangea + // // label: space + // // .getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), + // label: space.nameIncludingParents(context), + // // If user is not admin of space, button is grayed out + // textStyle: TextStyle( + // color: (firstSelectedRoom == null) + // ? Theme.of(context).colorScheme.outline + // : Theme.of(context).colorScheme.surfaceTint, + // ), + // // Pangea# + // ), + // ) + // .toList(), + // ); + // if (selectedSpace == null) return; + // final result = await showFutureLoadingDialog( + // context: context, + // future: () async { + // final space = Matrix.of(context).client.getRoomById(selectedSpace)!; + // // #Pangea + // if (firstSelectedRoom == null) { + // throw L10n.of(context)!.nonexistentSelection; + // } + + // if (space.canSendDefaultStates) { + // for (final roomId in selectedRoomIds) { + // await space.pangeaSetSpaceChild(roomId, suggested: true); + // } + // } + // // Pangea# + // }, + // ); + // if (result.error == null) { + // if (!mounted) return; + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar( + // // #Pangea + // // content: Text(L10n.of(context)!.chatHasBeenAddedToThisSpace), + // content: Text(L10n.of(context)!.roomAddedToSpace), + // // Pangea# + // ), + // ); + // } + + // // #Pangea + // // setState(() => selectedRoomIds.clear()); + // if (firstSelectedRoom != null) { + // toggleSelection(firstSelectedRoom.id); + // } + // // Pangea# + // } diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index b9b388da9..fda313141 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -1,11 +1,11 @@ -import 'package:animations/animations.dart'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pages/chat_list/chat_list_header.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/space_view.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; import 'package:fluffychat/pangea/widgets/chat_list/chat_list_body_text.dart'; -import 'package:fluffychat/pangea/widgets/chat_list/chat_list_header_wrapper.dart'; -import 'package:fluffychat/pangea/widgets/chat_list/chat_list_item_wrapper.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; @@ -25,6 +25,17 @@ class ChatListViewBody extends StatelessWidget { @override Widget build(BuildContext context) { + final activeSpace = controller.activeSpaceId; + if (activeSpace != null) { + return SpaceView( + spaceId: activeSpace, + onBack: controller.clearActiveSpace, + onChatTab: (room) => controller.onChatTap(room, context), + onChatContext: (room) => controller.chatContextAction(room), + activeChat: controller.activeChat, + toParentSpace: controller.setActiveSpace, + ); + } final publicRooms = controller.roomSearchResult?.chunk .where((room) => room.roomType != 'm.space') .toList(); @@ -39,242 +50,290 @@ class ChatListViewBody extends StatelessWidget { final subtitleColor = Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(50); final filter = controller.searchController.text.toLowerCase(); - return PageTransitionSwitcher( - transitionBuilder: ( - Widget child, - Animation primaryAnimation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.vertical, - fillColor: Theme.of(context).scaffoldBackgroundColor, - child: child, - ); - }, - child: StreamBuilder( - key: ValueKey( - client.userID.toString() + - controller.activeFilter.toString() + - controller.activeSpaceId.toString(), - ), - stream: client.onSync.stream - .where((s) => s.hasRoomUpdate) - .rateLimit(const Duration(seconds: 1)), - builder: (context, _) { - if (controller.activeFilter == ActiveFilter.spaces) { - return SpaceView( - controller, - scrollController: controller.scrollController, - key: Key(controller.activeSpaceId ?? 'Spaces'), - ); + return StreamBuilder( + key: ValueKey( + client.userID.toString(), + ), + stream: client.onSync.stream + .where((s) => s.hasRoomUpdate) + .rateLimit(const Duration(seconds: 1)), + builder: (context, _) { + final rooms = controller.filteredRooms; + + final spaces = rooms.where((r) => r.isSpace); + final spaceDelegateCandidates = {}; + for (final space in spaces) { + spaceDelegateCandidates[space.id] = space; + for (final spaceChild in space.spaceChildren) { + final roomId = spaceChild.roomId; + if (roomId == null) continue; + spaceDelegateCandidates[roomId] = space; } - final rooms = controller.filteredRooms; - return SafeArea( - child: CustomScrollView( - controller: controller.scrollController, - slivers: [ - // #Pangea - // ChatListHeader(controller: controller), - ChatListHeaderWrapper(controller: controller), - // Pangea# - SliverList( - delegate: SliverChildListDelegate( - [ - if (controller.isSearchMode) ...[ - SearchTitle( - title: L10n.of(context)!.publicRooms, - icon: const Icon(Icons.explore_outlined), - ), - PublicRoomsHorizontalList(publicRooms: publicRooms), - SearchTitle( - title: L10n.of(context)!.publicSpaces, - icon: const Icon(Icons.workspaces_outlined), - ), - PublicRoomsHorizontalList(publicRooms: publicSpaces), - SearchTitle( - title: L10n.of(context)!.users, - icon: const Icon(Icons.group_outlined), + } + final spaceDelegates = {}; + + return SafeArea( + child: CustomScrollView( + controller: controller.scrollController, + slivers: [ + ChatListHeader(controller: controller), + SliverList( + delegate: SliverChildListDelegate( + [ + if (controller.isSearchMode) ...[ + SearchTitle( + title: L10n.of(context)!.publicRooms, + icon: const Icon(Icons.explore_outlined), + ), + PublicRoomsHorizontalList(publicRooms: publicRooms), + SearchTitle( + title: L10n.of(context)!.publicSpaces, + icon: const Icon(Icons.workspaces_outlined), + ), + PublicRoomsHorizontalList(publicRooms: publicSpaces), + SearchTitle( + title: L10n.of(context)!.users, + icon: const Icon(Icons.group_outlined), + ), + AnimatedContainer( + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + height: userSearchResult == null || + userSearchResult.results.isEmpty + ? 0 + : 106, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + child: userSearchResult == null + ? null + : ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: userSearchResult.results.length, + itemBuilder: (context, i) => _SearchItem( + title: + userSearchResult.results[i].displayName ?? + userSearchResult + .results[i].userId.localpart ?? + L10n.of(context)!.unknownDevice, + avatar: userSearchResult.results[i].avatarUrl, + onPressed: () => showAdaptiveBottomSheet( + context: context, + builder: (c) => UserBottomSheet( + profile: userSearchResult.results[i], + outerContext: context, + ), + ), + ), + ), + ), + ], + // #Pangea + // if (!controller.isSearchMode && AppConfig.showPresences) + // GestureDetector( + // onLongPress: () => controller.dismissStatusList(), + // child: StatusMessageList( + // onStatusEdit: controller.setStatus, + // ), + // ), + // Pangea# + const ConnectionStatusHeader(), + AnimatedContainer( + height: controller.isTorBrowser ? 64 : 0, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + child: Material( + color: Theme.of(context).colorScheme.surface, + child: ListTile( + leading: const Icon(Icons.vpn_key), + title: Text(L10n.of(context)!.dehydrateTor), + subtitle: Text(L10n.of(context)!.dehydrateTorLong), + trailing: const Icon(Icons.chevron_right_outlined), + onTap: controller.dehydrate, ), - AnimatedContainer( - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - height: userSearchResult == null || - userSearchResult.results.isEmpty - ? 0 - : 106, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - child: userSearchResult == null - ? null - : ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: userSearchResult.results.length, - itemBuilder: (context, i) => _SearchItem( - title: userSearchResult - .results[i].displayName ?? - userSearchResult - .results[i].userId.localpart ?? - L10n.of(context)!.unknownDevice, - avatar: - userSearchResult.results[i].avatarUrl, - onPressed: () => showAdaptiveBottomSheet( - context: context, - builder: (c) => UserBottomSheet( - profile: userSearchResult.results[i], - outerContext: context, + ), + ), + if (client.rooms.isNotEmpty && !controller.isSearchMode) + SizedBox( + height: 44, + child: ListView( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 6, + ), + shrinkWrap: true, + scrollDirection: Axis.horizontal, + children: ActiveFilter.values + .map( + (filter) => Padding( + padding: + const EdgeInsets.symmetric(horizontal: 4), + child: InkWell( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + onTap: () => + controller.setActiveFilter(filter), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: filter == controller.activeFilter + ? Theme.of(context) + .colorScheme + .primary + : Theme.of(context) + .colorScheme + .secondaryContainer, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + ), + alignment: Alignment.center, + child: Text( + filter.toLocalizedString(context), + style: TextStyle( + fontWeight: + filter == controller.activeFilter + ? FontWeight.bold + : FontWeight.normal, + color: + filter == controller.activeFilter + ? Theme.of(context) + .colorScheme + .onPrimary + : Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), ), ), ), ), + ) + .toList(), ), - ], + ), + if (controller.isSearchMode) + SearchTitle( + title: L10n.of(context)!.chats, + icon: const Icon(Icons.forum_outlined), + ), + if (client.prevBatch != null && + rooms.isEmpty && + !controller.isSearchMode) ...[ // #Pangea - // if (!controller.isSearchMode && - // controller.activeFilter != ActiveFilter.groups && - // AppConfig.showPresences) - // GestureDetector( - // onLongPress: () => controller.dismissStatusList(), - // child: StatusMessageList( - // onStatusEdit: controller.setStatus, - // ), - // ), - // Pangea# - const ConnectionStatusHeader(), - AnimatedContainer( - height: controller.isTorBrowser ? 64 : 0, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - child: Material( - color: Theme.of(context).colorScheme.surface, - child: ListTile( - leading: const Icon(Icons.vpn_key), - title: Text(L10n.of(context)!.dehydrateTor), - subtitle: Text(L10n.of(context)!.dehydrateTorLong), - trailing: const Icon(Icons.chevron_right_outlined), - onTap: controller.dehydrate, - ), + // Padding( + // padding: const EdgeInsets.all(32.0), + // child: Icon( + // CupertinoIcons.chat_bubble_2, + // size: 128, + // color: Theme.of(context).colorScheme.secondary, + // ), + // ), + Center( + child: ChatListBodyStartText( + controller: controller, ), ), - if (controller.isSearchMode) - SearchTitle( - title: L10n.of(context)!.chats, - icon: const Icon(Icons.forum_outlined), - ), - if (client.prevBatch != null && - rooms.isEmpty && - !controller.isSearchMode) ...[ - // #Pangea - // Padding( - // padding: const EdgeInsets.all(32.0), - // child: Icon( - // CupertinoIcons.chat_bubble_2, - // size: 128, - // color: - // Theme.of(context).colorScheme.onInverseSurface, - // ), - // ), - Center( - child: ChatListBodyStartText( - controller: controller, - ), - ), - // Pangea# - ], + // Pangea# ], - ), + ], ), - if (client.prevBatch == null) - SliverList( - delegate: SliverChildBuilderDelegate( - (context, i) => Opacity( - opacity: (dummyChatCount - i) / dummyChatCount, - child: ListTile( - leading: CircleAvatar( - backgroundColor: titleColor, - child: CircularProgressIndicator( - strokeWidth: 1, - color: - Theme.of(context).textTheme.bodyLarge!.color, - ), + ), + if (client.prevBatch == null) + SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => Opacity( + opacity: (dummyChatCount - i) / dummyChatCount, + child: ListTile( + leading: CircleAvatar( + backgroundColor: titleColor, + child: CircularProgressIndicator( + strokeWidth: 1, + color: Theme.of(context).textTheme.bodyLarge!.color, ), - title: Row( - children: [ - Expanded( - child: Container( - height: 14, - decoration: BoxDecoration( - color: titleColor, - borderRadius: BorderRadius.circular(3), - ), - ), - ), - const SizedBox(width: 36), - Container( + ), + title: Row( + children: [ + Expanded( + child: Container( height: 14, - width: 14, decoration: BoxDecoration( - color: subtitleColor, - borderRadius: BorderRadius.circular(14), + color: titleColor, + borderRadius: BorderRadius.circular(3), ), ), - const SizedBox(width: 12), - Container( - height: 14, - width: 14, - decoration: BoxDecoration( - color: subtitleColor, - borderRadius: BorderRadius.circular(14), - ), + ), + const SizedBox(width: 36), + Container( + height: 14, + width: 14, + decoration: BoxDecoration( + color: subtitleColor, + borderRadius: BorderRadius.circular(14), ), - ], - ), - subtitle: Container( - decoration: BoxDecoration( - color: subtitleColor, - borderRadius: BorderRadius.circular(3), ), - height: 12, - margin: const EdgeInsets.only(right: 22), + const SizedBox(width: 12), + Container( + height: 14, + width: 14, + decoration: BoxDecoration( + color: subtitleColor, + borderRadius: BorderRadius.circular(14), + ), + ), + ], + ), + subtitle: Container( + decoration: BoxDecoration( + color: subtitleColor, + borderRadius: BorderRadius.circular(3), ), + height: 12, + margin: const EdgeInsets.only(right: 22), ), ), - childCount: dummyChatCount, ), + childCount: dummyChatCount, ), - if (client.prevBatch != null) - SliverList.builder( - itemCount: rooms.length, - itemBuilder: (BuildContext context, int i) { - // #Pangea - // return ChatListItem( - return ChatListItemWrapper( - controller: controller, - // Pangea# - rooms[i], - key: Key('chat_list_item_${rooms[i].id}'), - filter: filter, - // #Pangea - // selected: - // controller.selectedRoomIds.contains(rooms[i].id), - // onTap: controller.selectMode == SelectMode.select - // ? () => controller.toggleSelection(rooms[i].id) - // : () => onChatTap(rooms[i], context), - // onLongPress: () => - // controller.toggleSelection(rooms[i].id), - // Pangea# - activeChat: controller.activeChat == rooms[i].id, - ); - }, - ), - ], - ), - ); - }, - ), + ), + if (client.prevBatch != null) + SliverList.builder( + itemCount: rooms.length, + itemBuilder: (BuildContext context, int i) { + var room = rooms[i]; + if (controller.activeFilter != ActiveFilter.groups) { + final parent = room.isSpace + ? room + : spaceDelegateCandidates[room.id]; + if (parent != null) { + if (spaceDelegates.contains(parent.id)) { + return const SizedBox.shrink(); + } + spaceDelegates.add(parent.id); + room = parent; + } + } + + return ChatListItem( + room, + lastEventRoom: rooms[i], + key: Key('chat_list_item_${room.id}'), + filter: filter, + onTap: () => controller.onChatTap(room, context), + onLongPress: () => controller.chatContextAction(room), + activeChat: controller.activeChat == room.id, + ); + }, + ), + ], + ), + ); + }, ); } } diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index b60a8b5be..b16711021 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -1,8 +1,6 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_list/client_chooser_button.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -42,24 +40,23 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { L10n.of(context)!.share, key: const ValueKey(SelectMode.share), ) - : selectMode == SelectMode.select - ? Text( - controller.selectedRoomIds.length.toString(), - key: const ValueKey(SelectMode.select), - ) - // #Pangea - : ClientChooserButton(controller), + // #Pangea + : ClientChooserButton(controller), // : TextField( // controller: controller.searchController, // focusNode: controller.searchFocusNode, // textInputAction: TextInputAction.search, - // onChanged: controller.onSearchEnter, + // onChanged: (text) => controller.onSearchEnter( + // text, + // globalSearch: globalSearch, + // ), // decoration: InputDecoration( // fillColor: Theme.of(context).colorScheme.secondaryContainer, - // border: UnderlineInputBorder( + // border: OutlineInputBorder( // borderSide: BorderSide.none, // borderRadius: BorderRadius.circular(99), // ), + // contentPadding: EdgeInsets.zero, // hintText: L10n.of(context)!.searchChatsRooms, // hintStyle: TextStyle( // color: Theme.of(context).colorScheme.onPrimaryContainer, @@ -71,20 +68,17 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { // tooltip: L10n.of(context)!.cancel, // icon: const Icon(Icons.close_outlined), // onPressed: controller.cancelSearch, - // color: Theme.of(context) - // .colorScheme - // .onPrimaryContainer, + // color: Theme.of(context).colorScheme.onPrimaryContainer, // ) // : IconButton( // onPressed: controller.startSearch, // icon: Icon( // Icons.search_outlined, - // color: Theme.of(context) - // .colorScheme - // .onPrimaryContainer, + // color: + // Theme.of(context).colorScheme.onPrimaryContainer, // ), // ), - // suffixIcon: controller.isSearchMode + // suffixIcon: controller.isSearchMode && globalSearch // ? controller.isSearching // ? const Padding( // padding: EdgeInsets.symmetric( @@ -109,10 +103,7 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { // icon: const Icon(Icons.edit_outlined, size: 16), // label: Text( // controller.searchServer ?? - // Matrix.of(context) - // .client - // .homeserver! - // .host, + // Matrix.of(context).client.homeserver!.host, // maxLines: 2, // ), // ) @@ -135,80 +126,7 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { // ), // Pangea# ] - : selectMode == SelectMode.select - ? [ - // #Pangea - // if (controller.spaces.isNotEmpty) - if (controller.spaces.isNotEmpty && - controller.selectedRoomIds.length == 1) - // Pangea# - IconButton( - tooltip: L10n.of(context)!.addToSpace, - icon: const Icon(Icons.workspaces_outlined), - onPressed: controller.addToSpace, - ), - IconButton( - tooltip: L10n.of(context)!.toggleUnread, - icon: Icon( - controller.anySelectedRoomNotMarkedUnread - ? Icons.mark_chat_unread_outlined - : Icons.mark_chat_read_outlined, - ), - onPressed: controller.toggleUnread, - ), - IconButton( - tooltip: L10n.of(context)!.toggleFavorite, - icon: Icon( - controller.anySelectedRoomNotFavorite - ? Icons.push_pin - : Icons.push_pin_outlined, - ), - onPressed: controller.toggleFavouriteRoom, - ), - IconButton( - icon: Icon( - controller.anySelectedRoomNotMuted - ? Icons.notifications_off_outlined - : Icons.notifications_outlined, - ), - tooltip: L10n.of(context)!.toggleMuted, - onPressed: controller.toggleMuted, - ), - // #Pangea - if (controller.selectedRoomIds.length > 1) - IconButton( - icon: const Icon(Icons.arrow_forward), - tooltip: L10n.of(context)!.leave, - onPressed: controller.leaveAction, - ), - if (controller.selectedRoomIds.length == 1 && - !(Matrix.of(context) - .client - .getRoomById(controller.selectedRoomIds.single) - ?.isRoomAdmin ?? - false)) - IconButton( - icon: const Icon(Icons.arrow_forward), - tooltip: L10n.of(context)!.leave, - onPressed: controller.leaveAction, - ), - if (controller.selectedRoomIds.length == 1 && - (Matrix.of(context) - .client - .getRoomById(controller.selectedRoomIds.single) - ?.isRoomAdmin ?? - false)) - // Pangea# - IconButton( - // #Pangea - // icon: const Icon(Icons.delete_outlined), - icon: const Icon(Icons.archive_outlined), - // Pangea# - tooltip: L10n.of(context)!.archive, - onPressed: controller.archiveAction, - ), - ] - : null, + : null, ); } diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 49978e764..cf92ba9cd 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -19,8 +19,8 @@ enum ArchivedRoomAction { delete, rejoin } class ChatListItem extends StatelessWidget { final Room room; + final Room? lastEventRoom; final bool activeChat; - final bool selected; final void Function()? onLongPress; final void Function()? onForget; final void Function() onTap; @@ -29,11 +29,11 @@ class ChatListItem extends StatelessWidget { const ChatListItem( this.room, { this.activeChat = false, - this.selected = false, required this.onTap, this.onLongPress, this.onForget, this.filter, + this.lastEventRoom, super.key, }); @@ -57,10 +57,7 @@ class ChatListItem extends StatelessWidget { if (confirmed == OkCancelResult.cancel) return; await showFutureLoadingDialog( context: context, - // #Pangea - // future: () => room.leave(), - future: () => room.archive(), - // Pangea# + future: () => room.leave(), ); return; } @@ -69,24 +66,23 @@ class ChatListItem extends StatelessWidget { @override Widget build(BuildContext context) { final isMuted = room.pushRuleState != PushRuleState.notify; - final typingText = room.getLocalizedTypingText(context); - final lastEvent = room.lastEvent; + final lastEventRoom = this.lastEventRoom ?? room; + final typingText = lastEventRoom.getLocalizedTypingText(context); + final lastEvent = lastEventRoom.lastEvent; final ownMessage = lastEvent?.senderId == room.client.userID; - final unread = room.isUnread || room.membership == Membership.invite; + final unread = + lastEventRoom.isUnread || lastEventRoom.membership == Membership.invite; final theme = Theme.of(context); final directChatMatrixId = room.directChatMatrixID; final isDirectChat = directChatMatrixId != null; - final unreadBubbleSize = unread || room.hasNewMessages - ? room.notificationCount > 0 + final unreadBubbleSize = unread || lastEventRoom.hasNewMessages + ? lastEventRoom.notificationCount > 0 ? 20.0 : 14.0 : 0.0; - final hasNotifications = room.notificationCount > 0; - final backgroundColor = selected - ? theme.colorScheme.primaryContainer - : activeChat - ? theme.colorScheme.secondaryContainer - : null; + final hasNotifications = lastEventRoom.notificationCount > 0; + final backgroundColor = + activeChat ? theme.colorScheme.secondaryContainer : null; final displayname = room.getLocalizedDisplayname( MatrixLocals(L10n.of(context)!), ); @@ -124,11 +120,11 @@ class ChatListItem extends StatelessWidget { curve: FluffyThemes.animationCurve, scale: hovered ? 1.1 : 1.0, child: Avatar( + borderRadius: room.isSpace + ? BorderRadius.circular(AppConfig.borderRadius / 3) + : null, mxContent: room.avatar, name: displayname, - //#Pangea - littleIcon: room.roomTypeIcon, - // Pangea# presenceUserId: directChatMatrixId, presenceBackgroundColor: backgroundColor, onTap: onLongPress, @@ -141,14 +137,12 @@ class ChatListItem extends StatelessWidget { child: AnimatedScale( duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, - scale: (hovered || selected) ? 1.0 : 0.0, + scale: (hovered) ? 1.0 : 0.0, child: Material( color: backgroundColor, borderRadius: BorderRadius.circular(16), - child: Icon( - selected - ? Icons.check_circle - : Icons.check_circle_outlined, + child: const Icon( + Icons.check_circle_outlined, size: 18, ), ), @@ -188,7 +182,9 @@ class ChatListItem extends StatelessWidget { color: theme.colorScheme.primary, ), ), - if (lastEvent != null && room.membership != Membership.invite) + if (!room.isSpace && + lastEvent != null && + room.membership != Membership.invite) Padding( padding: const EdgeInsets.only(left: 4.0), child: Text( @@ -201,11 +197,30 @@ class ChatListItem extends StatelessWidget { ), ), ), + if (room.isSpace) + const Icon( + Icons.arrow_circle_right_outlined, + size: 18, + ), ], ), subtitle: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ + if (room.isSpace) ...[ + room.id != lastEventRoom.id && + lastEventRoom.isUnreadOrInvited + ? Avatar( + mxContent: lastEventRoom.avatar, + name: lastEventRoom.name, + size: 18, + ) + : const Icon( + Icons.workspaces_outlined, + size: 18, + ), + const SizedBox(width: 4), + ], if (typingText.isEmpty && ownMessage && room.lastEvent!.status.isSending) ...[ @@ -230,71 +245,80 @@ class ChatListItem extends StatelessWidget { ), ), Expanded( - child: typingText.isNotEmpty + child: room.isSpace && !lastEventRoom.isUnreadOrInvited ? Text( - typingText, - style: TextStyle( - color: theme.colorScheme.primary, + L10n.of(context)!.countChatsAndCountParticipants( + room.spaceChildren.length.toString(), + (room.summary.mJoinedMemberCount ?? 1).toString(), ), - maxLines: 1, - softWrap: false, ) - : FutureBuilder( - key: ValueKey( - '${lastEvent?.eventId}_${lastEvent?.type}', - ), - // #Pangea - future: room.lastEvent != null - ? GetChatListItemSubtitle().getSubtitle( - L10n.of(context)!, - room.lastEvent, - MatrixState.pangeaController, - ) - : Future.value(L10n.of(context)!.emptyChat), - // future: needLastEventSender - // ? lastEvent.calcLocalizedBody( - // MatrixLocals(L10n.of(context)!), - // hideReply: true, - // hideEdit: true, - // plaintextBody: true, - // removeMarkdown: true, - // withSenderNamePrefix: !isDirectChat || - // directChatMatrixId != - // room.lastEvent?.senderId, - // ) - // : null, - // Pangea# - initialData: lastEvent?.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), - hideReply: true, - hideEdit: true, - plaintextBody: true, - removeMarkdown: true, - withSenderNamePrefix: !isDirectChat || - directChatMatrixId != - room.lastEvent?.senderId, - ), - builder: (context, snapshot) => Text( - room.membership == Membership.invite - ? isDirectChat - ? L10n.of(context)!.invitePrivateChat - : L10n.of(context)!.inviteGroupChat - : snapshot.data ?? - L10n.of(context)!.emptyChat, - softWrap: false, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: unread || room.hasNewMessages - ? FontWeight.bold - : null, - color: theme.colorScheme.onSurfaceVariant, - decoration: room.lastEvent?.redacted == true - ? TextDecoration.lineThrough - : null, + : typingText.isNotEmpty + ? Text( + typingText, + style: TextStyle( + color: theme.colorScheme.primary, + ), + maxLines: 1, + softWrap: false, + ) + : FutureBuilder( + key: ValueKey( + '${lastEvent?.eventId}_${lastEvent?.type}', + ), + // #Pangea + future: room.lastEvent != null + ? GetChatListItemSubtitle().getSubtitle( + L10n.of(context)!, + room.lastEvent, + MatrixState.pangeaController, + ) + : Future.value(L10n.of(context)!.emptyChat), + // future: needLastEventSender + // ? lastEvent.calcLocalizedBody( + // MatrixLocals(L10n.of(context)!), + // hideReply: true, + // hideEdit: true, + // plaintextBody: true, + // removeMarkdown: true, + // withSenderNamePrefix: (!isDirectChat || + // directChatMatrixId != + // room.lastEvent?.senderId), + // ) + // : null, + // Pangea# + initialData: + lastEvent?.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)!), + hideReply: true, + hideEdit: true, + plaintextBody: true, + removeMarkdown: true, + withSenderNamePrefix: (!isDirectChat || + directChatMatrixId != + room.lastEvent?.senderId), + ), + builder: (context, snapshot) => Text( + room.membership == Membership.invite + ? isDirectChat + ? L10n.of(context)!.invitePrivateChat + : L10n.of(context)!.inviteGroupChat + : snapshot.data ?? + L10n.of(context)!.emptyChat, + softWrap: false, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: + unread || lastEventRoom.hasNewMessages + ? FontWeight.bold + : null, + color: theme.colorScheme.onSurfaceVariant, + decoration: room.lastEvent?.redacted == true + ? TextDecoration.lineThrough + : null, + ), + ), ), - ), - ), ), const SizedBox(width: 8), // #Pangea @@ -315,7 +339,9 @@ class ChatListItem extends StatelessWidget { width: !hasNotifications && !unread && !room.hasNewMessages ? 0 : (unreadBubbleSize - 9) * - room.notificationCount.toString().length + + lastEventRoom.notificationCount + .toString() + .length + 9, decoration: BoxDecoration( color: room.highlightCount > 0 || @@ -330,7 +356,7 @@ class ChatListItem extends StatelessWidget { child: Center( child: hasNotifications ? Text( - room.notificationCount.toString(), + lastEventRoom.notificationCount.toString(), style: TextStyle( color: room.highlightCount > 0 ? Colors.white diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 74eef375c..06e41bd77 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -1,104 +1,17 @@ -import 'package:badges/badges.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; -import 'package:fluffychat/pages/chat_list/navi_rail_item.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/unread_rooms_badge.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; import '../../widgets/matrix.dart'; import 'chat_list_body.dart'; -import 'start_chat_fab.dart'; class ChatListView extends StatelessWidget { final ChatListController controller; const ChatListView(this.controller, {super.key}); - List getNavigationDestinations(BuildContext context) { - final badgePosition = BadgePosition.topEnd(top: -12, end: -8); - return [ - if (AppConfig.separateChatTypes) ...[ - NavigationDestination( - icon: UnreadRoomsBadge( - badgePosition: badgePosition, - filter: - controller.getRoomFilterByActiveFilter(ActiveFilter.messages), - child: const Icon(Icons.chat_outlined), - ), - selectedIcon: UnreadRoomsBadge( - badgePosition: badgePosition, - filter: - controller.getRoomFilterByActiveFilter(ActiveFilter.messages), - child: const Icon(Icons.chat), - ), - //#Pangea - // label: L10n.of(context)!.messages, - label: L10n.of(context)!.directChats, - //Pangea# - ), - NavigationDestination( - icon: UnreadRoomsBadge( - badgePosition: badgePosition, - filter: controller.getRoomFilterByActiveFilter(ActiveFilter.groups), - child: const Icon(Icons.group_outlined), - ), - selectedIcon: UnreadRoomsBadge( - badgePosition: badgePosition, - filter: controller.getRoomFilterByActiveFilter(ActiveFilter.groups), - child: const Icon(Icons.group), - ), - label: L10n.of(context)!.groups, - ), - ] else - NavigationDestination( - icon: UnreadRoomsBadge( - badgePosition: badgePosition, - filter: - controller.getRoomFilterByActiveFilter(ActiveFilter.allChats), - child: const Icon(Icons.chat_outlined), - ), - selectedIcon: UnreadRoomsBadge( - badgePosition: badgePosition, - filter: - controller.getRoomFilterByActiveFilter(ActiveFilter.allChats), - child: const Icon(Icons.chat), - ), - // #Pangea - // label: L10n.of(context)!.chats, - label: L10n.of(context)!.allChats, - // Pangea# - ), - if (controller.spaces.isNotEmpty - // #Pangea - && - !FluffyThemes.isColumnMode(context) - // Pangea# - ) - // #Pangea - // const NavigationDestination( - // icon: Icon(Icons.workspaces_outlined), - // selectedIcon: Icon(Icons.workspaces), - // label: 'Spaces', - // ), - NavigationDestination( - icon: const Icon(Icons.workspaces_outlined), - selectedIcon: const Icon(Icons.workspaces), - label: L10n.of(context)!.allSpaces, - ), - // Pangea# - ]; - } - @override Widget build(BuildContext context) { - final client = Matrix.of(context).client; return StreamBuilder( stream: Matrix.of(context).onShareContentChanged.stream, builder: (_, __) { @@ -106,10 +19,7 @@ class ChatListView extends StatelessWidget { return PopScope( canPop: controller.selectMode == SelectMode.normal && !controller.isSearchMode && - controller.activeFilter == - (AppConfig.separateChatTypes - ? ActiveFilter.messages - : ActiveFilter.allChats), + controller.activeFilter == ActiveFilter.allChats, onPopInvoked: (pop) async { if (pop) return; final selMode = controller.selectMode; @@ -121,151 +31,35 @@ class ChatListView extends StatelessWidget { controller.cancelAction(); return; } - if (controller.activeFilter != - (AppConfig.separateChatTypes - ? ActiveFilter.messages - : ActiveFilter.allChats)) { - controller - .onDestinationSelected(AppConfig.separateChatTypes ? 1 : 0); - return; - } }, - child: Row( - children: [ - if (FluffyThemes.isColumnMode(context) && - controller.widget.displayNavigationRail) ...[ - Builder( - builder: (context) { - final allSpaces = - client.rooms.where((room) => room.isSpace); - final rootSpaces = allSpaces - .where( - (space) => !allSpaces.any( - (parentSpace) => parentSpace.spaceChildren - .any((child) => child.roomId == space.id), + child: GestureDetector( + onTap: FocusManager.instance.primaryFocus?.unfocus, + excludeFromSemantics: true, + behavior: HitTestBehavior.translucent, + child: Scaffold( + body: ChatListViewBody(controller), + floatingActionButton: + // #Pangea + // KeyBoardShortcuts( + // keysToPress: { + // LogicalKeyboardKey.controlLeft, + // LogicalKeyboardKey.keyN, + // }, + // onKeysPressed: () => context.go('/rooms/newprivatechat'), + // helpLabel: L10n.of(context)!.newChat, + // child: + // Pangea# + selectMode == SelectMode.normal && !controller.isSearchMode + ? FloatingActionButton.extended( + onPressed: controller.addChatAction, + icon: const Icon(Icons.add_outlined), + label: Text( + L10n.of(context)!.chat, + overflow: TextOverflow.fade, ), ) - .toList(); - final destinations = getNavigationDestinations(context); - - return SizedBox( - width: FluffyThemes.navRailWidth, - child: ListView.builder( - scrollDirection: Axis.vertical, - itemCount: rootSpaces.length + destinations.length, - itemBuilder: (context, i) { - if (i < destinations.length) { - return NaviRailItem( - // #Pangea - // isSelected: i == controller.selectedIndex, - isSelected: controller.isSelected(i), - // Pangea# - onTap: () => controller.onDestinationSelected(i), - icon: destinations[i].icon, - selectedIcon: destinations[i].selectedIcon, - toolTip: destinations[i].label, - ); - } - i -= destinations.length; - final isSelected = - controller.activeFilter == ActiveFilter.spaces && - rootSpaces[i].id == controller.activeSpaceId; - //#Pangea - final Room? room = Matrix.of(context) - .client - .getRoomById(rootSpaces[i].id); - // Pangea# - return NaviRailItem( - toolTip: rootSpaces[i].getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), - ), - isSelected: isSelected, - // #Pangea - // onTap: () => - // controller.setActiveSpace(rootSpaces[i].id), - onTap: () => chatListHandleSpaceTap( - context, - controller, - rootSpaces[i], - ), - // Pangea# - icon: Avatar( - mxContent: rootSpaces[i].avatar, - name: rootSpaces[i].getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), - ), - size: 32, - // #Pangea - littleIcon: room?.roomTypeIcon, - // Pangea# - ), - ); - }, - ), - ); - }, - ), - Container( - color: Theme.of(context).dividerColor, - width: 1, - ), - ], - Expanded( - child: GestureDetector( - onTap: FocusManager.instance.primaryFocus?.unfocus, - excludeFromSemantics: true, - behavior: HitTestBehavior.translucent, - child: Scaffold( - body: ChatListViewBody(controller), - bottomNavigationBar: controller.displayNavigationBar - ? NavigationBar( - elevation: 4, - labelBehavior: - NavigationDestinationLabelBehavior.alwaysShow, - shadowColor: - Theme.of(context).colorScheme.onSurface, - backgroundColor: - Theme.of(context).colorScheme.surface, - surfaceTintColor: - Theme.of(context).colorScheme.surface, - selectedIndex: controller.selectedIndex, - onDestinationSelected: - controller.onDestinationSelected, - destinations: getNavigationDestinations(context), - ) - : null, - // #Pangea - // floatingActionButton: KeyBoardShortcuts( - // keysToPress: { - // LogicalKeyboardKey.controlLeft, - // LogicalKeyboardKey.keyN, - // }, - // onKeysPressed: () => context.go('/rooms/newprivatechat'), - // helpLabel: L10n.of(context)!.newChat, - // child: selectMode == SelectMode.normal && - // !controller.isSearchMode - // ? StartChatFloatingActionButton( - // activeFilter: controller.activeFilter, - // roomsIsEmpty: false, - // scrolledToTop: controller.scrolledToTop, - // createNewSpace: controller.createNewSpace, - // ) - // : const SizedBox.shrink(), - // ), - floatingActionButton: selectMode == SelectMode.normal - ? StartChatFloatingActionButton( - activeFilter: controller.activeFilter, - roomsIsEmpty: false, - scrolledToTop: controller.scrolledToTop, - controller: controller, - createNewSpace: () {}, - ) - : null, - // Pangea# - ), - ), - ), - ], + : const SizedBox.shrink(), + ), ), ); }, diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 91c667545..801259095 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -392,13 +392,13 @@ class ClientChooserButton extends StatelessWidget { case SettingsAction.joinWithClassCode: SpaceCodeUtil.joinWithSpaceCodeDialog( context, - controller.pangeaController, + MatrixState.pangeaController, ); break; case SettingsAction.findAConversationPartner: findConversationPartnerDialog( context, - controller.pangeaController, + MatrixState.pangeaController, ); break; // case SettingsAction.spaceAnalytics: diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index a90c0d094..7191556c5 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -1,37 +1,34 @@ -import 'dart:async'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/config/app_config.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/utils/on_chat_tap.dart'; -import 'package:fluffychat/pangea/constants/class_default_values.dart'; -import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/pangea/widgets/chat_list/chat_list_header_wrapper.dart'; -import 'package:fluffychat/pangea/widgets/chat_list/chat_list_item_wrapper.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.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'; -import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; -import '../../utils/localized_exception_extension.dart'; -import '../../widgets/matrix.dart'; - class SpaceView extends StatefulWidget { - final ChatListController controller; - final ScrollController scrollController; - const SpaceView( - this.controller, { + final String spaceId; + final void Function() onBack; + final void Function(String spaceId) toParentSpace; + final void Function(Room room) onChatTab; + final void Function(Room room) onChatContext; + 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, - required this.scrollController, }); @override @@ -39,1078 +36,449 @@ class SpaceView extends StatefulWidget { } class _SpaceViewState extends State { - static final Map _lastResponse = {}; - - String? prevBatch; - Object? error; - bool loading = false; - // #Pangea - bool refreshing = false; - StreamSubscription? _roomSubscription; - - final String _chatCountsKey = 'chatCounts'; - Map get chatCounts => Map.from( - widget.controller.pangeaController.pStoreService.read(_chatCountsKey) ?? - {}, - ); - - /// Used to filter out sync updates with hierarchy updates for the active - /// space so that the view can be auto-reloaded in the room subscription - bool hasHierarchyUpdate(SyncUpdate update) { - final joinTimeline = - update.rooms?.join?[widget.controller.activeSpaceId]?.timeline; - final leaveTimeline = - update.rooms?.leave?[widget.controller.activeSpaceId]?.timeline; - if (joinTimeline == null && leaveTimeline == null) return false; - final bool hasJoinUpdate = joinTimeline?.events?.any( - (event) => event.type == EventTypes.SpaceChild, - ) ?? - false; - final bool hasLeaveUpdate = leaveTimeline?.events?.any( - (event) => event.type == EventTypes.SpaceChild, - ) ?? - false; - return hasJoinUpdate || hasLeaveUpdate; - } - // Pangea# + final List _discoveredChildren = []; + final TextEditingController _filterController = TextEditingController(); + String? _nextBatch; + bool _noMoreRooms = false; + bool _isLoading = false; @override void initState() { - // #Pangea - // loadHierarchy(); - - // If, on launch, this room has had updates to its children, - // ensure the hierarchy is properly reloaded - final bool hasUpdate = widget.controller.hasUpdates.contains( - widget.controller.activeSpaceId, - ); - - loadHierarchy(hasUpdate: hasUpdate).then( - // remove this space ID from the set of space IDs with updates - (_) => widget.controller.hasUpdates.remove( - widget.controller.activeSpaceId, - ), - ); - - loadChatCounts(); - - // Listen for changes to the activeSpace's hierarchy, - // and reload the hierarchy when they come through - final client = Matrix.of(context).client; - _roomSubscription ??= client.onSync.stream - .where(hasHierarchyUpdate) - .listen((update) => loadHierarchy(hasUpdate: true)); - // Pangea# + _loadHierarchy(); super.initState(); } - // #Pangea - @override - void dispose() { - _roomSubscription?.cancel(); - super.dispose(); - } - // Pangea# + void _loadHierarchy() async { + final room = Matrix.of(context).client.getRoomById(widget.spaceId); + if (room == null) return; - void _refresh() { - // #Pangea - // _lastResponse.remove(widget.controller.activseSpaceId); - // loadHierarchy(); - if (mounted) setState(() => refreshing = true); - loadHierarchy(hasUpdate: true).whenComplete(() { - if (mounted) setState(() => refreshing = false); + setState(() { + _isLoading = true; }); - // Pangea# - } - - // #Pangea - // Future loadHierarchy([String? prevBatch]) async { - // final activeSpaceId = widget.controller.activeSpaceId; - // if (activeSpaceId == null) return null; - // final client = Matrix.of(context).client; - - // final activeSpace = client.getRoomById(activeSpaceId); - // await activeSpace?.postLoad(); - - // setState(() { - // error = null; - // loading = true; - // }); - - // try { - // final response = await client.getSpaceHierarchy( - // activeSpaceId, - // maxDepth: 1, - // from: prevBatch, - // ); - - // if (prevBatch != null) { - // response.rooms.insertAll(0, _lastResponse[activeSpaceId]?.rooms ?? []); - // } - // setState(() { - // _lastResponse[activeSpaceId] = response; - // }); - // return _lastResponse[activeSpaceId]!; - // } catch (e) { - // setState(() { - // error = e; - // }); - // rethrow; - // } finally { - // setState(() { - // loading = false; - // }); - // } - // } - - /// Loads the hierarchy of the active space (or the given spaceId) and stores - /// it in _lastResponse map. If there's already a response in that map for the - /// spaceId, it will try to load the next batch and add the new rooms to the - /// already loaded ones. Displays a loading indicator while loading, and an error - /// message if an error occurs. - /// If hasUpdate is true, it will force the hierarchy to be reloaded. - Future loadHierarchy({ - String? spaceId, - bool hasUpdate = false, - }) async { - if ((widget.controller.activeSpaceId == null && spaceId == null) || - loading) { - return; - } - - loading = true; - error = null; - setState(() {}); try { - await _loadHierarchy(spaceId: spaceId, hasUpdate: hasUpdate); - } catch (e, s) { - if (mounted) { - setState(() => error = e); - } - ErrorHandler.logError(e: e, s: s); - } finally { - if (mounted) { - setState(() => loading = false); - } - } - } - - /// Internal logic of loadHierarchy. It will load the hierarchy of - /// the active space id (or specified spaceId). - Future _loadHierarchy({ - String? spaceId, - bool hasUpdate = false, - }) async { - final client = Matrix.of(context).client; - final activeSpaceId = (widget.controller.activeSpaceId ?? spaceId)!; - final activeSpace = client.getRoomById(activeSpaceId); - - if (activeSpace == null) { - ErrorHandler.logError( - e: Exception('Space not found in loadHierarchy'), - data: {'spaceId': activeSpaceId}, + final hierarchy = await room.client.getSpaceHierarchy( + widget.spaceId, + suggestedOnly: false, + maxDepth: 2, + from: _nextBatch, ); - return; - } - - // Load all of the space's state events. Space Child events - // are used to filtering out unsuggested, unjoined rooms. - await activeSpace.postLoad(); - - // The current number of rooms loaded for this space that are visible in the UI - final int prevLength = _lastResponse[activeSpaceId] != null && !hasUpdate - ? filterHierarchyResponse( - activeSpace, - _lastResponse[activeSpaceId]!.rooms, - ).length - : 0; - - // Failsafe to prevent too many calls to the server in a row - int callsToServer = 0; - - GetSpaceHierarchyResponse? currentHierarchy = - hasUpdate ? null : _lastResponse[activeSpaceId]; - - // Makes repeated calls to the server until 10 new visible rooms have - // been loaded, or there are no rooms left to load. Using a loop here, - // rather than one single call to the endpoint, because some spaces have - // so many invisible rooms (analytics rooms) that it might look like - // pressing the 'load more' button does nothing (Because the only rooms - // coming through from those calls are analytics rooms). - while (callsToServer < 5) { - // if this space has been loaded and there are no more rooms to load, break - if (currentHierarchy != null && currentHierarchy.nextBatch == null) { - break; - } - - // if this space has been loaded and 10 new rooms have been loaded, break - if (currentHierarchy != null) { - final int currentLength = filterHierarchyResponse( - activeSpace, - currentHierarchy.rooms, - ).length; - - if (currentLength - prevLength >= 10) { - break; + if (!mounted) return; + setState(() { + _nextBatch = hierarchy.nextBatch; + if (hierarchy.nextBatch == null) { + _noMoreRooms = true; } - } - - // make the call to the server - final response = await client.getSpaceHierarchy( - activeSpaceId, - maxDepth: 1, - from: currentHierarchy?.nextBatch, - limit: 100, - ); - callsToServer++; - - // if rooms have earlier been loaded for this space, add those - // previously loaded rooms to the front of the response list - if (currentHierarchy != null) { - response.rooms.insertAll( - 0, - currentHierarchy.rooms, + _discoveredChildren.addAll( + hierarchy.rooms + .where((c) => room.client.getRoomById(c.roomId) == null), ); - } - - // finally, set the response to the last response for this space - currentHierarchy = response; - } - - if (currentHierarchy != null) { - _lastResponse[activeSpaceId] = currentHierarchy; - } - - // After making those calls to the server, set the chat count for - // this space. Used for the UI of the 'All Spaces' view - setChatCount( - activeSpace, - _lastResponse[activeSpaceId] ?? - GetSpaceHierarchyResponse( - rooms: [], - ), - ); - } - // Pangea# - - void _onJoinSpaceChild(SpaceRoomsChunk spaceChild) async { - final client = Matrix.of(context).client; - final space = client.getRoomById(widget.controller.activeSpaceId!); - if (client.getRoomById(spaceChild.roomId) == null) { - final result = await showFutureLoadingDialog( - context: context, - future: () async { - await client.joinRoom( - spaceChild.roomId, - serverName: space?.spaceChildren - .firstWhereOrNull( - (child) => child.roomId == spaceChild.roomId, - ) - ?.via, - ); - if (client.getRoomById(spaceChild.roomId) == null) { - // Wait for room actually appears in sync - await client.waitForRoomInSync(spaceChild.roomId, join: true); - } - // #Pangea - final room = client.getRoomById(spaceChild.roomId); - if (room != null && (await room.leaveIfFull())) { - throw L10n.of(context)!.roomFull; - } - // Pangea# - }, - ); - if (result.error != null) return; - _refresh(); - } - // #Pangea - else { - final room = client.getRoomById(spaceChild.roomId)!; - if (room.membership != Membership.leave) return; - final joinResult = await showFutureLoadingDialog( - context: context, - future: () async { - final waitForRoom = room.client.waitForRoomInSync( - room.id, - join: true, - ); - await room.join(); - await waitForRoom; - if (await room.leaveIfFull()) { - throw L10n.of(context)!.roomFull; - } - }, - ); - if (joinResult.error != null) return; - } - // Pangea# - if (spaceChild.roomType == 'm.space') { - if (spaceChild.roomId == widget.controller.activeSpaceId) { - // #Pangea - // context.go('/rooms/${spaceChild.roomId}'); - context.go('/rooms/${spaceChild.roomId}/details'); - // Pangea# - } else { - widget.controller.setActiveSpace(spaceChild.roomId); - } - return; + _isLoading = false; + }); + } catch (e, s) { + Logs().w('Unable to load hierarchy', e, s); + if (!mounted) return; + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(e.toLocalizedString(context)))); + setState(() { + _isLoading = false; + }); } - context.go('/rooms/${spaceChild.roomId}'); } - void _onSpaceChildContextMenu([ - SpaceRoomsChunk? spaceChild, - Room? room, - ]) async { + void _joinChildRoom(SpaceRoomsChunk item) async { final client = Matrix.of(context).client; - final activeSpaceId = widget.controller.activeSpaceId; - final activeSpace = - activeSpaceId == null ? null : client.getRoomById(activeSpaceId); - final action = await showModalActionSheet( - context: context, - title: spaceChild?.name ?? - room?.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), - ), - // #Pangea - // message: spaceChild?.topic ?? room?.topic, - // Pangea# - actions: [ - // #Pangea - // if (room == null) - if (room == null || room.membership == Membership.leave) - // Pangea# - SheetAction( - key: SpaceChildContextAction.join, - label: L10n.of(context)!.joinRoom, - icon: Icons.send_outlined, - ), - if (spaceChild != null && - // #Pangea - room != null && - room.ownPowerLevel >= ClassDefaultValues.powerLevelOfAdmin && - // Pangea# - (activeSpace?.canChangeStateEvent(EventTypes.SpaceChild) ?? false)) - SheetAction( - key: SpaceChildContextAction.removeFromSpace, - label: L10n.of(context)!.removeFromSpace, - icon: Icons.delete_sweep_outlined, - ), - // #Pangea - if (room != null && - room.ownPowerLevel >= ClassDefaultValues.powerLevelOfAdmin) - SheetAction( - key: SpaceChildContextAction.addToSpace, - label: L10n.of(context)!.addToSpace, - icon: Icons.workspaces_outlined, - ), - if (room != null && - room.isRoomAdmin && - room.membership != Membership.leave) - SheetAction( - key: SpaceChildContextAction.archive, - label: room.isSpace - ? L10n.of(context)!.archiveSpace - : L10n.of(context)!.archive, - icon: Icons.architecture_outlined, - isDestructiveAction: true, - ), - // if (room != null) - if (room != null && room.membership != Membership.leave) - // Pangea# - SheetAction( - key: SpaceChildContextAction.leave, - label: L10n.of(context)!.leave, - // #Pangea - // icon: Icons.delete_outlined, - icon: Icons.arrow_forward, - // Pangea# - isDestructiveAction: true, - ), - ], - ); - if (action == null) return; - - switch (action) { - case SpaceChildContextAction.join: - _onJoinSpaceChild(spaceChild!); - break; - case SpaceChildContextAction.leave: - // #Pangea - widget.controller.cancelAction(); - if (room == null) return; - if (room.isSpace) { - await room.isOnlyAdmin() - ? await room.archiveSpace( - context, - Matrix.of(context).client, - onlyAdmin: true, - ) - : await room.leaveSpace( - context, - Matrix.of(context).client, - ); - } else { - widget.controller.toggleSelection(room.id); - await widget.controller.leaveAction(); - } - _refresh(); - break; - // await showFutureLoadingDialog( - // context: context, - // future: room!.leave, - // ); - // break; - // Pangea# - case SpaceChildContextAction.removeFromSpace: - await showFutureLoadingDialog( - context: context, - future: () => activeSpace!.removeSpaceChild(spaceChild!.roomId), - ); - break; - // #Pangea - case SpaceChildContextAction.archive: - widget.controller.cancelAction(); - // #Pangea - if (room == null || room.membership == Membership.leave) return; - if (room.isSpace) { - await room.archiveSpace( - context, - Matrix.of(context).client, - onlyAdmin: false, - ); - } else { - widget.controller.toggleSelection(room.id); - await widget.controller.archiveAction(); - } - // Pangea# - _refresh(); - break; - case SpaceChildContextAction.addToSpace: - widget.controller.cancelAction(); - // #Pangea - if (room == null || room.membership == Membership.leave) return; - // Pangea# - widget.controller.toggleSelection(room.id); - await widget.controller.addToSpace(); - // #Pangea - setState(() => widget.controller.selectedRoomIds.clear()); - // Pangea# - break; - } - } + final space = client.getRoomById(widget.spaceId); - void _addChatOrSubSpace() async { - final roomType = await showConfirmationDialog( + final consent = await showOkCancelAlertDialog( context: context, - title: L10n.of(context)!.addChatOrSubSpace, - actions: [ - AlertDialogAction( - key: AddRoomType.subspace, - label: L10n.of(context)!.createNewSpace, - ), - AlertDialogAction( - key: AddRoomType.chat, - label: L10n.of(context)!.createGroup, - ), - ], - ); - if (roomType == null) return; - - final names = await showTextInputDialog( - context: context, - title: roomType == AddRoomType.subspace - ? L10n.of(context)!.createNewSpace - : L10n.of(context)!.createGroup, - textFields: [ - DialogTextField( - hintText: roomType == AddRoomType.subspace - ? L10n.of(context)!.spaceName - : L10n.of(context)!.groupName, - minLines: 1, - maxLines: 1, - maxLength: 64, - validator: (text) { - if (text == null || text.isEmpty) { - return L10n.of(context)!.pleaseChoose; - } - return null; - }, - ), - DialogTextField( - hintText: L10n.of(context)!.chatDescription, - minLines: 4, - maxLines: 8, - maxLength: 255, - ), - ], - okLabel: L10n.of(context)!.create, + title: item.name ?? item.canonicalAlias ?? L10n.of(context)!.emptyChat, + message: item.topic, + okLabel: L10n.of(context)!.joinRoom, cancelLabel: L10n.of(context)!.cancel, ); - if (names == null) return; - final client = Matrix.of(context).client; - final result = await showFutureLoadingDialog( + if (consent != OkCancelResult.ok) return; + if (!mounted) return; + + await showFutureLoadingDialog( context: context, future: () async { - late final String roomId; - final activeSpace = client.getRoomById( - widget.controller.activeSpaceId!, - )!; - - if (roomType == AddRoomType.subspace) { - roomId = await client.createSpace( - name: names.first, - topic: names.last.isEmpty ? null : names.last, - visibility: activeSpace.joinRules == JoinRules.public - ? sdk.Visibility.public - : sdk.Visibility.private, - ); - } else { - roomId = await client.createGroupChat( - groupName: names.first, - initialState: names.length > 1 && names.last.isNotEmpty - ? [ - sdk.StateEvent( - type: sdk.EventTypes.RoomTopic, - content: {'topic': names.last}, - ), - ] - : null, - ); - } - await activeSpace.setSpaceChild( - roomId, - // #Pangea - suggested: true, - // Pangea# + 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); + } }, ); - if (result.error != null) return; - _refresh(); - } + if (!mounted) return; - // #Pangea - Future loadChatCounts() async { - // if not in the call spaces view, don't load chat count yet - if (widget.controller.activeSpaceId != null) return; - - final List allSpaces = - Matrix.of(context).client.rooms.where((room) => room.isSpace).toList(); - - for (final Room space in allSpaces) { - // check if the space is visible in the all spaces list - final bool isRootSpace = !allSpaces.any( - (parentSpace) => - parentSpace.spaceChildren.any((child) => child.roomId == space.id), - ); - - // if it's visible, and it hasn't been loaded yet, load chat count - if (isRootSpace && !chatCounts.containsKey(space.id)) { - loadHierarchy(spaceId: space.id); - } - } - } - - bool includeSpaceChild( - Room space, - SpaceRoomsChunk hierarchyMember, - ) { - if (!mounted) return false; - final bool isAnalyticsRoom = - hierarchyMember.roomType == PangeaRoomTypes.analytics; - - final bool isMember = [Membership.join, Membership.invite].contains( - Matrix.of(context).client.getRoomById(hierarchyMember.roomId)?.membership, - ); - - final bool isSuggested = - space.spaceChildSuggestionStatus[hierarchyMember.roomId] ?? true; - - return !isAnalyticsRoom && (isMember || isSuggested); - } - - List filterHierarchyResponse( - Room space, - List hierarchyResponse, - ) { - final List filteredChildren = []; - for (final child in hierarchyResponse) { - final isDuplicate = filteredChildren.any( - (filtered) => filtered.roomId == child.roomId, - ); - if (isDuplicate) continue; - - if (includeSpaceChild(space, child)) { - filteredChildren.add(child); - } - } - return filteredChildren; - } - - int sortSpaceChildren( - SpaceRoomsChunk a, - SpaceRoomsChunk b, - ) { - final bool aIsSpace = a.roomType == 'm.space'; - final bool bIsSpace = b.roomType == 'm.space'; - - if (aIsSpace && !bIsSpace) { - return -1; - } else if (!aIsSpace && bIsSpace) { - return 1; - } - return 0; + setState(() { + _discoveredChildren.remove(item); + }); } - Future setChatCount( - Room space, - GetSpaceHierarchyResponse? response, - ) async { - final Map updatedChatCounts = Map.from(chatCounts); - final List spaceChildren = response?.rooms ?? []; - final filteredChildren = filterHierarchyResponse(space, spaceChildren) - .where((sc) => sc.roomId != space.id) - .toList(); - updatedChatCounts[space.id] = filteredChildren.length; - - await widget.controller.pangeaController.pStoreService.save( - _chatCountsKey, - updatedChatCounts, - ); - } + void _onSpaceAction(SpaceActions action) async { + final space = Matrix.of(context).client.getRoomById(widget.spaceId); - bool roomCountLoading(Room space) => - space.membership == Membership.join && !chatCounts.containsKey(space.id); + switch (action) { + case SpaceActions.settings: + await space?.postLoad(); + context.push('/rooms/${widget.spaceId}/details'); + break; + case SpaceActions.invite: + await space?.postLoad(); + context.push('/rooms/${widget.spaceId}/invite'); + break; + case SpaceActions.leave: + final confirmed = await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.areYouSure, + okLabel: L10n.of(context)!.ok, + cancelLabel: L10n.of(context)!.cancel, + message: L10n.of(context)!.archiveRoomDescription, + ); + if (!mounted) return; + if (confirmed != OkCancelResult.ok) return; - Widget spaceSubtitle(Room space) { - if (roomCountLoading(space)) { - return const CircularProgressIndicator.adaptive(); + final success = await showFutureLoadingDialog( + context: context, + future: () async => await space?.leave(), + ); + if (!mounted) return; + if (success.error != null) return; + widget.onBack(); } - - return Text( - space.membership == Membership.join - ? L10n.of(context)!.numChats( - chatCounts[space.id].toString(), - ) - : L10n.of(context)!.youreInvited, - ); } - // Pangea# @override Widget build(BuildContext context) { - final client = Matrix.of(context).client; - final activeSpaceId = widget.controller.activeSpaceId; - final activeSpace = activeSpaceId == null - ? null - : client.getRoomById( - activeSpaceId, - ); - final allSpaces = client.rooms.where((room) => room.isSpace); - if (activeSpaceId == null) { - final rootSpaces = allSpaces - .where( - (space) => - !allSpaces.any( - (parentSpace) => parentSpace.spaceChildren - .any((child) => child.roomId == space.id), - ) && - space - .getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)) - .toLowerCase() - .contains( - widget.controller.searchController.text.toLowerCase(), - ), - ) - .toList(); - - return SafeArea( - child: CustomScrollView( - controller: widget.scrollController, - slivers: [ - // #Pangea - // ChatListHeader(controller: widget.controller), - ChatListHeaderWrapper(controller: widget.controller), - // Pangea# - SliverList( - delegate: SliverChildBuilderDelegate( - (context, i) { - final rootSpace = rootSpaces[i]; - final displayname = rootSpace.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), - ); - return Material( - color: Theme.of(context).colorScheme.surface, - child: ListTile( - leading: Avatar( - mxContent: rootSpace.avatar, - name: displayname, - // #Pangea - littleIcon: rootSpace.roomTypeIcon, - // Pangea# - ), - title: Text( - displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - // #Pangea - subtitle: Row( - children: [ - spaceSubtitle(rootSpace), - if (rootSpace.isLocked) - const Padding( - padding: EdgeInsets.only(left: 4.0), - child: Icon( - Icons.lock_outlined, - size: 16, - ), - ), - ], - ), - onTap: () => chatListHandleSpaceTap( - context, - widget.controller, - rootSpaces[i], - ), - // subtitle: Text( - // L10n.of(context)!.numChats( - // rootSpace.spaceChildren.length.toString(), - // ), - // ), - // onTap: () => - // widget.controller.setActiveSpace(rootSpace.id), - // Pangea# - onLongPress: () => - _onSpaceChildContextMenu(null, rootSpace), - trailing: const Icon(Icons.chevron_right_outlined), - ), - ); - }, - childCount: rootSpaces.length, - ), - ), - ], + final room = Matrix.of(context).client.getRoomById(widget.spaceId); + final displayname = + room?.getLocalizedDisplayname() ?? L10n.of(context)!.nothingFound; + return Scaffold( + appBar: AppBar( + leading: Center( + child: CloseButton( + onPressed: widget.onBack, + ), ), - ); - } - - final parentSpace = allSpaces.firstWhereOrNull( - (space) => - space.spaceChildren.any((child) => child.roomId == activeSpaceId), - ); - return PopScope( - canPop: parentSpace == null, - onPopInvoked: (pop) async { - if (pop) return; - if (parentSpace != null) { - widget.controller.setActiveSpace(parentSpace.id); - } - }, - child: SafeArea( - child: CustomScrollView( - controller: widget.scrollController, - slivers: [ - // #Pangea - // ChatListHeader(controller: widget.controller, globalSearch: false), - ChatListHeaderWrapper( - controller: widget.controller, - globalSearch: false, - ), - // Pangea# - SliverAppBar( - automaticallyImplyLeading: false, - primary: false, - titleSpacing: 0, - title: ListTile( - leading: BackButton( - // #Pangea - onPressed: () { - !FluffyThemes.isColumnMode(context) || - parentSpace?.id != null - ? widget.controller.setActiveSpace(parentSpace?.id) - : widget.controller.onDestinationSelected(0); - }, - // onPressed: () => - // widget.controller.setActiveSpace(parentSpace?.id), - // Pangea# + 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, + ), + subtitle: room == null + ? null + : Text( + L10n.of(context)!.countChatsAndCountParticipants( + room.spaceChildren.length, + room.summary.mJoinedMemberCount ?? 1, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - title: Text( - parentSpace == null - // #Pangea - // ? L10n.of(context)!.allSpaces - ? !FluffyThemes.isColumnMode(context) - ? L10n.of(context)!.allSpaces - : L10n.of(context)!.allChats - // Pangea# - : parentSpace.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), - ), + ), + actions: [ + PopupMenuButton( + 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), + ], ), - // #Pangea - // trailing: IconButton( - // icon: loading - // ? const CircularProgressIndicator.adaptive(strokeWidth: 2) - // : const Icon(Icons.refresh_outlined), - // onPressed: loading ? null : _refresh, - // ), - trailing: Tooltip( - message: L10n.of(context)!.refresh, - child: IconButton( - icon: loading - ? const CircularProgressIndicator.adaptive( - strokeWidth: 2, - ) - : const Icon(Icons.refresh_outlined), - onPressed: loading ? null : _refresh, - ), + ), + 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), + ], ), - // Pangea# ), - ), - Builder( - builder: (context) { - final response = _lastResponse[activeSpaceId]; - final error = this.error; - if (error != null) { - return SliverFillRemaining( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Text(error.toLocalizedString(context)), - ), - IconButton( - onPressed: _refresh, - icon: const Icon(Icons.refresh_outlined), + 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 joinedRooms = room.spaceChildren + .map((child) { + final roomId = child.roomId; + if (roomId == null) return null; + return room.client.getRoomById(roomId); + }) + .whereType() + .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), + ), + ); + + final joinedParents = room.spaceParents + .map((parent) { + final roomId = parent.roomId; + if (roomId == null) return null; + return room.client.getRoomById(roomId); + }) + .whereType() + .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) + .colorScheme + .onPrimaryContainer, + fontWeight: FontWeight.normal, + ), + floatingLabelBehavior: FloatingLabelBehavior.never, + prefixIcon: IconButton( + onPressed: () {}, + icon: Icon( + Icons.search_outlined, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), ), - ], - ), - ); - } - if (response == null) { - return SliverFillRemaining( - child: Center( - child: Text(L10n.of(context)!.loadingPleaseWait), + ), ), - ); - } - // #Pangea - // final spaceChildren = response.rooms; - List spaceChildren = response.rooms; - final space = - Matrix.of(context).client.getRoomById(activeSpaceId); - if (space != null) { - spaceChildren = filterHierarchyResponse(space, spaceChildren); - } - spaceChildren.sort(sortSpaceChildren); - // Pangea# - final canLoadMore = response.nextBatch != null; - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, i) { - if (canLoadMore && i == spaceChildren.length) { + SliverList.builder( + itemCount: joinedParents.length, + itemBuilder: (context, i) { + final displayname = + joinedParents[i].getLocalizedDisplayname(); return Padding( - padding: const EdgeInsets.all(16.0), - child: OutlinedButton.icon( - label: loading - ? const LinearProgressIndicator() - : Text(L10n.of(context)!.loadMore), - icon: const Icon(Icons.chevron_right_outlined), - onPressed: loading - ? null - : () { - // #Pangea - // loadHierarchy(response.nextBatch); - loadHierarchy(); - // Pangea# - }, + 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, + ), + ), + const SizedBox(width: 8), + Expanded(child: Text(displayname)), + ], + ), + onTap: () => + widget.toParentSpace(joinedParents[i].id), + ), ), ); - } - final spaceChild = spaceChildren[i]; - final room = client.getRoomById(spaceChild.roomId); - if (room != null && - !room.isSpace - // #Pangea - && - room.membership != Membership.leave - // Pangea# - ) { - // #Pangea - // return ChatListItem( - return ChatListItemWrapper( - controller: widget.controller, - // Pangea# + }, + ), + 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, - onLongPress: () => - _onSpaceChildContextMenu(spaceChild, room), - activeChat: widget.controller.activeChat == room.id, - onTap: () => onChatTap(room, context), + filter: filter, + onTap: () => widget.onChatTab(room), + onLongPress: () => widget.onChatContext(room), + activeChat: widget.activeChat == room.id, ); - } - final isSpace = spaceChild.roomType == 'm.space'; - final topic = spaceChild.topic?.isEmpty ?? true - ? null - : spaceChild.topic; - if (spaceChild.roomId == activeSpaceId) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - SearchTitle( - title: spaceChild.name ?? - spaceChild.canonicalAlias ?? - 'Space', - icon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10.0, - ), - child: Avatar( - size: 24, - mxContent: spaceChild.avatarUrl, - name: spaceChild.name, + }, + ), + 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), ), ), - color: Theme.of(context) - .colorScheme - .secondaryContainer - .withAlpha(128), - trailing: const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), - // #Pangea - // child: Icon(Icons.edit_outlined), - child: Icon(Icons.settings_outlined), - // Pangea# - ), - // #Pangea - // onTap: () => _onJoinSpaceChild(spaceChild), - onTap: () => context.go( - '/rooms/${spaceChild.roomId}/details', - ), - // Pangea# + ); + } + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 2.0, ), - if (activeSpace?.canChangeStateEvent( - EventTypes.SpaceChild, - ) == - true) - Material( - child: ListTile( - leading: const CircleAvatar( - child: Icon(Icons.group_add_outlined), + 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( + 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, + ), ), - title: - Text(L10n.of(context)!.addChatOrSubSpace), - trailing: - const Icon(Icons.chevron_right_outlined), - onTap: _addChatOrSubSpace, - ), + const SizedBox(width: 8), + const Icon(Icons.add_circle_outline_outlined), + ], ), - ], - ); - } - final name = spaceChild.name ?? - spaceChild.canonicalAlias ?? - L10n.of(context)!.chat; - if (widget.controller.isSearchMode && - !name.toLowerCase().contains( - widget.controller.searchController.text - .toLowerCase(), - )) { - return const SizedBox.shrink(); - } - return Material( - child: ListTile( - leading: Avatar( - mxContent: spaceChild.avatarUrl, - name: spaceChild.name, - //#Pangea - littleIcon: room?.roomTypeIcon, - //Pangea# - ), - title: Row( - children: [ - // #Pangea - // Expanded( - // child: - // Pangea# - Text( - name, + subtitle: Text( + item.topic ?? + L10n.of(context)!.countParticipants( + item.numJoinedMembers, + ), maxLines: 1, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), + overflow: TextOverflow.ellipsis, ), - // #Pangea - // ), - // Pangea# - if (!isSpace) ...[ - const Icon( - Icons.people_outline, - size: 16, - ), - const SizedBox(width: 4), - Text( - spaceChild.numJoinedMembers.toString(), - style: const TextStyle(fontSize: 14), - ), - ], - ], - ), - // #Pangea - // onTap: () => room?.isSpace == true - // ? widget.controller.setActiveSpace(room!.id) - // : _onSpaceChildContextMenu(spaceChild, room), - onTap: room?.isSpace ?? false - ? () => chatListHandleSpaceTap( - context, - widget.controller, - room!, - ) - : () => _onJoinSpaceChild(spaceChild), - // Pangea# - onLongPress: () => - _onSpaceChildContextMenu(spaceChild, room), - subtitle: Text( - topic ?? - (isSpace - ? L10n.of(context)!.enterSpace - : L10n.of(context)!.enterRoom), - maxLines: 1, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, ), ), - trailing: isSpace - ? const Icon(Icons.chevron_right_outlined) - : null, - ), - ); - }, - childCount: spaceChildren.length + (canLoadMore ? 1 : 0), - ), + ); + }, + ), + ], ); }, ), - ], - ), - ), ); } } -enum SpaceChildContextAction { - join, +enum SpaceActions { + settings, + invite, leave, - removeFromSpace, - // #Pangea - // deleteChat, - archive, - addToSpace - // Pangea# } - -enum AddRoomType { chat, subspace } diff --git a/lib/pages/chat_list/start_chat_fab.dart b/lib/pages/chat_list/start_chat_fab.dart deleted file mode 100644 index f25374f4e..000000000 --- a/lib/pages/chat_list/start_chat_fab.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'dart:core'; - -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; - -import '../../config/themes.dart'; -import 'chat_list.dart'; - -class StartChatFloatingActionButton extends StatelessWidget { - final ActiveFilter activeFilter; - final ValueNotifier scrolledToTop; - final bool roomsIsEmpty; - // #Pangea - final ChatListController controller; - // Pangea# - final void Function() createNewSpace; - - const StartChatFloatingActionButton({ - super.key, - required this.activeFilter, - required this.scrolledToTop, - required this.roomsIsEmpty, - required this.createNewSpace, - // #Pangea - required this.controller, - // Pangea# - }); - - void _onPressed(BuildContext context) async { - //#Pangea - if (controller.activeSpaceId != null) { - context.go( - '/rooms/newgroup${controller.activeSpaceId != null ? '?spaceId=${controller.activeSpaceId}' : ''}', - ); - return; - } - //Pangea# - switch (activeFilter) { - case ActiveFilter.allChats: - case ActiveFilter.messages: - // #Pangea - // context.go('/rooms/newprivatechat'); - // break; - // Pangea# - case ActiveFilter.groups: - // #Pangea - // context.go('/rooms/newgroup'); - context.go( - '/rooms/newgroup${controller.activeSpaceId != null ? '?spaceId=${controller.activeSpaceId}' : ''}', - ); - // Pangea# - break; - case ActiveFilter.spaces: - // #Pangea - // createNewSpace(); - context.go('/rooms/newspace'); - // Pangea# - break; - } - } - - IconData get icon { - // #Pangea - if (controller.activeSpaceId != null) { - return Icons.group_add_outlined; - } - // Pangea# - switch (activeFilter) { - case ActiveFilter.allChats: - case ActiveFilter.messages: - // #Pangea - // return Icons.add_outlined; - // Pangea# - case ActiveFilter.groups: - return Icons.group_add_outlined; - case ActiveFilter.spaces: - return Icons.workspaces_outlined; - } - } - - String getLabel(BuildContext context) { - // #Pangea - if (controller.activeSpaceId != null) { - return L10n.of(context)!.newGroup; - } - // Pangea# - switch (activeFilter) { - case ActiveFilter.allChats: - case ActiveFilter.messages: - return roomsIsEmpty - ? L10n.of(context)!.startFirstChat - : L10n.of(context)!.newChat; - case ActiveFilter.groups: - return L10n.of(context)!.newGroup; - case ActiveFilter.spaces: - return L10n.of(context)!.newSpace; - } - } - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: scrolledToTop, - builder: (context, scrolledToTop, _) => AnimatedSize( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - clipBehavior: Clip.none, - child: scrolledToTop - ? FloatingActionButton.extended( - onPressed: () => _onPressed(context), - icon: Icon(icon), - label: Text( - getLabel(context), - overflow: TextOverflow.fade, - ), - ) - : FloatingActionButton( - onPressed: () => _onPressed(context), - child: Icon(icon), - ), - ), - ); - } -} diff --git a/lib/pages/chat_list/utils/on_chat_tap.dart b/lib/pages/chat_list/utils/on_chat_tap.dart deleted file mode 100644 index f7daa248e..000000000 --- a/lib/pages/chat_list/utils/on_chat_tap.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:fluffychat/pages/chat/send_file_dialog.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.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'; -import 'package:matrix/matrix.dart'; - -void onChatTap(Room room, BuildContext context) async { - if (room.membership == Membership.invite) { - final inviterId = - room.getState(EventTypes.RoomMember, room.client.userID!)?.senderId; - final inviteAction = await showModalActionSheet( - context: context, - message: room.isDirectChat - ? L10n.of(context)!.invitePrivateChat - : L10n.of(context)!.inviteGroupChat, - title: room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), - actions: [ - SheetAction( - key: InviteActions.accept, - label: L10n.of(context)!.accept, - icon: Icons.check_outlined, - isDefaultAction: true, - ), - SheetAction( - key: InviteActions.decline, - label: L10n.of(context)!.decline, - icon: Icons.close_outlined, - isDestructiveAction: true, - ), - SheetAction( - key: InviteActions.block, - label: L10n.of(context)!.block, - icon: Icons.block_outlined, - isDestructiveAction: true, - ), - ], - ); - if (inviteAction == null) return; - if (inviteAction == InviteActions.block) { - context.go('/rooms/settings/security/ignorelist', extra: inviterId); - return; - } - if (inviteAction == InviteActions.decline) { - // #Pangea - if (!room.isSpace && - room.membership == Membership.join && - room.isUnread) { - await room.markUnread(false); - } - // Pangea# - await showFutureLoadingDialog( - context: context, - future: room.leave, - ); - return; - } - final joinResult = await showFutureLoadingDialog( - context: context, - future: () async { - final waitForRoom = room.client.waitForRoomInSync( - room.id, - join: true, - ); - if (await room.leaveIfFull()) { - throw L10n.of(context)!.roomFull; - } - await room.join(); - await waitForRoom; - }, - ); - if (joinResult.error != null) return; - } - - if (room.membership == Membership.ban) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.youHaveBeenBannedFromThisChat), - ), - ); - return; - } - - if (room.membership == Membership.leave) { - context.go('/rooms/archive/${room.id}'); - return; - } - - // Share content into this room - final shareContent = Matrix.of(context).shareContent; - if (shareContent != null) { - final shareFile = shareContent.tryGet('file'); - if (shareContent.tryGet('msgtype') == 'chat.fluffy.shared_file' && - shareFile != null) { - await showDialog( - context: context, - useRootNavigator: false, - builder: (c) => SendFileDialog( - files: [shareFile], - room: room, - ), - ); - Matrix.of(context).shareContent = null; - } else { - final consent = await showOkCancelAlertDialog( - context: context, - title: L10n.of(context)!.forward, - message: L10n.of(context)!.forwardMessageTo( - room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), - ), - okLabel: L10n.of(context)!.forward, - cancelLabel: L10n.of(context)!.cancel, - ); - if (consent == OkCancelResult.cancel) { - Matrix.of(context).shareContent = null; - return; - } - if (consent == OkCancelResult.ok) { - room.sendEvent(shareContent); - Matrix.of(context).shareContent = null; - } - } - } - - context.go('/rooms/${room.id}'); -} - -enum InviteActions { - accept, - decline, - block, -} diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index 86f48fe87..0b505d597 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -185,12 +185,6 @@ class SettingsStyleView extends StatelessWidget { storeKey: SettingKeys.showPresences, defaultValue: AppConfig.showPresences, ), - SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.separateChatTypes, - onChanged: (b) => AppConfig.separateChatTypes = b, - storeKey: SettingKeys.separateChatTypes, - defaultValue: AppConfig.separateChatTypes, - ), Divider( height: 1, color: Theme.of(context).dividerColor, diff --git a/lib/pangea/pages/analytics/analytics_list_tile.dart b/lib/pangea/pages/analytics/analytics_list_tile.dart index a49ef4cb4..bd967b374 100644 --- a/lib/pangea/pages/analytics/analytics_list_tile.dart +++ b/lib/pangea/pages/analytics/analytics_list_tile.dart @@ -1,11 +1,9 @@ import 'dart:async'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; -import 'package:matrix/matrix.dart'; import '../../../../utils/date_time_extension.dart'; import '../../../widgets/avatar.dart'; @@ -84,8 +82,7 @@ class AnalyticsListTileState extends State { @override Widget build(BuildContext context) { - final Room? room = - Matrix.of(context).client.getRoomById(widget.selected.id); + final room = Matrix.of(context).client.getRoomById(widget.selected.id); return Material( color: widget.isSelected ? Theme.of(context).colorScheme.secondaryContainer @@ -101,7 +98,6 @@ class AnalyticsListTileState extends State { : Avatar( mxContent: widget.avatar, name: widget.selected.displayName, - littleIcon: room?.roomTypeIcon, ), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/pangea/widgets/chat_list/chat_list_header_wrapper.dart b/lib/pangea/widgets/chat_list/chat_list_header_wrapper.dart deleted file mode 100644 index 7944f37ab..000000000 --- a/lib/pangea/widgets/chat_list/chat_list_header_wrapper.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'dart:async'; - -import 'package:fluffychat/pages/chat_list/chat_list.dart'; -import 'package:fluffychat/pages/chat_list/chat_list_header.dart'; -import 'package:flutter/material.dart'; - -/// A wrapper around ChatListHeader to allow rebuilding on state changes. -/// Prevents having to rebuild the entire ChatList when a single item changes. -class ChatListHeaderWrapper extends StatefulWidget { - final ChatListController controller; - final bool globalSearch; - - const ChatListHeaderWrapper({ - super.key, - required this.controller, - this.globalSearch = true, - }); - - @override - ChatListHeaderWrapperState createState() => ChatListHeaderWrapperState(); -} - -class ChatListHeaderWrapperState extends State { - StreamSubscription? stateSub; - - @override - void initState() { - super.initState(); - stateSub = widget.controller.selectionsStream.stream.listen((roomID) { - setState(() {}); - }); - } - - @override - void dispose() { - stateSub?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ChatListHeader( - controller: widget.controller, - globalSearch: widget.globalSearch, - ); - } -} diff --git a/lib/pangea/widgets/chat_list/chat_list_item_wrapper.dart b/lib/pangea/widgets/chat_list/chat_list_item_wrapper.dart deleted file mode 100644 index 2d500bf93..000000000 --- a/lib/pangea/widgets/chat_list/chat_list_item_wrapper.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:async'; - -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/utils/on_chat_tap.dart'; -import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; - -/// A wrapper around ChatListItem to allow rebuilding on state changes. -/// Prevents having to rebuild the entire ChatList when a single item changes. -class ChatListItemWrapper extends StatefulWidget { - final Room room; - final bool activeChat; - final void Function()? onForget; - final String? filter; - final ChatListController controller; - - final void Function()? onLongPress; - final void Function()? onTap; - - const ChatListItemWrapper( - this.room, { - this.activeChat = false, - this.onForget, - this.filter, - required this.controller, - this.onLongPress, - this.onTap, - super.key, - }); - - @override - ChatListItemWrapperState createState() => ChatListItemWrapperState(); -} - -class ChatListItemWrapperState extends State { - StreamSubscription? stateSub; - - @override - void initState() { - super.initState(); - stateSub = widget.controller.selectionsStream.stream.listen((roomID) { - if (roomID == widget.room.id) { - setState(() {}); - } - }); - } - - @override - void dispose() { - stateSub?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ChatListItem( - widget.room, - activeChat: widget.activeChat, - selected: widget.controller.selectedRoomIds.contains(widget.room.id), - onTap: widget.onTap ?? - (widget.controller.selectMode == SelectMode.select - ? () => widget.controller.toggleSelection(widget.room.id) - : () => onChatTap(widget.room, context)), - onLongPress: widget.onLongPress ?? - () => widget.controller.toggleSelection(widget.room.id), - onForget: widget.onForget, - filter: widget.filter, - ); - } -} diff --git a/lib/widgets/avatar.dart b/lib/widgets/avatar.dart index 2a80451ca..edf53cc9a 100644 --- a/lib/widgets/avatar.dart +++ b/lib/widgets/avatar.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; +import 'package:fluffychat/widgets/presence_builder.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; @@ -12,9 +13,8 @@ class Avatar extends StatelessWidget { final Client? client; final String? presenceUserId; final Color? presenceBackgroundColor; - //#Pangea - final IconData? littleIcon; - // Pangea# + final BorderRadius? borderRadius; + final IconData? icon; const Avatar({ this.mxContent, @@ -24,9 +24,8 @@ class Avatar extends StatelessWidget { this.client, this.presenceUserId, this.presenceBackgroundColor, - //#Pangea - this.littleIcon, - // Pangea# + this.borderRadius, + this.icon, super.key, }); @@ -53,18 +52,25 @@ class Avatar extends StatelessWidget { ), ), ); - final borderRadius = BorderRadius.circular(size / 2); + final borderRadius = this.borderRadius ?? BorderRadius.circular(size / 2); final presenceUserId = this.presenceUserId; final color = noPic ? name?.lightColorAvatar : Theme.of(context).secondaryHeaderColor; final container = Stack( children: [ - ClipRRect( - borderRadius: borderRadius, - child: Container( - width: size, - height: size, + SizedBox( + width: size, + height: size, + child: Material( color: color, + shape: RoundedRectangleBorder( + borderRadius: borderRadius, + side: BorderSide( + width: 0, + color: Theme.of(context).dividerColor, + ), + ), + clipBehavior: Clip.hardEdge, child: noPic ? textWidget : MxcImage( @@ -78,71 +84,49 @@ class Avatar extends StatelessWidget { ), ), ), - // #Pangea - if (littleIcon != null) - Positioned( - bottom: 0, - right: 0, - child: ClipRRect( - borderRadius: borderRadius, - child: Container( - height: 16, - width: 16, - color: Colors.white, - child: Icon( - littleIcon, - color: noPic - ? name?.lightColorAvatar - : Theme.of(context).secondaryHeaderColor, - size: 14, + if (presenceUserId != null) + PresenceBuilder( + client: client, + userId: presenceUserId, + builder: (context, presence) { + if (presence == null || + (presence.presence == PresenceType.offline && + presence.lastActiveTimestamp == null)) { + return const SizedBox.shrink(); + } + final dotColor = presence.presence.isOnline + ? Colors.green + : presence.presence.isUnavailable + ? Colors.orange + : Colors.grey; + return Positioned( + bottom: -3, + right: -3, + child: Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: presenceBackgroundColor ?? + Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(32), + ), + alignment: Alignment.center, + child: Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: dotColor, + borderRadius: BorderRadius.circular(16), + border: Border.all( + width: 1, + color: Theme.of(context).colorScheme.surface, + ), + ), + ), ), - ), - ), + ); + }, ), - // #Pangea - // PresenceBuilder( - // client: client, - // userId: presenceUserId, - // builder: (context, presence) { - // if (presence == null || - // (presence.presence == PresenceType.offline && - // presence.lastActiveTimestamp == null)) { - // return const SizedBox.shrink(); - // } - // final dotColor = presence.presence.isOnline - // ? Colors.green - // : presence.presence.isUnavailable - // ? Colors.orange - // : Colors.grey; - // return Positioned( - // bottom: -3, - // right: -3, - // child: Container( - // width: 16, - // height: 16, - // decoration: BoxDecoration( - // color: presenceBackgroundColor ?? - // Theme.of(context).colorScheme.surface, - // borderRadius: BorderRadius.circular(32), - // ), - // alignment: Alignment.center, - // child: Container( - // width: 10, - // height: 10, - // decoration: BoxDecoration( - // color: dotColor, - // borderRadius: BorderRadius.circular(16), - // border: Border.all( - // width: 1, - // color: Theme.of(context).colorScheme.surface, - // ), - // ), - // ), - // ), - // ); - // }, - // ), - // Pangea# ], ); if (onTap == null) return container; diff --git a/lib/widgets/layouts/two_column_layout.dart b/lib/widgets/layouts/two_column_layout.dart index a6f4c8bdf..c270f120a 100644 --- a/lib/widgets/layouts/two_column_layout.dart +++ b/lib/widgets/layouts/two_column_layout.dart @@ -3,13 +3,11 @@ import 'package:flutter/material.dart'; class TwoColumnLayout extends StatelessWidget { final Widget mainView; final Widget sideView; - final bool displayNavigationRail; const TwoColumnLayout({ super.key, required this.mainView, required this.sideView, - required this.displayNavigationRail, }); @override Widget build(BuildContext context) { @@ -20,7 +18,7 @@ class TwoColumnLayout extends StatelessWidget { Container( clipBehavior: Clip.antiAlias, decoration: const BoxDecoration(), - width: 360.0 + (displayNavigationRail ? 64 : 0), + width: 384.0, child: mainView, ), Container( diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 0f85950ff..7b9ab0ec2 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -485,10 +485,6 @@ class MatrixState extends State with WidgetsBindingObserver { store.getBool(SettingKeys.hideUnimportantStateEvents) ?? AppConfig.hideUnimportantStateEvents; - AppConfig.separateChatTypes = - store.getBool(SettingKeys.separateChatTypes) ?? - AppConfig.separateChatTypes; - AppConfig.autoplayImages = store.getBool(SettingKeys.autoplayImages) ?? AppConfig.autoplayImages; diff --git a/linux/my_application.cc b/linux/my_application.cc index c185bcd78..0abe77c60 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -60,7 +60,7 @@ static void my_application_activate(GApplication* application) { gtk_window_set_title(window, "FluffyChat"); } - gtk_window_set_default_size(window, 864, 680); + gtk_window_set_default_size(window, 800, 600); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new();