feat: fkm существует
This commit is contained in:
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import '../../backend/api.dart';
|
||||
import '../../backend/modules/calls.dart';
|
||||
import '../protocol/opcode_map.dart';
|
||||
import '../push/fkm_controller.dart';
|
||||
import '../protocol/packet.dart';
|
||||
import '../utils/parse.dart';
|
||||
import 'call_bridge.dart';
|
||||
@@ -72,7 +73,6 @@ class CallController {
|
||||
|
||||
void _onPush(Packet packet) {
|
||||
if (packet.opcode != Opcode.notifCallStart) return;
|
||||
if (!appResumed) return;
|
||||
final payload = packet.payload;
|
||||
if (payload is! Map) return;
|
||||
|
||||
@@ -81,6 +81,13 @@ class CallController {
|
||||
final callerId = payload['callerId'] as int?;
|
||||
if (vcp == null || conversationId == null || callerId == null) return;
|
||||
|
||||
// Приложение свёрнуто — звонок показывает FKM отдельным уведомлением,
|
||||
// приём оттуда вернётся через injectFromNative.
|
||||
if (!appResumed) {
|
||||
unawaited(FkmController.instance.showIncomingCall(payload));
|
||||
return;
|
||||
}
|
||||
|
||||
final params = ConversationParams.decode(vcp);
|
||||
if (params == null) return;
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../utils/logger.dart';
|
||||
|
||||
/// Канал к нативному сервису FKM (foreground komet messaging).
|
||||
class FkmBridge {
|
||||
FkmBridge._();
|
||||
static final FkmBridge instance = FkmBridge._();
|
||||
|
||||
static const _method = MethodChannel('ru.komet.app/fkm');
|
||||
|
||||
VoidCallback? _onDisabled;
|
||||
bool _handlerSet = false;
|
||||
|
||||
bool get isSupported {
|
||||
try {
|
||||
return Platform.isAndroid;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Вызывается, когда пользователь выключил FKM кнопкой в самом уведомлении.
|
||||
void setDisabledCallback(VoidCallback callback) {
|
||||
_onDisabled = callback;
|
||||
if (_handlerSet || !isSupported) return;
|
||||
_handlerSet = true;
|
||||
_method.setMethodCallHandler((call) async {
|
||||
if (call.method == 'disabled') _onDisabled?.call();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> isEnabled() async {
|
||||
if (!isSupported) return false;
|
||||
return await _invoke<bool>('isEnabled') ?? false;
|
||||
}
|
||||
|
||||
Future<void> setEnabled(bool enabled) =>
|
||||
_invoke<void>('setEnabled', {'enabled': enabled});
|
||||
|
||||
Future<void> setConnected(bool connected) =>
|
||||
_invoke<void>('setConnected', {'connected': connected});
|
||||
|
||||
Future<void> showMessage(Map<String, String> data) =>
|
||||
_invoke<void>('showMessage', {'data': data});
|
||||
|
||||
Future<void> showCall(Map<String, String> data) =>
|
||||
_invoke<void>('showCall', {'data': data});
|
||||
|
||||
Future<bool> hasNotificationPermission() async {
|
||||
if (!isSupported) return false;
|
||||
return await _invoke<bool>('hasNotificationPermission') ?? false;
|
||||
}
|
||||
|
||||
Future<bool> requestNotificationPermission() async {
|
||||
if (!isSupported) return false;
|
||||
return await _invoke<bool>('requestNotificationPermission') ?? false;
|
||||
}
|
||||
|
||||
Future<bool> isIgnoringBatteryOptimizations() async {
|
||||
if (!isSupported) return true;
|
||||
return await _invoke<bool>('isIgnoringBatteryOptimizations') ?? true;
|
||||
}
|
||||
|
||||
Future<void> requestIgnoreBatteryOptimizations() =>
|
||||
_invoke<void>('requestIgnoreBatteryOptimizations');
|
||||
|
||||
Future<T?> _invoke<T>(String method, [Map<String, dynamic>? args]) async {
|
||||
if (!isSupported) return null;
|
||||
try {
|
||||
return await _method.invokeMethod<T>(method, args);
|
||||
} catch (e) {
|
||||
logger.w('FkmBridge.$method: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../backend/api.dart';
|
||||
import '../../backend/modules/account/account_models.dart';
|
||||
import '../../backend/modules/chat_preview.dart';
|
||||
import '../../backend/modules/chats.dart';
|
||||
import '../../backend/modules/messages.dart';
|
||||
import '../../core/protocol/opcode_map.dart';
|
||||
import '../../core/protocol/packet.dart';
|
||||
import '../../core/storage/app_database.dart';
|
||||
import '../../core/storage/token_storage.dart';
|
||||
import '../utils/logger.dart';
|
||||
import 'fkm_bridge.dart';
|
||||
import 'push_service.dart';
|
||||
|
||||
const _fallbackSender = 'MAX';
|
||||
const _hiddenPreview = 'Новое сообщение';
|
||||
|
||||
/// FKM — уведомления через собственное фоновое соединение, без FCM.
|
||||
///
|
||||
/// Пуш из сокета превращается в тот же набор полей, что присылает FCM, и
|
||||
/// отрисовывается нативным `KometNotifier` — общий код с пушевой версией.
|
||||
class FkmController {
|
||||
FkmController._();
|
||||
static final FkmController instance = FkmController._();
|
||||
|
||||
final ValueNotifier<bool> enabled = ValueNotifier(false);
|
||||
|
||||
Api? _api;
|
||||
StreamSubscription<Packet>? _pushSub;
|
||||
StreamSubscription<SessionState>? _stateSub;
|
||||
bool _started = false;
|
||||
|
||||
bool get isSupported => FkmBridge.instance.isSupported;
|
||||
|
||||
Future<void> init(Api api) async {
|
||||
if (_started || !isSupported) return;
|
||||
_started = true;
|
||||
_api = api;
|
||||
|
||||
FkmBridge.instance.setDisabledCallback(_onDisabledFromNotification);
|
||||
enabled.value = await FkmBridge.instance.isEnabled();
|
||||
|
||||
_pushSub = api.pushStream
|
||||
.where((packet) => packet.opcode == Opcode.notifMessage)
|
||||
.listen(_onMessagePush);
|
||||
_stateSub = api.stateStream.listen(_onSessionState);
|
||||
|
||||
if (enabled.value) {
|
||||
await initLocalNotificationActions();
|
||||
await FkmBridge.instance.setEnabled(true);
|
||||
await _pushConnectionState();
|
||||
}
|
||||
}
|
||||
|
||||
/// Возвращает false, если пользователь не выдал разрешение на уведомления.
|
||||
Future<bool> setEnabled(bool value) async {
|
||||
if (!isSupported) return false;
|
||||
if (value && !await FkmBridge.instance.requestNotificationPermission()) {
|
||||
return false;
|
||||
}
|
||||
if (value) await initLocalNotificationActions();
|
||||
await FkmBridge.instance.setEnabled(value);
|
||||
enabled.value = value;
|
||||
if (value) await _pushConnectionState();
|
||||
return true;
|
||||
}
|
||||
|
||||
void _onDisabledFromNotification() => enabled.value = false;
|
||||
|
||||
void _onSessionState(SessionState state) {
|
||||
if (!enabled.value) return;
|
||||
unawaited(FkmBridge.instance.setConnected(state == SessionState.online));
|
||||
}
|
||||
|
||||
Future<void> _pushConnectionState() => FkmBridge.instance.setConnected(
|
||||
_api?.state == SessionState.online,
|
||||
);
|
||||
|
||||
/// Входящий звонок, когда приложение не на переднем плане.
|
||||
///
|
||||
/// Отдаётся тому же нативному коду, что и FCM-пуш: CallStyle, полноэкранный
|
||||
/// интент, рингтон, приём и отклонение уже реализованы там.
|
||||
Future<void> showIncomingCall(Map<dynamic, dynamic> payload) async {
|
||||
if (!enabled.value) return;
|
||||
try {
|
||||
final data = await _buildCallNotification(payload);
|
||||
if (data != null) await FkmBridge.instance.showCall(data);
|
||||
} catch (e) {
|
||||
logger.w('FKM: не удалось показать звонок: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, String>?> _buildCallNotification(
|
||||
Map<dynamic, dynamic> payload,
|
||||
) async {
|
||||
final vcp = payload['vcp'];
|
||||
final conversationId = payload['conversationId'];
|
||||
final callerId = payload['callerId'];
|
||||
if (vcp is! String || conversationId is! String || callerId is! int) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
if (accountId == null) return null;
|
||||
|
||||
final rawConfig = await AppDatabase.getPrivacyConfig(accountId);
|
||||
if (rawConfig != null &&
|
||||
PrivacyConfig.fromJson(rawConfig).mCallPushNotification != 'ON') {
|
||||
return null;
|
||||
}
|
||||
|
||||
final name = ContactCache.get(callerId) ?? _fallbackSender;
|
||||
|
||||
return {
|
||||
'type': 'InboundCall',
|
||||
'vcp': vcp,
|
||||
'conversationId': conversationId,
|
||||
'callerId': '$callerId',
|
||||
'suid': '$callerId',
|
||||
'userName': name,
|
||||
'title': name,
|
||||
'c': '$accountId',
|
||||
'iv': payload['type'] == 'VIDEO' ? 'true' : 'false',
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _onMessagePush(Packet packet) async {
|
||||
if (!enabled.value) return;
|
||||
try {
|
||||
final data = await _buildNotification(packet);
|
||||
if (data != null) await FkmBridge.instance.showMessage(data);
|
||||
} catch (e) {
|
||||
logger.w('FKM: не удалось показать уведомление: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, String>?> _buildNotification(Packet packet) async {
|
||||
final payload = packet.payload;
|
||||
if (payload is! Map) return null;
|
||||
|
||||
final chatId = payload['chatId'];
|
||||
if (chatId is! int) return null;
|
||||
|
||||
final msg = payload['message'];
|
||||
if (msg is! Map) return null;
|
||||
|
||||
if (payload['postId'] != null || msg['postId'] != null) return null;
|
||||
|
||||
final status = msg['status']?.toString();
|
||||
if (status == 'REMOVED' || status == 'EDITED') return null;
|
||||
|
||||
final senderId = msg['sender'];
|
||||
if (senderId is! int) return null;
|
||||
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
if (accountId == null || senderId == accountId) return null;
|
||||
|
||||
final rawConfig = await AppDatabase.getPrivacyConfig(accountId);
|
||||
var showPreview = true;
|
||||
if (rawConfig != null) {
|
||||
final config = PrivacyConfig.fromJson(rawConfig);
|
||||
if (config.chatsPushNotification != 'ON') return null;
|
||||
showPreview = config.pushDetails;
|
||||
}
|
||||
|
||||
final rows = await AppDatabase.loadChat(accountId, chatId);
|
||||
final chat = rows.isEmpty ? null : CachedChat.fromDbRow(rows.first);
|
||||
if (chat != null && chat.isMuted) return null;
|
||||
|
||||
final senderName =
|
||||
ContactCache.get(senderId) ?? chat?.title ?? _fallbackSender;
|
||||
final chatTitle = (chat != null && chat.isGroupChat)
|
||||
? (chat.title ?? senderName)
|
||||
: senderName;
|
||||
|
||||
final text = showPreview ? _previewText(msg) : _hiddenPreview;
|
||||
final time = msg['time'];
|
||||
final msgId = msg['id'];
|
||||
|
||||
return {
|
||||
'mc': '$chatId',
|
||||
'c': '$accountId',
|
||||
'suid': '$senderId',
|
||||
'userName': senderName,
|
||||
'title': chatTitle,
|
||||
'msg': text,
|
||||
'ctime': '${time is int ? time : DateTime.now().millisecondsSinceEpoch}',
|
||||
if (msgId != null) 'msgid': '$msgId',
|
||||
};
|
||||
}
|
||||
|
||||
String _previewText(Map<dynamic, dynamic> msg) {
|
||||
final text = msg['text']?.toString().trim();
|
||||
if (text != null && text.isNotEmpty) return text;
|
||||
final attach = attachPreviewLabel(msg['attaches']);
|
||||
if (attach != null && attach.isNotEmpty) return attach;
|
||||
return _hiddenPreview;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_pushSub?.cancel();
|
||||
_stateSub?.cancel();
|
||||
_pushSub = null;
|
||||
_stateSub = null;
|
||||
_started = false;
|
||||
}
|
||||
}
|
||||
@@ -148,6 +148,36 @@ Future<void> _handleReply(String payloadJson, String text) async {
|
||||
}
|
||||
}
|
||||
|
||||
bool _localActionsReady = false;
|
||||
|
||||
/// Инициализация локальных уведомлений и их action-коллбэков.
|
||||
///
|
||||
/// Нужна и FCM, и FKM: без неё кнопка «Ответить» в уведомлении не доезжает
|
||||
/// до фонового изолята.
|
||||
Future<void> initLocalNotificationActions() async {
|
||||
if (_localActionsReady) return;
|
||||
_localActionsReady = true;
|
||||
final plugin = FlutterLocalNotificationsPlugin();
|
||||
await plugin.initialize(
|
||||
settings: const InitializationSettings(
|
||||
android: AndroidInitializationSettings('ic_notification'),
|
||||
),
|
||||
onDidReceiveNotificationResponse: _onNotificationResponse,
|
||||
onDidReceiveBackgroundNotificationResponse: _onNotificationResponse,
|
||||
);
|
||||
await plugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin
|
||||
>()
|
||||
?.createNotificationChannel(
|
||||
const AndroidNotificationChannel(
|
||||
_channelId,
|
||||
_channelName,
|
||||
importance: Importance.high,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class PushService {
|
||||
PushService._();
|
||||
static final PushService instance = PushService._();
|
||||
@@ -158,9 +188,6 @@ class PushService {
|
||||
await _clearHistory(chatId);
|
||||
}
|
||||
|
||||
final FlutterLocalNotificationsPlugin _local =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
Api? _api;
|
||||
AccountModule? _account;
|
||||
String? _token;
|
||||
@@ -180,24 +207,7 @@ class PushService {
|
||||
|
||||
_initialized = true;
|
||||
|
||||
await _local.initialize(
|
||||
settings: const InitializationSettings(
|
||||
android: AndroidInitializationSettings('ic_notification'),
|
||||
),
|
||||
onDidReceiveNotificationResponse: _onNotificationResponse,
|
||||
onDidReceiveBackgroundNotificationResponse: _onNotificationResponse,
|
||||
);
|
||||
await _local
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin
|
||||
>()
|
||||
?.createNotificationChannel(
|
||||
const AndroidNotificationChannel(
|
||||
_channelId,
|
||||
_channelName,
|
||||
importance: Importance.high,
|
||||
),
|
||||
);
|
||||
await initLocalNotificationActions();
|
||||
|
||||
final messaging = FirebaseMessaging.instance;
|
||||
await messaging.requestPermission();
|
||||
|
||||
@@ -90,6 +90,7 @@ import 'package:komet/core/config/app_frost.dart';
|
||||
import 'package:komet/core/config/app_composer_style.dart';
|
||||
import '../../../core/config/komet_settings.dart';
|
||||
import '../../../models/attachment.dart';
|
||||
import '../../../models/contact_info.dart';
|
||||
import '../../../models/sticker.dart';
|
||||
import '../../commands/command_registry.dart';
|
||||
import '../../commands/slash_command.dart';
|
||||
@@ -842,17 +843,24 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
final peerId = widget.chatId ^ _myId;
|
||||
if (peerId <= 0) return;
|
||||
final cached = ContactInfoFetch.peek(peerId);
|
||||
if (cached != null) _applyPeerKind(cached.isBot);
|
||||
if (cached != null) _applyPeerInfo(peerId, cached);
|
||||
final info = await ContactInfoFetch.get(peerId);
|
||||
if (info != null) _applyPeerKind(info.isBot);
|
||||
if (info != null) _applyPeerInfo(peerId, info);
|
||||
if ((info ?? cached)?.isBot ?? false) {
|
||||
unawaited(BotInfoFetch.get(peerId));
|
||||
}
|
||||
}
|
||||
|
||||
void _applyPeerKind(bool isBot) {
|
||||
if (!mounted || _peerIsBot == isBot) return;
|
||||
setState(() => _peerIsBot = isBot);
|
||||
void _applyPeerInfo(int peerId, ContactInfo info) {
|
||||
if (!mounted) return;
|
||||
final avatar = info.avatarUrl;
|
||||
final avatarIsNew =
|
||||
avatar != null &&
|
||||
avatar.isNotEmpty &&
|
||||
ContactCache.getAvatar(peerId) != avatar;
|
||||
if (avatarIsNew) ContactCache.putAvatar(peerId, avatar);
|
||||
if (_peerIsBot == info.isBot && !avatarIsNew) return;
|
||||
setState(() => _peerIsBot = info.isBot);
|
||||
}
|
||||
|
||||
Future<void> _fastPreloadCache() async {
|
||||
@@ -1202,7 +1210,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
builder: (_) => ChatInfoScreen(
|
||||
chatId: widget.chatId,
|
||||
name: _headerName(),
|
||||
imageUrl: widget.imageUrl,
|
||||
imageUrl: _headerAvatarUrl(),
|
||||
chatType: widget.chatType,
|
||||
heroTag: _profileHeroTag,
|
||||
initialTab: initialTab,
|
||||
@@ -3078,6 +3086,17 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
String _headerAvatarUrl() {
|
||||
if (!_commentsMode && widget.chatType == 'DIALOG') {
|
||||
final otherId = _resolveOtherId();
|
||||
if (otherId != null) {
|
||||
final cached = ContactCache.getAvatar(otherId);
|
||||
if (cached != null && cached.isNotEmpty) return cached;
|
||||
}
|
||||
}
|
||||
return widget.imageUrl;
|
||||
}
|
||||
|
||||
String _headerName() {
|
||||
if (_commentsMode) return AppLocalizations.of(context)!.commentsTitle;
|
||||
if (widget.chatType == 'DIALOG') {
|
||||
@@ -3178,7 +3197,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
chatId: widget.chatId,
|
||||
heroTag: _profileHeroTag,
|
||||
name: _headerName(),
|
||||
imageUrl: widget.imageUrl,
|
||||
imageUrl: _headerAvatarUrl(),
|
||||
chatType: widget.chatType,
|
||||
isOfficial: chat?.isOfficial ?? false,
|
||||
encrypted: _encryptionEnabled,
|
||||
|
||||
@@ -3,9 +3,12 @@ import 'dart:io' show Platform;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../../core/push/fkm_bridge.dart';
|
||||
import '../../../core/push/fkm_controller.dart';
|
||||
import '../../../core/utils/haptics.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../../main.dart' show accountModule, isOnemeFlavor;
|
||||
import '../../widgets/confirm_dialog.dart';
|
||||
import '../../widgets/connection_status.dart';
|
||||
import '../../widgets/reload_on_reconnect.dart';
|
||||
import '../../widgets/custom_notification.dart';
|
||||
@@ -24,6 +27,7 @@ class _NotificationsScreenState extends State<NotificationsScreen>
|
||||
with ReloadOnReconnect {
|
||||
bool _loading = true;
|
||||
bool _saving = false;
|
||||
bool _fkmBusy = false;
|
||||
|
||||
bool _allNotifications = true;
|
||||
bool _messagePreview = true;
|
||||
@@ -85,17 +89,55 @@ class _NotificationsScreenState extends State<NotificationsScreen>
|
||||
if (mounted) setState(() => _hapticsEnabled = value);
|
||||
}
|
||||
|
||||
void _onFkmTap() {
|
||||
Future<void> _onFkmChanged(bool value) async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final String message;
|
||||
if (Platform.isIOS) {
|
||||
message = l10n.notificationsFkmIosUnsupported;
|
||||
} else if (isOnemeFlavor) {
|
||||
message = l10n.notificationsFkmAlreadyHasFcm;
|
||||
} else {
|
||||
message = l10n.notificationsFkmDownloadFcm;
|
||||
if (!FkmController.instance.isSupported) {
|
||||
showCustomNotification(
|
||||
context,
|
||||
Platform.isIOS
|
||||
? l10n.notificationsFkmIosUnsupported
|
||||
: l10n.notificationsFkmUnsupported,
|
||||
);
|
||||
return;
|
||||
}
|
||||
showCustomNotification(context, message);
|
||||
if (_fkmBusy) return;
|
||||
|
||||
if (value && isOnemeFlavor) {
|
||||
final confirmed = await showConfirmDialog(
|
||||
context,
|
||||
title: l10n.notificationsFkmAlreadyHasFcm,
|
||||
message: l10n.notificationsFkmConfirmMessage,
|
||||
confirmLabel: l10n.notificationsFkmConfirmAction,
|
||||
);
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
setState(() => _fkmBusy = true);
|
||||
try {
|
||||
final applied = await FkmController.instance.setEnabled(value);
|
||||
if (!mounted) return;
|
||||
if (!applied) {
|
||||
showCustomNotification(context, l10n.notificationsFkmPermissionDenied);
|
||||
return;
|
||||
}
|
||||
if (value) await _offerBatteryExemption();
|
||||
} finally {
|
||||
if (mounted) setState(() => _fkmBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _offerBatteryExemption() async {
|
||||
if (await FkmBridge.instance.isIgnoringBatteryOptimizations()) return;
|
||||
if (!mounted) return;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final confirmed = await showConfirmDialog(
|
||||
context,
|
||||
title: l10n.notificationsFkmBatteryTitle,
|
||||
message: l10n.notificationsFkmBatteryMessage,
|
||||
confirmLabel: l10n.notificationsFkmBatteryAction,
|
||||
);
|
||||
if (!confirmed) return;
|
||||
await FkmBridge.instance.requestIgnoreBatteryOptimizations();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -124,12 +166,16 @@ class _NotificationsScreenState extends State<NotificationsScreen>
|
||||
),
|
||||
SettingsCard(
|
||||
children: [
|
||||
SettingsToggleTile(
|
||||
icon: Symbols.notifications_active,
|
||||
label: l10n.notificationsFkmEnableLabel,
|
||||
subtitle: l10n.notificationsFkmEnableSubtitle,
|
||||
value: false,
|
||||
onChanged: (_) => _onFkmTap(),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: FkmController.instance.enabled,
|
||||
builder: (context, fkmEnabled, _) => SettingsToggleTile(
|
||||
icon: Symbols.notifications_active,
|
||||
label: l10n.notificationsFkmEnableLabel,
|
||||
subtitle: l10n.notificationsFkmEnableSubtitle,
|
||||
value: fkmEnabled,
|
||||
enabled: !_fkmBusy,
|
||||
onChanged: _onFkmChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -72,16 +72,21 @@ Future<bool> openChatAtMessage(
|
||||
}
|
||||
|
||||
final title = chat.title?.trim();
|
||||
final peerId = (chat.type == 'DIALOG' && chatId != 0) ? chatId ^ myId : 0;
|
||||
final name = (title != null && title.isNotEmpty)
|
||||
? title
|
||||
: (ContactCache.get(chatId ^ myId) ?? 'Чат');
|
||||
final imageUrl =
|
||||
(peerId > 0 ? ContactCache.getAvatar(peerId) : null) ??
|
||||
chat.iconUrl ??
|
||||
'';
|
||||
|
||||
await pushSwipeable(
|
||||
context,
|
||||
(_) => ChatScreen(
|
||||
chatId: chatId,
|
||||
name: name,
|
||||
imageUrl: chat.iconUrl ?? '',
|
||||
imageUrl: imageUrl,
|
||||
chatType: chat.type,
|
||||
initialMessageId: messageId,
|
||||
initialMessageTime: messageTime,
|
||||
|
||||
+7
-1
@@ -257,12 +257,18 @@
|
||||
}
|
||||
},
|
||||
"notificationsFkmAlreadyHasFcm": "Why? You already have FCM.",
|
||||
"notificationsFkmDownloadFcm": "Better download the FCM version.",
|
||||
"notificationsFkmIosUnsupported": "Push notifications are not available on iOS yet.",
|
||||
"notificationsTitle": "Notifications",
|
||||
"notificationsFkmSectionTitle": "FKM",
|
||||
"notificationsFkmEnableLabel": "Enable notifications",
|
||||
"notificationsFkmEnableSubtitle": "For FKM notifications to work, the app will need to keep a notification in the shade.",
|
||||
"notificationsFkmUnsupported": "FKM is Android-only",
|
||||
"notificationsFkmBatteryAction": "Open settings",
|
||||
"notificationsFkmBatteryMessage": "Otherwise the system will put the background connection to sleep and notifications will be late or lost.",
|
||||
"notificationsFkmBatteryTitle": "Turn off battery saving?",
|
||||
"notificationsFkmPermissionDenied": "FKM cannot work without the notification permission",
|
||||
"notificationsFkmConfirmAction": "Enable FKM",
|
||||
"notificationsFkmConfirmMessage": "Notifications will arrive over the app’s own background connection, and a permanent service notification will stay in the shade. You can turn FKM off right from it.",
|
||||
"notificationsMainSectionTitle": "Notifications",
|
||||
"notificationsAllLabel": "All notifications",
|
||||
"notificationsNewSectionTitle": "New notifications",
|
||||
|
||||
@@ -1412,12 +1412,6 @@ abstract class AppLocalizations {
|
||||
/// **'Why? You already have FCM.'**
|
||||
String get notificationsFkmAlreadyHasFcm;
|
||||
|
||||
/// No description provided for @notificationsFkmDownloadFcm.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Better download the FCM version.'**
|
||||
String get notificationsFkmDownloadFcm;
|
||||
|
||||
/// No description provided for @notificationsFkmIosUnsupported.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1448,6 +1442,48 @@ abstract class AppLocalizations {
|
||||
/// **'For FKM notifications to work, the app will need to keep a notification in the shade.'**
|
||||
String get notificationsFkmEnableSubtitle;
|
||||
|
||||
/// No description provided for @notificationsFkmUnsupported.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'FKM is Android-only'**
|
||||
String get notificationsFkmUnsupported;
|
||||
|
||||
/// No description provided for @notificationsFkmBatteryAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open settings'**
|
||||
String get notificationsFkmBatteryAction;
|
||||
|
||||
/// No description provided for @notificationsFkmBatteryMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Otherwise the system will put the background connection to sleep and notifications will be late or lost.'**
|
||||
String get notificationsFkmBatteryMessage;
|
||||
|
||||
/// No description provided for @notificationsFkmBatteryTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Turn off battery saving?'**
|
||||
String get notificationsFkmBatteryTitle;
|
||||
|
||||
/// No description provided for @notificationsFkmPermissionDenied.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'FKM cannot work without the notification permission'**
|
||||
String get notificationsFkmPermissionDenied;
|
||||
|
||||
/// No description provided for @notificationsFkmConfirmAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enable FKM'**
|
||||
String get notificationsFkmConfirmAction;
|
||||
|
||||
/// No description provided for @notificationsFkmConfirmMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Notifications will arrive over the app’s own background connection, and a permanent service notification will stay in the shade. You can turn FKM off right from it.'**
|
||||
String get notificationsFkmConfirmMessage;
|
||||
|
||||
/// No description provided for @notificationsMainSectionTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -696,9 +696,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get notificationsFkmAlreadyHasFcm => 'Why? You already have FCM.';
|
||||
|
||||
@override
|
||||
String get notificationsFkmDownloadFcm => 'Better download the FCM version.';
|
||||
|
||||
@override
|
||||
String get notificationsFkmIosUnsupported =>
|
||||
'Push notifications are not available on iOS yet.';
|
||||
@@ -716,6 +713,30 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get notificationsFkmEnableSubtitle =>
|
||||
'For FKM notifications to work, the app will need to keep a notification in the shade.';
|
||||
|
||||
@override
|
||||
String get notificationsFkmUnsupported => 'FKM is Android-only';
|
||||
|
||||
@override
|
||||
String get notificationsFkmBatteryAction => 'Open settings';
|
||||
|
||||
@override
|
||||
String get notificationsFkmBatteryMessage =>
|
||||
'Otherwise the system will put the background connection to sleep and notifications will be late or lost.';
|
||||
|
||||
@override
|
||||
String get notificationsFkmBatteryTitle => 'Turn off battery saving?';
|
||||
|
||||
@override
|
||||
String get notificationsFkmPermissionDenied =>
|
||||
'FKM cannot work without the notification permission';
|
||||
|
||||
@override
|
||||
String get notificationsFkmConfirmAction => 'Enable FKM';
|
||||
|
||||
@override
|
||||
String get notificationsFkmConfirmMessage =>
|
||||
'Notifications will arrive over the app’s own background connection, and a permanent service notification will stay in the shade. You can turn FKM off right from it.';
|
||||
|
||||
@override
|
||||
String get notificationsMainSectionTitle => 'Notifications';
|
||||
|
||||
|
||||
@@ -699,10 +699,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get notificationsFkmAlreadyHasFcm => 'А зачем? У тебя уже FCM.';
|
||||
|
||||
@override
|
||||
String get notificationsFkmDownloadFcm =>
|
||||
'Установите FCM версию с официального источника';
|
||||
|
||||
@override
|
||||
String get notificationsFkmIosUnsupported =>
|
||||
'На iOS пуш-уведомления пока недоступны';
|
||||
@@ -720,6 +716,30 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get notificationsFkmEnableSubtitle =>
|
||||
'Для работы FKM уведомлений, приложению понадобится держать уведомление в шторке.';
|
||||
|
||||
@override
|
||||
String get notificationsFkmUnsupported => 'FKM работает только на Android';
|
||||
|
||||
@override
|
||||
String get notificationsFkmBatteryAction => 'Настроить';
|
||||
|
||||
@override
|
||||
String get notificationsFkmBatteryMessage =>
|
||||
'Иначе система усыпит фоновое соединение, и уведомления начнут опаздывать или пропадать.';
|
||||
|
||||
@override
|
||||
String get notificationsFkmBatteryTitle => 'Отключить экономию батареи?';
|
||||
|
||||
@override
|
||||
String get notificationsFkmPermissionDenied =>
|
||||
'Без разрешения на уведомления FKM не заработает';
|
||||
|
||||
@override
|
||||
String get notificationsFkmConfirmAction => 'Включить FKM';
|
||||
|
||||
@override
|
||||
String get notificationsFkmConfirmMessage =>
|
||||
'Уведомления начнут приходить через собственное фоновое соединение, а в шторке будет постоянно висеть уведомление сервиса. Выключить FKM можно прямо в нём.';
|
||||
|
||||
@override
|
||||
String get notificationsMainSectionTitle => 'Уведомления';
|
||||
|
||||
|
||||
+7
-1
@@ -236,12 +236,18 @@
|
||||
"msgActionsNoText": "(без текста)",
|
||||
"notificationsSaveFailed": "Не удалось сохранить: {error}",
|
||||
"notificationsFkmAlreadyHasFcm": "А зачем? У тебя уже FCM.",
|
||||
"notificationsFkmDownloadFcm": "Установите FCM версию с официального источника",
|
||||
"notificationsFkmIosUnsupported": "На iOS пуш-уведомления пока недоступны",
|
||||
"notificationsTitle": "Уведомления",
|
||||
"notificationsFkmSectionTitle": "FKM",
|
||||
"notificationsFkmEnableLabel": "Включить уведомления",
|
||||
"notificationsFkmEnableSubtitle": "Для работы FKM уведомлений, приложению понадобится держать уведомление в шторке.",
|
||||
"notificationsFkmUnsupported": "FKM работает только на Android",
|
||||
"notificationsFkmBatteryAction": "Настроить",
|
||||
"notificationsFkmBatteryMessage": "Иначе система усыпит фоновое соединение, и уведомления начнут опаздывать или пропадать.",
|
||||
"notificationsFkmBatteryTitle": "Отключить экономию батареи?",
|
||||
"notificationsFkmPermissionDenied": "Без разрешения на уведомления FKM не заработает",
|
||||
"notificationsFkmConfirmAction": "Включить FKM",
|
||||
"notificationsFkmConfirmMessage": "Уведомления начнут приходить через собственное фоновое соединение, а в шторке будет постоянно висеть уведомление сервиса. Выключить FKM можно прямо в нём.",
|
||||
"notificationsMainSectionTitle": "Уведомления",
|
||||
"notificationsAllLabel": "Все уведомления",
|
||||
"notificationsNewSectionTitle": "Все новые уведомления",
|
||||
|
||||
@@ -73,6 +73,7 @@ import 'core/calls/call_bridge.dart';
|
||||
import 'core/calls/call_controller.dart';
|
||||
import 'core/links/deep_link_service.dart';
|
||||
import 'frontend/screens/calls/call_screen.dart';
|
||||
import 'core/push/fkm_controller.dart';
|
||||
import 'core/push/notification_bridge.dart';
|
||||
import 'core/push/push_service.dart';
|
||||
import 'core/storage/app_database.dart';
|
||||
@@ -195,6 +196,7 @@ void main(List<String> args) async {
|
||||
}
|
||||
attachInfoCacheApi(api);
|
||||
chats.attachGlobalPushHandlers(api);
|
||||
unawaited(FkmController.instance.init(api));
|
||||
FoldersModule.attachGlobalPushHandlers(api);
|
||||
TranscriptionPushHandler.attach(api);
|
||||
commentsModule.attachPushHandlers(api);
|
||||
|
||||
Reference in New Issue
Block a user