mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-01 16:06:59 +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)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
@ -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?
|
||||||
|
@ -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?()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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 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))
|
transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: dismissOffsetY))
|
||||||
|
|
||||||
component.externalState.minimizedHeight = minimizedHeight
|
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 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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user