feat(push): FCM via MAX server — oneme flavor, dedicated FCM workflow, opcode 22, notifications + CI/lint fixes
This commit is contained in:
@@ -210,6 +210,12 @@ enum AuthRequestType {
|
||||
|
||||
enum LoginStatus { idle, loading, success, error }
|
||||
|
||||
class WrongDeviceTokenException implements Exception {
|
||||
const WrongDeviceTokenException();
|
||||
@override
|
||||
String toString() => 'WrongDeviceTokenException';
|
||||
}
|
||||
|
||||
class RequestCodeResult {
|
||||
final String token;
|
||||
|
||||
@@ -429,6 +435,33 @@ class AccountModule {
|
||||
return config;
|
||||
}
|
||||
|
||||
Future<void> registerPushToken(String pushToken) async {
|
||||
_ensureOnline();
|
||||
final packet = await _api.sendRequest(Opcode.config, <dynamic, dynamic>{
|
||||
'pushToken': pushToken,
|
||||
});
|
||||
if (packet.isError) {
|
||||
final msg = messageFromErrorPayload(packet.payload).toUpperCase();
|
||||
if (msg.contains('WRONG_DEVICE_TOKEN') ||
|
||||
msg.contains('WRONG.DEVICE.TOKEN')) {
|
||||
throw const WrongDeviceTokenException();
|
||||
}
|
||||
throw PacketError(messageFromErrorPayload(packet.payload));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> unregisterPushToken(String pushToken) async {
|
||||
if (_api.state != SessionState.online) return;
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
if (accountId == null) return;
|
||||
final authToken = await TokenStorage.readToken(accountId);
|
||||
if (authToken == null) return;
|
||||
await _api.sendRequest(Opcode.logout, <dynamic, dynamic>{
|
||||
'token': authToken,
|
||||
'pushToken': pushToken,
|
||||
});
|
||||
}
|
||||
|
||||
Future<ProfileData> updateProfileName(String firstName, String? lastName) async {
|
||||
_ensureOnline();
|
||||
final payload = <dynamic, dynamic>{
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../backend/api.dart';
|
||||
import '../../backend/modules/account.dart';
|
||||
import '../utils/logger.dart';
|
||||
|
||||
const _channelId = 'komet_messages';
|
||||
const _channelName = 'Сообщения';
|
||||
const _prefsTokenKey = 'fcm_push_token';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _backgroundHandler(RemoteMessage message) async {
|
||||
await Firebase.initializeApp();
|
||||
if (message.notification != null) return;
|
||||
final plugin = FlutterLocalNotificationsPlugin();
|
||||
await plugin.initialize(
|
||||
settings: const InitializationSettings(
|
||||
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
|
||||
),
|
||||
);
|
||||
await _display(plugin, message);
|
||||
}
|
||||
|
||||
Future<void> _display(
|
||||
FlutterLocalNotificationsPlugin plugin,
|
||||
RemoteMessage message,
|
||||
) async {
|
||||
final data = message.data;
|
||||
final title = message.notification?.title ??
|
||||
data['title']?.toString() ??
|
||||
data['sender']?.toString() ??
|
||||
'MAX';
|
||||
final body = message.notification?.body ??
|
||||
data['body']?.toString() ??
|
||||
data['text']?.toString() ??
|
||||
data['message']?.toString() ??
|
||||
'Новое сообщение';
|
||||
await plugin.show(
|
||||
id: message.messageId?.hashCode ??
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
title: title,
|
||||
body: body,
|
||||
notificationDetails: const NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
_channelId,
|
||||
_channelName,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class PushService {
|
||||
PushService._();
|
||||
static final PushService instance = PushService._();
|
||||
|
||||
final FlutterLocalNotificationsPlugin _local =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
Api? _api;
|
||||
AccountModule? _account;
|
||||
String? _token;
|
||||
bool _initialized = false;
|
||||
|
||||
Future<void> init({required Api api, required AccountModule account}) async {
|
||||
if (_initialized) return;
|
||||
_api = api;
|
||||
_account = account;
|
||||
|
||||
try {
|
||||
await Firebase.initializeApp();
|
||||
} catch (e) {
|
||||
logger.w('Push: Firebase init не удался: $e');
|
||||
return;
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
|
||||
await _local.initialize(
|
||||
settings: const InitializationSettings(
|
||||
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
|
||||
),
|
||||
);
|
||||
await _local
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.createNotificationChannel(
|
||||
const AndroidNotificationChannel(
|
||||
_channelId,
|
||||
_channelName,
|
||||
importance: Importance.high,
|
||||
),
|
||||
);
|
||||
|
||||
final messaging = FirebaseMessaging.instance;
|
||||
await messaging.requestPermission();
|
||||
|
||||
FirebaseMessaging.onBackgroundMessage(_backgroundHandler);
|
||||
FirebaseMessaging.onMessage.listen((m) {
|
||||
_display(_local, m);
|
||||
});
|
||||
messaging.onTokenRefresh.listen((t) async {
|
||||
_token = t;
|
||||
await _persistToken(t);
|
||||
await _registerWithServer();
|
||||
});
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_token = prefs.getString(_prefsTokenKey);
|
||||
try {
|
||||
_token = await messaging.getToken() ?? _token;
|
||||
if (_token != null) await _persistToken(_token!);
|
||||
logger.i('Push: FCM-токен получен (${_token?.length ?? 0} симв.)');
|
||||
} catch (e) {
|
||||
logger.w('Push: getToken не удался: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onLoginSuccess() async {
|
||||
if (!_initialized) return;
|
||||
if (_token == null) {
|
||||
try {
|
||||
_token = await FirebaseMessaging.instance.getToken();
|
||||
if (_token != null) await _persistToken(_token!);
|
||||
} catch (_) {}
|
||||
}
|
||||
await _registerWithServer();
|
||||
}
|
||||
|
||||
Future<void> unregister() async {
|
||||
if (!_initialized || _token == null) return;
|
||||
final account = _account;
|
||||
if (account != null) {
|
||||
try {
|
||||
await account.unregisterPushToken(_token!);
|
||||
} catch (e) {
|
||||
logger.w('Push: unregister не удался: $e');
|
||||
}
|
||||
}
|
||||
try {
|
||||
await FirebaseMessaging.instance.deleteToken();
|
||||
} catch (_) {}
|
||||
_token = null;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_prefsTokenKey);
|
||||
}
|
||||
|
||||
Future<void> _registerWithServer() async {
|
||||
final account = _account;
|
||||
final api = _api;
|
||||
if (account == null || api == null) return;
|
||||
if (api.state != SessionState.online) return;
|
||||
final token = _token;
|
||||
if (token == null || token.isEmpty) return;
|
||||
|
||||
try {
|
||||
await account.registerPushToken(token);
|
||||
logger.i('Push: токен зарегистрирован на сервере MAX');
|
||||
} on WrongDeviceTokenException {
|
||||
logger.w('Push: WRONG_DEVICE_TOKEN, переполучаю токен');
|
||||
try {
|
||||
await FirebaseMessaging.instance.deleteToken();
|
||||
final fresh = await FirebaseMessaging.instance.getToken();
|
||||
if (fresh != null && fresh.isNotEmpty) {
|
||||
_token = fresh;
|
||||
await _persistToken(fresh);
|
||||
await account.registerPushToken(fresh);
|
||||
logger.i('Push: токен перерегистрирован');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.w('Push: повторная регистрация не удалась: $e');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.w('Push: регистрация токена не удалась: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistToken(String token) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_prefsTokenKey, token);
|
||||
}
|
||||
}
|
||||
@@ -1085,7 +1085,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
child: Text(
|
||||
'Кажется, тут пусто...',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface.withOpacity(0.6),
|
||||
color: cs.onSurface.withValues(alpha: 0.6),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
@@ -1146,10 +1146,6 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
? ContactCache.get(chat.lastMsgSenderId!)
|
||||
: null;
|
||||
|
||||
final avatar = chat.lastMsgSenderId != null
|
||||
? ContactCache.getAvatar(chat.lastMsgSenderId!)
|
||||
: null;
|
||||
|
||||
String fullMsg = "";
|
||||
|
||||
if (name?.isNotEmpty == true && chat.id != 0) {
|
||||
@@ -2073,7 +2069,6 @@ Navigator.push(
|
||||
}
|
||||
|
||||
Widget _buildFabMenu() {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
|
||||
@@ -11,7 +11,6 @@ import '../../../backend/modules/messages.dart';
|
||||
import '../../../core/storage/app_database.dart';
|
||||
import '../../../core/utils/haptics.dart';
|
||||
import '../../../models/attachment.dart';
|
||||
import '../../../backend/modules/messages.dart' show ContactCache;
|
||||
import '../../widgets/message_bubble.dart';
|
||||
import '../../widgets/attachment_panel.dart';
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../../core/storage/app_database.dart';
|
||||
@@ -63,6 +62,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
);
|
||||
_avatarUrl = newProfile.baseUrl;
|
||||
_photoId = newProfile.photoId;
|
||||
if (!mounted) return;
|
||||
KometApp.stateOf(context)?.notifyProfileUpdate();
|
||||
if (mounted) {
|
||||
showCustomNotification(context, 'Имя сохранено');
|
||||
@@ -93,6 +93,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
final newProfile = await accountModule.removeProfilePhoto(_photoId!);
|
||||
_avatarUrl = newProfile.baseUrl;
|
||||
_photoId = newProfile.photoId;
|
||||
if (!mounted) return;
|
||||
KometApp.stateOf(context)?.notifyProfileUpdate();
|
||||
if (mounted) {
|
||||
showCustomNotification(context, 'Фото удалено');
|
||||
|
||||
@@ -15,8 +15,6 @@ class PasswordEntryScreen extends StatefulWidget {
|
||||
class _PasswordEntryScreenState extends State<PasswordEntryScreen> {
|
||||
bool _isLoading = true;
|
||||
bool _is2faEnabled = false;
|
||||
String? _email;
|
||||
String? _hint;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
||||
@@ -943,7 +943,7 @@ class _SecurityScreenState extends State<SecurityScreen>
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
if (trailing != null) trailing,
|
||||
?trailing,
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Symbols.chevron_right,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:komet/backend/modules/chats.dart';
|
||||
import 'package:komet/backend/modules/contacts.dart';
|
||||
import 'package:komet/main.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../backend/modules/messages.dart';
|
||||
import '../../core/utils/haptics.dart';
|
||||
@@ -23,16 +20,18 @@ class MessageBubble extends StatelessWidget {
|
||||
static const double compactTimePadding = 8.0;
|
||||
|
||||
bool get _hasPhotoWithCaption {
|
||||
if (message.attachments == null || message.attachments!.isEmpty)
|
||||
if (message.attachments == null || message.attachments!.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final hasPhoto = message.attachments!.any((a) => a is PhotoAttachment);
|
||||
final hasCaption = message.text != null && message.text!.isNotEmpty;
|
||||
return hasPhoto && hasCaption;
|
||||
}
|
||||
|
||||
bool get _hasMultiplePhotosNoCaption {
|
||||
if (message.attachments == null || message.attachments!.isEmpty)
|
||||
if (message.attachments == null || message.attachments!.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final photoCount = message.attachments!.whereType<PhotoAttachment>().length;
|
||||
final hasCaption = message.text != null && message.text!.isNotEmpty;
|
||||
return photoCount >= 2 && !hasCaption;
|
||||
@@ -226,7 +225,6 @@ class MessageBubble extends StatelessWidget {
|
||||
case MessageType.control:
|
||||
return 4;
|
||||
}
|
||||
return 4;
|
||||
}
|
||||
|
||||
double get bottomMargin {
|
||||
@@ -253,17 +251,6 @@ class MessageBubble extends StatelessWidget {
|
||||
case BubbleShape.groupedMiddle:
|
||||
return 1;
|
||||
}
|
||||
case MessageType.attachment:
|
||||
switch (shape) {
|
||||
case BubbleShape.singleTop:
|
||||
return 1;
|
||||
case BubbleShape.singleBottom:
|
||||
return 1;
|
||||
case BubbleShape.singleMiddle:
|
||||
return 4;
|
||||
case BubbleShape.groupedMiddle:
|
||||
return 1;
|
||||
}
|
||||
case MessageType.voice:
|
||||
switch (shape) {
|
||||
case BubbleShape.singleTop:
|
||||
@@ -278,7 +265,6 @@ class MessageBubble extends StatelessWidget {
|
||||
case MessageType.control:
|
||||
return 4;
|
||||
}
|
||||
return 4;
|
||||
}
|
||||
|
||||
// Внутренний отступ, размеры типа я хз
|
||||
@@ -311,7 +297,6 @@ class MessageBubble extends StatelessWidget {
|
||||
case MessageType.control:
|
||||
return const EdgeInsets.symmetric(horizontal: 14, vertical: 4);
|
||||
}
|
||||
return const EdgeInsets.symmetric(horizontal: 14, vertical: 10);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -491,7 +476,7 @@ class MessageBubble extends StatelessWidget {
|
||||
children: [
|
||||
Flexible(
|
||||
child: isForwarded
|
||||
? _buildForwardedInlineText(context, forwarded!, textColor)
|
||||
? _buildForwardedInlineText(context, forwarded, textColor)
|
||||
: Text(
|
||||
message.text ?? '',
|
||||
style: TextStyle(color: textColor, fontSize: 16, height: 1.3),
|
||||
@@ -590,67 +575,15 @@ class MessageBubble extends StatelessWidget {
|
||||
}
|
||||
|
||||
ForwardedMessageAttachment? _getForwardedAttachment() {
|
||||
if (message.attachments == null || message.attachments!.isEmpty)
|
||||
if (message.attachments == null || message.attachments!.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
for (final a in message.attachments!) {
|
||||
if (a is ForwardedMessageAttachment) return a;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Widget _buildForwardedHeader(
|
||||
BuildContext context,
|
||||
ForwardedMessageAttachment forwarded,
|
||||
) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final isDark = cs.brightness == Brightness.dark;
|
||||
final textColor = isMe
|
||||
? Colors.white
|
||||
: (isDark ? cs.onSurface : const Color(0xFF1C1C1E));
|
||||
final headerColor = isMe
|
||||
? Colors.white.withValues(alpha: 0.7)
|
||||
: (isDark ? cs.onSurfaceVariant : const Color(0xFF8E8E93));
|
||||
|
||||
final senderName = forwarded.originalSenderName;
|
||||
final displaySender = senderName ?? forwarded.originalSenderId.toString();
|
||||
final origText = forwarded.originalText;
|
||||
final hasOrigText = origText != null && origText.isNotEmpty;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Symbols.forward, size: 14, color: headerColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
displaySender,
|
||||
style: TextStyle(
|
||||
color: headerColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (hasOrigText) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
origText,
|
||||
style: TextStyle(color: textColor, fontSize: 14),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAttachmentContent(BuildContext context) {
|
||||
final attachments = message.attachments;
|
||||
if (attachments == null || attachments.isEmpty) {
|
||||
@@ -843,65 +776,6 @@ class MessageBubble extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildForwardedFileContent(
|
||||
BuildContext context,
|
||||
ForwardedMessageAttachment forwarded,
|
||||
List<FileAttachment> files,
|
||||
) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final headerColor = isMe
|
||||
? Colors.white.withValues(alpha: 0.7)
|
||||
: cs.onSurfaceVariant;
|
||||
final displaySender =
|
||||
forwarded.originalSenderName ?? forwarded.originalSenderId.toString();
|
||||
final senderAvatar = forwarded.originalSenderAvatar;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Symbols.forward, size: 14, color: headerColor),
|
||||
const SizedBox(width: 4),
|
||||
if (senderAvatar != null && senderAvatar.isNotEmpty)
|
||||
CircleAvatar(
|
||||
radius: 10,
|
||||
backgroundImage: CachedNetworkImageProvider(senderAvatar),
|
||||
backgroundColor: cs.primaryContainer,
|
||||
)
|
||||
else
|
||||
CircleAvatar(
|
||||
radius: 10,
|
||||
backgroundColor: cs.primaryContainer,
|
||||
child: Text(
|
||||
displaySender.isNotEmpty
|
||||
? displaySender[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(fontSize: 9, color: cs.onPrimaryContainer),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
displaySender,
|
||||
style: TextStyle(
|
||||
color: headerColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
...files.map((file) => _buildFileAttachment(context, file)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildForwardedGenericContent(
|
||||
BuildContext context,
|
||||
ForwardedMessageAttachment forwarded,
|
||||
@@ -1001,7 +875,7 @@ class MessageBubble extends StatelessWidget {
|
||||
height: constrainedHeight,
|
||||
fit: BoxFit.cover,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
errorWidget: (_, __, ___) => _buildPhotoPlaceholder(
|
||||
errorWidget: (_, _, _) => _buildPhotoPlaceholder(
|
||||
ctx,
|
||||
constrainedWidth,
|
||||
constrainedHeight,
|
||||
@@ -1106,7 +980,7 @@ class MessageBubble extends StatelessWidget {
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
errorWidget: (_, __, ___) =>
|
||||
errorWidget: (_, _, _) =>
|
||||
_buildPhotoPlaceholder(ctx, 100, 100),
|
||||
)
|
||||
else
|
||||
@@ -1141,7 +1015,7 @@ class MessageBubble extends StatelessWidget {
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
errorWidget: (_, __, ___) =>
|
||||
errorWidget: (_, _, _) =>
|
||||
_buildPhotoPlaceholder(ctx, 100, 100),
|
||||
)
|
||||
else
|
||||
@@ -1209,12 +1083,6 @@ class MessageBubble extends StatelessWidget {
|
||||
BuildContext ctx,
|
||||
MessageAttachment attachment,
|
||||
) {
|
||||
final cs = Theme.of(ctx).colorScheme;
|
||||
final isDark = cs.brightness == Brightness.dark;
|
||||
final textColor = isMe
|
||||
? Colors.white
|
||||
: (isDark ? cs.onSurface : const Color(0xFF1C1C1E));
|
||||
|
||||
switch (attachment.type) {
|
||||
case AttachmentType.video:
|
||||
return _buildVideoAttachment(ctx, attachment);
|
||||
@@ -1371,7 +1239,7 @@ class MessageBubble extends StatelessWidget {
|
||||
height: 150,
|
||||
fit: BoxFit.contain,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
errorWidget: (_, __, ___) =>
|
||||
errorWidget: (_, _, _) =>
|
||||
_buildPhotoPlaceholder(ctx, 150, 150),
|
||||
)
|
||||
else
|
||||
@@ -1427,7 +1295,7 @@ class MessageBubble extends StatelessWidget {
|
||||
imageUrl: photoUrl,
|
||||
fit: BoxFit.cover,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
errorWidget: (_, __, ___) => Icon(
|
||||
errorWidget: (_, _, _) => Icon(
|
||||
Symbols.person,
|
||||
color: isMe ? Colors.white : cs.primary,
|
||||
size: 24,
|
||||
@@ -1573,7 +1441,7 @@ class MessageBubble extends StatelessWidget {
|
||||
imageUrl: photoUrl,
|
||||
fit: BoxFit.cover,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
errorWidget: (_, __, ___) => Icon(
|
||||
errorWidget: (_, _, _) => Icon(
|
||||
Symbols.person,
|
||||
color: isMe ? Colors.white : cs.primary,
|
||||
size: 24,
|
||||
@@ -1680,7 +1548,6 @@ class MessageBubble extends StatelessWidget {
|
||||
|
||||
Widget _buildMeta(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final isDark = cs.brightness == Brightness.dark;
|
||||
final timeColor = isMe ? Colors.white70 : cs.onSurfaceVariant;
|
||||
|
||||
return Padding(
|
||||
@@ -1700,8 +1567,6 @@ class MessageBubble extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildCompactTime(BuildContext ctx) {
|
||||
final cs = Theme.of(ctx).colorScheme;
|
||||
final isDark = cs.brightness == Brightness.dark;
|
||||
final bgColor = isMe
|
||||
? Colors.black.withValues(alpha: 0.4)
|
||||
: Colors.black.withValues(alpha: 0.5);
|
||||
@@ -1761,12 +1626,6 @@ class MessageBubble extends StatelessWidget {
|
||||
final minute = dt.minute.toString().padLeft(2, '0');
|
||||
return '$hour:$minute';
|
||||
}
|
||||
|
||||
String _formatDuration(int seconds) {
|
||||
final min = seconds ~/ 60;
|
||||
final sec = seconds % 60;
|
||||
return '$min:${sec.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
class _VoiceMessageBubble extends StatefulWidget {
|
||||
@@ -1866,6 +1725,7 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
||||
return Icon(icon, size: 14, color: color);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = widget.cs.brightness == Brightness.dark;
|
||||
final waveInactiveColor = widget.isMe
|
||||
@@ -2042,16 +1902,6 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressBar(Color inactive, Color active) {
|
||||
return Container(
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: inactive,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _togglePlay() {
|
||||
setState(() {
|
||||
_isPlaying = !_isPlaying;
|
||||
|
||||
@@ -4,11 +4,13 @@ import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:komet/l10n/app_localizations.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'backend/api.dart';
|
||||
import 'backend/modules/account.dart';
|
||||
import 'backend/modules/contacts.dart';
|
||||
import 'backend/modules/messages.dart';
|
||||
import 'core/push/push_service.dart';
|
||||
import 'core/storage/app_database.dart';
|
||||
import 'core/storage/token_storage.dart';
|
||||
import 'core/utils/haptics.dart';
|
||||
@@ -43,6 +45,12 @@ void main() async {
|
||||
await ContactsModule.primeCacheFromDb(activeAccountId);
|
||||
}
|
||||
await api.connect();
|
||||
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
if (packageInfo.packageName == 'ru.oneme.app') {
|
||||
await PushService.instance.init(api: api, account: accountModule);
|
||||
}
|
||||
|
||||
final initialLocale = await _loadInitialLocale();
|
||||
|
||||
await Haptics.load();
|
||||
@@ -82,6 +90,7 @@ class KometAppState extends State<KometApp> {
|
||||
late Locale _locale;
|
||||
bool _isLoggingOut = false;
|
||||
StreamSubscription<SessionExpiredException>? _sessionExpiredSub;
|
||||
StreamSubscription<LoginStatus>? _loginStatusSub;
|
||||
late final ValueNotifier<bool> fpsOverlayEnabled = ValueNotifier(
|
||||
widget.initialFpsOverlay,
|
||||
);
|
||||
@@ -105,10 +114,18 @@ class KometAppState extends State<KometApp> {
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
_loginStatusSub = accountModule.loginStatusStream.listen((status) {
|
||||
if (status == LoginStatus.success) {
|
||||
PushService.instance.onLoginSuccess();
|
||||
}
|
||||
});
|
||||
|
||||
_sessionExpiredSub = api.sessionExpiredStream.listen((SessionExpiredException e) async {
|
||||
if (_isLoggingOut) return;
|
||||
_isLoggingOut = true;
|
||||
|
||||
await PushService.instance.unregister();
|
||||
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
if (accountId != null) {
|
||||
await accountModule.removeAccount(accountId);
|
||||
@@ -133,6 +150,7 @@ class KometAppState extends State<KometApp> {
|
||||
@override
|
||||
void dispose() {
|
||||
_sessionExpiredSub?.cancel();
|
||||
_loginStatusSub?.cancel();
|
||||
_profileUpdateController.close();
|
||||
fpsOverlayEnabled.dispose();
|
||||
super.dispose();
|
||||
|
||||
Reference in New Issue
Block a user