This commit is contained in:
Ali 2023-08-11 02:23:27 +04:00
parent a46c372d68
commit 848e342991
8 changed files with 242 additions and 41 deletions

View 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
}
}

View File

@ -45,6 +45,7 @@ public enum PostboxViewKey: Hashable {
case storyItems(peerId: PeerId)
case storyExpirationTimeItems
case peerStoryStats(peerIds: Set<PeerId>)
case story(id: StoryId)
public func hash(into hasher: inout Hasher) {
switch self {
@ -150,6 +151,8 @@ public enum PostboxViewKey: Hashable {
hasher.combine(19)
case let .peerStoryStats(peerIds):
hasher.combine(peerIds)
case let .story(id):
hasher.combine(id)
}
}
@ -419,6 +422,12 @@ public enum PostboxViewKey: Hashable {
} else {
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)
case let .peerStoryStats(peerIds):
return MutablePeerStoryStatsView(postbox: postbox, peerIds: peerIds)
case let .story(id):
return MutableStoryView(postbox: postbox, id: id)
}
}

View File

@ -3250,11 +3250,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
transition.setFrame(view: self.selectionContainerView, frame: CGRect(origin: .zero, size: previewFrame.size))
self.interaction?.containerLayoutUpdated(layout: layout, transition: transition)
var presentationContextLayout = layout
presentationContextLayout.intrinsicInsets.top = max(presentationContextLayout.intrinsicInsets.top, topInset)
// var layout = layout
// layout.intrinsicInsets.top = topInset
// layout.intrinsicInsets.bottom = bottomInset + 60.0
controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
controller.presentationContext.containerLayoutUpdated(presentationContextLayout, transition: transition.containedViewLayoutTransition)
if isFirstTime {
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
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)
}
return false }

View File

@ -339,7 +339,7 @@ public final class PeerListItemComponent: Component {
if let reaction = component.reaction, case .custom = reaction.reaction {
reactionLayer.isVisibleForAnimations = true
}
self.layer.addSublayer(reactionLayer)
self.containerButton.layer.addSublayer(reactionLayer)
if var iconFrame = self.iconFrame {
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)
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 let reaction = component.reaction, case .builtin("") = reaction.reaction {
self.file = nil
self.updateReactionLayer()
var reactionTransition = transition
let heartReactionIcon: UIImageView
if let current = self.heartReactionIcon {
heartReactionIcon = current
} else {
reactionTransition = reactionTransition.withAnimation(.none)
reactionIconTransition = reactionIconTransition.withAnimation(.none)
heartReactionIcon = UIImageView()
self.heartReactionIcon = heartReactionIcon
self.containerButton.addSubview(heartReactionIcon)
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 {
if let heartReactionIcon = self.heartReactionIcon {
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 {
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
}

View File

@ -961,32 +961,36 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: storyId.peerId),
TelegramEngine.EngineData.Item.NotificationSettings.Global()
),
context.account.postbox.transaction { transaction -> (Stories.StoredItem?, [PeerId: Peer], [MediaId: TelegramMediaFile]) in
guard let item = transaction.getStory(id: storyId)?.get(Stories.StoredItem.self) else {
return (nil, [:], [:])
}
var peers: [PeerId: Peer] = [:]
var allEntityFiles: [MediaId: TelegramMediaFile] = [:]
if case let .item(item) = item {
if let views = item.views {
for id in views.seenPeerIds {
if let peer = transaction.getPeer(id) {
peers[peer.id] = peer
context.account.postbox.combinedView(keys: [PostboxViewKey.story(id: storyId)]) |> mapToSignal { views -> Signal<(Stories.StoredItem?, [PeerId: Peer], [MediaId: TelegramMediaFile]), NoError> in
let item = (views.views[PostboxViewKey.story(id: storyId)] as? StoryView)?.item?.get(Stories.StoredItem.self)
return context.account.postbox.transaction { transaction -> (Stories.StoredItem?, [PeerId: Peer], [MediaId: TelegramMediaFile]) in
guard let item else {
return (nil, [:], [:])
}
var peers: [PeerId: Peer] = [:]
var allEntityFiles: [MediaId: TelegramMediaFile] = [:]
if case let .item(item) = item {
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 {
if case let .CustomEmoji(_, fileId) = entity.type {
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
if allEntityFiles[mediaId] == nil {
if let file = transaction.getMedia(mediaId) as? TelegramMediaFile {
allEntityFiles[file.fileId] = file
for entity in item.entities {
if case let .CustomEmoji(_, fileId) = entity.type {
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
if allEntityFiles[mediaId] == nil {
if let file = transaction.getMedia(mediaId) as? TelegramMediaFile {
allEntityFiles[file.fileId] = file
}
}
}
}
}
return (item, peers, allEntityFiles)
}
return (item, peers, allEntityFiles)
}
)
|> deliverOnMainQueue).start(next: { [weak self] data, itemAndPeers in

View File

@ -153,6 +153,7 @@ private final class StoryPinchGesture: UIPinchGestureRecognizer {
private(set) var currentTransform: (CGFloat, CGPoint, CGPoint)?
var shouldBegin: ((CGPoint) -> Bool)?
var began: (() -> Void)?
var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
var ended: (() -> Void)?
@ -181,6 +182,11 @@ private final class StoryPinchGesture: UIPinchGestureRecognizer {
}
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)
//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 {
return false
}
if !itemSetComponentView.allowsExternalGestures(point: touch.location(in: itemSetComponentView)) {
return false
}
if !itemSetComponentView.isPointInsideContentArea(point: touch.location(in: itemSetComponentView)) {
return false
}
@ -467,6 +476,23 @@ private final class StoryContainerScreenComponent: Component {
let pinchRecognizer = StoryPinchGesture()
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
guard let self else {
return

View File

@ -542,6 +542,12 @@ public final class StoryItemSetContainerComponent: Component {
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.isPointInsideContentArea(point: point) {
return []
@ -670,6 +676,13 @@ public final class StoryItemSetContainerComponent: Component {
self.audioRecorderStatusDisposable?.dispose()
}
func allowsExternalGestures(point: CGPoint) -> Bool {
if self.viewListDisplayState != .hidden {
return false
}
return true
}
func isPointInsideContentArea(point: CGPoint) -> Bool {
if let inputPanelView = self.inputPanel.view, inputPanelView.alpha != 0.0 {
if inputPanelView.frame.contains(point) {
@ -1146,6 +1159,9 @@ public final class StoryItemSetContainerComponent: Component {
}
if result === self.scroller {
if self.viewListDisplayState == .full {
return self
}
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
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 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)
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 {
footerAlpha = 0.0
}
@ -1711,8 +1731,17 @@ public final class StoryItemSetContainerComponent: Component {
if canReply {
if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View {
return { [weak inputPanelView] in
inputPanelView?.activateInput()
return { [weak self, weak inputPanelView] in
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 {
return
}
if component.slice.item.storyItem.privacy == privacy {
return
}
let _ = component.context.engine.messages.editStoryPrivacy(id: component.slice.item.storyItem.id, privacy: privacy).start()
self.presentPrivacyTooltip(privacy: privacy)

View File

@ -164,21 +164,23 @@ final class StoryItemSetViewListComponent: Component {
var sideInset: CGFloat
var itemHeight: CGFloat
var itemCount: Int
var premiumFooterSize: 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.bottomInset = bottomInset
self.topInset = topInset
self.sideInset = sideInset
self.itemHeight = itemHeight
self.itemCount = itemCount
self.premiumFooterSize = premiumFooterSize
self.contentSize = CGSize(width: containerSize.width, height: topInset + CGFloat(itemCount) * itemHeight + bottomInset)
#if DEBUG && false
self.contentSize.height += 1000.0
#endif
if let premiumFooterSize {
self.contentSize.height += 13.0 + premiumFooterSize.height + 12.0
}
}
func visibleItems(for rect: CGRect) -> Range<Int>? {
@ -256,6 +258,8 @@ final class StoryItemSetViewListComponent: Component {
var emptyText: ComponentView<Empty>?
var emptyButton: ComponentView<Empty>?
var premiumFooterText: ComponentView<Empty>?
let scrollView: UIScrollView
var itemLayout: ItemLayout?
@ -447,7 +451,7 @@ final class StoryItemSetViewListComponent: Component {
)
},
selectionState: .none,
hasNext: index != viewListState.totalCount - 1,
hasNext: index != viewListState.totalCount - 1 || itemLayout.premiumFooterSize != nil,
action: { [weak self] peer in
guard let self, let component = self.component else {
return
@ -513,6 +517,17 @@ final class StoryItemSetViewListComponent: Component {
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 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(
containerSize: CGSize(width: availableSize.width, height: visualHeight),
bottomInset: component.safeInsets.bottom,
topInset: navigationHeight,
sideInset: sideInset,
itemHeight: measureItemSize.height,
itemCount: self.viewListState?.items.count ?? 0
itemCount: self.viewListState?.items.count ?? 0,
premiumFooterSize: premiumFooterSize
)
self.itemLayout = itemLayout
@ -766,9 +832,6 @@ final class StoryItemSetViewListComponent: Component {
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 {
self.scrollView.isUserInteractionEnabled = false
@ -990,6 +1053,9 @@ final class StoryItemSetViewListComponent: Component {
emptyButton.view?.removeFromSuperview()
}
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
}
}