Фикс: просмотр фото — убрана нерабочая hero-анимация, фотки теперь заполняют экран

This commit is contained in:
2026-04-07 19:09:40 +05:00
parent d84c867bd3
commit 62c24d19cf
5 changed files with 26 additions and 79 deletions

View File

@@ -203,7 +203,7 @@ extension MessageCellLayout {
} else {
messageType = .text
}
// Emoji-only: 1-3 emoji without other text large font, no bubble
// Emoji-only: single emoji without other text large font, no bubble
if messageType == .text && !config.text.isEmpty && EmojiParser.isEmojiOnly(config.text) {
messageType = .emojiOnly
}

View File

@@ -277,6 +277,9 @@ extension CallManager {
didReceiveCallKitDeactivation = false
var finalState = CallUiState()
finalState.peerPublicKey = uiState.peerPublicKey
finalState.peerTitle = uiState.peerTitle
finalState.peerUsername = uiState.peerUsername
if let reason, !reason.isEmpty {
finalState.statusText = reason
}

View File

@@ -68,15 +68,14 @@ enum EmojiParser {
return ":emoji_\(codes.joined(separator: "-")):"
}
// MARK: - Emoji-Only Detection (Telegram Parity)
// MARK: - Emoji-Only Detection
/// Returns true if text contains ONLY emoji characters (no text, no limit on count).
/// Telegram parity: `ChatMessageItemCommon.swift:243-252` no max count,
/// just `message.text.containsOnlyEmoji` (EmojiUtils.swift:77-78).
/// Returns true if text is exactly ONE emoji character (enlarged display).
/// Multiple emoji use regular text size.
static func isEmojiOnly(_ text: String) -> Bool {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return false }
return trimmed.allSatisfy { $0.isEmojiCharacter }
guard trimmed.count == 1 else { return false }
return trimmed.first!.isEmojiCharacter
}
}

View File

@@ -162,10 +162,6 @@ struct ImageGalleryViewer: View {
state.images.indices.contains(currentPage) ? state.images[currentPage] : nil
}
private var heroAnimation: Animation {
.interpolatingSpring(duration: 0.3, bounce: 0, initialVelocity: 0)
}
private var backgroundOpacity: CGFloat {
let progress = min(abs(panCoordinator.dragOffset.height) / 80, 1)
return isExpanded ? max(1 - progress, 0) : 0
@@ -184,13 +180,8 @@ struct ImageGalleryViewer: View {
// MARK: - Body
var body: some View {
let sf = state.sourceFrame
TabView(selection: $currentPage) {
ForEach(Array(state.images.enumerated()), id: \.element.attachmentId) { index, info in
let isHeroPage = index == state.initialIndex
let heroActive = isHeroPage && !isExpanded
ZoomableImagePage(
attachmentId: info.attachmentId,
onDismiss: { dismissAction() },
@@ -198,14 +189,7 @@ struct ImageGalleryViewer: View {
currentScale: $currentZoomScale,
onEdgeTap: { dir in navigateEdgeTap(direction: dir) }
)
// Hero page: animate from sourceFrame fullscreen.
// Non-hero pages: NO explicit frame fill TabView page naturally.
.modifier(GalleryPageModifier(
heroActive: heroActive,
sourceFrame: sf,
fullSize: screenSize,
dragOffset: panCoordinator.dragOffset
))
.offset(panCoordinator.dragOffset)
.tag(index)
}
}
@@ -222,7 +206,7 @@ struct ImageGalleryViewer: View {
.task {
prefetchAdjacentImages(around: state.initialIndex)
guard !isExpanded else { return }
withAnimation(heroAnimation) { isExpanded = true }
withAnimation(.easeOut(duration: 0.2)) { isExpanded = true }
}
.onChange(of: currentPage) { _, p in prefetchAdjacentImages(around: p) }
.onChange(of: currentZoomScale) { _, s in panCoordinator.isEnabled = s <= 1.05 }
@@ -237,7 +221,7 @@ struct ImageGalleryViewer: View {
if y > 50 || v > 1000 {
dismissAction()
} else {
withAnimation(heroAnimation.speed(1.2)) {
withAnimation(.spring(response: 0.25, dampingFraction: 0.9)) {
panCoordinator.dragOffset = .zero
}
}
@@ -373,25 +357,7 @@ struct ImageGalleryViewer: View {
// MARK: - Dismiss
private func dismissAction() {
if currentZoomScale > 1.05 || currentPage != state.initialIndex {
fadeDismiss()
} else {
heroDismiss()
}
}
private func heroDismiss() {
guard !isDismissing else { return }
isDismissing = true
panCoordinator.isEnabled = false
Task {
withAnimation(heroAnimation.speed(1.2)) {
panCoordinator.dragOffset = .zero
isExpanded = false
}
try? await Task.sleep(for: .seconds(0.35))
onDismiss()
}
}
private func fadeDismiss() {
@@ -448,30 +414,3 @@ struct ImageGalleryViewer: View {
}
}
// MARK: - GalleryPageModifier
/// Applies hero transition frame/offset for the initial page.
/// Uses a SINGLE view branch (no if/else) to preserve SwiftUI structural identity
/// across the hero expanded transition. This prevents GeometryReader inside
/// ZoomableImagePage from receiving stale/incorrect proposed sizes during the swap.
private struct GalleryPageModifier: ViewModifier {
let heroActive: Bool
let sourceFrame: CGRect
let fullSize: CGSize
let dragOffset: CGSize
func body(content: Content) -> some View {
content
.frame(
width: heroActive ? sourceFrame.width : fullSize.width,
height: heroActive ? sourceFrame.height : fullSize.height
)
.clipped()
.offset(
x: heroActive ? sourceFrame.minX : 0,
y: heroActive ? sourceFrame.minY : 0
)
.offset(dragOffset)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}

View File

@@ -4,8 +4,9 @@ import UIKit
// MARK: - ZoomableImagePage
/// Single page in the image gallery viewer.
/// Uses GeometryReader + explicit frame calculation (Telegram parity) instead of
/// `.scaledToFit()` which is unreliable inside TabView `.page` style overlay chains.
/// Uses screen bounds for image sizing (Telegram parity) GeometryReader is unreliable
/// inside TabView `.page` style, especially during hero animation transitions.
/// GeometryReader is kept only for centering via `.position()`.
struct ZoomableImagePage: View {
let attachmentId: String
@@ -20,14 +21,20 @@ struct ZoomableImagePage: View {
@State private var zoomOffset: CGSize = .zero
@GestureState private var pinchScale: CGFloat = 1.0
/// Fixed screen bounds for fittedSize calculation not dependent on GeometryReader.
/// Telegram uses the same approach: sizes are computed against screen dimensions,
/// UIScrollView handles the rest.
private let screenSize = UIScreen.main.bounds.size
var body: some View {
let effectiveScale = zoomScale * pinchScale
GeometryReader { geo in
let viewSize = geo.size
let centerX = geo.size.width / 2
let centerY = geo.size.height / 2
if let image {
let fitted = fittedSize(image.size, in: viewSize)
let fitted = fittedSize(image.size, in: screenSize)
Image(uiImage: image)
.resizable()
@@ -37,13 +44,12 @@ struct ZoomableImagePage: View {
x: effectiveScale > 1.05 ? zoomOffset.width : 0,
y: effectiveScale > 1.05 ? zoomOffset.height : 0
)
.position(x: viewSize.width / 2, y: viewSize.height / 2)
.position(x: centerX, y: centerY)
} else {
placeholder
.position(x: viewSize.width / 2, y: viewSize.height / 2)
.position(x: centerX, y: centerY)
}
}
.ignoresSafeArea()
.contentShape(Rectangle())
.onTapGesture(count: 2) {
withAnimation(.spring(response: 0.35, dampingFraction: 0.9)) {