mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-07-31 15:37:01 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
af6c9a9868
@ -75,6 +75,7 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
|
||||
super.init(target: target, action: action)
|
||||
|
||||
self.maximumNumberOfTouches = 1
|
||||
self.delaysTouchesBegan = false
|
||||
}
|
||||
|
||||
override public func reset() {
|
||||
@ -136,7 +137,9 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
|
||||
|
||||
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.validatedGesture {
|
||||
@ -177,12 +180,18 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
|
||||
self.state = .failed
|
||||
} else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX {
|
||||
self.validatedGesture = true
|
||||
fireBegan = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.validatedGesture {
|
||||
super.touchesMoved(touches, with: event)
|
||||
if fireBegan {
|
||||
if self.state == .possible {
|
||||
self.state = .began
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -332,7 +332,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
|
||||
|
||||
transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
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 scrollNodeFrame = CGRect(origin: CGPoint(x: self.horizontalDismissOffset ?? 0.0, y: 0.0), size: layout.size)
|
||||
self.scrollNode.frame = scrollNodeFrame
|
||||
@ -348,7 +348,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
|
||||
}
|
||||
self.ignoreScrolling = false
|
||||
|
||||
self.scrollNode.view.isScrollEnabled = !isStandaloneModal
|
||||
self.scrollNode.view.isScrollEnabled = !isStandaloneModal && !self.isFlat
|
||||
|
||||
let isLandscape = layout.orientation == .landscape
|
||||
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)) {
|
||||
return self.dim.view
|
||||
}
|
||||
if self.isFlat {
|
||||
return result
|
||||
}
|
||||
var currentParent: UIView? = result
|
||||
var enableScrolling = true
|
||||
while true {
|
||||
@ -562,7 +565,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
|
||||
enableScrolling = false
|
||||
}
|
||||
}
|
||||
self.scrollNode.view.isScrollEnabled = enableScrolling
|
||||
self.scrollNode.view.isScrollEnabled = enableScrolling && !self.isFlat
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
case .none:
|
||||
return InternalState(totalCount: 0, items: [], canLoadMore: false, nextOffset: nil)
|
||||
|
@ -23,6 +23,7 @@ swift_library(
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/PeerPresenceStatusManager",
|
||||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -13,6 +13,7 @@ import CheckNode
|
||||
import TelegramStringFormatting
|
||||
import AppBundle
|
||||
import PeerPresenceStatusManager
|
||||
import EmojiStatusComponent
|
||||
|
||||
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
||||
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 separatorLayer: SimpleLayer
|
||||
private let avatarNode: AvatarNode
|
||||
private var avatarIcon: ComponentView<Empty>?
|
||||
|
||||
private var iconView: UIImageView?
|
||||
private var checkLayer: CheckLayer?
|
||||
@ -316,6 +318,8 @@ public final class PeerListItemComponent: Component {
|
||||
} else {
|
||||
transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame)
|
||||
}
|
||||
|
||||
var statusIcon: EmojiStatusComponent.Content?
|
||||
if let peer = component.peer {
|
||||
let clipStyle: AvatarNodeClipStyle
|
||||
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||
@ -324,6 +328,18 @@ public final class PeerListItemComponent: Component {
|
||||
clipStyle = .round
|
||||
}
|
||||
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(
|
||||
@ -350,7 +366,7 @@ public final class PeerListItemComponent: Component {
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||
)
|
||||
|
||||
let titleSpacing: CGFloat = 1.0
|
||||
let titleSpacing: CGFloat = 2.0
|
||||
let centralContentHeight: CGFloat
|
||||
if labelSize.height > 0.0, case .generic = component.style {
|
||||
centralContentHeight = titleSize.height + labelSize.height + titleSpacing
|
||||
@ -358,7 +374,7 @@ public final class PeerListItemComponent: Component {
|
||||
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 titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
@ -419,6 +435,48 @@ public final class PeerListItemComponent: Component {
|
||||
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 {
|
||||
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ swift_library(
|
||||
"//submodules/TinyThumbnail",
|
||||
"//submodules/ImageBlur",
|
||||
"//submodules/StickerPackPreviewUI",
|
||||
|
||||
"//submodules/Components/AnimatedStickerComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -398,7 +398,7 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
private var requestStoryDisposables = DisposableSet()
|
||||
|
||||
private var preloadStoryResourceDisposables: [MediaId: Disposable] = [:]
|
||||
private var pollStoryMetadataDisposables = DisposableSet()
|
||||
private var pollStoryMetadataDisposables: [StoryId: Disposable] = [:]
|
||||
|
||||
private var singlePeerListContext: PeerExpiringStoryListContext?
|
||||
|
||||
@ -615,7 +615,9 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
for (_, disposable) in self.preloadStoryResourceDisposables {
|
||||
disposable.dispose()
|
||||
}
|
||||
self.pollStoryMetadataDisposables.dispose()
|
||||
for (_, disposable) in self.pollStoryMetadataDisposables {
|
||||
disposable.dispose()
|
||||
}
|
||||
self.storySubscriptionsDisposable?.dispose()
|
||||
}
|
||||
|
||||
@ -805,7 +807,11 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@ func hasFirstResponder(_ view: UIView) -> Bool {
|
||||
}
|
||||
|
||||
private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
|
||||
var shouldBegin: ((UITouch) -> Bool)?
|
||||
var updateIsTracking: ((Bool) -> Void)?
|
||||
|
||||
override var state: UIGestureRecognizer.State {
|
||||
@ -50,10 +51,12 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
|
||||
}
|
||||
|
||||
private var isTracking: Bool = false
|
||||
private var isValidated: Bool = false
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
|
||||
self.isValidated = false
|
||||
if self.isTracking {
|
||||
self.isTracking = false
|
||||
self.updateIsTracking?(false)
|
||||
@ -61,11 +64,21 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
|
||||
}
|
||||
|
||||
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 {
|
||||
self.isTracking = true
|
||||
self.updateIsTracking?(true)
|
||||
if self.isValidated {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
if !self.isTracking {
|
||||
self.isTracking = true
|
||||
self.updateIsTracking?(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -153,9 +166,8 @@ private final class StoryContainerScreenComponent: Component {
|
||||
self.didBegin = didBegin
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||
final class View: UIView, UIGestureRecognizerDelegate {
|
||||
private var component: StoryContainerScreenComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var environment: ViewControllerComponentContainer.Environment?
|
||||
@ -190,6 +202,8 @@ private final class StoryContainerScreenComponent: Component {
|
||||
|
||||
var dismissWithoutTransitionOut: Bool = false
|
||||
|
||||
var longPressRecognizer: StoryLongPressRecognizer?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundLayer = SimpleLayer()
|
||||
self.backgroundLayer.backgroundColor = UIColor.black.cgColor
|
||||
@ -236,7 +250,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
return []
|
||||
}
|
||||
}
|
||||
if !itemSetComponentView.allowsInteractiveGestures() {
|
||||
if !itemSetComponentView.allowsVerticalPanGesture() {
|
||||
return []
|
||||
}
|
||||
|
||||
@ -253,6 +267,19 @@ private final class StoryContainerScreenComponent: Component {
|
||||
self.isHoldingTouch = isTracking
|
||||
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)
|
||||
|
||||
let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGesture(_:)))
|
||||
@ -406,7 +433,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
print("began: \(CFAbsoluteTimeGetCurrent())")
|
||||
//print("began: \(CFAbsoluteTimeGetCurrent())")
|
||||
self.beginHorizontalPan(translation: recognizer.translation(in: self))
|
||||
case .changed:
|
||||
self.updateHorizontalPan(translation: recognizer.translation(in: self))
|
||||
@ -450,7 +477,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
self.verticalPanState = nil
|
||||
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.environment?.controller()?.dismiss()
|
||||
} else if translation.y < -200.0 || (translation.y < -100.0 && velocity.y < -100.0) {
|
||||
|
@ -218,7 +218,7 @@ final class StoryContentCaptionComponent: Component {
|
||||
let edgeDistanceFraction = edgeDistance / 7.0
|
||||
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))
|
||||
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)))
|
||||
@ -479,7 +479,7 @@ final class StoryContentCaptionComponent: Component {
|
||||
var locations: [NSNumber] = []
|
||||
var colors: [CGColor] = []
|
||||
let numStops = 10
|
||||
let baseAlpha: CGFloat = 0.3
|
||||
let baseAlpha: CGFloat = 0.5
|
||||
for i in 0 ..< numStops {
|
||||
let step = 1.0 - CGFloat(i) / CGFloat(numStops - 1)
|
||||
locations.append((1.0 - step) as NSNumber)
|
||||
|
@ -410,10 +410,8 @@ final class StoryItemContentComponent: Component {
|
||||
var fetchSignal: Signal<Never, NoError>?
|
||||
switch messageMedia {
|
||||
case .image:
|
||||
self.contentLoaded = true
|
||||
break
|
||||
case let .file(file):
|
||||
self.contentLoaded = true
|
||||
|
||||
fetchSignal = fetchedMediaResource(
|
||||
mediaBox: component.context.account.postbox.mediaBox,
|
||||
userLocation: .other,
|
||||
@ -446,6 +444,16 @@ final class StoryItemContentComponent: Component {
|
||||
}
|
||||
|
||||
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(
|
||||
context: component.context,
|
||||
peer: component.peer,
|
||||
@ -456,6 +464,10 @@ final class StoryItemContentComponent: Component {
|
||||
attemptSynchronous: synchronousLoad,
|
||||
transition: transition
|
||||
)
|
||||
applyState = true
|
||||
if self.imageView.isContentLoaded {
|
||||
self.contentLoaded = true
|
||||
}
|
||||
transition.setFrame(view: self.imageView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
var dimensions: CGSize?
|
||||
|
@ -16,6 +16,9 @@ final class StoryItemImageView: UIView {
|
||||
private var disposable: Disposable?
|
||||
private var fetchDisposable: Disposable?
|
||||
|
||||
private(set) var isContentLoaded: Bool = false
|
||||
var didLoadContents: (() -> Void)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.contentView = UIImageView()
|
||||
self.contentView.contentMode = .scaleAspectFill
|
||||
@ -55,6 +58,8 @@ final class StoryItemImageView: UIView {
|
||||
self.updateImage(image: image)
|
||||
}
|
||||
}
|
||||
self.isContentLoaded = true
|
||||
self.didLoadContents?()
|
||||
} else {
|
||||
if let thumbnailData = image.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) {
|
||||
self.contentView.image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3)
|
||||
@ -89,6 +94,8 @@ final class StoryItemImageView: UIView {
|
||||
}
|
||||
if let image {
|
||||
self.updateImage(image: image)
|
||||
self.isContentLoaded = true
|
||||
self.didLoadContents?()
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -110,6 +117,8 @@ final class StoryItemImageView: UIView {
|
||||
self.updateImage(image: image)
|
||||
}
|
||||
}
|
||||
self.isContentLoaded = true
|
||||
self.didLoadContents?()
|
||||
} else {
|
||||
if let thumbnailData = file.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) {
|
||||
self.contentView.image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3)
|
||||
@ -141,6 +150,8 @@ final class StoryItemImageView: UIView {
|
||||
}
|
||||
if let image {
|
||||
self.updateImage(image: image)
|
||||
self.isContentLoaded = true
|
||||
self.didLoadContents?()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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 {
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
@ -319,7 +327,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
var preparingToDisplayViewList: Bool = false
|
||||
var displayViewList: Bool = false
|
||||
var viewList: ViewList?
|
||||
var viewLists: [Int32: ViewList] = [:]
|
||||
|
||||
var isEditingStory: Bool = false
|
||||
|
||||
@ -354,6 +362,11 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
private var animateNextNavigationId: Int32?
|
||||
private var initializedOffset: Bool = false
|
||||
|
||||
private var viewListPanState: PanState?
|
||||
private var viewListSwipeRecognizer: InteractiveTransitionGestureRecognizer?
|
||||
|
||||
private var verticalPanState: PanState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.sendMessageContext = StoryItemSetContainerSendMessage()
|
||||
|
||||
@ -365,6 +378,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.scroller.showsVerticalScrollIndicator = false
|
||||
self.scroller.showsHorizontalScrollIndicator = false
|
||||
self.scroller.decelerationRate = .fast
|
||||
self.scroller.delaysContentTouches = false
|
||||
|
||||
self.controlsContainerView = SparseContainerView()
|
||||
self.controlsContainerView.clipsToBounds = true
|
||||
@ -404,6 +418,33 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
tapRecognizer.delegate = self
|
||||
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()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] audioRecorder in
|
||||
guard let self else {
|
||||
@ -531,6 +572,13 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
func allowsVerticalPanGesture() -> Bool {
|
||||
if self.displayViewList {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func rewindCurrentItem() {
|
||||
guard let component = self.component else {
|
||||
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() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
@ -814,7 +934,11 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
for index in 0 ..< component.slice.allItems.count {
|
||||
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 centerFraction: CGFloat = CGFloat(centerIndexOffset)
|
||||
|
||||
@ -996,7 +1120,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
return false
|
||||
}
|
||||
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
|
||||
if component.verticalPanFraction == 0.0 {
|
||||
self.preparingToDisplayViewList = true
|
||||
@ -1062,15 +1186,17 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
)
|
||||
inputPanelView.layer.animateAlpha(from: 0.0, to: inputPanelView.alpha, duration: 0.28)
|
||||
}
|
||||
if let viewListView = self.viewList?.view.view {
|
||||
viewListView.layer.animatePosition(
|
||||
from: CGPoint(x: 0.0, y: self.bounds.height - self.controlsContainerView.frame.maxY),
|
||||
to: CGPoint(),
|
||||
duration: 0.3,
|
||||
timingFunction: kCAMediaTimingFunctionSpring,
|
||||
additive: true
|
||||
)
|
||||
viewListView.layer.animateAlpha(from: 0.0, to: viewListView.alpha, duration: 0.28)
|
||||
for (_, viewList) in self.viewLists {
|
||||
if let viewListView = viewList.view.view {
|
||||
viewListView.layer.animatePosition(
|
||||
from: CGPoint(x: 0.0, y: self.bounds.height - self.controlsContainerView.frame.maxY),
|
||||
to: CGPoint(),
|
||||
duration: 0.3,
|
||||
timingFunction: kCAMediaTimingFunctionSpring,
|
||||
additive: true
|
||||
)
|
||||
viewListView.layer.animateAlpha(from: 0.0, to: viewListView.alpha, duration: 0.28)
|
||||
}
|
||||
}
|
||||
if let captionItemView = self.captionItem?.view.view {
|
||||
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)
|
||||
}
|
||||
if let viewListView = self.viewList?.view.view {
|
||||
viewListView.layer.animatePosition(
|
||||
from: CGPoint(),
|
||||
to: CGPoint(x: 0.0, y: self.bounds.height - self.controlsContainerView.frame.maxY),
|
||||
duration: 0.3,
|
||||
timingFunction: kCAMediaTimingFunctionSpring,
|
||||
removeOnCompletion: false,
|
||||
additive: true
|
||||
)
|
||||
viewListView.layer.animateAlpha(from: viewListView.alpha, to: 0.0, duration: 0.28, removeOnCompletion: false)
|
||||
for (_, viewList) in self.viewLists {
|
||||
if let viewListView = viewList.view.view {
|
||||
viewListView.layer.animatePosition(
|
||||
from: CGPoint(),
|
||||
to: CGPoint(x: 0.0, y: self.bounds.height - self.controlsContainerView.frame.maxY),
|
||||
duration: 0.3,
|
||||
timingFunction: kCAMediaTimingFunctionSpring,
|
||||
removeOnCompletion: false,
|
||||
additive: true
|
||||
)
|
||||
viewListView.layer.animateAlpha(from: viewListView.alpha, to: 0.0, duration: 0.28, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
if let captionItemView = self.captionItem?.view.view {
|
||||
captionItemView.layer.animatePosition(
|
||||
@ -1439,12 +1567,12 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
var itemsTransition = transition
|
||||
if let animateNextNavigationId = self.animateNextNavigationId, animateNextNavigationId == component.slice.item.storyItem.id {
|
||||
self.animateNextNavigationId = nil
|
||||
self.viewListPanState = nil
|
||||
itemsTransition = transition.withAnimation(.curve(duration: 0.3, curve: .spring))
|
||||
}
|
||||
|
||||
@ -1705,144 +1833,213 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
inputPanelIsOverlay = true
|
||||
}
|
||||
|
||||
if component.slice.peer.id == component.context.account.peerId {
|
||||
let viewList: ViewList
|
||||
var viewListTransition = transition
|
||||
if let current = self.viewList {
|
||||
viewList = current
|
||||
} else {
|
||||
if !transition.animation.isImmediate {
|
||||
viewListTransition = .immediate
|
||||
}
|
||||
viewList = ViewList()
|
||||
self.viewList = viewList
|
||||
}
|
||||
|
||||
let outerExpansionFraction: CGFloat
|
||||
var validViewListIds: [Int32] = []
|
||||
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 visibleViewListIds: [Int32] = [component.slice.item.storyItem.id]
|
||||
if self.displayViewList {
|
||||
outerExpansionFraction = 1.0
|
||||
} else if let views = component.slice.item.storyItem.views, !views.seenPeers.isEmpty {
|
||||
outerExpansionFraction = component.verticalPanFraction
|
||||
} else {
|
||||
outerExpansionFraction = 0.0
|
||||
if currentIndex != 0 {
|
||||
visibleViewListIds.append(component.slice.allItems[currentIndex - 1].storyItem.id)
|
||||
}
|
||||
if currentIndex != component.slice.allItems.count - 1 {
|
||||
visibleViewListIds.append(component.slice.allItems[currentIndex + 1].storyItem.id)
|
||||
}
|
||||
}
|
||||
|
||||
viewList.view.parentState = state
|
||||
let viewListSize = viewList.view.update(
|
||||
transition: viewListTransition.withUserData(PeerListItemComponent.TransitionHint(
|
||||
synchronousLoad: false
|
||||
)).withUserData(StoryItemSetViewListComponent.AnimationHint(
|
||||
synchronous: false
|
||||
)),
|
||||
component: AnyComponent(StoryItemSetViewListComponent(
|
||||
externalState: viewList.externalState,
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
sharedListsContext: component.sharedViewListsContext,
|
||||
peerId: component.slice.peer.id,
|
||||
safeInsets: component.safeInsets,
|
||||
storyItem: component.slice.item.storyItem,
|
||||
outerExpansionFraction: outerExpansionFraction,
|
||||
close: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
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
|
||||
var viewListBaseOffsetX: CGFloat = 0.0
|
||||
if let viewListPanState = self.viewListPanState {
|
||||
viewListBaseOffsetX = viewListPanState.fraction * availableSize.width
|
||||
}
|
||||
|
||||
var fixedAnimationOffset: CGFloat = 0.0
|
||||
var applyFixedAnimationOffsetIds: [Int32] = []
|
||||
|
||||
for id in visibleViewListIds {
|
||||
guard let itemIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == id }) else {
|
||||
continue
|
||||
}
|
||||
let item = component.slice.allItems[itemIndex]
|
||||
validViewListIds.append(id)
|
||||
|
||||
let viewList: ViewList
|
||||
var viewListTransition = itemsTransition
|
||||
if let current = self.viewLists[id] {
|
||||
viewList = current
|
||||
} else {
|
||||
if !itemsTransition.animation.isImmediate {
|
||||
viewListTransition = .immediate
|
||||
}
|
||||
viewList = ViewList()
|
||||
self.viewLists[id] = viewList
|
||||
applyFixedAnimationOffsetIds.append(id)
|
||||
}
|
||||
|
||||
let outerExpansionFraction: CGFloat
|
||||
let outerExpansionDirection: Bool
|
||||
if self.displayViewList {
|
||||
if let verticalPanState = self.verticalPanState {
|
||||
outerExpansionFraction = max(0.0, min(1.0, 1.0 - verticalPanState.fraction))
|
||||
} else {
|
||||
outerExpansionFraction = 1.0
|
||||
}
|
||||
outerExpansionDirection = false
|
||||
} else if let _ = item.storyItem.views {
|
||||
outerExpansionFraction = component.verticalPanFraction
|
||||
outerExpansionDirection = true
|
||||
} else {
|
||||
outerExpansionFraction = 0.0
|
||||
outerExpansionDirection = true
|
||||
}
|
||||
|
||||
viewList.view.parentState = state
|
||||
let viewListSize = viewList.view.update(
|
||||
transition: viewListTransition.withUserData(PeerListItemComponent.TransitionHint(
|
||||
synchronousLoad: false
|
||||
)).withUserData(StoryItemSetViewListComponent.AnimationHint(
|
||||
synchronous: false
|
||||
)),
|
||||
component: AnyComponent(StoryItemSetViewListComponent(
|
||||
externalState: viewList.externalState,
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
sharedListsContext: component.sharedViewListsContext,
|
||||
peerId: component.slice.peer.id,
|
||||
safeInsets: component.safeInsets,
|
||||
storyItem: item.storyItem,
|
||||
outerExpansionFraction: outerExpansionFraction,
|
||||
outerExpansionDirection: outerExpansionDirection,
|
||||
close: { [weak self] in
|
||||
guard let self else {
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
if animateIn, !transition.animation.isImmediate {
|
||||
viewListView.animateIn(transition: transition)
|
||||
var viewListFrame = CGRect(origin: CGPoint(x: viewListBaseOffsetX, y: availableSize.height - viewListSize.height), size: viewListSize)
|
||||
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
|
||||
} else if let viewList = self.viewList {
|
||||
self.viewList = nil
|
||||
if let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View {
|
||||
viewListView.animateOut(transition: transition, completion: { [weak viewListView] in
|
||||
viewListView?.removeFromSuperview()
|
||||
})
|
||||
|
||||
if fixedAnimationOffset == 0.0 {
|
||||
for (id, viewList) in self.viewLists {
|
||||
if let viewListView = viewList.view.view, !visibleViewListIds.contains(id), let itemIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == id }) {
|
||||
let viewListSize = viewListView.bounds.size
|
||||
var viewListFrame = CGRect(origin: CGPoint(x: viewListBaseOffsetX, y: availableSize.height - viewListSize.height), size: viewListSize)
|
||||
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))
|
||||
@ -2000,7 +2197,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
let tooltipScreen = TooltipScreen(
|
||||
account: component.context.account,
|
||||
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)
|
||||
}
|
||||
)
|
||||
|
@ -13,6 +13,7 @@ import TelegramStringFormatting
|
||||
import ShimmerEffect
|
||||
import StoryFooterPanelComponent
|
||||
import PeerListItemComponent
|
||||
import AnimatedStickerComponent
|
||||
|
||||
final class StoryItemSetViewListComponent: Component {
|
||||
final class AnimationHint {
|
||||
@ -47,6 +48,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
let safeInsets: UIEdgeInsets
|
||||
let storyItem: EngineStoryItem
|
||||
let outerExpansionFraction: CGFloat
|
||||
let outerExpansionDirection: Bool
|
||||
let close: () -> Void
|
||||
let expandViewStats: () -> Void
|
||||
let deleteAction: () -> Void
|
||||
@ -63,6 +65,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
safeInsets: UIEdgeInsets,
|
||||
storyItem: EngineStoryItem,
|
||||
outerExpansionFraction: CGFloat,
|
||||
outerExpansionDirection: Bool,
|
||||
close: @escaping () -> Void,
|
||||
expandViewStats: @escaping () -> Void,
|
||||
deleteAction: @escaping () -> Void,
|
||||
@ -78,6 +81,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
self.safeInsets = safeInsets
|
||||
self.storyItem = storyItem
|
||||
self.outerExpansionFraction = outerExpansionFraction
|
||||
self.outerExpansionDirection = outerExpansionDirection
|
||||
self.close = close
|
||||
self.expandViewStats = expandViewStats
|
||||
self.deleteAction = deleteAction
|
||||
@ -104,6 +108,9 @@ final class StoryItemSetViewListComponent: Component {
|
||||
if lhs.outerExpansionFraction != rhs.outerExpansionFraction {
|
||||
return false
|
||||
}
|
||||
if lhs.outerExpansionDirection != rhs.outerExpansionDirection {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -194,6 +201,9 @@ final class StoryItemSetViewListComponent: Component {
|
||||
|
||||
private var visibleItems: [EnginePeer.Id: ComponentView<Empty>] = [:]
|
||||
private var visiblePlaceholderViews: [Int: UIImageView] = [:]
|
||||
|
||||
private var emptyIcon: ComponentView<Empty>?
|
||||
private var emptyText: ComponentView<Empty>?
|
||||
|
||||
private var component: StoryItemSetViewListComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
@ -231,7 +241,9 @@ final class StoryItemSetViewListComponent: Component {
|
||||
self.addSubview(self.navigationBarBackground)
|
||||
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
|
||||
self.addGestureRecognizer(panRecognizer)
|
||||
}
|
||||
@ -245,7 +257,11 @@ final class StoryItemSetViewListComponent: Component {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -321,7 +337,10 @@ final class StoryItemSetViewListComponent: Component {
|
||||
}
|
||||
|
||||
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 result !== navigationPanelView {
|
||||
return result
|
||||
@ -455,7 +474,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
style: .generic,
|
||||
sideInset: itemLayout.sideInset,
|
||||
sideInset: 0.0,
|
||||
title: item.peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
|
||||
peer: item.peer,
|
||||
subtitle: dateText,
|
||||
@ -530,6 +549,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
func update(component: StoryItemSetViewListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
let itemUpdated = self.component?.storyItem.id != component.storyItem.id
|
||||
let viewsNilUpdated = (self.component?.storyItem.views == nil) != (component.storyItem.views == nil)
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
@ -539,7 +559,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
synchronous = animationHint.synchronous
|
||||
}
|
||||
|
||||
let minimizedHeight = min(availableSize.height, 500.0)
|
||||
let minimizedHeight = max(100.0, availableSize.height - (325.0 + 12.0))
|
||||
|
||||
if themeUpdated {
|
||||
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
|
||||
}
|
||||
|
||||
if itemUpdated {
|
||||
if itemUpdated || viewsNilUpdated {
|
||||
self.viewListState = nil
|
||||
self.viewListDisposable?.dispose()
|
||||
|
||||
@ -606,7 +626,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
environment: {},
|
||||
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 navigationLeftButtonView.superview == nil {
|
||||
self.addSubview(navigationLeftButtonView)
|
||||
@ -621,7 +641,17 @@ final class StoryItemSetViewListComponent: Component {
|
||||
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))
|
||||
|
||||
@ -744,6 +774,82 @@ final class StoryItemSetViewListComponent: Component {
|
||||
self.ignoreScrolling = false
|
||||
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 isn’t 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))
|
||||
|
||||
component.externalState.minimizedHeight = minimizedHeight
|
||||
|
@ -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 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 viewStatsTextCenter = viewStatsCollapsedFrame.center.interpolate(to: viewStatsExpandedFrame.center, amount: component.expandFraction)
|
||||
|
@ -721,7 +721,7 @@ public final class StoryPeerListComponent: Component {
|
||||
|
||||
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)
|
||||
|
||||
@ -1135,7 +1135,7 @@ public final class StoryPeerListComponent: Component {
|
||||
let collapsedTitleOffset = targetCollapsedTitleOffset - defaultCollapsedTitleOffset
|
||||
|
||||
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?
|
||||
if collapsedState.activityFraction != 0.0 {
|
||||
|
@ -49,7 +49,7 @@ private final class ShapeImageView: UIView {
|
||||
|
||||
context.setBlendMode(.sourceIn)
|
||||
let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: params.borderColors.map {
|
||||
UIColor(rgb: $0).cgColor
|
||||
UIColor(argb: $0).cgColor
|
||||
} as CFArray, locations: nil)!
|
||||
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.hasUnseen {
|
||||
borderColors = [
|
||||
0x34C76F,
|
||||
0x3DA1FD
|
||||
0xFF34C76F,
|
||||
0xFF3DA1FD
|
||||
]
|
||||
} else {
|
||||
borderColors = [
|
||||
0x48484A,
|
||||
0x48484A
|
||||
UIColor(white: 1.0, alpha: 0.3).argb,
|
||||
UIColor(white: 1.0, alpha: 0.3).argb
|
||||
]
|
||||
}
|
||||
} else {
|
||||
if component.hasUnseen {
|
||||
borderColors = [
|
||||
0x34C76F,
|
||||
0x3DA1FD
|
||||
0xFF34C76F,
|
||||
0xFF3DA1FD
|
||||
]
|
||||
} else {
|
||||
borderColors = [
|
||||
0xD8D8E1,
|
||||
0xD8D8E1
|
||||
UIColor(white: 1.0, alpha: 0.3).argb,
|
||||
UIColor(white: 1.0, alpha: 0.3).argb
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user