feat: фул интеграция FCM с андроидом нах

This commit is contained in:
Jganenokk
2026-07-02 23:32:31 +07:00
parent bcef107c25
commit a904b733d2
20 changed files with 1941 additions and 90 deletions
@@ -33,6 +33,7 @@ class CallScreen extends StatefulWidget {
final CallSession? session;
final IncomingCall? incoming;
final bool isGroup;
final bool autoAccept;
const CallScreen({
super.key,
@@ -41,6 +42,7 @@ class CallScreen extends StatefulWidget {
this.session,
this.incoming,
this.isGroup = false,
this.autoAccept = false,
});
@override
@@ -51,6 +53,7 @@ class _CallScreenState extends State<CallScreen>
with TickerProviderStateMixin {
CallSession? _session;
StreamSubscription<CallSessionState>? _stateSub;
StreamSubscription<void>? _canceledSub;
StreamSubscription<void>? _infoSub;
StreamSubscription<void>? _kometSub;
StreamSubscription<CallChatMessage>? _chatSub;
@@ -112,6 +115,16 @@ class _CallScreenState extends State<CallScreen>
if (incoming != null && (_name.isEmpty || _avatarUrl == null)) {
_resolvePeerInfo(incoming.callerId);
}
if (incoming != null) {
_canceledSub = CallController.instance.incomingCanceled.listen((_) {
if (mounted && _incomingPending) _close();
});
}
if (widget.autoAccept && incoming != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && _incomingPending) _accept();
});
}
}
Future<void> _resolvePeerInfo(int id) async {
@@ -364,6 +377,7 @@ class _CallScreenState extends State<CallScreen>
@override
void dispose() {
_stateSub?.cancel();
_canceledSub?.cancel();
_infoSub?.cancel();
_kometSub?.cancel();
_chatSub?.cancel();
@@ -25,7 +25,10 @@ import '../profile/settings_tab.dart';
import '../auth/login_screen.dart';
import '../digital_id/digital_id_web_screen.dart';
import '../../widgets/account_switcher_overlay.dart';
import '../../widgets/animated_text_swap.dart';
import '../../../backend/api.dart';
import '../../../core/protocol/opcode_map.dart';
import '../../../core/protocol/packet.dart';
import '../../../core/utils/haptics.dart';
import '../../../core/config/app_stories.dart';
import '../../../backend/models/chat_folder.dart';
@@ -36,6 +39,7 @@ import '../../../backend/modules/folders.dart';
import '../../../core/storage/app_database.dart';
import '../../../core/storage/draft_store.dart';
import '../../../core/storage/token_storage.dart';
import '../../../core/storage/typing_store.dart';
import '../../../main.dart'
show accountModule, api, messagesModule, appRouteObserver;
@@ -157,6 +161,8 @@ class _ChatListScreenState extends State<ChatListScreen>
StreamSubscription? _stateSub;
StreamSubscription<LoginStatus>? _loginSub;
StreamSubscription<Packet>? _typingSub;
StreamSubscription<MessageEvent>? _typingMsgSub;
Widget? _cachedChatsBody;
Object? _chatsBodyCacheKey;
@@ -500,9 +506,29 @@ class _ChatListScreenState extends State<ChatListScreen>
ChatsModule.chatsChanged.addListener(_onChatsChanged);
DraftStore.instance.revision.addListener(_onDraftsChanged);
AppStories.current.addListener(_onStoriesEnabledChanged);
_typingSub = api.pushStream
.where((p) => p.opcode == Opcode.notifTyping)
.listen(_onTypingPush);
_typingMsgSub = ChatsModule.messageEvents.listen(_onTypingMessageEvent);
unawaited(_runReload());
}
void _onTypingPush(Packet packet) {
final payload = packet.payload;
if (payload is! Map) return;
final chatId = payload['chatId'];
final userId = payload['userId'];
if (chatId is! int || userId is! int) return;
if (userId == (_profile?.id ?? 0)) return;
TypingStore.instance.markTyping(chatId, userId);
}
void _onTypingMessageEvent(MessageEvent event) {
if (event is MessageAddedEvent) {
TypingStore.instance.clearUser(event.chatId, event.message.senderId);
}
}
void _onDraftsChanged() {
if (mounted) _requestReload();
}
@@ -1065,6 +1091,8 @@ class _ChatListScreenState extends State<ChatListScreen>
AppStories.current.removeListener(_onStoriesEnabledChanged);
_loginSub?.cancel();
_stateSub?.cancel();
_typingSub?.cancel();
_typingMsgSub?.cancel();
_fabController.dispose();
_navPageAnimController.dispose();
_storiesRevealController
@@ -2171,7 +2199,6 @@ class _ChatListScreenState extends State<ChatListScreen>
String time,
String imageUrl, {
int presenceUserId = 0,
bool isTyping = false,
bool isRead = false,
int unreadCount = 0,
bool isMuted = false,
@@ -2366,45 +2393,66 @@ class _ChatListScreenState extends State<ChatListScreen>
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: draft != null
? Text.rich(
TextSpan(
children: [
TextSpan(
text: 'Черновик: ',
style: TextStyle(color: cs.error),
child: ValueListenableBuilder<bool>(
valueListenable: TypingStore.instance
.listenable(int.tryParse(id) ?? 0),
child: draft != null
? Text.rich(
TextSpan(
children: [
TextSpan(
text: 'Черновик: ',
style: TextStyle(color: cs.error),
),
TextSpan(
text: draft,
style: TextStyle(
color: cs.outline,
),
),
],
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
fontStyle: FontStyle.italic,
height: 1.2,
),
TextSpan(
text: draft,
style: TextStyle(color: cs.outline),
),
],
style: const TextStyle(
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Text(
message,
style: TextStyle(
color: cs.outline,
fontSize: 14,
fontWeight: FontWeight.w400,
fontStyle: FontStyle.italic,
fontStyle: messageItalic
? FontStyle.italic
: FontStyle.normal,
height: 1.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Text(
message,
builder: (context, typing, base) {
return AnimatedTextSwap(
showAlternate: typing,
alternate: Text(
'печатает...',
style: TextStyle(
color: isTyping ? cs.primary : cs.outline,
color: cs.primary,
fontSize: 14,
fontWeight: isTyping
? FontWeight.w500
: FontWeight.w400,
fontStyle: messageItalic
? FontStyle.italic
: FontStyle.normal,
fontWeight: FontWeight.w500,
height: 1.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
child: base!,
);
},
),
),
?statusIcon,
const SizedBox(width: 8),
@@ -34,6 +34,7 @@ import '../../../core/calls/call_controller.dart';
import '../calls/call_screen.dart';
import '../../../core/protocol/opcode_map.dart';
import '../../../core/protocol/packet.dart';
import '../../../core/push/push_service.dart';
import '../../../core/storage/app_database.dart';
import '../../../core/storage/draft_store.dart';
import '../../../core/cache/info_cache.dart';
@@ -333,6 +334,7 @@ class _ChatScreenState extends State<ChatScreen>
@override
void initState() {
super.initState();
unawaited(PushService.clearChatNotification(widget.chatId));
WidgetsBinding.instance.addObserver(this);
ChatsModule.chatsChanged.addListener(_onChatsBump);
_messageController.addListener(_onTextChanged);
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
class AnimatedTextSwap extends StatefulWidget {
const AnimatedTextSwap({
super.key,
required this.showAlternate,
required this.child,
required this.alternate,
this.duration = const Duration(milliseconds: 260),
this.curve = Curves.easeOutCubic,
this.slideExtent = 0.45,
this.alignment = AlignmentDirectional.centerStart,
});
final bool showAlternate;
final Widget child;
final Widget alternate;
final Duration duration;
final Curve curve;
final double slideExtent;
final AlignmentGeometry alignment;
@override
State<AnimatedTextSwap> createState() => _AnimatedTextSwapState();
}
class _AnimatedTextSwapState extends State<AnimatedTextSwap>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _t;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: widget.duration,
value: widget.showAlternate ? 1 : 0,
);
_t = CurvedAnimation(
parent: _controller,
curve: widget.curve,
reverseCurve: widget.curve.flipped,
);
}
@override
void didUpdateWidget(AnimatedTextSwap oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.duration != oldWidget.duration) {
_controller.duration = widget.duration;
}
if (widget.showAlternate != oldWidget.showAlternate) {
widget.showAlternate ? _controller.forward() : _controller.reverse();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _t,
builder: (context, _) {
final t = _t.value;
if (t <= 0) return widget.child;
if (t >= 1) return widget.alternate;
return Stack(
alignment: widget.alignment,
children: [
Opacity(
opacity: 1 - t,
child: FractionalTranslation(
translation: Offset(0, -widget.slideExtent * t),
child: widget.child,
),
),
Opacity(
opacity: t,
child: FractionalTranslation(
translation: Offset(0, widget.slideExtent * (1 - t)),
child: widget.alternate,
),
),
],
);
},
);
}
}