Initial commit
This commit is contained in:
126
lib/pages/settings_notifications/push_rule_extensions.dart
Normal file
126
lib/pages/settings_notifications/push_rule_extensions.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
|
||||
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
extension PushRuleExtension on PushRule {
|
||||
String getPushRuleName(L10n l10n) {
|
||||
switch (ruleId) {
|
||||
case '.m.rule.contains_user_name':
|
||||
return l10n.notificationRuleContainsUserName;
|
||||
case '.m.rule.master':
|
||||
return l10n.notificationRuleMaster;
|
||||
case '.m.rule.suppress_notices':
|
||||
return l10n.notificationRuleSuppressNotices;
|
||||
case '.m.rule.invite_for_me':
|
||||
return l10n.notificationRuleInviteForMe;
|
||||
case '.m.rule.member_event':
|
||||
return l10n.notificationRuleMemberEvent;
|
||||
case '.m.rule.is_user_mention':
|
||||
return l10n.notificationRuleIsUserMention;
|
||||
case '.m.rule.contains_display_name':
|
||||
return l10n.notificationRuleContainsDisplayName;
|
||||
case '.m.rule.is_room_mention':
|
||||
return l10n.notificationRuleIsRoomMention;
|
||||
case '.m.rule.roomnotif':
|
||||
return l10n.notificationRuleRoomnotif;
|
||||
case '.m.rule.tombstone':
|
||||
return l10n.notificationRuleTombstone;
|
||||
case '.m.rule.reaction':
|
||||
return l10n.notificationRuleReaction;
|
||||
case '.m.rule.room_server_acl':
|
||||
return l10n.notificationRuleRoomServerAcl;
|
||||
case '.m.rule.suppress_edits':
|
||||
return l10n.notificationRuleSuppressEdits;
|
||||
case '.m.rule.call':
|
||||
return l10n.notificationRuleCall;
|
||||
case '.m.rule.encrypted_room_one_to_one':
|
||||
return l10n.notificationRuleEncryptedRoomOneToOne;
|
||||
case '.m.rule.room_one_to_one':
|
||||
return l10n.notificationRuleRoomOneToOne;
|
||||
case '.m.rule.message':
|
||||
return l10n.notificationRuleMessage;
|
||||
case '.m.rule.encrypted':
|
||||
return l10n.notificationRuleEncrypted;
|
||||
case '.m.rule.room.server_acl':
|
||||
return l10n.notificationRuleServerAcl;
|
||||
case '.im.vector.jitsi':
|
||||
return l10n.notificationRuleJitsi;
|
||||
default:
|
||||
return ruleId.split('.').last.replaceAll('_', ' ').capitalize();
|
||||
}
|
||||
}
|
||||
|
||||
String getPushRuleDescription(L10n l10n) {
|
||||
switch (ruleId) {
|
||||
case '.m.rule.contains_user_name':
|
||||
return l10n.notificationRuleContainsUserNameDescription;
|
||||
case '.m.rule.master':
|
||||
return l10n.notificationRuleMasterDescription;
|
||||
case '.m.rule.suppress_notices':
|
||||
return l10n.notificationRuleSuppressNoticesDescription;
|
||||
case '.m.rule.invite_for_me':
|
||||
return l10n.notificationRuleInviteForMeDescription;
|
||||
case '.m.rule.member_event':
|
||||
return l10n.notificationRuleMemberEventDescription;
|
||||
case '.m.rule.is_user_mention':
|
||||
return l10n.notificationRuleIsUserMentionDescription;
|
||||
case '.m.rule.contains_display_name':
|
||||
return l10n.notificationRuleContainsDisplayNameDescription;
|
||||
case '.m.rule.is_room_mention':
|
||||
return l10n.notificationRuleIsRoomMentionDescription;
|
||||
case '.m.rule.roomnotif':
|
||||
return l10n.notificationRuleRoomnotifDescription;
|
||||
case '.m.rule.tombstone':
|
||||
return l10n.notificationRuleTombstoneDescription;
|
||||
case '.m.rule.reaction':
|
||||
return l10n.notificationRuleReactionDescription;
|
||||
case '.m.rule.room_server_acl':
|
||||
return l10n.notificationRuleRoomServerAclDescription;
|
||||
case '.m.rule.suppress_edits':
|
||||
return l10n.notificationRuleSuppressEditsDescription;
|
||||
case '.m.rule.call':
|
||||
return l10n.notificationRuleCallDescription;
|
||||
case '.m.rule.encrypted_room_one_to_one':
|
||||
return l10n.notificationRuleEncryptedRoomOneToOneDescription;
|
||||
case '.m.rule.room_one_to_one':
|
||||
return l10n.notificationRuleRoomOneToOneDescription;
|
||||
case '.m.rule.message':
|
||||
return l10n.notificationRuleMessageDescription;
|
||||
case '.m.rule.encrypted':
|
||||
return l10n.notificationRuleEncryptedDescription;
|
||||
case '.m.rule.room.server_acl':
|
||||
return l10n.notificationRuleServerAclDescription;
|
||||
case '.im.vector.jitsi':
|
||||
return l10n.notificationRuleJitsiDescription;
|
||||
default:
|
||||
return l10n.unknownPushRule(ruleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PushRuleKindLocal on PushRuleKind {
|
||||
String localized(L10n l10n) {
|
||||
switch (this) {
|
||||
case PushRuleKind.content:
|
||||
return l10n.contentNotificationSettings;
|
||||
case PushRuleKind.override:
|
||||
return l10n.generalNotificationSettings;
|
||||
case PushRuleKind.room:
|
||||
return l10n.roomNotificationSettings;
|
||||
case PushRuleKind.sender:
|
||||
return l10n.userSpecificNotificationSettings;
|
||||
case PushRuleKind.underride:
|
||||
return l10n.otherNotificationSettings;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on String {
|
||||
String capitalize() {
|
||||
return '${this[0].toUpperCase()}${substring(1).toLowerCase()}';
|
||||
}
|
||||
}
|
||||
196
lib/pages/settings_notifications/settings_notifications.dart
Normal file
196
lib/pages/settings_notifications/settings_notifications.dart
Normal file
@@ -0,0 +1,196 @@
|
||||
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
|
||||
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:tatchat/config/app_config.dart';
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:tatchat/pages/settings_notifications/push_rule_extensions.dart';
|
||||
import 'package:tatchat/utils/localized_exception_extension.dart';
|
||||
import 'package:tatchat/widgets/adaptive_dialogs/adaptive_dialog_action.dart';
|
||||
import 'package:tatchat/widgets/adaptive_dialogs/show_modal_action_popup.dart';
|
||||
import 'package:tatchat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
import 'package:tatchat/widgets/future_loading_dialog.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../widgets/matrix.dart';
|
||||
import 'settings_notifications_view.dart';
|
||||
|
||||
class SettingsNotifications extends StatefulWidget {
|
||||
const SettingsNotifications({super.key});
|
||||
|
||||
@override
|
||||
SettingsNotificationsController createState() =>
|
||||
SettingsNotificationsController();
|
||||
}
|
||||
|
||||
class SettingsNotificationsController extends State<SettingsNotifications> {
|
||||
bool isLoading = false;
|
||||
|
||||
Future<void> onPusherTap(Pusher pusher) async {
|
||||
final delete = await showModalActionPopup<bool>(
|
||||
context: context,
|
||||
title: pusher.deviceDisplayName,
|
||||
message: '${pusher.appDisplayName} (${pusher.appId})',
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
actions: [
|
||||
AdaptiveModalAction(
|
||||
label: L10n.of(context).delete,
|
||||
isDestructive: true,
|
||||
value: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (delete != true) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.deletePusher(
|
||||
PusherId(appId: pusher.appId, pushkey: pusher.pushkey),
|
||||
),
|
||||
);
|
||||
|
||||
if (success.error != null) return;
|
||||
|
||||
setState(() {
|
||||
pusherFuture = null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<Pusher>?>? pusherFuture;
|
||||
|
||||
Future<void> togglePushRule(PushRuleKind kind, PushRule pushRule) async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
try {
|
||||
final updateFromSync = Matrix.of(context).client.onSync.stream
|
||||
.where(
|
||||
(syncUpdate) =>
|
||||
syncUpdate.accountData?.any(
|
||||
(accountData) => accountData.type == 'm.push_rules',
|
||||
) ??
|
||||
false,
|
||||
)
|
||||
.first;
|
||||
await Matrix.of(
|
||||
context,
|
||||
).client.setPushRuleEnabled(kind, pushRule.ruleId, !pushRule.enabled);
|
||||
await updateFromSync;
|
||||
} catch (e, s) {
|
||||
Logs().w('Unable to toggle push rule', e, s);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> editPushRule(PushRule rule, PushRuleKind kind) async {
|
||||
final theme = Theme.of(context);
|
||||
final action = await showAdaptiveDialog<PushRuleDialogAction>(
|
||||
context: context,
|
||||
builder: (context) => ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 256),
|
||||
child: AlertDialog.adaptive(
|
||||
title: Text(rule.getPushRuleName(L10n.of(context))),
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SelectableText(
|
||||
prettyJson(rule.toJson()),
|
||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
AdaptiveDialogAction(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: Text(L10n.of(context).close),
|
||||
),
|
||||
if (!rule.ruleId.startsWith('.m.'))
|
||||
AdaptiveDialogAction(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(PushRuleDialogAction.delete),
|
||||
child: Text(
|
||||
L10n.of(context).delete,
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (action == null) return;
|
||||
if (!mounted) return;
|
||||
switch (action) {
|
||||
case PushRuleDialogAction.delete:
|
||||
final consent = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
message: L10n.of(context).deletePushRuleCanNotBeUndone,
|
||||
okLabel: L10n.of(context).delete,
|
||||
isDestructive: true,
|
||||
);
|
||||
if (consent != OkCancelResult.ok) return;
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
try {
|
||||
final updateFromSync = Matrix.of(context).client.onSync.stream
|
||||
.where(
|
||||
(syncUpdate) =>
|
||||
syncUpdate.accountData?.any(
|
||||
(accountData) => accountData.type == 'm.push_rules',
|
||||
) ??
|
||||
false,
|
||||
)
|
||||
.first;
|
||||
await Matrix.of(context).client.deletePushRule(kind, rule.ruleId);
|
||||
await updateFromSync;
|
||||
} catch (e, s) {
|
||||
Logs().w('Unable to delete push rule', e, s);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SettingsNotificationsView(this);
|
||||
}
|
||||
|
||||
enum PushRuleDialogAction { delete }
|
||||
|
||||
String prettyJson(Map<String, Object?> json) {
|
||||
const decoder = JsonDecoder();
|
||||
const encoder = JsonEncoder.withIndent(' ');
|
||||
final object = decoder.convert(jsonEncode(json));
|
||||
return encoder.convert(object);
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
|
||||
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import 'package:tatchat/config/setting_keys.dart';
|
||||
import 'package:tatchat/config/themes.dart';
|
||||
import 'package:tatchat/l10n/l10n.dart';
|
||||
import 'package:tatchat/pages/settings_notifications/push_rule_extensions.dart';
|
||||
import 'package:tatchat/utils/background_push.dart';
|
||||
import 'package:tatchat/utils/platform_infos.dart';
|
||||
import 'package:tatchat/utils/push_helper.dart';
|
||||
import 'package:tatchat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:tatchat/widgets/settings_switch_list_tile.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:unifiedpush/unifiedpush.dart';
|
||||
import 'package:unifiedpush_ui/unifiedpush_ui.dart';
|
||||
|
||||
import '../../utils/localized_exception_extension.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
import 'settings_notifications.dart';
|
||||
|
||||
class SettingsNotificationsView extends StatelessWidget {
|
||||
final SettingsNotificationsController controller;
|
||||
|
||||
const SettingsNotificationsView(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pushRules = Matrix.of(context).client.globalPushRules;
|
||||
final pushCategories = [
|
||||
if (pushRules?.override?.isNotEmpty ?? false)
|
||||
(rules: pushRules?.override ?? [], kind: PushRuleKind.override),
|
||||
if (pushRules?.content?.isNotEmpty ?? false)
|
||||
(rules: pushRules?.content ?? [], kind: PushRuleKind.content),
|
||||
if (pushRules?.sender?.isNotEmpty ?? false)
|
||||
(rules: pushRules?.sender ?? [], kind: PushRuleKind.sender),
|
||||
if (pushRules?.underride?.isNotEmpty ?? false)
|
||||
(rules: pushRules?.underride ?? [], kind: PushRuleKind.underride),
|
||||
];
|
||||
final pushService = Matrix.of(context).backgroundPush;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: !FluffyThemes.isColumnMode(context),
|
||||
centerTitle: FluffyThemes.isColumnMode(context),
|
||||
title: Text(L10n.of(context).notifications),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
child: StreamBuilder(
|
||||
stream: Matrix.of(context).client.onSync.stream.where(
|
||||
(syncUpdate) =>
|
||||
syncUpdate.accountData?.any(
|
||||
(accountData) => accountData.type == 'm.push_rules',
|
||||
) ??
|
||||
false,
|
||||
),
|
||||
builder: (BuildContext context, _) {
|
||||
final theme = Theme.of(context);
|
||||
final lastReceivedPush =
|
||||
lastReceivedPushNotification[Matrix.of(
|
||||
context,
|
||||
).client.clientName];
|
||||
return SelectionArea(
|
||||
child: Column(
|
||||
children: [
|
||||
if (kDebugMode && lastReceivedPush != null)
|
||||
ListTile(
|
||||
title: Text('Last received push notification'),
|
||||
subtitle: Text(lastReceivedPush.toIso8601String()),
|
||||
),
|
||||
if (kIsWeb)
|
||||
SettingsSwitchListTile.adaptive(
|
||||
title: L10n.of(context).playSoundOnNotification,
|
||||
setting: AppSettings.webNotificationSound,
|
||||
),
|
||||
if (pushRules != null)
|
||||
for (final category in pushCategories) ...[
|
||||
ListTile(
|
||||
title: Text(
|
||||
category.kind.localized(L10n.of(context)),
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final rule in category.rules)
|
||||
ListTile(
|
||||
title: Text(rule.getPushRuleName(L10n.of(context))),
|
||||
subtitle: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: rule.getPushRuleDescription(
|
||||
L10n.of(context),
|
||||
),
|
||||
),
|
||||
const TextSpan(text: ' '),
|
||||
WidgetSpan(
|
||||
child: InkWell(
|
||||
onTap: () => controller.editPushRule(
|
||||
rule,
|
||||
category.kind,
|
||||
),
|
||||
child: Text(
|
||||
L10n.of(context).more,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor:
|
||||
theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: Switch.adaptive(
|
||||
value: rule.enabled,
|
||||
onChanged: controller.isLoading
|
||||
? null
|
||||
: rule.ruleId != '.m.rule.master' &&
|
||||
Matrix.of(
|
||||
context,
|
||||
).client.allPushNotificationsMuted
|
||||
? null
|
||||
: (_) => controller.togglePushRule(
|
||||
category.kind,
|
||||
rule,
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(color: theme.dividerColor),
|
||||
],
|
||||
|
||||
if (pushService?.firebaseEnabled != true)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).buildDoesNotSupportFirebase),
|
||||
leading: Icon(
|
||||
Icons.close,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
if (PlatformInfos.isAndroid)
|
||||
FutureBuilder(
|
||||
future: UnifiedPush.getDistributors(),
|
||||
builder: (context, snapshot) {
|
||||
final distributors = snapshot.data;
|
||||
if (distributors == null || distributors.isEmpty) {
|
||||
if (pushService?.firebaseEnabled == true &&
|
||||
pushService?.fcmToken == null) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
L10n.of(
|
||||
context,
|
||||
).unableToRegisterDeviceForFirebase,
|
||||
),
|
||||
leading: Icon(
|
||||
Icons.close,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
return ListTile(
|
||||
title: Text(L10n.of(context).unifiedPushDistributors),
|
||||
leading: Icon(Icons.info_outlined),
|
||||
subtitle: SelectableText(distributors.join(', ')),
|
||||
trailing: distributors.length >= 2
|
||||
? IconButton(
|
||||
onPressed: () => UnifiedPushUi(
|
||||
context: context,
|
||||
instances: ['default'],
|
||||
unifiedPushFunctions: UPFunctions(),
|
||||
showNoDistribDialog: false,
|
||||
onNoDistribDialogDismissed:
|
||||
() {}, // TODO: Implement me
|
||||
).registerAppWithDialog(),
|
||||
icon: Icon(Icons.edit_outlined),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).devices,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
FutureBuilder<List<Pusher>?>(
|
||||
future: controller.pusherFuture ??= Matrix.of(
|
||||
context,
|
||||
).client.getPushers(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
Center(
|
||||
child: Text(
|
||||
snapshot.error!.toLocalizedString(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
const Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
final pushers = snapshot.data ?? [];
|
||||
if (pushers.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text(L10n.of(context).noOtherDevicesFound),
|
||||
),
|
||||
);
|
||||
}
|
||||
return SelectionArea(
|
||||
child: ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: pushers.length,
|
||||
itemBuilder: (_, i) => ListTile(
|
||||
title: Text(pushers[i].appId),
|
||||
subtitle: Text(pushers[i].data.url.toString()),
|
||||
onTap: () => controller.onPusherTap(pushers[i]),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user