Merge branch 'main' of https://github.com/krille-chan/fluffychat
Some checks failed
Main Deploy Workflow / deploy_web (push) Has been cancelled
Main Deploy Workflow / deploy_playstore_internal (push) Has been cancelled

This commit is contained in:
Alexey 2026-04-23 10:59:25 +03:00
commit e12723fdaa
98 changed files with 1450 additions and 1014 deletions

View file

@ -25,6 +25,7 @@ class PublicRoomDialog extends StatelessWidget {
const PublicRoomDialog({super.key, this.roomAlias, this.chunk, this.via});
Future<void> _joinRoom(BuildContext context) async {
final l10n = L10n.of(context);
final client = Matrix.of(context).client;
final chunk = this.chunk;
final knock = chunk?.joinRule == 'knock';
@ -48,12 +49,13 @@ class PublicRoomDialog extends StatelessWidget {
);
final roomId = result.result;
if (roomId == null) return;
if (!context.mounted) return;
if (knock && client.getRoomById(roomId) == null) {
Navigator.of(context).pop<bool>(true);
await showOkAlertDialog(
context: context,
title: L10n.of(context).youHaveKnocked,
message: L10n.of(context).pleaseWaitUntilInvited,
title: l10n.youHaveKnocked,
message: l10n.pleaseWaitUntilInvited,
);
return;
}
@ -73,6 +75,7 @@ class PublicRoomDialog extends StatelessWidget {
bool _testRoom(PublishedRoomsChunk r) => r.canonicalAlias == roomAlias;
Future<PublishedRoomsChunk> _search(BuildContext context) async {
final l10n = L10n.of(context);
final chunk = this.chunk;
if (chunk != null) return chunk;
final query = await Matrix.of(context).client.queryPublicRooms(
@ -80,7 +83,7 @@ class PublicRoomDialog extends StatelessWidget {
filter: PublicRoomQueryFilter(genericSearchTerm: roomAlias),
);
if (!query.chunk.any(_testRoom)) {
throw (L10n.of(context).noRoomsFound);
throw (l10n.noRoomsFound);
}
return query.chunk.firstWhere(_testRoom);
}
@ -248,6 +251,7 @@ class PublicRoomDialog extends StatelessWidget {
hintText: L10n.of(context).reason,
);
if (reason == null || reason.isEmpty) return;
if (!context.mounted) return;
await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context).client.reportRoom(

View file

@ -220,6 +220,7 @@ class UserDialog extends StatelessWidget {
hintText: L10n.of(context).reason,
);
if (reason == null || reason.isEmpty) return;
if (!context.mounted) return;
await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(

View file

@ -53,16 +53,18 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
onSelected: (choice) async {
switch (choice) {
case ChatPopupMenuActions.leave:
final l10n = L10n.of(context);
final router = GoRouter.of(context);
final confirmed = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).areYouSure,
message: L10n.of(context).archiveRoomDescription,
okLabel: L10n.of(context).leave,
cancelLabel: L10n.of(context).cancel,
title: l10n.areYouSure,
message: l10n.archiveRoomDescription,
okLabel: l10n.leave,
cancelLabel: l10n.cancel,
isDestructive: true,
);
if (confirmed != OkCancelResult.ok) return;
if (!context.mounted) return;
final result = await showFutureLoadingDialog(
context: context,
future: () => widget.room.leave(),

View file

@ -43,7 +43,7 @@ class FluffyChatApp extends StatelessWidget {
// Pass deep links to app:
if (state.uri.toString().startsWith(AppConfig.deepLinkPrefix)) {
return '/rooms/newprivatechat?deeplink=${state.uri}';
return '/rooms/newprivatechat#${state.uri}';
}
return null;
},

View file

@ -5,6 +5,7 @@ import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix_api_lite/utils/logs.dart';
/// Displays a loading dialog which reacts to the given [future]. The dialog
/// will be dismissed and the value will be returned when the future completes.
@ -40,6 +41,15 @@ Future<Result<T>> showFutureLoadingDialog<T>({
}
}
if (!context.mounted) {
Logs().e(
'Unable to show loading dialog!',
Exception('The BuildContext is not mounted!'),
StackTrace.current,
);
return Result.capture(futureExec);
}
final result = await showAdaptiveDialog<Result<T>>(
context: context,
barrierDismissible: barrierDismissible,

View file

@ -16,7 +16,12 @@ import 'package:matrix/matrix.dart';
import 'package:universal_html/html.dart' as html;
extension LocalNotificationsExtension on MatrixState {
static final html.AudioElement _audioPlayer = html.AudioElement()
..src = 'assets/assets/sounds/notification.ogg'
..load();
Future<void> showLocalNotification(Event event) async {
final l10n = L10n.of(context);
final roomId = event.room.id;
if (activeRoomId == roomId) {
if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) {
@ -68,6 +73,8 @@ extension LocalNotificationsExtension on MatrixState {
);
}
if (AppSettings.webNotificationSound.value) _audioPlayer.play();
html.Notification(
title,
body: body,
@ -114,11 +121,11 @@ extension LocalNotificationsExtension on MatrixState {
actions: [
NotificationAction(
DesktopNotificationActions.openChat.name,
L10n.of(context).openChat,
l10n.openChat,
),
NotificationAction(
DesktopNotificationActions.seen.name,
L10n.of(context).markAsRead,
l10n.markAsRead,
),
],
hints: hints,

View file

@ -265,12 +265,19 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
InitWithRestoreExtension.deleteSessionBackup(name);
if (loggedInWithMultipleClients) {
final snackbarContext =
FluffyChatApp
.router
.routerDelegate
.navigatorKey
.currentContext ??
context;
if (!snackbarContext.mounted) return;
final l10n = L10n.of(snackbarContext);
ScaffoldMessenger.of(
FluffyChatApp.router.routerDelegate.navigatorKey.currentContext ??
context,
).showSnackBar(
SnackBar(content: Text(L10n.of(context).oneClientLoggedOut)),
);
snackbarContext,
).showSnackBar(SnackBar(content: Text(l10n.oneClientLoggedOut)));
return;
}
FluffyChatApp.router.go('/');
@ -382,15 +389,17 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
}
Future<void> dehydrateAction(BuildContext context) async {
final l10n = L10n.of(context);
final response = await showOkCancelAlertDialog(
context: context,
isDestructive: true,
title: L10n.of(context).dehydrate,
message: L10n.of(context).dehydrateWarning,
title: l10n.dehydrate,
message: l10n.dehydrateWarning,
);
if (response != OkCancelResult.ok) {
return;
}
if (!context.mounted) return;
final result = await showFutureLoadingDialog(
context: context,
future: client.exportDump,
@ -404,6 +413,7 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
'fluffychat-export-${DateFormat(DateFormat.YEAR_MONTH_DAY).format(DateTime.now())}.fluffybackup';
final file = MatrixFile(bytes: exportBytes, name: exportFileName);
if (!context.mounted) return;
file.save(context);
}
}

View file

@ -14,6 +14,8 @@ Future<void> showMemberActionsPopupMenu({
required User user,
void Function()? onMention,
}) async {
final l10n = L10n.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
final theme = Theme.of(context);
final displayname = user.calcDisplayname();
final isMe = user.room.client.userID == user.id;
@ -79,7 +81,7 @@ Future<void> showMemberActionsPopupMenu({
),
),
if (user.canChangeUserPowerLevel) ...[
if (user.powerLevel < 100)
if (user.powerLevel.level < 100)
PopupMenuItem(
value: _MemberActions.makeAdmin,
child: Row(
@ -90,7 +92,7 @@ Future<void> showMemberActionsPopupMenu({
],
),
),
if (user.powerLevel < 50)
if (user.powerLevel.level < 50)
PopupMenuItem(
value: _MemberActions.makeModerator,
child: Row(
@ -101,7 +103,7 @@ Future<void> showMemberActionsPopupMenu({
],
),
),
if (user.powerLevel >= 100)
if (user.powerLevel.role == PowerLevelRole.admin)
PopupMenuItem(
value: _MemberActions.removeAdmin,
child: Row(
@ -112,7 +114,7 @@ Future<void> showMemberActionsPopupMenu({
],
),
)
else if (user.powerLevel >= 50)
else if (user.powerLevel.role == PowerLevelRole.moderator)
PopupMenuItem(
value: _MemberActions.removeModerator,
child: Row(
@ -125,7 +127,7 @@ Future<void> showMemberActionsPopupMenu({
),
],
if (user.canChangeUserPowerLevel ||
!defaultPowerLevels.contains(user.powerLevel))
!defaultPowerLevels.contains(user.powerLevel.level))
PopupMenuItem(
value: _MemberActions.setPowerLevel,
enabled: user.canChangeUserPowerLevel,
@ -138,7 +140,7 @@ Future<void> showMemberActionsPopupMenu({
? L10n.of(context).setPowerLevel
: L10n.of(context).powerLevel,
),
if (!defaultPowerLevels.contains(user.powerLevel))
if (!defaultPowerLevels.contains(user.powerLevel.level))
Text(' (${user.powerLevel})'),
],
),
@ -217,8 +219,8 @@ Future<void> showMemberActionsPopupMenu({
case _MemberActions.setPowerLevel:
final power = await showPermissionChooser(
context,
currentLevel: user.powerLevel,
maxLevel: user.room.ownPowerLevel,
currentLevel: user.powerLevel.level,
maxLevel: user.room.ownPowerLevel.level,
);
if (power == null) return;
if (!context.mounted) return;
@ -245,12 +247,13 @@ Future<void> showMemberActionsPopupMenu({
case _MemberActions.kick:
if (await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes,
cancelLabel: L10n.of(context).no,
message: L10n.of(context).kickUserDescription,
title: l10n.areYouSure,
okLabel: l10n.yes,
cancelLabel: l10n.no,
message: l10n.kickUserDescription,
) ==
OkCancelResult.ok) {
if (!context.mounted) return;
await showFutureLoadingDialog(
context: context,
future: () => user.kick(),
@ -260,12 +263,13 @@ Future<void> showMemberActionsPopupMenu({
case _MemberActions.ban:
if (await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes,
cancelLabel: L10n.of(context).no,
message: L10n.of(context).banUserDescription,
title: l10n.areYouSure,
okLabel: l10n.yes,
cancelLabel: l10n.no,
message: l10n.banUserDescription,
) ==
OkCancelResult.ok) {
if (!context.mounted) return;
await showFutureLoadingDialog(
context: context,
future: () => user.ban(),
@ -275,20 +279,22 @@ Future<void> showMemberActionsPopupMenu({
case _MemberActions.report:
final reason = await showTextInputDialog(
context: context,
title: L10n.of(context).whyDoYouWantToReportThis,
okLabel: L10n.of(context).report,
cancelLabel: L10n.of(context).cancel,
hintText: L10n.of(context).reason,
title: l10n.whyDoYouWantToReportThis,
okLabel: l10n.report,
cancelLabel: l10n.cancel,
hintText: l10n.reason,
);
if (reason == null || reason.isEmpty) return;
if (!context.mounted) return;
final result = await showFutureLoadingDialog(
context: context,
future: () => user.room.client.reportUser(user.id, reason),
);
if (result.error != null) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).contentHasBeenReported)),
if (!context.mounted) return;
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(l10n.contentHasBeenReported)),
);
return;
case _MemberActions.info:
@ -304,19 +310,20 @@ Future<void> showMemberActionsPopupMenu({
case _MemberActions.unban:
if (await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes,
cancelLabel: L10n.of(context).no,
message: L10n.of(context).unbanUserDescription,
title: l10n.areYouSure,
okLabel: l10n.yes,
cancelLabel: l10n.no,
message: l10n.unbanUserDescription,
) ==
OkCancelResult.ok) {
if (!context.mounted) return;
await showFutureLoadingDialog(
context: context,
future: () => user.unban(),
);
}
case _MemberActions.makeAdmin:
if (user.room.ownPowerLevel <= 100) {
if (user.room.ownPowerLevel.level <= 100) {
final consent = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).areYouSure,

View file

@ -54,11 +54,11 @@ class SpacesNavigationRail extends StatelessWidget {
isSelected: activeSpaceId == null,
onTap: onGoToChats,
icon: const Padding(
padding: EdgeInsets.all(10.0),
padding: EdgeInsets.all(8.0),
child: Icon(Icons.forum_outlined),
),
selectedIcon: const Padding(
padding: EdgeInsets.all(10.0),
padding: EdgeInsets.all(8.0),
child: Icon(Icons.forum),
),
toolTip: L10n.of(context).chats,
@ -71,7 +71,7 @@ class SpacesNavigationRail extends StatelessWidget {
isSelected: false,
onTap: () => context.go('/rooms/newspace'),
icon: const Padding(
padding: EdgeInsets.all(8.0),
padding: EdgeInsets.all(6.0),
child: Icon(Icons.add),
),
toolTip: L10n.of(context).createNewSpace,
@ -94,6 +94,7 @@ class SpacesNavigationRail extends StatelessWidget {
icon: Avatar(
mxContent: allSpaces[i].avatar,
name: displayname,
size: 36,
shapeBorder: RoundedSuperellipseBorder(
side: BorderSide(
width: 1,