CallKit/PushKit интеграция + фикс PacketPushNotification (tokenType, deviceId)

This commit is contained in:
2026-04-01 00:39:34 +05:00
parent 0470b306a9
commit 8f69781a66
16 changed files with 1058 additions and 63 deletions

View File

@@ -1447,7 +1447,9 @@ private extension ChatDetailView {
shouldScrollOnNextMessage = true
messageText = ""
pendingAttachments = []
replyingToMessage = nil
// replyingToMessage cleared INSIDE Task after message is inserted into cache.
// This ensures reply panel disappears in the same SwiftUI render pass as
// the new bubble appears no empty gap (Telegram parity).
sendError = nil
// Desktop parity: delete draft after sending.
DraftManager.shared.deleteDraft(for: route.publicKey)
@@ -1482,7 +1484,13 @@ private extension ChatDetailView {
opponentUsername: route.username
)
}
// Clear reply panel AFTER send message is already in cache
// (upsertFromMessagePacket + refreshDialogCache + notification).
// SwiftUI batches this with the ViewModel's messages update
// reply bar disappears and bubble appears in the same frame.
replyingToMessage = nil
} catch {
replyingToMessage = nil
sendError = "Failed to send message"
if messageText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
messageText = message

View File

@@ -70,6 +70,25 @@ final class ChatDetailViewModel: ObservableObject {
}
.store(in: &cancellables)
// Fast path: immediately update messages when a sent message is inserted,
// bypassing the 50ms Combine debounce for instant bubble appearance.
// No .receive(on:) notification is always posted from @MainActor
// (SessionManager upsertFromMessagePacket), so subscriber fires
// synchronously on main thread. This is critical: the message must be
// in ViewModel.messages BEFORE sendCurrentMessage() clears replyingToMessage,
// so SwiftUI batches both changes into one render pass.
NotificationCenter.default.publisher(for: .sentMessageInserted)
.compactMap { $0.userInfo?["opponentKey"] as? String }
.filter { $0 == key }
.sink { [weak self] _ in
let fresh = repo.messages(for: key)
self?.messages = fresh
if self?.isLoading == true {
self?.isLoading = false
}
}
.store(in: &cancellables)
// Subscribe to typing state changes, filtered to our dialog
let typingPublisher = repo.$typingDialogs
.map { (dialogs: Set<String>) -> Bool in

View File

@@ -298,7 +298,8 @@ final class NativeMessageListController: UIViewController {
} else {
// Reply quote
replyName = name
replyText = first.message.isEmpty ? "Photo" : first.message
let rawReplyMsg = first.message.isEmpty ? "Photo" : first.message
replyText = EmojiParser.replaceShortcodes(in: rawReplyMsg)
replyMessageId = first.message_id
}
}
@@ -861,11 +862,11 @@ final class NativeMessageListController: UIViewController {
updateScrollToBottomBadge()
}
/// Telegram-style message insertion animation.
/// New messages: slide up from below (-height*1.6 offset) + alpha fade (0.2s).
/// Telegram-style message insertion animation (iOS 26+ parity).
/// New messages: slide up from below (-height*1.2 offset) + alpha fade (0.12s).
/// Existing messages: spring position animation from old Y to new Y.
/// All position animations use CASpringAnimation (stiffness=443.7, damping=31.82).
/// Source: ChatMessageItemView.animateInsertion + ListView.insertNodeAtIndex.
/// All position animations use CASpringAnimation (stiffness=555, damping=47).
/// Source: UIKitUtils.m (iOS 26+ branch) + ListView.insertNodeAtIndex.
private func applyInsertionAnimations(newIds: Set<String>, oldPositions: [String: CGFloat]) {
for ip in collectionView.indexPathsForVisibleItems {
guard let cellId = dataSource.itemIdentifier(for: ip),
@@ -874,25 +875,25 @@ final class NativeMessageListController: UIViewController {
if newIds.contains(cellId) {
// NEW cell: slide up from below + alpha fade
// In inverted CV: negative offset = below on screen
let slideOffset = -cell.bounds.height * 1.6
let slideOffset = -cell.bounds.height * 1.2
let slide = CASpringAnimation(keyPath: "position.y")
slide.fromValue = slideOffset
slide.toValue = 0.0
slide.isAdditive = true
slide.stiffness = 443.7
slide.damping = 31.82
slide.stiffness = 555.0
slide.damping = 47.0
slide.mass = 1.0
slide.initialVelocity = 0
slide.duration = slide.settlingDuration
slide.fillMode = .backwards
cell.layer.add(slide, forKey: "insertionSlide")
// Alpha fade: 0 1 (0.2s)
// Alpha fade: 0 1 (Telegram-parity: fast fade)
let alpha = CABasicAnimation(keyPath: "opacity")
alpha.fromValue = 0.0
alpha.toValue = 1.0
alpha.duration = 0.2
alpha.duration = 0.12
alpha.fillMode = .backwards
cell.contentView.layer.add(alpha, forKey: "insertionAlpha")
@@ -905,8 +906,8 @@ final class NativeMessageListController: UIViewController {
move.fromValue = delta
move.toValue = 0.0
move.isAdditive = true
move.stiffness = 443.7
move.damping = 31.82
move.stiffness = 555.0
move.damping = 47.0
move.mass = 1.0
move.initialVelocity = 0
move.duration = move.settlingDuration