feat: фул интеграция FCM с андроидом нах
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user