mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Stories
This commit is contained in:
parent
a46c372d68
commit
848e342991
52
submodules/Postbox/Sources/StoryView.swift
Normal file
52
submodules/Postbox/Sources/StoryView.swift
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class MutableStoryView: MutablePostboxView {
|
||||||
|
let id: StoryId
|
||||||
|
var item: CodableEntry?
|
||||||
|
|
||||||
|
init(postbox: PostboxImpl, id: StoryId) {
|
||||||
|
self.id = id
|
||||||
|
self.item = postbox.storyTable.get(id: self.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool {
|
||||||
|
var updated = false
|
||||||
|
|
||||||
|
for event in transaction.storyEvents {
|
||||||
|
switch event {
|
||||||
|
case .updated(self.id):
|
||||||
|
let item = postbox.storyTable.get(id: self.id)
|
||||||
|
if self.item != item {
|
||||||
|
self.item = item
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool {
|
||||||
|
let item = postbox.storyTable.get(id: self.id)
|
||||||
|
if self.item != item {
|
||||||
|
self.item = item
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func immutableView() -> PostboxView {
|
||||||
|
return StoryView(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class StoryView: PostboxView {
|
||||||
|
public let item: CodableEntry?
|
||||||
|
|
||||||
|
init(_ view: MutableStoryView) {
|
||||||
|
self.item = view.item
|
||||||
|
}
|
||||||
|
}
|
@ -45,6 +45,7 @@ public enum PostboxViewKey: Hashable {
|
|||||||
case storyItems(peerId: PeerId)
|
case storyItems(peerId: PeerId)
|
||||||
case storyExpirationTimeItems
|
case storyExpirationTimeItems
|
||||||
case peerStoryStats(peerIds: Set<PeerId>)
|
case peerStoryStats(peerIds: Set<PeerId>)
|
||||||
|
case story(id: StoryId)
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
switch self {
|
switch self {
|
||||||
@ -150,6 +151,8 @@ public enum PostboxViewKey: Hashable {
|
|||||||
hasher.combine(19)
|
hasher.combine(19)
|
||||||
case let .peerStoryStats(peerIds):
|
case let .peerStoryStats(peerIds):
|
||||||
hasher.combine(peerIds)
|
hasher.combine(peerIds)
|
||||||
|
case let .story(id):
|
||||||
|
hasher.combine(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -419,6 +422,12 @@ public enum PostboxViewKey: Hashable {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
case let .story(id):
|
||||||
|
if case .story(id) = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -513,5 +522,7 @@ func postboxViewForKey(postbox: PostboxImpl, key: PostboxViewKey) -> MutablePost
|
|||||||
return MutableStoryExpirationTimeItemsView(postbox: postbox)
|
return MutableStoryExpirationTimeItemsView(postbox: postbox)
|
||||||
case let .peerStoryStats(peerIds):
|
case let .peerStoryStats(peerIds):
|
||||||
return MutablePeerStoryStatsView(postbox: postbox, peerIds: peerIds)
|
return MutablePeerStoryStatsView(postbox: postbox, peerIds: peerIds)
|
||||||
|
case let .story(id):
|
||||||
|
return MutableStoryView(postbox: postbox, id: id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3250,11 +3250,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
transition.setFrame(view: self.selectionContainerView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
transition.setFrame(view: self.selectionContainerView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
||||||
|
|
||||||
self.interaction?.containerLayoutUpdated(layout: layout, transition: transition)
|
self.interaction?.containerLayoutUpdated(layout: layout, transition: transition)
|
||||||
|
|
||||||
|
var presentationContextLayout = layout
|
||||||
|
presentationContextLayout.intrinsicInsets.top = max(presentationContextLayout.intrinsicInsets.top, topInset)
|
||||||
// var layout = layout
|
// var layout = layout
|
||||||
// layout.intrinsicInsets.top = topInset
|
// layout.intrinsicInsets.top = topInset
|
||||||
// layout.intrinsicInsets.bottom = bottomInset + 60.0
|
// layout.intrinsicInsets.bottom = bottomInset + 60.0
|
||||||
controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
|
controller.presentationContext.containerLayoutUpdated(presentationContextLayout, transition: transition.containedViewLayoutTransition)
|
||||||
|
|
||||||
if isFirstTime {
|
if isFirstTime {
|
||||||
self.animateIn()
|
self.animateIn()
|
||||||
@ -3683,7 +3685,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
|
|
||||||
let controller = UndoOverlayController(presentationData: presentationData, content: .autoDelete(isOn: true, title: nil, text: text, customUndoText: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { [weak self] action in
|
let controller = UndoOverlayController(presentationData: presentationData, content: .autoDelete(isOn: true, title: nil, text: text, customUndoText: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { [weak self] action in
|
||||||
if case .info = action, let self {
|
if case .info = action, let self {
|
||||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories, forceDark: true, dismissed: nil)
|
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesExpirationDurations, forceDark: true, dismissed: nil)
|
||||||
self.push(controller)
|
self.push(controller)
|
||||||
}
|
}
|
||||||
return false }
|
return false }
|
||||||
|
@ -339,7 +339,7 @@ public final class PeerListItemComponent: Component {
|
|||||||
if let reaction = component.reaction, case .custom = reaction.reaction {
|
if let reaction = component.reaction, case .custom = reaction.reaction {
|
||||||
reactionLayer.isVisibleForAnimations = true
|
reactionLayer.isVisibleForAnimations = true
|
||||||
}
|
}
|
||||||
self.layer.addSublayer(reactionLayer)
|
self.containerButton.layer.addSublayer(reactionLayer)
|
||||||
|
|
||||||
if var iconFrame = self.iconFrame {
|
if var iconFrame = self.iconFrame {
|
||||||
if let reaction = component.reaction, case .builtin = reaction.reaction {
|
if let reaction = component.reaction, case .builtin = reaction.reaction {
|
||||||
@ -671,26 +671,22 @@ public final class PeerListItemComponent: Component {
|
|||||||
let imageSize = CGSize(width: 22.0, height: 22.0)
|
let imageSize = CGSize(width: 22.0, height: 22.0)
|
||||||
self.iconFrame = CGRect(origin: CGPoint(x: availableSize.width - (contextInset * 2.0 + 14.0 + component.sideInset) - imageSize.width, y: floor((height - verticalInset * 2.0 - imageSize.height) * 0.5)), size: imageSize)
|
self.iconFrame = CGRect(origin: CGPoint(x: availableSize.width - (contextInset * 2.0 + 14.0 + component.sideInset) - imageSize.width, y: floor((height - verticalInset * 2.0 - imageSize.height) * 0.5)), size: imageSize)
|
||||||
|
|
||||||
|
var reactionIconTransition = transition
|
||||||
if previousComponent?.reaction != component.reaction {
|
if previousComponent?.reaction != component.reaction {
|
||||||
if let reaction = component.reaction, case .builtin("❤") = reaction.reaction {
|
if let reaction = component.reaction, case .builtin("❤") = reaction.reaction {
|
||||||
self.file = nil
|
self.file = nil
|
||||||
self.updateReactionLayer()
|
self.updateReactionLayer()
|
||||||
|
|
||||||
var reactionTransition = transition
|
|
||||||
let heartReactionIcon: UIImageView
|
let heartReactionIcon: UIImageView
|
||||||
if let current = self.heartReactionIcon {
|
if let current = self.heartReactionIcon {
|
||||||
heartReactionIcon = current
|
heartReactionIcon = current
|
||||||
} else {
|
} else {
|
||||||
reactionTransition = reactionTransition.withAnimation(.none)
|
reactionIconTransition = reactionIconTransition.withAnimation(.none)
|
||||||
heartReactionIcon = UIImageView()
|
heartReactionIcon = UIImageView()
|
||||||
self.heartReactionIcon = heartReactionIcon
|
self.heartReactionIcon = heartReactionIcon
|
||||||
self.containerButton.addSubview(heartReactionIcon)
|
self.containerButton.addSubview(heartReactionIcon)
|
||||||
heartReactionIcon.image = PresentationResourcesChat.storyViewListLikeIcon(component.theme)
|
heartReactionIcon.image = PresentationResourcesChat.storyViewListLikeIcon(component.theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let image = heartReactionIcon.image, let iconFrame = self.iconFrame {
|
|
||||||
reactionTransition.setFrame(view: heartReactionIcon, frame: image.size.centered(around: iconFrame.center))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if let heartReactionIcon = self.heartReactionIcon {
|
if let heartReactionIcon = self.heartReactionIcon {
|
||||||
self.heartReactionIcon = nil
|
self.heartReactionIcon = nil
|
||||||
@ -719,6 +715,18 @@ public final class PeerListItemComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let heartReactionIcon = self.heartReactionIcon, let image = heartReactionIcon.image, let iconFrame = self.iconFrame {
|
||||||
|
reactionIconTransition.setFrame(view: heartReactionIcon, frame: image.size.centered(around: iconFrame.center))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let reactionLayer = self.reactionLayer, let iconFrame = self.iconFrame {
|
||||||
|
var adjustedIconFrame = iconFrame
|
||||||
|
if let reaction = component.reaction, case .builtin = reaction.reaction {
|
||||||
|
adjustedIconFrame = adjustedIconFrame.insetBy(dx: -adjustedIconFrame.width * 0.5, dy: -adjustedIconFrame.height * 0.5)
|
||||||
|
}
|
||||||
|
transition.setFrame(layer: reactionLayer, frame: adjustedIconFrame)
|
||||||
|
}
|
||||||
|
|
||||||
if themeUpdated {
|
if themeUpdated {
|
||||||
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
||||||
}
|
}
|
||||||
|
@ -961,32 +961,36 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
|||||||
TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: storyId.peerId),
|
TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: storyId.peerId),
|
||||||
TelegramEngine.EngineData.Item.NotificationSettings.Global()
|
TelegramEngine.EngineData.Item.NotificationSettings.Global()
|
||||||
),
|
),
|
||||||
context.account.postbox.transaction { transaction -> (Stories.StoredItem?, [PeerId: Peer], [MediaId: TelegramMediaFile]) in
|
context.account.postbox.combinedView(keys: [PostboxViewKey.story(id: storyId)]) |> mapToSignal { views -> Signal<(Stories.StoredItem?, [PeerId: Peer], [MediaId: TelegramMediaFile]), NoError> in
|
||||||
guard let item = transaction.getStory(id: storyId)?.get(Stories.StoredItem.self) else {
|
let item = (views.views[PostboxViewKey.story(id: storyId)] as? StoryView)?.item?.get(Stories.StoredItem.self)
|
||||||
return (nil, [:], [:])
|
|
||||||
}
|
return context.account.postbox.transaction { transaction -> (Stories.StoredItem?, [PeerId: Peer], [MediaId: TelegramMediaFile]) in
|
||||||
var peers: [PeerId: Peer] = [:]
|
guard let item else {
|
||||||
var allEntityFiles: [MediaId: TelegramMediaFile] = [:]
|
return (nil, [:], [:])
|
||||||
if case let .item(item) = item {
|
}
|
||||||
if let views = item.views {
|
var peers: [PeerId: Peer] = [:]
|
||||||
for id in views.seenPeerIds {
|
var allEntityFiles: [MediaId: TelegramMediaFile] = [:]
|
||||||
if let peer = transaction.getPeer(id) {
|
if case let .item(item) = item {
|
||||||
peers[peer.id] = peer
|
if let views = item.views {
|
||||||
|
for id in views.seenPeerIds {
|
||||||
|
if let peer = transaction.getPeer(id) {
|
||||||
|
peers[peer.id] = peer
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
for entity in item.entities {
|
||||||
for entity in item.entities {
|
if case let .CustomEmoji(_, fileId) = entity.type {
|
||||||
if case let .CustomEmoji(_, fileId) = entity.type {
|
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
|
||||||
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
|
if allEntityFiles[mediaId] == nil {
|
||||||
if allEntityFiles[mediaId] == nil {
|
if let file = transaction.getMedia(mediaId) as? TelegramMediaFile {
|
||||||
if let file = transaction.getMedia(mediaId) as? TelegramMediaFile {
|
allEntityFiles[file.fileId] = file
|
||||||
allEntityFiles[file.fileId] = file
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return (item, peers, allEntityFiles)
|
||||||
}
|
}
|
||||||
return (item, peers, allEntityFiles)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] data, itemAndPeers in
|
|> deliverOnMainQueue).start(next: { [weak self] data, itemAndPeers in
|
||||||
|
@ -153,6 +153,7 @@ private final class StoryPinchGesture: UIPinchGestureRecognizer {
|
|||||||
|
|
||||||
private(set) var currentTransform: (CGFloat, CGPoint, CGPoint)?
|
private(set) var currentTransform: (CGFloat, CGPoint, CGPoint)?
|
||||||
|
|
||||||
|
var shouldBegin: ((CGPoint) -> Bool)?
|
||||||
var began: (() -> Void)?
|
var began: (() -> Void)?
|
||||||
var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
|
var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
|
||||||
var ended: (() -> Void)?
|
var ended: (() -> Void)?
|
||||||
@ -181,6 +182,11 @@ private final class StoryPinchGesture: UIPinchGestureRecognizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
|
if let touch = touches.first, let shouldBegin = self.shouldBegin, !shouldBegin(touch.location(in: self.view)) {
|
||||||
|
self.state = .failed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
super.touchesBegan(touches, with: event)
|
super.touchesBegan(touches, with: event)
|
||||||
|
|
||||||
//self.currentTouches.formUnion(touches)
|
//self.currentTouches.formUnion(touches)
|
||||||
@ -457,6 +463,9 @@ private final class StoryContainerScreenComponent: Component {
|
|||||||
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 {
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
if !itemSetComponentView.allowsExternalGestures(point: touch.location(in: itemSetComponentView)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if !itemSetComponentView.isPointInsideContentArea(point: touch.location(in: itemSetComponentView)) {
|
if !itemSetComponentView.isPointInsideContentArea(point: touch.location(in: itemSetComponentView)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -467,6 +476,23 @@ private final class StoryContainerScreenComponent: Component {
|
|||||||
|
|
||||||
let pinchRecognizer = StoryPinchGesture()
|
let pinchRecognizer = StoryPinchGesture()
|
||||||
pinchRecognizer.delegate = self
|
pinchRecognizer.delegate = self
|
||||||
|
pinchRecognizer.shouldBegin = { [weak self] pinchLocation in
|
||||||
|
guard let self else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] {
|
||||||
|
if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
|
||||||
|
let itemLocation = self.convert(pinchLocation, to: itemSetComponentView)
|
||||||
|
if itemSetComponentView.allowsExternalGestures(point: itemLocation) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
pinchRecognizer.updated = { [weak self] scale, pinchLocation, offset in
|
pinchRecognizer.updated = { [weak self] scale, pinchLocation, offset in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
|
@ -542,6 +542,12 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (_, viewList) in self.viewLists {
|
||||||
|
if let view = viewList.view.view, view.hitTest(self.convert(point, to: view), with: nil) != nil {
|
||||||
|
return [.down]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if self.itemsContainerView.frame.contains(point) {
|
if self.itemsContainerView.frame.contains(point) {
|
||||||
if !self.isPointInsideContentArea(point: point) {
|
if !self.isPointInsideContentArea(point: point) {
|
||||||
return []
|
return []
|
||||||
@ -670,6 +676,13 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.audioRecorderStatusDisposable?.dispose()
|
self.audioRecorderStatusDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func allowsExternalGestures(point: CGPoint) -> Bool {
|
||||||
|
if self.viewListDisplayState != .hidden {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func isPointInsideContentArea(point: CGPoint) -> Bool {
|
func isPointInsideContentArea(point: CGPoint) -> Bool {
|
||||||
if let inputPanelView = self.inputPanel.view, inputPanelView.alpha != 0.0 {
|
if let inputPanelView = self.inputPanel.view, inputPanelView.alpha != 0.0 {
|
||||||
if inputPanelView.frame.contains(point) {
|
if inputPanelView.frame.contains(point) {
|
||||||
@ -1146,6 +1159,9 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if result === self.scroller {
|
if result === self.scroller {
|
||||||
|
if self.viewListDisplayState == .full {
|
||||||
|
return self
|
||||||
|
}
|
||||||
return self.itemsContainerView
|
return self.itemsContainerView
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1598,7 +1614,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var footerPanelY: CGFloat = self.itemsContainerView.frame.minY + itemLayout.contentFrame.center.y + itemLayout.contentFrame.height * 0.5 * itemScale
|
var footerPanelY: CGFloat = self.itemsContainerView.frame.minY + itemLayout.contentFrame.center.y + itemLayout.contentFrame.height * 0.5 * itemScale
|
||||||
footerPanelY += (1.0 - footerExpandFraction) * 4.0 + footerExpandFraction * (-41.0)
|
footerPanelY += (1.0 - footerExpandFraction) * (10.0) + footerExpandFraction * (-41.0)
|
||||||
|
|
||||||
let footerPanelMinScale: CGFloat = (1.0 - scaleFraction) + (itemLayout.sideVisibleItemScale / itemLayout.contentMinScale) * scaleFraction
|
let footerPanelMinScale: CGFloat = (1.0 - scaleFraction) + (itemLayout.sideVisibleItemScale / itemLayout.contentMinScale) * scaleFraction
|
||||||
let footerPanelScale = itemLayout.contentScaleFraction * footerPanelMinScale + 1.0 * (1.0 - itemLayout.contentScaleFraction)
|
let footerPanelScale = itemLayout.contentScaleFraction * footerPanelMinScale + 1.0 * (1.0 - itemLayout.contentScaleFraction)
|
||||||
@ -1612,6 +1628,10 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
itemTransition.setScale(view: footerPanelView, scale: footerPanelScale)
|
itemTransition.setScale(view: footerPanelView, scale: footerPanelScale)
|
||||||
|
|
||||||
var footerAlpha: CGFloat = 1.0 - itemLayout.contentOverflowFraction
|
var footerAlpha: CGFloat = 1.0 - itemLayout.contentOverflowFraction
|
||||||
|
|
||||||
|
let minFooterAlpha: CGFloat = 1.0 - fractionDistanceToCenter
|
||||||
|
footerAlpha = footerAlpha * itemLayout.contentScaleFraction + minFooterAlpha * (1.0 - itemLayout.contentScaleFraction)
|
||||||
|
|
||||||
if component.hideUI || self.isEditingStory {
|
if component.hideUI || self.isEditingStory {
|
||||||
footerAlpha = 0.0
|
footerAlpha = 0.0
|
||||||
}
|
}
|
||||||
@ -1711,8 +1731,17 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
|
|
||||||
if canReply {
|
if canReply {
|
||||||
if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View {
|
if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View {
|
||||||
return { [weak inputPanelView] in
|
return { [weak self, weak inputPanelView] in
|
||||||
inputPanelView?.activateInput()
|
guard let self, let inputPanelView else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.displayLikeReactions {
|
||||||
|
self.displayLikeReactions = false
|
||||||
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
|
||||||
|
}
|
||||||
|
|
||||||
|
inputPanelView.activateInput()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4273,6 +4302,9 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
guard let self, let component = self.component else {
|
guard let self, let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if component.slice.item.storyItem.privacy == privacy {
|
||||||
|
return
|
||||||
|
}
|
||||||
let _ = component.context.engine.messages.editStoryPrivacy(id: component.slice.item.storyItem.id, privacy: privacy).start()
|
let _ = component.context.engine.messages.editStoryPrivacy(id: component.slice.item.storyItem.id, privacy: privacy).start()
|
||||||
|
|
||||||
self.presentPrivacyTooltip(privacy: privacy)
|
self.presentPrivacyTooltip(privacy: privacy)
|
||||||
|
@ -164,21 +164,23 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
var sideInset: CGFloat
|
var sideInset: CGFloat
|
||||||
var itemHeight: CGFloat
|
var itemHeight: CGFloat
|
||||||
var itemCount: Int
|
var itemCount: Int
|
||||||
|
var premiumFooterSize: CGSize?
|
||||||
|
|
||||||
var contentSize: CGSize
|
var contentSize: CGSize
|
||||||
|
|
||||||
init(containerSize: CGSize, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, itemHeight: CGFloat, itemCount: Int) {
|
init(containerSize: CGSize, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, itemHeight: CGFloat, itemCount: Int, premiumFooterSize: CGSize?) {
|
||||||
self.containerSize = containerSize
|
self.containerSize = containerSize
|
||||||
self.bottomInset = bottomInset
|
self.bottomInset = bottomInset
|
||||||
self.topInset = topInset
|
self.topInset = topInset
|
||||||
self.sideInset = sideInset
|
self.sideInset = sideInset
|
||||||
self.itemHeight = itemHeight
|
self.itemHeight = itemHeight
|
||||||
self.itemCount = itemCount
|
self.itemCount = itemCount
|
||||||
|
self.premiumFooterSize = premiumFooterSize
|
||||||
|
|
||||||
self.contentSize = CGSize(width: containerSize.width, height: topInset + CGFloat(itemCount) * itemHeight + bottomInset)
|
self.contentSize = CGSize(width: containerSize.width, height: topInset + CGFloat(itemCount) * itemHeight + bottomInset)
|
||||||
#if DEBUG && false
|
if let premiumFooterSize {
|
||||||
self.contentSize.height += 1000.0
|
self.contentSize.height += 13.0 + premiumFooterSize.height + 12.0
|
||||||
#endif
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
||||||
@ -256,6 +258,8 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
var emptyText: ComponentView<Empty>?
|
var emptyText: ComponentView<Empty>?
|
||||||
var emptyButton: ComponentView<Empty>?
|
var emptyButton: ComponentView<Empty>?
|
||||||
|
|
||||||
|
var premiumFooterText: ComponentView<Empty>?
|
||||||
|
|
||||||
let scrollView: UIScrollView
|
let scrollView: UIScrollView
|
||||||
var itemLayout: ItemLayout?
|
var itemLayout: ItemLayout?
|
||||||
|
|
||||||
@ -447,7 +451,7 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
selectionState: .none,
|
selectionState: .none,
|
||||||
hasNext: index != viewListState.totalCount - 1,
|
hasNext: index != viewListState.totalCount - 1 || itemLayout.premiumFooterSize != nil,
|
||||||
action: { [weak self] peer in
|
action: { [weak self] peer in
|
||||||
guard let self, let component = self.component else {
|
guard let self, let component = self.component else {
|
||||||
return
|
return
|
||||||
@ -513,6 +517,17 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
self.visiblePlaceholderViews.removeValue(forKey: id)
|
self.visiblePlaceholderViews.removeValue(forKey: id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let premiumFooterTextView = self.premiumFooterText?.view, let premiumFooterSize = itemLayout.premiumFooterSize {
|
||||||
|
var premiumFooterTransition = transition
|
||||||
|
if premiumFooterTextView.superview == nil {
|
||||||
|
premiumFooterTransition = premiumFooterTransition.withAnimation(.none)
|
||||||
|
self.scrollView.addSubview(premiumFooterTextView)
|
||||||
|
}
|
||||||
|
let premiumFooterFrame = CGRect(origin: CGPoint(x: floor((itemLayout.contentSize.width - premiumFooterSize.width) * 0.5), y: itemLayout.itemFrame(for: itemLayout.itemCount - 1).maxY + 13.0), size: premiumFooterSize)
|
||||||
|
premiumFooterTransition.setPosition(view: premiumFooterTextView, position: premiumFooterFrame.center)
|
||||||
|
premiumFooterTransition.setBounds(view: premiumFooterTextView, bounds: CGRect(origin: CGPoint(), size: premiumFooterFrame.size))
|
||||||
|
}
|
||||||
|
|
||||||
if let viewList = self.viewList, let viewListState = self.viewListState, viewListState.loadMoreToken != nil, visibleBounds.maxY >= self.scrollView.contentSize.height - 200.0 {
|
if let viewList = self.viewList, let viewListState = self.viewListState, viewListState.loadMoreToken != nil, visibleBounds.maxY >= self.scrollView.contentSize.height - 200.0 {
|
||||||
if self.requestedLoadMoreToken != viewListState.loadMoreToken {
|
if self.requestedLoadMoreToken != viewListState.loadMoreToken {
|
||||||
self.requestedLoadMoreToken = viewListState.loadMoreToken
|
self.requestedLoadMoreToken = viewListState.loadMoreToken
|
||||||
@ -737,13 +752,64 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var premiumFooterSize: CGSize?
|
||||||
|
if !component.hasPremium, let viewListState = self.viewListState, viewListState.loadMoreToken == nil, !viewListState.items.isEmpty, let views = component.storyItem.views, views.seenCount > viewListState.totalCount, component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) {
|
||||||
|
let premiumFooterText: ComponentView<Empty>
|
||||||
|
if let current = self.premiumFooterText {
|
||||||
|
premiumFooterText = current
|
||||||
|
} else {
|
||||||
|
premiumFooterText = ComponentView()
|
||||||
|
self.premiumFooterText = premiumFooterText
|
||||||
|
}
|
||||||
|
|
||||||
|
let fontSize: CGFloat = 13.0
|
||||||
|
let body = MarkdownAttributeSet(font: Font.regular(fontSize), textColor: component.theme.list.itemSecondaryTextColor)
|
||||||
|
let bold = MarkdownAttributeSet(font: Font.semibold(fontSize), textColor: component.theme.list.itemSecondaryTextColor)
|
||||||
|
let link = MarkdownAttributeSet(font: Font.semibold(fontSize), textColor: component.theme.list.itemAccentColor)
|
||||||
|
let attributes = MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return ("URL", "") })
|
||||||
|
|
||||||
|
//TODO:localize
|
||||||
|
let text = "To unlock viewers' lists for expired and saved stories, subscribe to [Telegram Premium]()."
|
||||||
|
premiumFooterSize = premiumFooterText.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(BalancedTextComponent(
|
||||||
|
text: .markdown(text: text, attributes: attributes),
|
||||||
|
horizontalAlignment: .center,
|
||||||
|
maximumNumberOfLines: 0,
|
||||||
|
lineSpacing: 0.2,
|
||||||
|
highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.5),
|
||||||
|
highlightAction: { attributes in
|
||||||
|
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
|
||||||
|
return NSAttributedString.Key(rawValue: "URL")
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tapAction: { [weak self] _, _ in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.openPremiumIntro()
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: min(320.0, availableSize.width - 16.0 * 2.0), height: 1000.0)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if let premiumFooterText = self.premiumFooterText {
|
||||||
|
self.premiumFooterText = nil
|
||||||
|
premiumFooterText.view?.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let itemLayout = ItemLayout(
|
let itemLayout = ItemLayout(
|
||||||
containerSize: CGSize(width: availableSize.width, height: visualHeight),
|
containerSize: CGSize(width: availableSize.width, height: visualHeight),
|
||||||
bottomInset: component.safeInsets.bottom,
|
bottomInset: component.safeInsets.bottom,
|
||||||
topInset: navigationHeight,
|
topInset: navigationHeight,
|
||||||
sideInset: sideInset,
|
sideInset: sideInset,
|
||||||
itemHeight: measureItemSize.height,
|
itemHeight: measureItemSize.height,
|
||||||
itemCount: self.viewListState?.items.count ?? 0
|
itemCount: self.viewListState?.items.count ?? 0,
|
||||||
|
premiumFooterSize: premiumFooterSize
|
||||||
)
|
)
|
||||||
self.itemLayout = itemLayout
|
self.itemLayout = itemLayout
|
||||||
|
|
||||||
@ -766,9 +832,6 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
self.scrollView.contentSize = scrollContentSize
|
self.scrollView.contentSize = scrollContentSize
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ignoreScrolling = false
|
|
||||||
self.updateScrolling(transition: transition)
|
|
||||||
|
|
||||||
if let viewListState = self.viewListState, viewListState.loadMoreToken == nil, viewListState.items.isEmpty, viewListState.totalCount == 0 {
|
if let viewListState = self.viewListState, viewListState.loadMoreToken == nil, viewListState.items.isEmpty, viewListState.totalCount == 0 {
|
||||||
self.scrollView.isUserInteractionEnabled = false
|
self.scrollView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
@ -990,6 +1053,9 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
emptyButton.view?.removeFromSuperview()
|
emptyButton.view?.removeFromSuperview()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.ignoreScrolling = false
|
||||||
|
self.updateScrolling(transition: transition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user