CallKit/PushKit интеграция + фикс PacketPushNotification (tokenType, deviceId)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user