Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2023-07-03 11:52:41 +02:00
commit af6c9a9868
16 changed files with 674 additions and 194 deletions

View File

@ -75,6 +75,7 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
super.init(target: target, action: action) super.init(target: target, action: action)
self.maximumNumberOfTouches = 1 self.maximumNumberOfTouches = 1
self.delaysTouchesBegan = false
} }
override public func reset() { override public func reset() {
@ -136,7 +137,9 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
let size = self.view?.bounds.size ?? CGSize() let size = self.view?.bounds.size ?? CGSize()
print("moved: \(CFAbsoluteTimeGetCurrent()) absTranslationX: \(absTranslationX) absTranslationY: \(absTranslationY)") //print("moved: \(CFAbsoluteTimeGetCurrent()) absTranslationX: \(absTranslationX) absTranslationY: \(absTranslationY)")
var fireBegan = false
if self.currentAllowedDirections.contains(.down) { if self.currentAllowedDirections.contains(.down) {
if !self.validatedGesture { if !self.validatedGesture {
@ -177,12 +180,18 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
self.state = .failed self.state = .failed
} else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX { } else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX {
self.validatedGesture = true self.validatedGesture = true
fireBegan = true
} }
} }
} }
if self.validatedGesture { if self.validatedGesture {
super.touchesMoved(touches, with: event) super.touchesMoved(touches, with: event)
if fireBegan {
if self.state == .possible {
self.state = .began
}
}
} }
} }
} }

View File

@ -332,7 +332,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size))
self.ignoreScrolling = true self.ignoreScrolling = true
self.scrollNode.view.isScrollEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0) && self.isInteractiveDimissEnabled self.scrollNode.view.isScrollEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0) && self.isInteractiveDimissEnabled && !self.isFlat
let previousBounds = self.scrollNode.bounds let previousBounds = self.scrollNode.bounds
let scrollNodeFrame = CGRect(origin: CGPoint(x: self.horizontalDismissOffset ?? 0.0, y: 0.0), size: layout.size) let scrollNodeFrame = CGRect(origin: CGPoint(x: self.horizontalDismissOffset ?? 0.0, y: 0.0), size: layout.size)
self.scrollNode.frame = scrollNodeFrame self.scrollNode.frame = scrollNodeFrame
@ -348,7 +348,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
} }
self.ignoreScrolling = false self.ignoreScrolling = false
self.scrollNode.view.isScrollEnabled = !isStandaloneModal self.scrollNode.view.isScrollEnabled = !isStandaloneModal && !self.isFlat
let isLandscape = layout.orientation == .landscape let isLandscape = layout.orientation == .landscape
let containerLayout: ContainerViewLayout let containerLayout: ContainerViewLayout
@ -515,6 +515,9 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
if !self.container.bounds.contains(self.view.convert(point, to: self.container.view)) { if !self.container.bounds.contains(self.view.convert(point, to: self.container.view)) {
return self.dim.view return self.dim.view
} }
if self.isFlat {
return result
}
var currentParent: UIView? = result var currentParent: UIView? = result
var enableScrolling = true var enableScrolling = true
while true { while true {
@ -562,7 +565,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
enableScrolling = false enableScrolling = false
} }
} }
self.scrollNode.view.isScrollEnabled = enableScrolling self.scrollNode.view.isScrollEnabled = enableScrolling && !self.isFlat
return result return result
} }
} }

View File

@ -1669,6 +1669,56 @@ public final class EngineStoryViewListContext {
} }
} }
if let storedItem = transaction.getStory(id: StoryId(peerId: account.peerId, id: storyId))?.get(Stories.StoredItem.self), case let .item(item) = storedItem, let currentViews = item.views {
let updatedItem: Stories.StoredItem = .item(Stories.Item(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: item.media,
text: item.text,
entities: item.entities,
views: Stories.Item.Views(seenCount: Int(count), seenPeerIds: currentViews.seenPeerIds),
privacy: item.privacy,
isPinned: item.isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
))
if let entry = CodableEntry(updatedItem) {
transaction.setStory(id: StoryId(peerId: account.peerId, id: storyId), value: entry)
}
}
var currentItems = transaction.getStoryItems(peerId: account.peerId)
for i in 0 ..< currentItems.count {
if currentItems[i].id == storyId {
if case let .item(item) = currentItems[i].value.get(Stories.StoredItem.self), let currentViews = item.views {
let updatedItem: Stories.StoredItem = .item(Stories.Item(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: item.media,
text: item.text,
entities: item.entities,
views: Stories.Item.Views(seenCount: Int(count), seenPeerIds: currentViews.seenPeerIds),
privacy: item.privacy,
isPinned: item.isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
))
if let entry = CodableEntry(updatedItem) {
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp)
}
}
}
}
transaction.setStoryItems(peerId: account.peerId, items: currentItems)
return InternalState(totalCount: Int(count), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset) return InternalState(totalCount: Int(count), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset)
case .none: case .none:
return InternalState(totalCount: 0, items: [], canLoadMore: false, nextOffset: nil) return InternalState(totalCount: 0, items: [], canLoadMore: false, nextOffset: nil)

View File

@ -23,6 +23,7 @@ swift_library(
"//submodules/TelegramStringFormatting", "//submodules/TelegramStringFormatting",
"//submodules/AppBundle", "//submodules/AppBundle",
"//submodules/PeerPresenceStatusManager", "//submodules/PeerPresenceStatusManager",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -13,6 +13,7 @@ import CheckNode
import TelegramStringFormatting import TelegramStringFormatting
import AppBundle import AppBundle
import PeerPresenceStatusManager import PeerPresenceStatusManager
import EmojiStatusComponent
private let avatarFont = avatarPlaceholderFont(size: 15.0) private let avatarFont = avatarPlaceholderFont(size: 15.0)
private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate) private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate)
@ -132,6 +133,7 @@ public final class PeerListItemComponent: Component {
private let label = ComponentView<Empty>() private let label = ComponentView<Empty>()
private let separatorLayer: SimpleLayer private let separatorLayer: SimpleLayer
private let avatarNode: AvatarNode private let avatarNode: AvatarNode
private var avatarIcon: ComponentView<Empty>?
private var iconView: UIImageView? private var iconView: UIImageView?
private var checkLayer: CheckLayer? private var checkLayer: CheckLayer?
@ -316,6 +318,8 @@ public final class PeerListItemComponent: Component {
} else { } else {
transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame) transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame)
} }
var statusIcon: EmojiStatusComponent.Content?
if let peer = component.peer { if let peer = component.peer {
let clipStyle: AvatarNodeClipStyle let clipStyle: AvatarNodeClipStyle
if case let .channel(channel) = peer, channel.flags.contains(.isForum) { if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
@ -324,6 +328,18 @@ public final class PeerListItemComponent: Component {
clipStyle = .round clipStyle = .round
} }
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
if peer.isScam {
statusIcon = .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_ScamAccount.uppercased())
} else if peer.isFake {
statusIcon = .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_FakeAccount.uppercased())
} else if case let .user(user) = peer, let emojiStatus = user.emojiStatus {
statusIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.theme.list.itemAccentColor, loopMode: .count(2))
} else if peer.isVerified {
statusIcon = .verified(fillColor: component.theme.list.itemCheckColors.fillColor, foregroundColor: component.theme.list.itemCheckColors.foregroundColor, sizeType: .compact)
} else if peer.isPremium {
statusIcon = .premium(color: component.theme.list.itemAccentColor)
}
} }
let labelSize = self.label.update( let labelSize = self.label.update(
@ -350,7 +366,7 @@ public final class PeerListItemComponent: Component {
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
) )
let titleSpacing: CGFloat = 1.0 let titleSpacing: CGFloat = 2.0
let centralContentHeight: CGFloat let centralContentHeight: CGFloat
if labelSize.height > 0.0, case .generic = component.style { if labelSize.height > 0.0, case .generic = component.style {
centralContentHeight = titleSize.height + labelSize.height + titleSpacing centralContentHeight = titleSize.height + labelSize.height + titleSpacing
@ -358,7 +374,7 @@ public final class PeerListItemComponent: Component {
centralContentHeight = titleSize.height centralContentHeight = titleSize.height
} }
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize) let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: -1.0 + floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
if let titleView = self.title.view { if let titleView = self.title.view {
if titleView.superview == nil { if titleView.superview == nil {
titleView.isUserInteractionEnabled = false titleView.isUserInteractionEnabled = false
@ -419,6 +435,48 @@ public final class PeerListItemComponent: Component {
transition.setFrame(view: labelView, frame: labelFrame) transition.setFrame(view: labelView, frame: labelFrame)
} }
if let statusIcon {
let animationCache = component.context.animationCache
let animationRenderer = component.context.animationRenderer
let avatarIcon: ComponentView<Empty>
var avatarIconTransition = transition
if let current = self.avatarIcon {
avatarIcon = current
} else {
avatarIconTransition = transition.withAnimation(.none)
avatarIcon = ComponentView<Empty>()
self.avatarIcon = avatarIcon
}
let avatarIconComponent = EmojiStatusComponent(
context: component.context,
animationCache: animationCache,
animationRenderer: animationRenderer,
content: statusIcon,
isVisibleForAnimations: true,
action: nil,
emojiFileUpdated: nil
)
let iconSize = avatarIcon.update(
transition: avatarIconTransition,
component: AnyComponent(avatarIconComponent),
environment: {},
containerSize: CGSize(width: 20.0, height: 20.0)
)
if let avatarIconView = avatarIcon.view {
if avatarIconView.superview == nil {
avatarIconView.isUserInteractionEnabled = false
self.containerButton.addSubview(avatarIconView)
}
avatarIconTransition.setFrame(view: avatarIconView, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize))
}
} else if let avatarIcon = self.avatarIcon {
self.avatarIcon = nil
avatarIcon.view?.removeFromSuperview()
}
if themeUpdated { if themeUpdated {
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
} }

View File

@ -72,7 +72,7 @@ swift_library(
"//submodules/TinyThumbnail", "//submodules/TinyThumbnail",
"//submodules/ImageBlur", "//submodules/ImageBlur",
"//submodules/StickerPackPreviewUI", "//submodules/StickerPackPreviewUI",
"//submodules/Components/AnimatedStickerComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -398,7 +398,7 @@ public final class StoryContentContextImpl: StoryContentContext {
private var requestStoryDisposables = DisposableSet() private var requestStoryDisposables = DisposableSet()
private var preloadStoryResourceDisposables: [MediaId: Disposable] = [:] private var preloadStoryResourceDisposables: [MediaId: Disposable] = [:]
private var pollStoryMetadataDisposables = DisposableSet() private var pollStoryMetadataDisposables: [StoryId: Disposable] = [:]
private var singlePeerListContext: PeerExpiringStoryListContext? private var singlePeerListContext: PeerExpiringStoryListContext?
@ -615,7 +615,9 @@ public final class StoryContentContextImpl: StoryContentContext {
for (_, disposable) in self.preloadStoryResourceDisposables { for (_, disposable) in self.preloadStoryResourceDisposables {
disposable.dispose() disposable.dispose()
} }
self.pollStoryMetadataDisposables.dispose() for (_, disposable) in self.pollStoryMetadataDisposables {
disposable.dispose()
}
self.storySubscriptionsDisposable?.dispose() self.storySubscriptionsDisposable?.dispose()
} }
@ -805,7 +807,11 @@ public final class StoryContentContextImpl: StoryContentContext {
} }
} }
for (peerId, ids) in pollIdByPeerId { for (peerId, ids) in pollIdByPeerId {
self.pollStoryMetadataDisposables.add(self.context.engine.messages.refreshStoryViews(peerId: peerId, ids: ids).start()) for id in ids {
if self.pollStoryMetadataDisposables[StoryId(peerId: peerId, id: id)] == nil {
self.pollStoryMetadataDisposables[StoryId(peerId: peerId, id: id)] = self.context.engine.messages.refreshStoryViews(peerId: peerId, ids: ids).start()
}
}
} }
} }

View File

@ -33,6 +33,7 @@ func hasFirstResponder(_ view: UIView) -> Bool {
} }
private final class StoryLongPressRecognizer: UILongPressGestureRecognizer { private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
var shouldBegin: ((UITouch) -> Bool)?
var updateIsTracking: ((Bool) -> Void)? var updateIsTracking: ((Bool) -> Void)?
override var state: UIGestureRecognizer.State { override var state: UIGestureRecognizer.State {
@ -50,10 +51,12 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
} }
private var isTracking: Bool = false private var isTracking: Bool = false
private var isValidated: Bool = false
override func reset() { override func reset() {
super.reset() super.reset()
self.isValidated = false
if self.isTracking { if self.isTracking {
self.isTracking = false self.isTracking = false
self.updateIsTracking?(false) self.updateIsTracking?(false)
@ -61,11 +64,21 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
} }
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) { override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event) if !self.isValidated, let touch = touches.first {
if let shouldBegin = self.shouldBegin, shouldBegin(touch) {
self.isValidated = true
} else {
return
}
}
if !self.isTracking { if self.isValidated {
self.isTracking = true super.touchesBegan(touches, with: event)
self.updateIsTracking?(true)
if !self.isTracking {
self.isTracking = true
self.updateIsTracking?(true)
}
} }
} }
} }
@ -153,9 +166,8 @@ private final class StoryContainerScreenComponent: Component {
self.didBegin = didBegin self.didBegin = didBegin
} }
} }
final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate { final class View: UIView, UIGestureRecognizerDelegate {
private var component: StoryContainerScreenComponent? private var component: StoryContainerScreenComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
private var environment: ViewControllerComponentContainer.Environment? private var environment: ViewControllerComponentContainer.Environment?
@ -190,6 +202,8 @@ private final class StoryContainerScreenComponent: Component {
var dismissWithoutTransitionOut: Bool = false var dismissWithoutTransitionOut: Bool = false
var longPressRecognizer: StoryLongPressRecognizer?
override init(frame: CGRect) { override init(frame: CGRect) {
self.backgroundLayer = SimpleLayer() self.backgroundLayer = SimpleLayer()
self.backgroundLayer.backgroundColor = UIColor.black.cgColor self.backgroundLayer.backgroundColor = UIColor.black.cgColor
@ -236,7 +250,7 @@ private final class StoryContainerScreenComponent: Component {
return [] return []
} }
} }
if !itemSetComponentView.allowsInteractiveGestures() { if !itemSetComponentView.allowsVerticalPanGesture() {
return [] return []
} }
@ -253,6 +267,19 @@ private final class StoryContainerScreenComponent: Component {
self.isHoldingTouch = isTracking self.isHoldingTouch = isTracking
self.state?.updated(transition: .immediate) self.state?.updated(transition: .immediate)
} }
longPressRecognizer.shouldBegin = { [weak self] touch in
guard let self else {
return false
}
guard let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else {
return false
}
if !itemSetComponentView.isPointInsideContentArea(point: touch.location(in: itemSetComponentView)) {
return false
}
return true
}
self.longPressRecognizer = longPressRecognizer
self.addGestureRecognizer(longPressRecognizer) self.addGestureRecognizer(longPressRecognizer)
let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGesture(_:))) let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGesture(_:)))
@ -406,7 +433,7 @@ private final class StoryContainerScreenComponent: Component {
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state { switch recognizer.state {
case .began: case .began:
print("began: \(CFAbsoluteTimeGetCurrent())") //print("began: \(CFAbsoluteTimeGetCurrent())")
self.beginHorizontalPan(translation: recognizer.translation(in: self)) self.beginHorizontalPan(translation: recognizer.translation(in: self))
case .changed: case .changed:
self.updateHorizontalPan(translation: recognizer.translation(in: self)) self.updateHorizontalPan(translation: recognizer.translation(in: self))
@ -450,7 +477,7 @@ private final class StoryContainerScreenComponent: Component {
self.verticalPanState = nil self.verticalPanState = nil
var updateState = true var updateState = true
if translation.y > 200.0 || (translation.y > 100.0 && velocity.y > 200.0) { if translation.y > 200.0 || (translation.y > 5.0 && velocity.y > 200.0) {
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
self.environment?.controller()?.dismiss() self.environment?.controller()?.dismiss()
} else if translation.y < -200.0 || (translation.y < -100.0 && velocity.y < -100.0) { } else if translation.y < -200.0 || (translation.y < -100.0 && velocity.y < -100.0) {

View File

@ -218,7 +218,7 @@ final class StoryContentCaptionComponent: Component {
let edgeDistanceFraction = edgeDistance / 7.0 let edgeDistanceFraction = edgeDistance / 7.0
transition.setAlpha(view: self.scrollFullMaskView, alpha: 1.0 - edgeDistanceFraction) transition.setAlpha(view: self.scrollFullMaskView, alpha: 1.0 - edgeDistanceFraction)
let shadowOverflow: CGFloat = 36.0 let shadowOverflow: CGFloat = 56.0
let shadowFrame = CGRect(origin: CGPoint(x: 0.0, y: -self.scrollView.contentOffset.y + itemLayout.containerSize.height - itemLayout.visibleTextHeight - itemLayout.verticalInset - shadowOverflow), size: CGSize(width: itemLayout.containerSize.width, height: itemLayout.visibleTextHeight + itemLayout.verticalInset + shadowOverflow)) let shadowFrame = CGRect(origin: CGPoint(x: 0.0, y: -self.scrollView.contentOffset.y + itemLayout.containerSize.height - itemLayout.visibleTextHeight - itemLayout.verticalInset - shadowOverflow), size: CGSize(width: itemLayout.containerSize.width, height: itemLayout.visibleTextHeight + itemLayout.verticalInset + shadowOverflow))
transition.setFrame(layer: self.shadowGradientLayer, frame: shadowFrame) transition.setFrame(layer: self.shadowGradientLayer, frame: shadowFrame)
transition.setFrame(layer: self.shadowPlainLayer, frame: CGRect(origin: CGPoint(x: shadowFrame.minX, y: shadowFrame.maxY), size: CGSize(width: shadowFrame.width, height: self.scrollView.contentSize.height + 1000.0))) transition.setFrame(layer: self.shadowPlainLayer, frame: CGRect(origin: CGPoint(x: shadowFrame.minX, y: shadowFrame.maxY), size: CGSize(width: shadowFrame.width, height: self.scrollView.contentSize.height + 1000.0)))
@ -479,7 +479,7 @@ final class StoryContentCaptionComponent: Component {
var locations: [NSNumber] = [] var locations: [NSNumber] = []
var colors: [CGColor] = [] var colors: [CGColor] = []
let numStops = 10 let numStops = 10
let baseAlpha: CGFloat = 0.3 let baseAlpha: CGFloat = 0.5
for i in 0 ..< numStops { for i in 0 ..< numStops {
let step = 1.0 - CGFloat(i) / CGFloat(numStops - 1) let step = 1.0 - CGFloat(i) / CGFloat(numStops - 1)
locations.append((1.0 - step) as NSNumber) locations.append((1.0 - step) as NSNumber)

View File

@ -410,10 +410,8 @@ final class StoryItemContentComponent: Component {
var fetchSignal: Signal<Never, NoError>? var fetchSignal: Signal<Never, NoError>?
switch messageMedia { switch messageMedia {
case .image: case .image:
self.contentLoaded = true break
case let .file(file): case let .file(file):
self.contentLoaded = true
fetchSignal = fetchedMediaResource( fetchSignal = fetchedMediaResource(
mediaBox: component.context.account.postbox.mediaBox, mediaBox: component.context.account.postbox.mediaBox,
userLocation: .other, userLocation: .other,
@ -446,6 +444,16 @@ final class StoryItemContentComponent: Component {
} }
if let messageMedia { if let messageMedia {
var applyState = false
self.imageView.didLoadContents = { [weak self] in
guard let self else {
return
}
self.contentLoaded = true
if applyState {
self.state?.updated(transition: .immediate)
}
}
self.imageView.update( self.imageView.update(
context: component.context, context: component.context,
peer: component.peer, peer: component.peer,
@ -456,6 +464,10 @@ final class StoryItemContentComponent: Component {
attemptSynchronous: synchronousLoad, attemptSynchronous: synchronousLoad,
transition: transition transition: transition
) )
applyState = true
if self.imageView.isContentLoaded {
self.contentLoaded = true
}
transition.setFrame(view: self.imageView, frame: CGRect(origin: CGPoint(), size: availableSize)) transition.setFrame(view: self.imageView, frame: CGRect(origin: CGPoint(), size: availableSize))
var dimensions: CGSize? var dimensions: CGSize?

View File

@ -16,6 +16,9 @@ final class StoryItemImageView: UIView {
private var disposable: Disposable? private var disposable: Disposable?
private var fetchDisposable: Disposable? private var fetchDisposable: Disposable?
private(set) var isContentLoaded: Bool = false
var didLoadContents: (() -> Void)?
override init(frame: CGRect) { override init(frame: CGRect) {
self.contentView = UIImageView() self.contentView = UIImageView()
self.contentView.contentMode = .scaleAspectFill self.contentView.contentMode = .scaleAspectFill
@ -55,6 +58,8 @@ final class StoryItemImageView: UIView {
self.updateImage(image: image) self.updateImage(image: image)
} }
} }
self.isContentLoaded = true
self.didLoadContents?()
} else { } else {
if let thumbnailData = image.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) { if let thumbnailData = image.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) {
self.contentView.image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3) self.contentView.image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3)
@ -89,6 +94,8 @@ final class StoryItemImageView: UIView {
} }
if let image { if let image {
self.updateImage(image: image) self.updateImage(image: image)
self.isContentLoaded = true
self.didLoadContents?()
} }
}) })
} }
@ -110,6 +117,8 @@ final class StoryItemImageView: UIView {
self.updateImage(image: image) self.updateImage(image: image)
} }
} }
self.isContentLoaded = true
self.didLoadContents?()
} else { } else {
if let thumbnailData = file.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) { if let thumbnailData = file.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) {
self.contentView.image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3) self.contentView.image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3)
@ -141,6 +150,8 @@ final class StoryItemImageView: UIView {
} }
if let image { if let image {
self.updateImage(image: image) self.updateImage(image: image)
self.isContentLoaded = true
self.didLoadContents?()
} }
}) })
} }

View File

@ -269,6 +269,14 @@ public final class StoryItemSetContainerComponent: Component {
} }
} }
private struct PanState: Equatable {
var fraction: CGFloat
init(fraction: CGFloat) {
self.fraction = fraction
}
}
private final class Scroller: UIScrollView { private final class Scroller: UIScrollView {
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
@ -319,7 +327,7 @@ public final class StoryItemSetContainerComponent: Component {
var preparingToDisplayViewList: Bool = false var preparingToDisplayViewList: Bool = false
var displayViewList: Bool = false var displayViewList: Bool = false
var viewList: ViewList? var viewLists: [Int32: ViewList] = [:]
var isEditingStory: Bool = false var isEditingStory: Bool = false
@ -354,6 +362,11 @@ public final class StoryItemSetContainerComponent: Component {
private var animateNextNavigationId: Int32? private var animateNextNavigationId: Int32?
private var initializedOffset: Bool = false private var initializedOffset: Bool = false
private var viewListPanState: PanState?
private var viewListSwipeRecognizer: InteractiveTransitionGestureRecognizer?
private var verticalPanState: PanState?
override init(frame: CGRect) { override init(frame: CGRect) {
self.sendMessageContext = StoryItemSetContainerSendMessage() self.sendMessageContext = StoryItemSetContainerSendMessage()
@ -365,6 +378,7 @@ public final class StoryItemSetContainerComponent: Component {
self.scroller.showsVerticalScrollIndicator = false self.scroller.showsVerticalScrollIndicator = false
self.scroller.showsHorizontalScrollIndicator = false self.scroller.showsHorizontalScrollIndicator = false
self.scroller.decelerationRate = .fast self.scroller.decelerationRate = .fast
self.scroller.delaysContentTouches = false
self.controlsContainerView = SparseContainerView() self.controlsContainerView = SparseContainerView()
self.controlsContainerView.clipsToBounds = true self.controlsContainerView.clipsToBounds = true
@ -404,6 +418,33 @@ public final class StoryItemSetContainerComponent: Component {
tapRecognizer.delegate = self tapRecognizer.delegate = self
self.itemsContainerView.addGestureRecognizer(tapRecognizer) self.itemsContainerView.addGestureRecognizer(tapRecognizer)
let verticalPanRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.viewListDismissPanGesture(_:)), allowedDirections: { [weak self] point in
guard let self else {
return []
}
if !self.displayViewList {
return []
}
return [.down]
})
self.addGestureRecognizer(verticalPanRecognizer)
let viewListSwipeRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.viewListPanGesture(_:)), allowedDirections: { [weak self] point in
guard let self else {
return []
}
if !self.displayViewList {
return []
}
if self.bounds.contains(point), !self.itemsContainerView.frame.contains(point) {
return [.left, .right]
} else {
return []
}
})
self.viewListSwipeRecognizer = viewListSwipeRecognizer
self.addGestureRecognizer(viewListSwipeRecognizer)
self.audioRecorderDisposable = (self.sendMessageContext.audioRecorder.get() self.audioRecorderDisposable = (self.sendMessageContext.audioRecorder.get()
|> deliverOnMainQueue).start(next: { [weak self] audioRecorder in |> deliverOnMainQueue).start(next: { [weak self] audioRecorder in
guard let self else { guard let self else {
@ -531,6 +572,13 @@ public final class StoryItemSetContainerComponent: Component {
return true return true
} }
func allowsVerticalPanGesture() -> Bool {
if self.displayViewList {
return false
}
return true
}
func rewindCurrentItem() { func rewindCurrentItem() {
guard let component = self.component else { guard let component = self.component else {
return return
@ -623,6 +671,78 @@ public final class StoryItemSetContainerComponent: Component {
} }
} }
@objc private func viewListPanGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
if !self.bounds.isEmpty {
let translation = recognizer.translation(in: self)
let fraction: CGFloat = max(-1.0, min(1.0, translation.x / self.bounds.width))
self.viewListPanState = PanState(fraction: fraction)
self.state?.updated(transition: .immediate)
}
case .changed:
if var viewListPanState = self.viewListPanState {
let translation = recognizer.translation(in: self)
let fraction: CGFloat = max(-1.0, min(1.0, translation.x / self.bounds.width))
viewListPanState.fraction = fraction
self.viewListPanState = viewListPanState
self.state?.updated(transition: .immediate)
}
case .cancelled, .ended:
if let viewListPanState = self.viewListPanState {
let velocity = recognizer.velocity(in: self)
var consumed = false
if let component = self.component, let currentIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) {
if (viewListPanState.fraction <= -0.3 || (viewListPanState.fraction <= -0.05 && velocity.x <= -200.0)), currentIndex != component.slice.allItems.count - 1 {
let nextItem = component.slice.allItems[currentIndex + 1]
self.animateNextNavigationId = nextItem.storyItem.id
component.navigate(.id(nextItem.storyItem.id))
consumed = true
} else if (viewListPanState.fraction >= 0.3 || (viewListPanState.fraction >= 0.05 && velocity.x >= 200.0)), currentIndex != 0 {
let previousItem = component.slice.allItems[currentIndex - 1]
self.animateNextNavigationId = previousItem.storyItem.id
component.navigate(.id(previousItem.storyItem.id))
consumed = true
}
}
if !consumed {
self.viewListPanState = nil
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
}
default:
break
}
}
@objc private func viewListDismissPanGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
self.verticalPanState = PanState(fraction: 0.0)
self.state?.updated(transition: .immediate)
case .changed:
let translation = recognizer.translation(in: self)
self.verticalPanState = PanState(fraction: max(-1.0, min(1.0, translation.y / self.bounds.height)))
self.state?.updated(transition: .immediate)
case .cancelled, .ended:
if let verticalPanState = self.verticalPanState {
self.verticalPanState = nil
let velocity = recognizer.velocity(in: self)
if verticalPanState.fraction >= 0.3 || (verticalPanState.fraction >= 0.05 && velocity.y >= 150.0) {
self.displayViewList = false
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
default:
break
}
}
@objc private func closePressed() { @objc private func closePressed() {
guard let component = self.component else { guard let component = self.component else {
return return
@ -814,7 +934,11 @@ public final class StoryItemSetContainerComponent: Component {
for index in 0 ..< component.slice.allItems.count { for index in 0 ..< component.slice.allItems.count {
let item = component.slice.allItems[index] let item = component.slice.allItems[index]
let offsetFraction: CGFloat = (self.scrollingCenterX - self.scrollingOffsetX) / fullItemScrollDistance var offsetFraction: CGFloat = (self.scrollingCenterX - self.scrollingOffsetX) / fullItemScrollDistance
if let viewListPanState = self.viewListPanState {
offsetFraction += viewListPanState.fraction
}
let centerIndexOffset = index - centralIndex let centerIndexOffset = index - centralIndex
let centerFraction: CGFloat = CGFloat(centerIndexOffset) let centerFraction: CGFloat = CGFloat(centerIndexOffset)
@ -996,7 +1120,7 @@ public final class StoryItemSetContainerComponent: Component {
return false return false
} }
if component.slice.peer.id == component.context.account.peerId { if component.slice.peer.id == component.context.account.peerId {
if let views = component.slice.item.storyItem.views, !views.seenPeers.isEmpty { if let _ = component.slice.item.storyItem.views {
self.displayViewList = true self.displayViewList = true
if component.verticalPanFraction == 0.0 { if component.verticalPanFraction == 0.0 {
self.preparingToDisplayViewList = true self.preparingToDisplayViewList = true
@ -1062,15 +1186,17 @@ public final class StoryItemSetContainerComponent: Component {
) )
inputPanelView.layer.animateAlpha(from: 0.0, to: inputPanelView.alpha, duration: 0.28) inputPanelView.layer.animateAlpha(from: 0.0, to: inputPanelView.alpha, duration: 0.28)
} }
if let viewListView = self.viewList?.view.view { for (_, viewList) in self.viewLists {
viewListView.layer.animatePosition( if let viewListView = viewList.view.view {
from: CGPoint(x: 0.0, y: self.bounds.height - self.controlsContainerView.frame.maxY), viewListView.layer.animatePosition(
to: CGPoint(), from: CGPoint(x: 0.0, y: self.bounds.height - self.controlsContainerView.frame.maxY),
duration: 0.3, to: CGPoint(),
timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3,
additive: true timingFunction: kCAMediaTimingFunctionSpring,
) additive: true
viewListView.layer.animateAlpha(from: 0.0, to: viewListView.alpha, duration: 0.28) )
viewListView.layer.animateAlpha(from: 0.0, to: viewListView.alpha, duration: 0.28)
}
} }
if let captionItemView = self.captionItem?.view.view { if let captionItemView = self.captionItem?.view.view {
captionItemView.layer.animatePosition( captionItemView.layer.animatePosition(
@ -1177,16 +1303,18 @@ public final class StoryItemSetContainerComponent: Component {
) )
inputPanelBackground.layer.animateAlpha(from: inputPanelBackground.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false) inputPanelBackground.layer.animateAlpha(from: inputPanelBackground.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false)
} }
if let viewListView = self.viewList?.view.view { for (_, viewList) in self.viewLists {
viewListView.layer.animatePosition( if let viewListView = viewList.view.view {
from: CGPoint(), viewListView.layer.animatePosition(
to: CGPoint(x: 0.0, y: self.bounds.height - self.controlsContainerView.frame.maxY), from: CGPoint(),
duration: 0.3, to: CGPoint(x: 0.0, y: self.bounds.height - self.controlsContainerView.frame.maxY),
timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3,
removeOnCompletion: false, timingFunction: kCAMediaTimingFunctionSpring,
additive: true removeOnCompletion: false,
) additive: true
viewListView.layer.animateAlpha(from: viewListView.alpha, to: 0.0, duration: 0.28, removeOnCompletion: false) )
viewListView.layer.animateAlpha(from: viewListView.alpha, to: 0.0, duration: 0.28, removeOnCompletion: false)
}
} }
if let captionItemView = self.captionItem?.view.view { if let captionItemView = self.captionItem?.view.view {
captionItemView.layer.animatePosition( captionItemView.layer.animatePosition(
@ -1439,12 +1567,12 @@ public final class StoryItemSetContainerComponent: Component {
} }
if self.component?.slice.item.storyItem.id != component.slice.item.storyItem.id { if self.component?.slice.item.storyItem.id != component.slice.item.storyItem.id {
component.markAsSeen(StoryId(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id))
self.initializedOffset = false self.initializedOffset = false
} }
var itemsTransition = transition var itemsTransition = transition
if let animateNextNavigationId = self.animateNextNavigationId, animateNextNavigationId == component.slice.item.storyItem.id { if let animateNextNavigationId = self.animateNextNavigationId, animateNextNavigationId == component.slice.item.storyItem.id {
self.animateNextNavigationId = nil self.animateNextNavigationId = nil
self.viewListPanState = nil
itemsTransition = transition.withAnimation(.curve(duration: 0.3, curve: .spring)) itemsTransition = transition.withAnimation(.curve(duration: 0.3, curve: .spring))
} }
@ -1705,144 +1833,213 @@ public final class StoryItemSetContainerComponent: Component {
inputPanelIsOverlay = true inputPanelIsOverlay = true
} }
if component.slice.peer.id == component.context.account.peerId { var validViewListIds: [Int32] = []
let viewList: ViewList if component.slice.peer.id == component.context.account.peerId, let currentIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) {
var viewListTransition = transition var visibleViewListIds: [Int32] = [component.slice.item.storyItem.id]
if let current = self.viewList {
viewList = current
} else {
if !transition.animation.isImmediate {
viewListTransition = .immediate
}
viewList = ViewList()
self.viewList = viewList
}
let outerExpansionFraction: CGFloat
if self.displayViewList { if self.displayViewList {
outerExpansionFraction = 1.0 if currentIndex != 0 {
} else if let views = component.slice.item.storyItem.views, !views.seenPeers.isEmpty { visibleViewListIds.append(component.slice.allItems[currentIndex - 1].storyItem.id)
outerExpansionFraction = component.verticalPanFraction }
} else { if currentIndex != component.slice.allItems.count - 1 {
outerExpansionFraction = 0.0 visibleViewListIds.append(component.slice.allItems[currentIndex + 1].storyItem.id)
}
} }
viewList.view.parentState = state var viewListBaseOffsetX: CGFloat = 0.0
let viewListSize = viewList.view.update( if let viewListPanState = self.viewListPanState {
transition: viewListTransition.withUserData(PeerListItemComponent.TransitionHint( viewListBaseOffsetX = viewListPanState.fraction * availableSize.width
synchronousLoad: false }
)).withUserData(StoryItemSetViewListComponent.AnimationHint(
synchronous: false var fixedAnimationOffset: CGFloat = 0.0
)), var applyFixedAnimationOffsetIds: [Int32] = []
component: AnyComponent(StoryItemSetViewListComponent(
externalState: viewList.externalState, for id in visibleViewListIds {
context: component.context, guard let itemIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == id }) else {
theme: component.theme, continue
strings: component.strings, }
sharedListsContext: component.sharedViewListsContext, let item = component.slice.allItems[itemIndex]
peerId: component.slice.peer.id, validViewListIds.append(id)
safeInsets: component.safeInsets,
storyItem: component.slice.item.storyItem, let viewList: ViewList
outerExpansionFraction: outerExpansionFraction, var viewListTransition = itemsTransition
close: { [weak self] in if let current = self.viewLists[id] {
guard let self else { viewList = current
return } else {
} if !itemsTransition.animation.isImmediate {
self.displayViewList = false viewListTransition = .immediate
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) }
}, viewList = ViewList()
expandViewStats: { [weak self] in self.viewLists[id] = viewList
guard let self else { applyFixedAnimationOffsetIds.append(id)
return }
}
let outerExpansionFraction: CGFloat
if !self.displayViewList { let outerExpansionDirection: Bool
self.displayViewList = true if self.displayViewList {
if let verticalPanState = self.verticalPanState {
self.preparingToDisplayViewList = true outerExpansionFraction = max(0.0, min(1.0, 1.0 - verticalPanState.fraction))
self.updateScrolling(transition: .immediate) } else {
self.preparingToDisplayViewList = false outerExpansionFraction = 1.0
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) outerExpansionDirection = false
} } else if let _ = item.storyItem.views {
}, outerExpansionFraction = component.verticalPanFraction
deleteAction: { [weak self] in outerExpansionDirection = true
guard let self, let component = self.component else { } else {
return outerExpansionFraction = 0.0
} outerExpansionDirection = true
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
let actionSheet = ActionSheetController(presentationData: presentationData) viewList.view.parentState = state
let viewListSize = viewList.view.update(
actionSheet.setItemGroups([ transition: viewListTransition.withUserData(PeerListItemComponent.TransitionHint(
ActionSheetItemGroup(items: [ synchronousLoad: false
ActionSheetButtonItem(title: "Delete Story", color: .destructive, action: { [weak self, weak actionSheet] in )).withUserData(StoryItemSetViewListComponent.AnimationHint(
actionSheet?.dismissAnimated() synchronous: false
)),
guard let self, let component = self.component else { component: AnyComponent(StoryItemSetViewListComponent(
return externalState: viewList.externalState,
} context: component.context,
component.delete() theme: component.theme,
}) strings: component.strings,
]), sharedListsContext: component.sharedViewListsContext,
ActionSheetItemGroup(items: [ peerId: component.slice.peer.id,
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in safeInsets: component.safeInsets,
actionSheet?.dismissAnimated() storyItem: item.storyItem,
}) outerExpansionFraction: outerExpansionFraction,
]) outerExpansionDirection: outerExpansionDirection,
]) close: { [weak self] in
actionSheet.dismissed = { [weak self] _ in
guard let self else { guard let self else {
return return
} }
self.sendMessageContext.actionSheet = nil self.displayViewList = false
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
},
expandViewStats: { [weak self] in
guard let self else {
return
}
if !self.displayViewList {
self.displayViewList = true
self.preparingToDisplayViewList = true
self.updateScrolling(transition: .immediate)
self.preparingToDisplayViewList = false
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
},
deleteAction: { [weak self] in
guard let self, let component = self.component else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: "Delete Story", color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let self, let component = self.component else {
return
}
component.delete()
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
actionSheet.dismissed = { [weak self] _ in
guard let self else {
return
}
self.sendMessageContext.actionSheet = nil
self.updateIsProgressPaused()
}
self.sendMessageContext.actionSheet = actionSheet
self.updateIsProgressPaused() self.updateIsProgressPaused()
component.presentController(actionSheet, nil)
},
moreAction: { [weak self] sourceView, gesture in
guard let self else {
return
}
self.performMoreAction(sourceView: sourceView, gesture: gesture)
},
openPeer: { [weak self] peer in
guard let self else {
return
}
self.navigateToPeer(peer: peer, chat: false)
} }
self.sendMessageContext.actionSheet = actionSheet )),
self.updateIsProgressPaused() environment: {},
containerSize: availableSize
component.presentController(actionSheet, nil) )
},
moreAction: { [weak self] sourceView, gesture in
guard let self else {
return
}
self.performMoreAction(sourceView: sourceView, gesture: gesture)
},
openPeer: { [weak self] peer in
guard let self else {
return
}
self.navigateToPeer(peer: peer, chat: false)
}
)),
environment: {},
containerSize: availableSize
)
let viewListFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - viewListSize.height), size: viewListSize)
if let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View {
var animateIn = false
if viewListView.superview == nil {
self.addSubview(viewListView)
animateIn = true
}
viewListTransition.setFrame(view: viewListView, frame: viewListFrame)
viewListTransition.setAlpha(view: viewListView, alpha: component.hideUI || self.isEditingStory ? 0.0 : 1.0)
if animateIn, !transition.animation.isImmediate { var viewListFrame = CGRect(origin: CGPoint(x: viewListBaseOffsetX, y: availableSize.height - viewListSize.height), size: viewListSize)
viewListView.animateIn(transition: transition) let indexDistance = CGFloat(max(-1, min(1, itemIndex - currentIndex)))
viewListFrame.origin.x += indexDistance * availableSize.width
if let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View {
var animateIn = false
if viewListView.superview == nil {
self.addSubview(viewListView)
animateIn = true
} else {
fixedAnimationOffset = viewListFrame.minX - viewListView.frame.minX
}
viewListTransition.setFrame(view: viewListView, frame: viewListFrame)
viewListTransition.setAlpha(view: viewListView, alpha: component.hideUI || self.isEditingStory ? 0.0 : 1.0)
if animateIn, !transition.animation.isImmediate {
viewListView.animateIn(transition: transition)
}
}
if id == component.slice.item.storyItem.id {
viewListInset = viewList.externalState.effectiveHeight
inputPanelBottomInset = viewListInset
} }
} }
viewListInset = viewList.externalState.effectiveHeight
inputPanelBottomInset = viewListInset if fixedAnimationOffset == 0.0 {
} else if let viewList = self.viewList { for (id, viewList) in self.viewLists {
self.viewList = nil if let viewListView = viewList.view.view, !visibleViewListIds.contains(id), let itemIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == id }) {
if let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View { let viewListSize = viewListView.bounds.size
viewListView.animateOut(transition: transition, completion: { [weak viewListView] in var viewListFrame = CGRect(origin: CGPoint(x: viewListBaseOffsetX, y: availableSize.height - viewListSize.height), size: viewListSize)
viewListView?.removeFromSuperview() let indexDistance = CGFloat(max(-1, min(1, itemIndex - currentIndex)))
}) viewListFrame.origin.x += indexDistance * availableSize.width
fixedAnimationOffset = viewListFrame.minX - viewListView.frame.minX
}
}
} }
if fixedAnimationOffset != 0.0 {
for id in applyFixedAnimationOffsetIds {
if let viewListView = self.viewLists[id]?.view.view {
itemsTransition.animatePosition(view: viewListView, from: CGPoint(x: -fixedAnimationOffset, y: 0.0), to: CGPoint(), additive: true)
}
}
}
}
var removeViewListIds: [Int32] = []
for (id, viewList) in self.viewLists {
if !validViewListIds.contains(id) {
removeViewListIds.append(id)
viewList.view.view?.removeFromSuperview()
}
}
for id in removeViewListIds {
self.viewLists.removeValue(forKey: id)
} }
let itemSize = CGSize(width: availableSize.width, height: ceil(availableSize.width * 1.77778)) let itemSize = CGSize(width: availableSize.width, height: ceil(availableSize.width * 1.77778))
@ -2000,7 +2197,7 @@ public final class StoryItemSetContainerComponent: Component {
let tooltipScreen = TooltipScreen( let tooltipScreen = TooltipScreen(
account: component.context.account, account: component.context.account,
sharedContext: component.context.sharedContext, sharedContext: component.context.sharedContext,
text: .plain(text: "This video has no sound"), style: .default, location: TooltipScreen.Location.point(soundButtonView.convert(soundButtonView.bounds, to: self).offsetBy(dx: 1.0, dy: -10.0), .top), displayDuration: .manual, shouldDismissOnTouch: { _, _ in text: .plain(text: "This video has no sound"), style: .default, location: TooltipScreen.Location.point(soundButtonView.convert(soundButtonView.bounds, to: self).offsetBy(dx: 1.0, dy: -10.0), .top), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: true) return .dismiss(consume: true)
} }
) )

View File

@ -13,6 +13,7 @@ import TelegramStringFormatting
import ShimmerEffect import ShimmerEffect
import StoryFooterPanelComponent import StoryFooterPanelComponent
import PeerListItemComponent import PeerListItemComponent
import AnimatedStickerComponent
final class StoryItemSetViewListComponent: Component { final class StoryItemSetViewListComponent: Component {
final class AnimationHint { final class AnimationHint {
@ -47,6 +48,7 @@ final class StoryItemSetViewListComponent: Component {
let safeInsets: UIEdgeInsets let safeInsets: UIEdgeInsets
let storyItem: EngineStoryItem let storyItem: EngineStoryItem
let outerExpansionFraction: CGFloat let outerExpansionFraction: CGFloat
let outerExpansionDirection: Bool
let close: () -> Void let close: () -> Void
let expandViewStats: () -> Void let expandViewStats: () -> Void
let deleteAction: () -> Void let deleteAction: () -> Void
@ -63,6 +65,7 @@ final class StoryItemSetViewListComponent: Component {
safeInsets: UIEdgeInsets, safeInsets: UIEdgeInsets,
storyItem: EngineStoryItem, storyItem: EngineStoryItem,
outerExpansionFraction: CGFloat, outerExpansionFraction: CGFloat,
outerExpansionDirection: Bool,
close: @escaping () -> Void, close: @escaping () -> Void,
expandViewStats: @escaping () -> Void, expandViewStats: @escaping () -> Void,
deleteAction: @escaping () -> Void, deleteAction: @escaping () -> Void,
@ -78,6 +81,7 @@ final class StoryItemSetViewListComponent: Component {
self.safeInsets = safeInsets self.safeInsets = safeInsets
self.storyItem = storyItem self.storyItem = storyItem
self.outerExpansionFraction = outerExpansionFraction self.outerExpansionFraction = outerExpansionFraction
self.outerExpansionDirection = outerExpansionDirection
self.close = close self.close = close
self.expandViewStats = expandViewStats self.expandViewStats = expandViewStats
self.deleteAction = deleteAction self.deleteAction = deleteAction
@ -104,6 +108,9 @@ final class StoryItemSetViewListComponent: Component {
if lhs.outerExpansionFraction != rhs.outerExpansionFraction { if lhs.outerExpansionFraction != rhs.outerExpansionFraction {
return false return false
} }
if lhs.outerExpansionDirection != rhs.outerExpansionDirection {
return false
}
return true return true
} }
@ -194,6 +201,9 @@ final class StoryItemSetViewListComponent: Component {
private var visibleItems: [EnginePeer.Id: ComponentView<Empty>] = [:] private var visibleItems: [EnginePeer.Id: ComponentView<Empty>] = [:]
private var visiblePlaceholderViews: [Int: UIImageView] = [:] private var visiblePlaceholderViews: [Int: UIImageView] = [:]
private var emptyIcon: ComponentView<Empty>?
private var emptyText: ComponentView<Empty>?
private var component: StoryItemSetViewListComponent? private var component: StoryItemSetViewListComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
@ -231,7 +241,9 @@ final class StoryItemSetViewListComponent: Component {
self.addSubview(self.navigationBarBackground) self.addSubview(self.navigationBarBackground)
self.layer.addSublayer(self.navigationSeparator) self.layer.addSublayer(self.navigationSeparator)
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { _ in
return [.down]
})
panRecognizer.delegate = self panRecognizer.delegate = self
self.addGestureRecognizer(panRecognizer) self.addGestureRecognizer(panRecognizer)
} }
@ -245,7 +257,11 @@ final class StoryItemSetViewListComponent: Component {
} }
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true if otherGestureRecognizer === self.scrollView.panGestureRecognizer {
return true
} else {
return false
}
} }
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
@ -321,7 +337,10 @@ final class StoryItemSetViewListComponent: Component {
} }
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let navigationPanelView = self.navigationPanel.view { if let navigationPanelView = self.navigationPanel.view as? StoryFooterPanelComponent.View {
if navigationPanelView.frame.contains(point), let result = navigationPanelView.externalContainerView.hitTest(self.convert(point, to: navigationPanelView.externalContainerView), with: event), result !== navigationPanelView.externalContainerView {
return result
}
if let result = navigationPanelView.hitTest(self.convert(point, to: navigationPanelView), with: event) { if let result = navigationPanelView.hitTest(self.convert(point, to: navigationPanelView), with: event) {
if result !== navigationPanelView { if result !== navigationPanelView {
return result return result
@ -455,7 +474,7 @@ final class StoryItemSetViewListComponent: Component {
theme: component.theme, theme: component.theme,
strings: component.strings, strings: component.strings,
style: .generic, style: .generic,
sideInset: itemLayout.sideInset, sideInset: 0.0,
title: item.peer.displayTitle(strings: component.strings, displayOrder: .firstLast), title: item.peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
peer: item.peer, peer: item.peer,
subtitle: dateText, subtitle: dateText,
@ -530,6 +549,7 @@ final class StoryItemSetViewListComponent: Component {
func update(component: StoryItemSetViewListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize { func update(component: StoryItemSetViewListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme let themeUpdated = self.component?.theme !== component.theme
let itemUpdated = self.component?.storyItem.id != component.storyItem.id let itemUpdated = self.component?.storyItem.id != component.storyItem.id
let viewsNilUpdated = (self.component?.storyItem.views == nil) != (component.storyItem.views == nil)
self.component = component self.component = component
self.state = state self.state = state
@ -539,7 +559,7 @@ final class StoryItemSetViewListComponent: Component {
synchronous = animationHint.synchronous synchronous = animationHint.synchronous
} }
let minimizedHeight = min(availableSize.height, 500.0) let minimizedHeight = max(100.0, availableSize.height - (325.0 + 12.0))
if themeUpdated { if themeUpdated {
self.backgroundView.backgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor self.backgroundView.backgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor
@ -547,7 +567,7 @@ final class StoryItemSetViewListComponent: Component {
self.navigationSeparator.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor self.navigationSeparator.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor
} }
if itemUpdated { if itemUpdated || viewsNilUpdated {
self.viewListState = nil self.viewListState = nil
self.viewListDisposable?.dispose() self.viewListDisposable?.dispose()
@ -606,7 +626,7 @@ final class StoryItemSetViewListComponent: Component {
environment: {}, environment: {},
containerSize: CGSize(width: 120.0, height: 100.0) containerSize: CGSize(width: 120.0, height: 100.0)
) )
let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: navigationBarFrame.minY), size: navigationLeftButtonSize) let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: navigationBarFrame.minY + 1.0), size: navigationLeftButtonSize)
if let navigationLeftButtonView = self.navigationLeftButton.view { if let navigationLeftButtonView = self.navigationLeftButton.view {
if navigationLeftButtonView.superview == nil { if navigationLeftButtonView.superview == nil {
self.addSubview(navigationLeftButtonView) self.addSubview(navigationLeftButtonView)
@ -621,7 +641,17 @@ final class StoryItemSetViewListComponent: Component {
dismissOffsetY = -dismissPanState.accumulatedOffset dismissOffsetY = -dismissPanState.accumulatedOffset
} }
dismissOffsetY -= (1.0 - component.outerExpansionFraction) * expansionOffset let selfFraction = expansionOffset / availableSize.height
var mappedOuterExpansionFraction: CGFloat
if component.outerExpansionDirection {
mappedOuterExpansionFraction = component.outerExpansionFraction / (1.0 - selfFraction)
} else {
mappedOuterExpansionFraction = 1.0 - (1.0 - component.outerExpansionFraction) / (1.0 - selfFraction)
}
mappedOuterExpansionFraction = max(0.0, min(1.0, mappedOuterExpansionFraction))
dismissOffsetY -= (1.0 - mappedOuterExpansionFraction) * expansionOffset
let dismissFraction: CGFloat = 1.0 - max(0.0, min(1.0, -dismissOffsetY / expansionOffset)) let dismissFraction: CGFloat = 1.0 - max(0.0, min(1.0, -dismissOffsetY / expansionOffset))
@ -744,6 +774,82 @@ final class StoryItemSetViewListComponent: Component {
self.ignoreScrolling = false self.ignoreScrolling = false
self.updateScrolling(transition: transition) self.updateScrolling(transition: transition)
if let viewListState = self.viewListState, viewListState.loadMoreToken == nil, viewListState.items.isEmpty, viewListState.totalCount == 0 {
var emptyTransition = transition
let emptyIcon: ComponentView<Empty>
if let current = self.emptyIcon {
emptyIcon = current
} else {
emptyTransition = emptyTransition.withAnimation(.none)
emptyIcon = ComponentView()
self.emptyIcon = emptyIcon
}
let emptyText: ComponentView<Empty>
if let current = self.emptyText {
emptyText = current
} else {
emptyText = ComponentView()
self.emptyText = emptyText
}
let emptyIconSize = emptyIcon.update(
transition: emptyTransition,
component: AnyComponent(AnimatedStickerComponent(
account: component.context.account,
animation: AnimatedStickerComponent.Animation(source: .bundle(name: "ChatListNoResults"), loop: true),
size: CGSize(width: 140.0, height: 140.0)
)),
environment: {},
containerSize: CGSize(width: 140.0, height: 140.0)
)
let text: String
if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) {
text = "List of viewers isnt available after\n24 hours of story expiration."
} else {
text = "Nobody has viewed\nyour story yet."
}
let textSize = emptyText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: text, font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0)
)
let emptyContentSpacing: CGFloat = 20.0
var emptyContentY = navigationBarFrame.minY + floor((availableSize.height - navigationBarFrame.minY - (emptyIconSize.height - emptyContentSpacing - textSize.height)) * 0.5) - 60.0
if let emptyIconView = emptyIcon.view {
if emptyIconView.superview == nil {
self.insertSubview(emptyIconView, belowSubview: self.scrollView)
}
emptyTransition.setFrame(view: emptyIconView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - emptyIconSize.width) * 0.5), y: emptyContentY), size: emptyIconSize))
emptyContentY += emptyIconSize.height + emptyContentSpacing
}
if let emptyTextView = emptyText.view {
if emptyTextView.superview == nil {
self.insertSubview(emptyTextView, belowSubview: self.scrollView)
}
emptyTransition.setFrame(view: emptyTextView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: emptyContentY), size: textSize))
}
} else {
if let emptyIcon = self.emptyIcon {
self.emptyIcon = nil
emptyIcon.view?.removeFromSuperview()
}
if let emptyText = self.emptyText {
self.emptyText = nil
emptyText.view?.removeFromSuperview()
}
}
transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: dismissOffsetY)) transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: dismissOffsetY))
component.externalState.minimizedHeight = minimizedHeight component.externalState.minimizedHeight = minimizedHeight

View File

@ -277,7 +277,7 @@ public final class StoryFooterPanelComponent: Component {
) )
let viewStatsCollapsedFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - viewStatsTextSize.height) * 0.5)), size: viewStatsTextSize) let viewStatsCollapsedFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - viewStatsTextSize.height) * 0.5)), size: viewStatsTextSize)
let viewStatsExpandedFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - viewStatsExpandedTextSize.width) * 0.5), y: 2.0 + floor((size.height - viewStatsExpandedTextSize.height) * 0.5)), size: viewStatsExpandedTextSize) let viewStatsExpandedFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - viewStatsExpandedTextSize.width) * 0.5), y: 3.0 + floor((size.height - viewStatsExpandedTextSize.height) * 0.5)), size: viewStatsExpandedTextSize)
let viewStatsCurrentFrame = viewStatsCollapsedFrame.interpolate(to: viewStatsExpandedFrame, amount: component.expandFraction) let viewStatsCurrentFrame = viewStatsCollapsedFrame.interpolate(to: viewStatsExpandedFrame, amount: component.expandFraction)
let viewStatsTextCenter = viewStatsCollapsedFrame.center.interpolate(to: viewStatsExpandedFrame.center, amount: component.expandFraction) let viewStatsTextCenter = viewStatsCollapsedFrame.center.interpolate(to: viewStatsExpandedFrame.center, amount: component.expandFraction)

View File

@ -721,7 +721,7 @@ public final class StoryPeerListComponent: Component {
let centralContentWidth: CGFloat = collapsedContentWidth + titleContentSpacing + collapsedState.titleWidth let centralContentWidth: CGFloat = collapsedContentWidth + titleContentSpacing + collapsedState.titleWidth
collapsedContentOrigin = floor((itemLayout.containerSize.width - centralContentWidth) * 0.5) collapsedContentOrigin = (itemLayout.containerSize.width - centralContentWidth) * 0.5
collapsedContentOrigin = min(collapsedContentOrigin, component.maxTitleX - centralContentWidth - 4.0) collapsedContentOrigin = min(collapsedContentOrigin, component.maxTitleX - centralContentWidth - 4.0)
@ -1135,7 +1135,7 @@ public final class StoryPeerListComponent: Component {
let collapsedTitleOffset = targetCollapsedTitleOffset - defaultCollapsedTitleOffset let collapsedTitleOffset = targetCollapsedTitleOffset - defaultCollapsedTitleOffset
let titleMinContentOffset: CGFloat = collapsedTitleOffset.interpolate(to: collapsedTitleOffset + 12.0, amount: collapsedState.minFraction * (1.0 - collapsedState.activityFraction)) let titleMinContentOffset: CGFloat = collapsedTitleOffset.interpolate(to: collapsedTitleOffset + 12.0, amount: collapsedState.minFraction * (1.0 - collapsedState.activityFraction))
var titleContentOffset: CGFloat = titleMinContentOffset.interpolate(to: floor((itemLayout.containerSize.width - collapsedState.titleWidth) * 0.5) as CGFloat, amount: collapsedState.maxFraction * (1.0 - collapsedState.activityFraction)) var titleContentOffset: CGFloat = titleMinContentOffset.interpolate(to: ((itemLayout.containerSize.width - collapsedState.titleWidth) * 0.5) as CGFloat, amount: collapsedState.maxFraction * (1.0 - collapsedState.activityFraction))
var titleIndicatorSize: CGSize? var titleIndicatorSize: CGSize?
if collapsedState.activityFraction != 0.0 { if collapsedState.activityFraction != 0.0 {

View File

@ -49,7 +49,7 @@ private final class ShapeImageView: UIView {
context.setBlendMode(.sourceIn) context.setBlendMode(.sourceIn)
let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: params.borderColors.map { let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: params.borderColors.map {
UIColor(rgb: $0).cgColor UIColor(argb: $0).cgColor
} as CFArray, locations: nil)! } as CFArray, locations: nil)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 50.0), options: []) context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 50.0), options: [])
@ -344,25 +344,25 @@ public final class StorySetIndicatorComponent: Component {
if component.theme.overallDarkAppearance { if component.theme.overallDarkAppearance {
if component.hasUnseen { if component.hasUnseen {
borderColors = [ borderColors = [
0x34C76F, 0xFF34C76F,
0x3DA1FD 0xFF3DA1FD
] ]
} else { } else {
borderColors = [ borderColors = [
0x48484A, UIColor(white: 1.0, alpha: 0.3).argb,
0x48484A UIColor(white: 1.0, alpha: 0.3).argb
] ]
} }
} else { } else {
if component.hasUnseen { if component.hasUnseen {
borderColors = [ borderColors = [
0x34C76F, 0xFF34C76F,
0x3DA1FD 0xFF3DA1FD
] ]
} else { } else {
borderColors = [ borderColors = [
0xD8D8E1, UIColor(white: 1.0, alpha: 0.3).argb,
0xD8D8E1 UIColor(white: 1.0, alpha: 0.3).argb
] ]
} }
} }