diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 877c522df..3deefea0c 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4871,5 +4871,6 @@ "type": "String" } } - } + }, + "knockSpaceSuccess": "You have requested to join this space! An admin will respond to your request when they receive it 😀" } diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index ab04a0513..6f0f706ae 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -16,6 +16,7 @@ import 'package:fluffychat/pages/chat_list/search_title.dart'; import 'package:fluffychat/pangea/chat/constants/default_power_level.dart'; import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/spaces/widgets/knocking_users_indicator.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/stream_extension.dart'; @@ -717,6 +718,9 @@ class _SpaceViewState extends State { ); }, ), + // #Pangea + KnockingUsersIndicator(room: room), + // Pangea# SliverList.builder( itemCount: joinedRooms.length, itemBuilder: (context, i) { diff --git a/lib/pangea/spaces/widgets/knocking_users_indicator.dart b/lib/pangea/spaces/widgets/knocking_users_indicator.dart new file mode 100644 index 000000000..76cc30959 --- /dev/null +++ b/lib/pangea/spaces/widgets/knocking_users_indicator.dart @@ -0,0 +1,109 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/utils/stream_extension.dart'; + +class KnockingUsersIndicator extends StatefulWidget { + final Room room; + const KnockingUsersIndicator({ + super.key, + required this.room, + }); + + @override + KnockingUsersIndicatorState createState() => KnockingUsersIndicatorState(); +} + +class KnockingUsersIndicatorState extends State { + List _knockingUsers = []; + StreamSubscription? _memberSubscription; + + KnockingUsersIndicatorState(); + + @override + void initState() { + super.initState(); + _memberSubscription ??= widget.room.client.onRoomState.stream + .where(_isMemberUpdate) + .rateLimit(const Duration(seconds: 1)) + .listen((_) => _setKnockingUsers()); + _setKnockingUsers(); + } + + bool _isMemberUpdate(({String roomId, StrippedStateEvent state}) event) => + event.roomId == widget.room.id && + event.state.type == EventTypes.RoomMember; + + @override + void dispose() { + _memberSubscription?.cancel(); + super.dispose(); + } + + Future _setKnockingUsers({bool loadParticipants = false}) async { + _knockingUsers = loadParticipants + ? await widget.room.requestParticipants([Membership.knock]) + : widget.room.getParticipants([Membership.knock]); + if (mounted) setState(() {}); + } + + @override + Widget build(BuildContext context) { + return SliverList.builder( + itemCount: 1, + itemBuilder: (context, i) { + return AnimatedSize( + duration: FluffyThemes.animationDuration, + child: _knockingUsers.isEmpty || !widget.room.isRoomAdmin + ? const SizedBox() + : Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 1, + ), + child: Material( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + clipBehavior: Clip.hardEdge, + child: ListTile( + minVerticalPadding: 0, + trailing: Icon( + Icons.adaptive.arrow_forward_outlined, + size: 16, + ), + title: Row( + spacing: 8.0, + children: [ + Icon( + Icons.notifications_outlined, + color: Theme.of(context).colorScheme.error, + ), + Expanded( + child: Text( + _knockingUsers.length == 1 + ? "1 user is requesting to join your space" + : "${_knockingUsers.length} users are requesting to join your space", + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ], + ), + onTap: () => context.push( + "/rooms/${widget.room.id}/details/members", + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/widgets/future_loading_dialog.dart b/lib/widgets/future_loading_dialog.dart index a2089406a..ede8b5201 100644 --- a/lib/widgets/future_loading_dialog.dart +++ b/lib/widgets/future_loading_dialog.dart @@ -22,6 +22,7 @@ Future> showFutureLoadingDialog({ ExceptionContext? exceptionContext, // #Pangea String? Function(Object, StackTrace?)? onError, + String? Function()? onSuccess, VoidCallback? onDismiss, // Pangea# }) async { @@ -55,6 +56,7 @@ Future> showFutureLoadingDialog({ // #Pangea onError: onError, onDismiss: onDismiss, + onSuccess: onSuccess, // Pangea# ), ); @@ -80,6 +82,7 @@ class LoadingDialog extends StatefulWidget { final ExceptionContext? exceptionContext; // #Pangea final String? Function(Object, StackTrace?)? onError; + final String? Function()? onSuccess; final VoidCallback? onDismiss; // Pangea# @@ -91,6 +94,7 @@ class LoadingDialog extends StatefulWidget { this.exceptionContext, // #Pangea this.onError, + this.onSuccess, this.onDismiss, // Pangea# }); @@ -101,40 +105,57 @@ class LoadingDialog extends StatefulWidget { class LoadingDialogState extends State { Object? exception; StackTrace? stackTrace; + // #Pangea + Object? _result; + String? _successMessage; + // Pangea# @override void initState() { super.initState(); - widget.future.then( - // #Pangea - // (result) => Navigator.of(context).pop>(Result.value(result)), - // onError: (e, s) => setState(() { - // exception = e; - // stackTrace = s; - // }), - (result) { - if (mounted && Navigator.of(context).canPop()) { - Navigator.of(context).pop>(Result.value(result)); - } - }, - onError: (e, s) { - if (mounted) { - setState(() { - exception = widget.onError?.call(e, s) ?? e; - stackTrace = s; - }); - } - }, - // Pangea# + // #Pangea + // widget.future.then( + // (result) => Navigator.of(context).pop>(Result.value(result)), + // onError: (e, s) => setState(() { + // exception = e; + // stackTrace = s; + // }), + // ); + WidgetsBinding.instance.addPostFrameCallback( + (_) => widget.future.then( + (result) { + if (mounted && widget.onSuccess != null) { + _successMessage = widget.onSuccess!(); + _result = result; + setState(() {}); + } else if (mounted && Navigator.of(context).canPop()) { + Navigator.of(context).pop>(Result.value(result)); + } + }, + onError: (e, s) { + if (mounted) { + setState(() { + exception = widget.onError?.call(e, s) ?? e; + stackTrace = s; + }); + } + }, + ), ); + // Pangea# } @override Widget build(BuildContext context) { final exception = this.exception; + // #Pangea + // final titleLabel = exception != null + // ? exception.toLocalizedString(context, widget.exceptionContext) + // : widget.title ?? L10n.of(context).loadingPleaseWait; final titleLabel = exception != null ? exception.toLocalizedString(context, widget.exceptionContext) - : widget.title ?? L10n.of(context).loadingPleaseWait; + : _successMessage ?? widget.title ?? L10n.of(context).loadingPleaseWait; + // Pangea# return AlertDialog.adaptive( title: exception == null @@ -149,7 +170,10 @@ class LoadingDialogState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (exception == null) ...[ + // #Pangea + // if (exception == null) ...[ + if (exception == null && _successMessage == null) ...[ + // Pangea# const CircularProgressIndicator.adaptive(), const SizedBox(width: 20), ], @@ -157,39 +181,65 @@ class LoadingDialogState extends State { child: Text( titleLabel, maxLines: 4, - textAlign: exception == null ? TextAlign.left : null, + // #Pangea + // textAlign: exception == null ? TextAlign.left : null, + textAlign: exception == null && _successMessage == null + ? TextAlign.left + : null, + // Pangea# overflow: TextOverflow.ellipsis, ), ), ], ), ), - actions: exception == null - // #Pangea - // ? null - ? widget.onDismiss != null - ? [ - AdaptiveDialogAction( - onPressed: () { - widget.onDismiss!(); - Navigator.of(context).pop(); - }, - child: Text(L10n.of(context).cancel), - ), - ] - : null - // Pangea# - : [ + // #Pangea + // actions: exception == null + // ? null + // : [ + // AdaptiveDialogAction( + // onPressed: () => Navigator.of(context).pop>( + // Result.error( + // exception, + // stackTrace, + // ), + // ), + // child: Text(widget.backLabel ?? L10n.of(context).close), + // ), + // ], + actions: _successMessage != null + ? [ AdaptiveDialogAction( onPressed: () => Navigator.of(context).pop>( - Result.error( - exception, - stackTrace, - ), + Result.value(_result as T), ), - child: Text(widget.backLabel ?? L10n.of(context).close), + child: Text(L10n.of(context).close), ), - ], + ] + : exception == null + ? widget.onDismiss != null + ? [ + AdaptiveDialogAction( + onPressed: () { + widget.onDismiss!(); + Navigator.of(context).pop(); + }, + child: Text(L10n.of(context).cancel), + ), + ] + : null + : [ + AdaptiveDialogAction( + onPressed: () => Navigator.of(context).pop>( + Result.error( + exception, + stackTrace, + ), + ), + child: Text(widget.backLabel ?? L10n.of(context).close), + ), + ], + // Pangea# ); } } diff --git a/lib/widgets/public_room_bottom_sheet.dart b/lib/widgets/public_room_bottom_sheet.dart index 4a8178d7d..41f7591a1 100644 --- a/lib/widgets/public_room_bottom_sheet.dart +++ b/lib/widgets/public_room_bottom_sheet.dart @@ -90,6 +90,10 @@ class PublicRoomBottomSheetState extends State { } return roomId; }, + // #Pangea + onSuccess: wasInRoom ? null : () => L10n.of(context).knockSpaceSuccess, + delay: false, + // Pangea# ); // #Pangea // if (knock) {