Фикс: просмотр фото — убрана нерабочая hero-анимация, фотки теперь заполняют экран
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
fadeDismiss()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user