feat: notification in space view for knocking users (#2068)

This commit is contained in:
ggurdin 2025-03-07 09:43:56 -05:00 committed by GitHub
parent 50af914f86
commit 6914d9d0d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 215 additions and 47 deletions

View file

@ -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 😀"
}

View file

@ -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<SpaceView> {
);
},
),
// #Pangea
KnockingUsersIndicator(room: room),
// Pangea#
SliverList.builder(
itemCount: joinedRooms.length,
itemBuilder: (context, i) {

View file

@ -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<KnockingUsersIndicator> {
List<User> _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<void> _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",
),
),
),
),
);
},
);
}
}

View file

@ -22,6 +22,7 @@ Future<Result<T>> showFutureLoadingDialog<T>({
ExceptionContext? exceptionContext,
// #Pangea
String? Function(Object, StackTrace?)? onError,
String? Function()? onSuccess,
VoidCallback? onDismiss,
// Pangea#
}) async {
@ -55,6 +56,7 @@ Future<Result<T>> showFutureLoadingDialog<T>({
// #Pangea
onError: onError,
onDismiss: onDismiss,
onSuccess: onSuccess,
// Pangea#
),
);
@ -80,6 +82,7 @@ class LoadingDialog<T> 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<T> extends StatefulWidget {
this.exceptionContext,
// #Pangea
this.onError,
this.onSuccess,
this.onDismiss,
// Pangea#
});
@ -101,40 +105,57 @@ class LoadingDialog<T> extends StatefulWidget {
class LoadingDialogState<T> extends State<LoadingDialog> {
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<T>>(Result.value(result)),
// onError: (e, s) => setState(() {
// exception = e;
// stackTrace = s;
// }),
(result) {
if (mounted && Navigator.of(context).canPop()) {
Navigator.of(context).pop<Result<T>>(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<T>>(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<T>>(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<T> extends State<LoadingDialog> {
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<T> extends State<LoadingDialog> {
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<T>>(
// Result.error(
// exception,
// stackTrace,
// ),
// ),
// child: Text(widget.backLabel ?? L10n.of(context).close),
// ),
// ],
actions: _successMessage != null
? [
AdaptiveDialogAction(
onPressed: () => Navigator.of(context).pop<Result<T>>(
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<T>>(
Result.error(
exception,
stackTrace,
),
),
child: Text(widget.backLabel ?? L10n.of(context).close),
),
],
// Pangea#
);
}
}

View file

@ -90,6 +90,10 @@ class PublicRoomBottomSheetState extends State<PublicRoomBottomSheet> {
}
return roomId;
},
// #Pangea
onSuccess: wasInRoom ? null : () => L10n.of(context).knockSpaceSuccess,
delay: false,
// Pangea#
);
// #Pangea
// if (knock) {