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

@ -122,7 +122,7 @@ class BackgroundPush {
//<GOOGLE_SERVICES>firebase.setListeners(
//<GOOGLE_SERVICES> onMessage: (message) => pushHelper(
//<GOOGLE_SERVICES> PushNotification.fromJson(
//<GOOGLE_SERVICES> Map<String, dynamic>.from(message['data'] ?? message),
//<GOOGLE_SERVICES> message.tryGetMap<String, Object>('data') ?? message,
//<GOOGLE_SERVICES> ),
//<GOOGLE_SERVICES> client: client,
//<GOOGLE_SERVICES> l10n: l10n,
@ -351,6 +351,9 @@ class BackgroundPush {
Future<void> setupFirebase() async {
Logs().v('Setup firebase');
if (_fcmToken?.isEmpty ?? true) {
if (PlatformInfos.isIOS) {
//<GOOGLE_SERVICES>await firebase.requestPermission();
}
try {
//<GOOGLE_SERVICES>_fcmToken = await firebase.getToken();
if (_fcmToken == null) throw ('PushToken is null');

View file

@ -13,7 +13,7 @@ Future<List<XFile>> selectFiles(
final result = await AppLock.of(context).pauseWhile(
showFutureLoadingDialog(
context: context,
future: () => FilePicker.platform.pickFiles(
future: () => FilePicker.pickFiles(
compressionQuality: 0,
allowMultiple: allowMultiple,
type: type,

View file

@ -12,6 +12,8 @@ abstract class FluffyShare {
BuildContext context, {
bool copyOnly = false,
}) async {
final l10n = L10n.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
if (PlatformInfos.isMobile && !copyOnly) {
final box = context.findRenderObject() as RenderBox;
await SharePlus.instance.share(
@ -24,21 +26,20 @@ abstract class FluffyShare {
}
await Clipboard.setData(ClipboardData(text: text));
if (!PlatformInfos.isMobile) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
showCloseIcon: true,
content: Text(L10n.of(context).copiedToClipboard),
),
scaffoldMessenger.showSnackBar(
SnackBar(showCloseIcon: true, content: Text(l10n.copiedToClipboard)),
);
}
return;
}
static Future<void> shareInviteLink(BuildContext context) async {
final l10n = L10n.of(context);
final client = Matrix.of(context).client;
final ownProfile = await client.fetchOwnProfile();
if (!context.mounted) return;
await FluffyShare.share(
L10n.of(context).inviteText(
l10n.inviteText(
ownProfile.displayName ?? client.userID!,
'https://matrix.to/#/${client.userID}?client=im.fluffychat',
),

View file

@ -25,12 +25,14 @@ extension LocalizedBody on Event {
Future<void> saveFile(BuildContext context) async {
final matrixFile = await _getFile(context);
if (!context.mounted) return;
matrixFile.result?.save(context);
}
Future<void> shareFile(BuildContext context) async {
final matrixFile = await _getFile(context);
if (!context.mounted) return;
matrixFile.result?.share(context);
}

View file

@ -9,7 +9,7 @@ extension MatrixFileExtension on MatrixFile {
Future<void> save(BuildContext context) async {
final scaffoldMessenger = ScaffoldMessenger.of(context);
final l10n = L10n.of(context);
final downloadPath = await FilePicker.platform.saveFile(
final downloadPath = await FilePicker.saveFile(
dialogTitle: l10n.saveFile,
fileName: name,
type: filePickerFileType,

View file

@ -47,15 +47,17 @@ abstract class PlatformInfos {
}
static Future<void> showDialog(BuildContext context) async {
final l10n = L10n.of(context);
final version = await PlatformInfos.getVersion();
if (!context.mounted) return;
showAboutDialog(
context: context,
children: [
Text(L10n.of(context).versionWithNumber(version)),
Text(l10n.versionWithNumber(version)),
TextButton.icon(
onPressed: () => launchUrlString(AppConfig.sourceCodeUrl),
icon: const Icon(Icons.source_outlined),
label: Text(L10n.of(context).sourceCode),
label: Text(l10n.sourceCode),
),
Builder(
builder: (innerContext) {
@ -65,7 +67,7 @@ abstract class PlatformInfos {
Navigator.of(innerContext).pop();
},
icon: const Icon(Icons.list_outlined),
label: Text(L10n.of(context).logs),
label: Text(l10n.logs),
);
},
),
@ -77,7 +79,7 @@ abstract class PlatformInfos {
Navigator.of(innerContext).pop();
},
icon: const Icon(Icons.settings_applications_outlined),
label: Text(L10n.of(context).advancedConfigs),
label: Text(l10n.advancedConfigs),
);
},
),

View file

@ -17,6 +17,8 @@ import 'package:flutter_shortcuts_new/flutter_shortcuts_new.dart';
import 'package:matrix/matrix.dart';
const notificationAvatarDimension = 128;
const String groupKey = 'im.fluffychat.messages';
const int summaryId = -1;
Future<void> pushHelper(
PushNotification notification, {
@ -53,6 +55,7 @@ Future<void> pushHelper(
AppSettings.applicationName.value,
(notification.counts?.unread ?? 0).toString(),
),
groupKey: groupKey,
importance: Importance.high,
priority: Priority.max,
shortcutId: notification.roomId,
@ -252,7 +255,7 @@ Future<void> _tryPushHelper(
),
importance: Importance.high,
priority: Priority.max,
groupKey: event.room.spaceParents.firstOrNull?.roomId ?? 'rooms',
groupKey: groupKey,
actions: event.type == EventTypes.RoomMember || !useNotificationActions
? null
: <AndroidNotificationAction>[
@ -300,6 +303,41 @@ Future<void> _tryPushHelper(
event.eventId,
).toString(),
);
// Send summary notification on Android
if (PlatformInfos.isAndroid) {
final activeNotifications =
(await flutterLocalNotificationsPlugin.getActiveNotifications())
.where((n) => n.groupKey == groupKey)
.toList();
if (activeNotifications.isEmpty) {
return;
}
final title = l10n.unreadChatsInApp(
AppSettings.applicationName.value,
activeNotifications.length.toString(),
);
await flutterLocalNotificationsPlugin.show(
id: summaryId,
notificationDetails: NotificationDetails(
android: AndroidNotificationDetails(
AppConfig.pushNotificationsChannelId,
l10n.incomingMessages,
groupKey: groupKey,
setAsGroupSummary: true,
styleInformation: InboxStyleInformation(
activeNotifications.map((n) => n.body ?? '').toList(),
contentTitle: title,
summaryText: title,
),
autoCancel: false,
),
),
);
}
Logs().v('Push helper has been completed!');
}

View file

@ -10,6 +10,7 @@ abstract class UpdateNotifier {
static Future<void> showUpdateSnackBar(BuildContext context) async {
final scaffoldMessenger = ScaffoldMessenger.of(context);
final l10n = L10n.of(context);
final currentVersion = await PlatformInfos.getVersion();
final store = await SharedPreferences.getInstance();
final storedVersion = store.getString(versionStoreKey);
@ -20,9 +21,9 @@ abstract class UpdateNotifier {
SnackBar(
duration: const Duration(seconds: 30),
showCloseIcon: true,
content: Text(L10n.of(context).updateInstalled(currentVersion)),
content: Text(l10n.updateInstalled(currentVersion)),
action: SnackBarAction(
label: L10n.of(context).changelog,
label: l10n.changelog,
onPressed: () => launchUrlString(AppConfig.changelogUrl),
),
),

View file

@ -38,6 +38,7 @@ Future<void> connectToHomeserverFlow(
if ((kIsWeb || PlatformInfos.isLinux) &&
(supportsSso || authMetadata != null || (signUp && regLink != null))) {
if (!context.mounted) return;
final consent = await showOkCancelAlertDialog(
context: context,
title: l10n.appWantsToUseForLogin(homeserverInput),
@ -45,7 +46,9 @@ Future<void> connectToHomeserverFlow(
okLabel: l10n.continueText,
);
if (consent != OkCancelResult.ok) return;
if (!context.mounted) return;
}
if (!context.mounted) return;
if (authMetadata != null && AppSettings.enableMatrixNativeOIDC.value) {
await oidcLoginFlow(client, context, signUp);
@ -55,6 +58,7 @@ Future<void> connectToHomeserverFlow(
if (signUp && regLink != null) {
await launchUrlString(regLink);
}
if (!context.mounted) return;
final pathSegments = List.of(
GoRouter.of(context).routeInformationProvider.value.uri.pathSegments,
);

View file

@ -27,6 +27,8 @@ class UrlLauncher {
const UrlLauncher(this.context, this.url, [this.name]);
Future<void> launchUrl() async {
final l10n = L10n.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
if (url!.toLowerCase().startsWith(AppConfig.deepLinkPrefix) ||
url!.toLowerCase().startsWith(AppConfig.inviteLinkPrefix) ||
{'#', '@', '!', '+', '\$'}.contains(url![0]) ||
@ -36,8 +38,8 @@ class UrlLauncher {
final uri = Uri.tryParse(url!);
if (uri == null) {
// we can't open this thing
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).cantOpenUri(url!))),
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(l10n.cantOpenUri(url!))),
);
return;
}
@ -47,10 +49,10 @@ class UrlLauncher {
// that the user can see the actual url before opening the browser.
final consent = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).openLinkInBrowser,
title: l10n.openLinkInBrowser,
message: url,
okLabel: L10n.of(context).open,
cancelLabel: L10n.of(context).cancel,
okLabel: l10n.open,
cancelLabel: l10n.cancel,
);
if (consent != OkCancelResult.ok) return;
}
@ -90,8 +92,8 @@ class UrlLauncher {
return;
}
if (uri.host.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).cantOpenUri(url!))),
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(l10n.cantOpenUri(url!))),
);
return;
}
@ -161,6 +163,7 @@ class UrlLauncher {
}
}
servers.addAll(identityParts.via);
if (!context.mounted) return;
if (room != null) {
if (room.isSpace) {
// TODO: Implement navigate to space
@ -178,6 +181,7 @@ class UrlLauncher {
}
return;
} else {
if (!context.mounted) return;
await showAdaptiveDialog(
context: context,
builder: (c) =>
@ -185,6 +189,7 @@ class UrlLauncher {
);
}
if (roomIdOrAlias.sigil == '!') {
if (!context.mounted) return;
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
@ -192,6 +197,7 @@ class UrlLauncher {
) ==
OkCancelResult.ok) {
roomId = roomIdOrAlias;
if (!context.mounted) return;
final response = await showFutureLoadingDialog(
context: context,
future: () => matrix.client.joinRoom(
@ -200,11 +206,13 @@ class UrlLauncher {
),
);
if (response.error != null) return;
if (!context.mounted) return;
// wait for two seconds so that it probably came down /sync
await showFutureLoadingDialog(
context: context,
future: () => Future.delayed(const Duration(seconds: 2)),
);
if (!context.mounted) return;
if (event != null) {
context.go(
Uri(
@ -228,6 +236,7 @@ class UrlLauncher {
return Profile(userId: userId);
}),
);
if (!context.mounted) return;
await UserDialog.show(
context: context,
profile: profileResult.result!,