[WIP] Stories

This commit is contained in:
Ali 2023-06-09 01:07:41 +04:00
parent 780168d30b
commit de8c3f055f
29 changed files with 1171 additions and 409 deletions

View File

@ -2362,6 +2362,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
return StoryContainerScreen.TransitionOut(
destinationView: transitionView,
transitionView: nil,
destinationRect: transitionView.bounds,
destinationCornerRadius: transitionView.bounds.height * 0.5,
destinationIsAvatar: true,

View File

@ -515,7 +515,7 @@ public class ContactsController: ViewController {
return
}
let storyContent = StoryContentContextImpl(context: self.context, includeHidden: false, focusedPeerId: peer?.id)
let storyContent = StoryContentContextImpl(context: self.context, includeHidden: true, focusedPeerId: peer?.id)
let _ = (storyContent.state
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] storyContentState in
@ -551,6 +551,7 @@ public class ContactsController: ViewController {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
return StoryContainerScreen.TransitionOut(
destinationView: transitionView,
transitionView: nil,
destinationRect: transitionView.bounds,
destinationCornerRadius: transitionView.bounds.height * 0.5,
destinationIsAvatar: true,

View File

@ -62,7 +62,7 @@ public final class GridMessageSelectionNode: ASDisplayNode {
public final class GridMessageSelectionLayer: CALayer {
private var selected = false
private let checkLayer: CheckLayer
public let checkLayer: CheckLayer
public init(theme: CheckNodeTheme) {
self.checkLayer = CheckLayer(theme: theme, content: .check)

View File

@ -17,7 +17,7 @@ public enum MediaTrackFrameResult {
}
private let traceEvents: Bool = {
#if DEBUG
#if DEBUG && false
return true
#else
return false

View File

@ -35,7 +35,7 @@ public protocol SparseItemGridBinding: AnyObject {
func unbindLayer(layer: SparseItemGridLayer)
func scrollerTextForTag(tag: Int32) -> String?
func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError>
func onTap(item: SparseItemGrid.Item)
func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint)
func onTagTap()
func didScroll()
func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition)
@ -667,6 +667,27 @@ public final class SparseItemGrid: ASDisplayNode {
return nil
}
func itemHitTest(at point: CGPoint) -> (Item, CALayer, CGPoint)? {
guard let items = self.items, !items.items.isEmpty else {
return nil
}
let localPoint = self.scrollView.convert(point, from: self.view)
for (id, visibleItem) in self.visibleItems {
if visibleItem.frame.contains(localPoint) {
for item in items.items {
if item.id == id {
return (item, visibleItem.displayLayer, self.view.layer.convert(point, to: visibleItem.displayLayer))
}
}
return nil
}
}
return nil
}
func anchorItem(at point: CGPoint, orLower: Bool = false) -> (Item, Int)? {
guard let items = self.items, !items.items.isEmpty, let layout = self.layout else {
@ -862,7 +883,12 @@ public final class SparseItemGrid: ASDisplayNode {
return
}
let contentHeight = layout.contentHeight(count: items.count)
let contentHeight: CGFloat
if items.items.isEmpty {
contentHeight = 0.0
} else {
contentHeight = layout.contentHeight(count: items.count)
}
let shimmerColors = items.itemBinding.getShimmerColors()
if resetScrolling {
@ -904,83 +930,82 @@ public final class SparseItemGrid: ASDisplayNode {
var validIds = Set<AnyHashable>()
var usedPlaceholderCount = 0
if !items.items.isEmpty {
var bindItems: [Item] = []
var bindLayers: [SparseItemGridDisplayItem] = []
var updateLayers: [SparseItemGridDisplayItem] = []
var bindItems: [Item] = []
var bindLayers: [SparseItemGridDisplayItem] = []
var updateLayers: [SparseItemGridDisplayItem] = []
let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count)
for index in visibleRange.minIndex ... visibleRange.maxIndex {
if let item = items.item(at: index) {
let itemFrame = layout.frame(at: index)
let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count)
for index in visibleRange.minIndex ... visibleRange.maxIndex {
if let item = items.item(at: index) {
let itemFrame = layout.frame(at: index)
let itemLayer: VisibleItem
if let current = self.visibleItems[item.id] {
itemLayer = current
updateLayers.append(itemLayer)
} else {
itemLayer = VisibleItem(layer: items.itemBinding.createLayer(), view: items.itemBinding.createView())
self.visibleItems[item.id] = itemLayer
bindItems.append(item)
bindLayers.append(itemLayer)
if let layer = itemLayer.layer {
self.scrollView.layer.addSublayer(layer)
} else if let view = itemLayer.view {
self.scrollView.addSubview(view)
}
}
if itemLayer.needsShimmer {
let placeholderLayer: SparseItemGridShimmerLayer
if let current = itemLayer.shimmerLayer {
placeholderLayer = current
} else {
placeholderLayer = items.itemBinding.createShimmerLayer() ?? Shimmer.Layer()
self.scrollView.layer.insertSublayer(placeholderLayer, at: 0)
itemLayer.shimmerLayer = placeholderLayer
}
placeholderLayer.frame = itemFrame
self.shimmer.update(colors: shimmerColors, layer: placeholderLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY))
placeholderLayer.update(size: itemFrame.size)
} else if let placeholderLayer = itemLayer.shimmerLayer {
itemLayer.shimmerLayer = nil
placeholderLayer.removeFromSuperlayer()
}
validIds.insert(item.id)
itemLayer.frame = itemFrame
let itemLayer: VisibleItem
if let current = self.visibleItems[item.id] {
itemLayer = current
updateLayers.append(itemLayer)
} else {
itemLayer = VisibleItem(layer: items.itemBinding.createLayer(), view: items.itemBinding.createView())
self.visibleItems[item.id] = itemLayer
bindItems.append(item)
bindLayers.append(itemLayer)
if let layer = itemLayer.layer {
self.scrollView.layer.addSublayer(layer)
} else if let view = itemLayer.view {
self.scrollView.addSubview(view)
}
}
if itemLayer.needsShimmer {
let placeholderLayer: SparseItemGridShimmerLayer
if self.visiblePlaceholders.count > usedPlaceholderCount {
placeholderLayer = self.visiblePlaceholders[usedPlaceholderCount]
if let current = itemLayer.shimmerLayer {
placeholderLayer = current
} else {
placeholderLayer = items.itemBinding.createShimmerLayer() ?? Shimmer.Layer()
self.scrollView.layer.addSublayer(placeholderLayer)
self.visiblePlaceholders.append(placeholderLayer)
self.scrollView.layer.insertSublayer(placeholderLayer, at: 0)
itemLayer.shimmerLayer = placeholderLayer
}
let itemFrame = layout.frame(at: index)
placeholderLayer.frame = itemFrame
self.shimmer.update(colors: shimmerColors, layer: placeholderLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY))
placeholderLayer.update(size: itemFrame.size)
usedPlaceholderCount += 1
} else if let placeholderLayer = itemLayer.shimmerLayer {
itemLayer.shimmerLayer = nil
placeholderLayer.removeFromSuperlayer()
}
}
if !bindItems.isEmpty {
items.itemBinding.bindLayers(items: bindItems, layers: bindLayers, size: layout.containerLayout.size, insets: layout.containerLayout.insets, synchronous: synchronous)
}
validIds.insert(item.id)
for item in updateLayers {
let item = item as! VisibleItem
if let layer = item.layer {
layer.update(size: layer.frame.size)
} else if let view = item.view {
view.update(size: layer.frame.size, insets: layout.containerLayout.insets)
itemLayer.frame = itemFrame
} else {
let placeholderLayer: SparseItemGridShimmerLayer
if self.visiblePlaceholders.count > usedPlaceholderCount {
placeholderLayer = self.visiblePlaceholders[usedPlaceholderCount]
} else {
placeholderLayer = items.itemBinding.createShimmerLayer() ?? Shimmer.Layer()
self.scrollView.layer.addSublayer(placeholderLayer)
self.visiblePlaceholders.append(placeholderLayer)
}
let itemFrame = layout.frame(at: index)
placeholderLayer.frame = itemFrame
self.shimmer.update(colors: shimmerColors, layer: placeholderLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY))
placeholderLayer.update(size: itemFrame.size)
usedPlaceholderCount += 1
}
}
if !bindItems.isEmpty {
items.itemBinding.bindLayers(items: bindItems, layers: bindLayers, size: layout.containerLayout.size, insets: layout.containerLayout.insets, synchronous: synchronous)
}
for item in updateLayers {
let item = item as! VisibleItem
if let layer = item.layer {
layer.update(size: layer.frame.size)
} else if let view = item.view {
view.update(size: layer.frame.size, insets: layout.containerLayout.insets)
}
}
@ -1398,8 +1423,8 @@ public final class SparseItemGrid: ASDisplayNode {
}
if case .ended = recognizer.state {
let location = recognizer.location(in: self.view)
if let item = currentViewport.item(at: self.view.convert(location, to: currentViewport.view)) {
items.itemBinding.onTap(item: item)
if let (item, itemLayer, point) = currentViewport.itemHitTest(at: self.view.convert(location, to: currentViewport.view)) {
items.itemBinding.onTap(item: item, itemLayer: itemLayer, point: point)
}
}
}

View File

@ -780,12 +780,6 @@ public final class ManagedAudioSession {
managedAudioSessionLog("ManagedAudioSession resetting options")
try AVAudioSession.sharedInstance().setCategory(nativeCategory, options: options)
}
/*if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
try AVAudioSession.sharedInstance().setCategory(nativeCategory, mode: mode, policy: .default, options: options)
} else {
AVAudioSession.sharedInstance().perform(NSSelectorFromString("setCategory:error:"), with: nativeCategory)
try AVAudioSession.sharedInstance().setMode(mode)
}*/
} catch let error {
managedAudioSessionLog("ManagedAudioSession setup error \(error)")
}

View File

@ -810,16 +810,25 @@ func _internal_editStory(account: Account, media: EngineStoryInputMedia?, id: In
}
}
func _internal_deleteStory(account: Account, id: Int32) -> Signal<Never, NoError> {
func _internal_deleteStories(account: Account, ids: [Int32]) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
var items = transaction.getStoryItems(peerId: account.peerId)
if let index = items.firstIndex(where: { $0.id == id }) {
items.remove(at: index)
var updated = false
for id in ids {
if let index = items.firstIndex(where: { $0.id == id }) {
items.remove(at: index)
updated = true
}
}
if updated {
transaction.setStoryItems(peerId: account.peerId, items: items)
}
account.stateManager.injectStoryUpdates(updates: ids.map { id in
return .deleted(peerId: account.peerId, id: id)
})
}
|> mapToSignal { _ -> Signal<Never, NoError> in
return account.network.request(Api.functions.stories.deleteStories(id: [id]))
return account.network.request(Api.functions.stories.deleteStories(id: ids))
|> `catch` { _ -> Signal<[Int32], NoError> in
return .single([])
}
@ -829,67 +838,114 @@ func _internal_deleteStory(account: Account, id: Int32) -> Signal<Never, NoError
}
}
func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Api.InputUser? in
if let peerStoryState = transaction.getPeerStoryState(peerId: peerId)?.get(Stories.PeerState.self) {
transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState(
subscriptionsOpaqueState: peerStoryState.subscriptionsOpaqueState,
maxReadId: max(peerStoryState.maxReadId, id)
)))
func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32, asPinned: Bool) -> Signal<Never, NoError> {
if asPinned {
return account.postbox.transaction { transaction -> Api.InputUser? in
return transaction.getPeer(peerId).flatMap(apiInputUser)
}
return transaction.getPeer(peerId).flatMap(apiInputUser)
}
|> mapToSignal { inputUser -> Signal<Never, NoError> in
guard let inputUser = inputUser else {
return .complete()
|> mapToSignal { inputUser -> Signal<Never, NoError> in
guard let inputUser = inputUser else {
return .complete()
}
#if DEBUG && false
if "".isEmpty {
return .complete()
}
#endif
return account.network.request(Api.functions.stories.incrementStoryViews(userId: inputUser, id: [id]))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
}
account.stateManager.injectStoryUpdates(updates: [.read(peerId: peerId, maxId: id)])
#if DEBUG
if "".isEmpty {
return .complete()
} else {
return account.postbox.transaction { transaction -> Api.InputUser? in
if let peerStoryState = transaction.getPeerStoryState(peerId: peerId)?.get(Stories.PeerState.self) {
transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState(
subscriptionsOpaqueState: peerStoryState.subscriptionsOpaqueState,
maxReadId: max(peerStoryState.maxReadId, id)
)))
}
return transaction.getPeer(peerId).flatMap(apiInputUser)
}
#endif
return account.network.request(Api.functions.stories.readStories(userId: inputUser, maxId: id))
|> `catch` { _ -> Signal<[Int32], NoError> in
return .single([])
|> mapToSignal { inputUser -> Signal<Never, NoError> in
guard let inputUser = inputUser else {
return .complete()
}
account.stateManager.injectStoryUpdates(updates: [.read(peerId: peerId, maxId: id)])
#if DEBUG && false
if "".isEmpty {
return .complete()
}
#endif
return account.network.request(Api.functions.stories.readStories(userId: inputUser, maxId: id))
|> `catch` { _ -> Signal<[Int32], NoError> in
return .single([])
}
|> ignoreValues
}
|> ignoreValues
}
}
func _internal_updateStoryIsPinned(account: Account, id: Int32, isPinned: Bool) -> Signal<Never, NoError> {
func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStoryItem], isPinned: Bool) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
var items = transaction.getStoryItems(peerId: account.peerId)
if let index = items.firstIndex(where: { $0.id == id }), case let .item(item) = items[index].value.get(Stories.StoredItem.self) {
let updatedItem = Stories.Item(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: item.media,
text: item.text,
entities: item.entities,
views: item.views,
privacy: item.privacy,
isPinned: isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic
)
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items[index] = StoryItemsTableEntry(value: entry, id: item.id)
transaction.setStoryItems(peerId: account.peerId, items: items)
var updatedItems: [Stories.Item] = []
for (id, referenceItem) in ids {
if let index = items.firstIndex(where: { $0.id == id }), case let .item(item) = items[index].value.get(Stories.StoredItem.self) {
let updatedItem = Stories.Item(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: item.media,
text: item.text,
entities: item.entities,
views: item.views,
privacy: item.privacy,
isPinned: isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic
)
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items[index] = StoryItemsTableEntry(value: entry, id: item.id)
}
updatedItems.append(updatedItem)
} else {
let item = referenceItem.asStoryItem()
let updatedItem = Stories.Item(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: item.media,
text: item.text,
entities: item.entities,
views: item.views,
privacy: item.privacy,
isPinned: isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic
)
updatedItems.append(updatedItem)
}
}
transaction.setStoryItems(peerId: account.peerId, items: items)
if !updatedItems.isEmpty {
DispatchQueue.main.async {
account.stateManager.injectStoryUpdates(updates: [.added(peerId: account.peerId, item: Stories.StoredItem.item(updatedItem))])
account.stateManager.injectStoryUpdates(updates: updatedItems.map { updatedItem in
return .added(peerId: account.peerId, item: Stories.StoredItem.item(updatedItem))
})
}
}
}
|> mapToSignal { _ -> Signal<Never, NoError> in
return account.network.request(Api.functions.stories.togglePinned(id: [id], pinned: isPinned ? .boolTrue : .boolFalse))
return account.network.request(Api.functions.stories.togglePinned(id: ids.keys.sorted(), pinned: isPinned ? .boolTrue : .boolFalse))
|> `catch` { _ -> Signal<[Int32], NoError> in
return .single([])
}

View File

@ -95,6 +95,34 @@ public final class EngineStoryItem: Equatable {
}
}
extension EngineStoryItem {
func asStoryItem() -> Stories.Item {
return Stories.Item(
id: self.id,
timestamp: self.timestamp,
expirationTimestamp: self.expirationTimestamp,
media: self.media._asMedia(),
text: self.text,
entities: self.entities,
views: self.views.flatMap { views in
return Stories.Item.Views(
seenCount: views.seenCount,
seenPeerIds: views.seenPeers.map(\.id)
)
},
privacy: self.privacy.flatMap { privacy in
return Stories.Item.Privacy(
base: privacy.base,
additionallyIncludePeers: privacy.additionallyIncludePeers
)
},
isPinned: self.isPinned,
isExpired: self.isExpired,
isPublic: self.isPublic
)
}
}
public final class StorySubscriptionsContext {
private enum OpaqueStateMark: Equatable {
case empty
@ -599,15 +627,17 @@ public final class PeerStoryListContext {
return
}
var finalUpdatedState: State?
for update in updates {
switch update {
case let .deleted(peerId, id):
if self.peerId == peerId {
if let index = self.stateValue.items.firstIndex(where: { $0.id == id }) {
var updatedState = self.stateValue
var updatedState = finalUpdatedState ?? self.stateValue
updatedState.items.remove(at: index)
updatedState.totalCount = max(0, updatedState.totalCount - 1)
self.stateValue = updatedState
finalUpdatedState = updatedState
}
}
case let .added(peerId, item):
@ -617,7 +647,7 @@ public final class PeerStoryListContext {
if case let .item(item) = item {
if item.isPinned {
if let media = item.media {
var updatedState = self.stateValue
var updatedState = finalUpdatedState ?? self.stateValue
updatedState.items[index] = EngineStoryItem(
id: item.id,
timestamp: item.timestamp,
@ -638,13 +668,47 @@ public final class PeerStoryListContext {
isExpired: item.isExpired,
isPublic: item.isPublic
)
self.stateValue = updatedState
finalUpdatedState = updatedState
}
} else {
var updatedState = self.stateValue
var updatedState = finalUpdatedState ?? self.stateValue
updatedState.items.remove(at: index)
updatedState.totalCount = max(0, updatedState.totalCount - 1)
self.stateValue = updatedState
finalUpdatedState = updatedState
}
}
}
} else {
if !self.isArchived {
if case let .item(item) = item {
if item.isPinned {
if let media = item.media {
var updatedState = finalUpdatedState ?? self.stateValue
updatedState.items.append(EngineStoryItem(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: EngineMedia(media),
text: item.text,
entities: item.entities,
views: item.views.flatMap { views in
return EngineStoryItem.Views(
seenCount: views.seenCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return peers[id].flatMap(EnginePeer.init)
}
)
},
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
isPinned: item.isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic
))
updatedState.items.sort(by: { lhs, rhs in
return lhs.timestamp > rhs.timestamp
})
finalUpdatedState = updatedState
}
}
}
}
@ -654,6 +718,10 @@ public final class PeerStoryListContext {
break
}
}
if let finalUpdatedState = finalUpdatedState {
self.stateValue = finalUpdatedState
}
})
})
}

View File

@ -926,16 +926,16 @@ public extension TelegramEngine {
return _internal_editStory(account: self.account, media: media, id: id, text: text, entities: entities, privacy: privacy)
}
public func deleteStory(id: Int32) -> Signal<Never, NoError> {
return _internal_deleteStory(account: self.account, id: id)
public func deleteStories(ids: [Int32]) -> Signal<Never, NoError> {
return _internal_deleteStories(account: self.account, ids: ids)
}
public func markStoryAsSeen(peerId: EnginePeer.Id, id: Int32) -> Signal<Never, NoError> {
return _internal_markStoryAsSeen(account: self.account, peerId: peerId, id: id)
public func markStoryAsSeen(peerId: EnginePeer.Id, id: Int32, asPinned: Bool) -> Signal<Never, NoError> {
return _internal_markStoryAsSeen(account: self.account, peerId: peerId, id: id, asPinned: asPinned)
}
public func updateStoryIsPinned(id: Int32, isPinned: Bool) -> Signal<Never, NoError> {
return _internal_updateStoryIsPinned(account: self.account, id: id, isPinned: isPinned)
public func updateStoriesArePinned(ids: [Int32: EngineStoryItem], isPinned: Bool) -> Signal<Never, NoError> {
return _internal_updateStoriesArePinned(account: self.account, ids: ids, isPinned: isPinned)
}
public func getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal<StoryViewList?, NoError> {

View File

@ -29,16 +29,14 @@ public struct PresentationResourcesSettings {
public static let devices = renderIcon(name: "Settings/Menu/Sessions")
public static let chatFolders = renderIcon(name: "Settings/Menu/ChatListFilters")
public static let stickers = renderIcon(name: "Settings/Menu/Stickers")
public static let notifications = renderIcon(name: "Settings/Menu/Notifications")
public static let security = renderIcon(name: "Settings/Menu/Security")
public static let dataAndStorage = renderIcon(name: "Settings/Menu/DataAndStorage")
public static let appearance = renderIcon(name: "Settings/Menu/Appearance")
public static let language = renderIcon(name: "Settings/Menu/Language")
public static let deleteAccount = renderIcon(name: "Chat/Info/GroupRemovedIcon")
public static let powerSaving = renderIcon(name: "Settings/Menu/PowerSaving")
public static let stories = renderIcon(name: "Settings/Menu/Stories")
public static let premium = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)

View File

@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "BottomButtonPanelComponent",
module_name = "BottomButtonPanelComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/SolidRoundedButtonComponent",
],
visibility = [
"//visibility:public",
],
)

View File

@ -3,19 +3,11 @@ import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import EmojiStatusComponent
import TelegramStringFormatting
import CheckNode
import SolidRoundedButtonComponent
final class StorageUsageScreenSelectionPanelComponent: Component {
public final class BottomButtonPanelComponent: Component {
let theme: PresentationTheme
let title: String
let label: String?
@ -23,7 +15,7 @@ final class StorageUsageScreenSelectionPanelComponent: Component {
let insets: UIEdgeInsets
let action: () -> Void
init(
public init(
theme: PresentationTheme,
title: String,
label: String?,
@ -39,7 +31,7 @@ final class StorageUsageScreenSelectionPanelComponent: Component {
self.action = action
}
static func ==(lhs: StorageUsageScreenSelectionPanelComponent, rhs: StorageUsageScreenSelectionPanelComponent) -> Bool {
public static func ==(lhs: BottomButtonPanelComponent, rhs: BottomButtonPanelComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
@ -58,14 +50,14 @@ final class StorageUsageScreenSelectionPanelComponent: Component {
return true
}
class View: UIView {
public class View: UIView {
private let backgroundView: BlurredBackgroundView
private let separatorLayer: SimpleLayer
private let actionButton = ComponentView<Empty>()
private var component: StorageUsageScreenSelectionPanelComponent?
private var component: BottomButtonPanelComponent?
override init(frame: CGRect) {
override public init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
self.separatorLayer = SimpleLayer()
@ -75,11 +67,11 @@ final class StorageUsageScreenSelectionPanelComponent: Component {
self.layer.addSublayer(self.separatorLayer)
}
required init?(coder: NSCoder) {
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: StorageUsageScreenSelectionPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
func update(component: BottomButtonPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
@ -146,11 +138,11 @@ final class StorageUsageScreenSelectionPanelComponent: Component {
}
}
func makeView() -> View {
public func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -117,6 +117,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
private let button: HighlightTrackingButtonNode
public var disableAnimations: Bool = false
var manualLayout: Bool = false
private var validLayout: (CGSize, CGRect)?
@ -356,7 +358,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
if !self.updateStatus() {
if updated {
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.2, curve: .easeInOut))
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: self.disableAnimations ? .immediate : .animated(duration: 0.2, curve: .easeInOut))
}
}
}

View File

@ -21,7 +21,10 @@ swift_library(
"//submodules/Components/ViewControllerComponent",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode",
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
"//submodules/TelegramUI/Components/ChatTitleView",
"//submodules/ContextUI",
"//submodules/UndoUI",
"//submodules/TelegramUI/Components/BottomButtonPanelComponent",
],
visibility = [
"//visibility:public",

View File

@ -10,6 +10,9 @@ import PeerInfoVisualMediaPaneNode
import ViewControllerComponent
import ChatListHeaderComponent
import ContextUI
import ChatTitleView
import BottomButtonPanelComponent
import UndoUI
final class PeerInfoStoryGridScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -48,6 +51,13 @@ final class PeerInfoStoryGridScreenComponent: Component {
private var environment: EnvironmentType?
private var paneNode: PeerInfoStoryPaneNode?
private var paneStatusDisposable: Disposable?
private(set) var paneStatusText: String?
private(set) var selectedCount: Int = 0
private var selectionStateDisposable: Disposable?
private var selectionPanel: ComponentView<Empty>?
private weak var mediaGalleryContextMenu: ContextController?
@ -59,6 +69,11 @@ final class PeerInfoStoryGridScreenComponent: Component {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.paneStatusDisposable?.dispose()
self.selectionStateDisposable?.dispose()
}
func morePressed(source: ContextReferenceContentNode) {
guard let component = self.component, let controller = self.environment?.controller(), let pane = self.paneNode else {
return
@ -68,120 +83,168 @@ final class PeerInfoStoryGridScreenComponent: Component {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)?
let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in
let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement
let canZoom: Bool = nextZoomLevel != nil
return ContextMenuActionItem(id: isZoomIn ? 0 : 1, text: isZoomIn ? strings.SharedMedia_ZoomIn : strings.SharedMedia_ZoomOut, textColor: canZoom ? .primary : .disabled, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4))
}, action: canZoom ? { action in
guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else {
return
}
pane.updateZoomLevel(level: zoomLevel)
if let recurseGenerateAction = recurseGenerateAction {
action.updateAction(0, recurseGenerateAction(true))
action.updateAction(1, recurseGenerateAction(false))
}
} : nil)
}
recurseGenerateAction = { isZoomIn in
return generateAction(isZoomIn)
}
items.append(.action(generateAction(true)))
items.append(.action(generateAction(false)))
if component.peerId == component.context.account.peerId, case .saved = component.scope {
var ignoreNextActions = false
if self.selectedCount != 0 {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Show Archive", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/StoryArchive"), color: theme.contextMenu.primaryColor)
//TODO:update icon
items.append(.action(ContextMenuActionItem(text: "Save to Photos", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
if ignoreNextActions {
return
}
ignoreNextActions = true
a(.default)
guard let self, let component = self.component else {
return
}
self.environment?.controller()?.push(PeerInfoStoryGridScreen(context: component.context, peerId: component.peerId, scope: .archive))
let _ = component
})))
items.append(.action(ContextMenuActionItem(text: strings.Common_Delete, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { [weak self] _, a in
a(.default)
guard let self, let component = self.component, let environment = self.environment else {
return
}
guard let paneNode = self.paneNode, !paneNode.selectedIds.isEmpty else {
return
}
let _ = component.context.engine.messages.deleteStories(ids: Array(paneNode.selectedIds)).start()
//TODO:localize
let text: String
if paneNode.selectedIds.count == 1 {
text = "1 story deleted."
} else {
text = "\(paneNode.selectedIds.count) stories deleted."
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
environment.controller()?.present(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: text, timeout: nil),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
), in: .current)
paneNode.clearSelection()
})))
} else {
var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)?
let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in
let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement
let canZoom: Bool = nextZoomLevel != nil
return ContextMenuActionItem(id: isZoomIn ? 0 : 1, text: isZoomIn ? strings.SharedMedia_ZoomIn : strings.SharedMedia_ZoomOut, textColor: canZoom ? .primary : .disabled, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4))
}, action: canZoom ? { action in
guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else {
return
}
pane.updateZoomLevel(level: zoomLevel)
if let recurseGenerateAction = recurseGenerateAction {
action.updateAction(0, recurseGenerateAction(true))
action.updateAction(1, recurseGenerateAction(false))
}
} : nil)
}
recurseGenerateAction = { isZoomIn in
return generateAction(isZoomIn)
}
items.append(.action(generateAction(true)))
items.append(.action(generateAction(false)))
if component.peerId == component.context.account.peerId, case .saved = component.scope {
var ignoreNextActions = false
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Show Archive", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/StoryArchive"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
if ignoreNextActions {
return
}
ignoreNextActions = true
a(.default)
guard let self, let component = self.component else {
return
}
self.environment?.controller()?.push(PeerInfoStoryGridScreen(context: component.context, peerId: component.peerId, scope: .archive))
})))
}
/*if photoCount != 0 && videoCount != 0 {
items.append(.separator)
let showPhotos: Bool
switch pane.contentType {
case .photo, .photoOrVideo:
showPhotos = true
default:
showPhotos = false
}
let showVideos: Bool
switch pane.contentType {
case .video, .photoOrVideo:
showVideos = true
default:
showVideos = false
}
items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowPhotos, icon: { theme in
if !showPhotos {
return nil
}
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, action: { [weak pane] _, a in
a(.default)
guard let pane = pane else {
return
}
let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType
switch pane.contentType {
case .photoOrVideo:
updatedContentType = .video
case .photo:
updatedContentType = .photo
case .video:
updatedContentType = .photoOrVideo
default:
updatedContentType = pane.contentType
}
pane.updateContentType(contentType: updatedContentType)
})))
items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowVideos, icon: { theme in
if !showVideos {
return nil
}
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, action: { [weak pane] _, a in
a(.default)
guard let pane = pane else {
return
}
let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType
switch pane.contentType {
case .photoOrVideo:
updatedContentType = .photo
case .photo:
updatedContentType = .photoOrVideo
case .video:
updatedContentType = .video
default:
updatedContentType = pane.contentType
}
pane.updateContentType(contentType: updatedContentType)
})))
}*/
}
/*if photoCount != 0 && videoCount != 0 {
items.append(.separator)
let showPhotos: Bool
switch pane.contentType {
case .photo, .photoOrVideo:
showPhotos = true
default:
showPhotos = false
}
let showVideos: Bool
switch pane.contentType {
case .video, .photoOrVideo:
showVideos = true
default:
showVideos = false
}
items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowPhotos, icon: { theme in
if !showPhotos {
return nil
}
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, action: { [weak pane] _, a in
a(.default)
guard let pane = pane else {
return
}
let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType
switch pane.contentType {
case .photoOrVideo:
updatedContentType = .video
case .photo:
updatedContentType = .photo
case .video:
updatedContentType = .photoOrVideo
default:
updatedContentType = pane.contentType
}
pane.updateContentType(contentType: updatedContentType)
})))
items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowVideos, icon: { theme in
if !showVideos {
return nil
}
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, action: { [weak pane] _, a in
a(.default)
guard let pane = pane else {
return
}
let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType
switch pane.contentType {
case .photoOrVideo:
updatedContentType = .photo
case .photo:
updatedContentType = .photoOrVideo
case .video:
updatedContentType = .video
default:
updatedContentType = pane.contentType
}
pane.updateContentType(contentType: updatedContentType)
})))
}*/
let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
contextController.passthroughTouchEvent = { [weak self] sourceView, point in
guard let self else {
@ -217,6 +280,8 @@ final class PeerInfoStoryGridScreenComponent: Component {
self.component = component
self.state = state
let sideInset: CGFloat = 14.0
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
@ -227,6 +292,82 @@ final class PeerInfoStoryGridScreenComponent: Component {
self.backgroundColor = environment.theme.list.plainBackgroundColor
}
var bottomInset: CGFloat = environment.safeInsets.bottom
if self.selectedCount != 0 {
let selectionPanel: ComponentView<Empty>
var selectionPanelTransition = transition
if let current = self.selectionPanel {
selectionPanel = current
} else {
selectionPanelTransition = .immediate
selectionPanel = ComponentView()
self.selectionPanel = selectionPanel
}
//TODO:localize
let selectionPanelSize = selectionPanel.update(
transition: selectionPanelTransition,
component: AnyComponent(BottomButtonPanelComponent(
theme: environment.theme,
title: "Save to Profile",
label: nil,
isEnabled: true,
insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: environment.safeInsets.bottom, right: sideInset),
action: { [weak self] in
guard let self, let component = self.component, let environment = self.environment else {
return
}
guard let paneNode = self.paneNode, !paneNode.selectedIds.isEmpty else {
return
}
let _ = component.context.engine.messages.updateStoriesArePinned(ids: paneNode.selectedItems, isPinned: true).start()
//TODO:localize
let title: String
if paneNode.selectedIds.count == 1 {
title = "Story saved to your profile"
} else {
title = "\(paneNode.selectedIds.count) saved to your profile"
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
environment.controller()?.present(UndoOverlayController(
presentationData: presentationData,
content: .info(title: title, text: "Saved stories can be viewed by others on your profile until you remove them.", timeout: nil),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
), in: .current)
paneNode.clearSelection()
}
)),
environment: {},
containerSize: availableSize
)
if let selectionPanelView = selectionPanel.view {
var animateIn = false
if selectionPanelView.superview == nil {
self.addSubview(selectionPanelView)
animateIn = true
}
selectionPanelTransition.setFrame(view: selectionPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - selectionPanelSize.height), size: selectionPanelSize))
if animateIn {
transition.animatePosition(view: selectionPanelView, from: CGPoint(x: 0.0, y: selectionPanelSize.height), to: CGPoint(), additive: true)
}
}
bottomInset = selectionPanelSize.height
} else if let selectionPanel = self.selectionPanel {
self.selectionPanel = nil
if let selectionPanelView = selectionPanel.view {
transition.setPosition(view: selectionPanelView, position: CGPoint(x: selectionPanelView.center.x, y: availableSize.height + selectionPanelView.bounds.height * 0.5), completion: { [weak selectionPanelView] _ in
selectionPanelView?.removeFromSuperview()
})
}
}
let paneNode: PeerInfoStoryPaneNode
if let current = self.paneNode {
paneNode = current
@ -237,6 +378,7 @@ final class PeerInfoStoryGridScreenComponent: Component {
chatLocation: .peer(id: component.peerId),
contentType: .photoOrVideo,
captureProtected: false,
isSaved: true,
isArchive: component.scope == .archive,
navigationController: { [weak self] in
guard let self else {
@ -247,13 +389,41 @@ final class PeerInfoStoryGridScreenComponent: Component {
)
self.paneNode = paneNode
self.addSubview(paneNode.view)
self.paneStatusDisposable = (paneNode.status
|> deliverOnMainQueue).start(next: { [weak self] status in
guard let self else {
return
}
if self.paneStatusText != status?.text {
self.paneStatusText = status?.text
(self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle()
}
})
var applyState = false
self.selectionStateDisposable = (paneNode.updatedSelectedIds
|> distinctUntilChanged
|> deliverOnMainQueue).start(next: { [weak self] selectedIds in
guard let self else {
return
}
self.selectedCount = selectedIds.count
if applyState {
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
(self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle()
})
applyState = true
}
paneNode.update(
size: availableSize,
topInset: environment.navigationHeight,
sideInset: environment.safeInsets.left,
bottomInset: environment.safeInsets.bottom,
bottomInset: bottomInset,
visibleHeight: availableSize.height,
isScrollingLockedAtTop: false,
expandProgress: 1.0,
@ -283,8 +453,11 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
}
private let context: AccountContext
private let scope: Scope
private var isDismissed: Bool = false
private var titleView: ChatTitleView?
private var moreBarButton: MoreHeaderButton?
private var moreBarButtonItem: UIBarButtonItem?
@ -294,6 +467,7 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
scope: Scope
) {
self.context = context
self.scope = scope
super.init(context: context, component: PeerInfoStoryGridScreenComponent(
context: context,
@ -301,9 +475,6 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
scope: scope
), navigationBarAppearance: .default, theme: .default)
//TODO:localize
self.navigationItem.title = "My Stories"
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
let moreBarButton = MoreHeaderButton(color: presentationData.theme.rootController.navigationBar.buttonColor)
moreBarButton.isUserInteractionEnabled = true
@ -321,6 +492,21 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
moreBarButton.addTarget(self, action: #selector(self.morePressed), forControlEvents: .touchUpInside)
self.navigationItem.setRightBarButton(moreBarButtonItem, animated: false)
self.titleView = ChatTitleView(
context: context, theme:
presentationData.theme,
strings: presentationData.strings,
dateTimeFormat: presentationData.dateTimeFormat,
nameDisplayOrder: presentationData.nameDisplayOrder,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer
)
self.titleView?.disableAnimations = true
self.navigationItem.titleView = self.titleView
self.updateTitle()
}
required public init(coder aDecoder: NSCoder) {
@ -330,6 +516,34 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
deinit {
}
func updateTitle() {
//TODO:localize
switch self.scope {
case .saved:
guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else {
return
}
let title: String?
if let paneStatusText = componentView.paneStatusText, !paneStatusText.isEmpty {
title = paneStatusText
} else {
title = nil
}
self.titleView?.titleContent = .custom("My Stories", title, false)
case .archive:
guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else {
return
}
let title: String
if componentView.selectedCount != 0 {
title = "\(componentView.selectedCount) Selected"
} else {
title = "Stories Archive"
}
self.titleView?.titleContent = .custom(title, nil, false)
}
}
@objc private func morePressed() {
guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else {
return
@ -342,6 +556,8 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.titleView?.layout = layout
}
}

View File

@ -476,6 +476,49 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL
}
}
private final class ItemTransitionView: UIView {
private weak var itemLayer: ItemLayer?
private var copyDurationLayer: SimpleLayer?
private var durationLayerBottomLeftPosition: CGPoint?
init(itemLayer: ItemLayer?) {
self.itemLayer = itemLayer
super.init(frame: CGRect())
if let itemLayer {
self.layer.contents = itemLayer.contents
self.layer.contentsRect = itemLayer.contentsRect
if let durationLayer = itemLayer.durationLayer {
let copyDurationLayer = SimpleLayer()
copyDurationLayer.contents = durationLayer.contents
copyDurationLayer.contentsRect = durationLayer.contentsRect
copyDurationLayer.contentsGravity = durationLayer.contentsGravity
copyDurationLayer.contentsScale = durationLayer.contentsScale
copyDurationLayer.frame = durationLayer.frame
self.layer.addSublayer(copyDurationLayer)
self.copyDurationLayer = copyDurationLayer
self.durationLayerBottomLeftPosition = CGPoint(x: itemLayer.bounds.width - durationLayer.frame.maxX, y: itemLayer.bounds.height - durationLayer.frame.maxY)
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(state: StoryContainerScreen.TransitionState, transition: Transition) {
let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)
if let copyDurationLayer = self.copyDurationLayer, let durationLayerBottomLeftPosition = self.durationLayerBottomLeftPosition {
transition.setFrame(layer: copyDurationLayer, frame: CGRect(origin: CGPoint(x: size.width - durationLayerBottomLeftPosition.x - copyDurationLayer.bounds.width, y: size.height - durationLayerBottomLeftPosition.y - copyDurationLayer.bounds.height), size: copyDurationLayer.bounds.size))
}
}
}
private final class SparseItemGridBindingImpl: SparseItemGridBinding {
let context: AccountContext
let chatLocation: ChatLocation
@ -485,8 +528,9 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
var chatPresentationData: ChatPresentationData
var checkNodeTheme: CheckNodeTheme
var itemInteraction: VisualMediaItemInteraction?
var loadHoleImpl: ((SparseItemGrid.HoleAnchor, SparseItemGrid.HoleLocation) -> Signal<Never, NoError>)?
var onTapImpl: ((VisualMediaItem) -> Void)?
var onTapImpl: ((VisualMediaItem, CALayer, CGPoint) -> Void)?
var onTagTapImpl: (() -> Void)?
var didScrollImpl: (() -> Void)?
var coveringInsetOffsetUpdatedImpl: ((ContainedViewLayoutTransition) -> Void)?
@ -671,8 +715,11 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
layer.updateDuration(duration: duration, isMin: isMin, minFactor: min(1.0, layer.bounds.height / 74.0))
}
//TODO:selection
layer.updateSelection(theme: self.checkNodeTheme, isSelected: nil, animated: false)
var isSelected: Bool?
if let selectedIds = self.itemInteraction?.selectedIds {
isSelected = selectedIds.contains(story.id)
}
layer.updateSelection(theme: self.checkNodeTheme, isSelected: isSelected, animated: false)
layer.bind(item: item)
}
@ -698,11 +745,11 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
}
}
func onTap(item: SparseItemGrid.Item) {
func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint) {
guard let item = item as? VisualMediaItem else {
return
}
self.onTapImpl?(item)
self.onTapImpl?(item, itemLayer, point)
}
func onTagTap() {
@ -756,6 +803,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
private let context: AccountContext
private let peerId: PeerId
private let chatLocation: ChatLocation
private let isSaved: Bool
private let isArchive: Bool
public private(set) var contentType: ContentType
private var contentTypePromise: ValuePromise<ContentType>
@ -778,6 +826,30 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return self._itemInteraction!
}
public var selectedIds: Set<Int32> {
return self.itemInteraction.selectedIds ?? Set()
}
private let selectedIdsPromise = ValuePromise<Set<Int32>>(Set())
public var updatedSelectedIds: Signal<Set<Int32>, NoError> {
return self.selectedIdsPromise.get()
}
public var selectedItems: [Int32: EngineStoryItem] {
var result: [Int32: EngineStoryItem] = [:]
for id in self.selectedIds {
if let items = self.items {
for item in items.items {
if let item = item as? VisualMediaItem {
if item.story.id == id {
result[id] = item.story
}
}
}
}
}
return result
}
private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)?
private let ready = Promise<Bool>()
@ -818,13 +890,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
public init(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, contentType: ContentType, captureProtected: Bool, isArchive: Bool, navigationController: @escaping () -> NavigationController?) {
public init(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, contentType: ContentType, captureProtected: Bool, isSaved: Bool, isArchive: Bool, navigationController: @escaping () -> NavigationController?) {
self.context = context
self.peerId = peerId
self.chatLocation = chatLocation
self.contentType = contentType
self.contentTypePromise = ValuePromise<ContentType>(contentType)
self.navigationController = navigationController
self.isSaved = isSaved
self.isArchive = isArchive
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
@ -867,10 +940,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return strongSelf.loadHole(anchor: hole, at: location)
}
self.itemGridBinding.onTapImpl = { [weak self] item in
self.itemGridBinding.onTapImpl = { [weak self] item, itemLayer, point in
guard let self else {
return
}
if let selectedIds = self.itemInteraction.selectedIds, let itemLayer = itemLayer as? ItemLayer, let selectionLayer = itemLayer.selectionLayer {
if selectionLayer.checkLayer.frame.insetBy(dx: -4.0, dy: -4.0).contains(point) {
self.itemInteraction.toggleSelection(item.story.id, !selectedIds.contains(item.story.id))
return
}
}
//TODO:selection
let listContext = PeerStoryListContentContextImpl(
context: self.context,
@ -928,6 +1009,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let itemRect = self.itemGrid.frameForItem(layer: foundItemLayer)
return StoryContainerScreen.TransitionOut(
destinationView: self.view,
transitionView: StoryContainerScreen.TransitionView(
makeView: { [weak foundItemLayer] in
return ItemTransitionView(itemLayer: foundItemLayer as? ItemLayer)
},
updateView: { view, state, transition in
(view as? ItemTransitionView)?.update(state: state, transition: transition)
}
),
destinationRect: self.itemGrid.view.convert(itemRect, to: self.view),
destinationCornerRadius: 0.0,
destinationIsAvatar: false,
@ -938,6 +1027,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return nil
}
)
self.hiddenMediaDisposable?.dispose()
self.hiddenMediaDisposable = (storyContainerScreen.focusedItem
|> deliverOnMainQueue).start(next: { [weak self] itemId in
guard let self else {
return
}
if let itemId {
self.itemInteraction.hiddenMedia = Set([itemId.id])
} else {
self.itemInteraction.hiddenMedia = Set()
}
self.updateHiddenItems()
})
navigationController.pushViewController(storyContainerScreen)
})
}
@ -1043,14 +1147,26 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let _ = self
},
toggleSelection: { [weak self] id, value in
guard let self else {
guard let self, let itemInteraction = self._itemInteraction else {
return
}
let _ = self
if var selectedIds = itemInteraction.selectedIds {
if value {
selectedIds.insert(id)
} else {
selectedIds.remove(id)
}
itemInteraction.selectedIds = selectedIds
self.selectedIdsPromise.set(selectedIds)
self.updateSelectedItems(animated: true)
}
}
)
//TODO:selection
//self.itemInteraction.selectedItemIds =
if isArchive {
self._itemInteraction?.selectedIds = Set()
}
self.itemGridBinding.itemInteraction = self._itemInteraction
self.contextGestureContainerNode.isGestureEnabled = true
self.contextGestureContainerNode.addSubnode(self.itemGrid)
@ -1136,6 +1252,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
strongSelf.itemGrid.cancelGestures()
}
self.statusPromise.set(.single(PeerInfoStatusData(text: "", isActivity: false, key: .stories)))
/*self.storedStateDisposable = (visualMediaStoredState(engine: context.engine, peerId: peerId, messageTag: self.stateTag)
|> deliverOnMainQueue).start(next: { [weak self] value in
@ -1385,6 +1503,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return
}
let title: String
if state.totalCount == 0 {
title = ""
} else if state.totalCount == 1 {
if self.isSaved {
title = "1 saved story"
} else {
title = "1 story"
}
} else {
if self.isSaved {
title = "\(state.totalCount) saved stories"
} else {
title = "\(state.totalCount) stories"
}
}
self.statusPromise.set(.single(PeerInfoStatusData(text: title, isActivity: false, key: .stories)))
let timezoneOffset = Int32(TimeZone.current.secondsFromGMT())
var mappedItems: [SparseItemGrid.Item] = []
@ -1402,6 +1538,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
totalCount = state.totalCount
totalCount = max(mappedItems.count, totalCount)
if totalCount == 0 {
totalCount = 100
}
Queue.mainQueue().async { [weak self] in
guard let strongSelf = self else {
@ -1626,67 +1766,64 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
}
public func clearSelection() {
self.itemInteraction.selectedIds = Set()
self.selectedIdsPromise.set(Set())
self.updateSelectedItems(animated: true)
}
public func updateSelectedMessages(animated: Bool) {
/*switch self.contentType {
case .files, .music, .voiceAndVideoMessages:
self.itemGrid.forEachVisibleItem { item in
guard let itemView = item.view as? ItemView, let (size, topInset, sideInset, bottomInset, _, _, _, _) = self.currentParams else {
return
}
if let item = itemView.item {
itemView.bind(
item: item,
presentationData: self.itemGridBinding.chatPresentationData,
context: self.itemGridBinding.context,
chatLocation: self.itemGridBinding.chatLocation,
interaction: self.itemGridBinding.listItemInteraction,
isSelected: self.chatControllerInteraction.selectionState?.selectedIds.contains(item.message.id),
size: CGSize(width: size.width, height: itemView.bounds.height),
insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset)
)
}
}
case .photo, .video, .photoOrVideo, .gifs:
self.itemGrid.forEachVisibleItem { item in
guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item else {
return
}
itemLayer.updateSelection(theme: self.itemGridBinding.checkNodeTheme, isSelected: self.chatControllerInteraction.selectionState?.selectedIds.contains(item.message.id), animated: animated)
}
private func updateSelectedItems(animated: Bool) {
self.itemGrid.forEachVisibleItem { item in
guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item else {
return
}
itemLayer.updateSelection(theme: self.itemGridBinding.checkNodeTheme, isSelected: self.itemInteraction.selectedIds?.contains(item.story.id), animated: animated)
}
let isSelecting = self.chatControllerInteraction.selectionState != nil
self.itemGrid.pinchEnabled = !isSelecting
if isSelecting {
if self.gridSelectionGesture == nil {
let selectionGesture = MediaPickerGridSelectionGesture<EngineMessage.Id>()
selectionGesture.delegate = self
selectionGesture.sideInset = 44.0
selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in
self?.itemGrid.isScrollEnabled = isEnabled
}
selectionGesture.itemAt = { [weak self] point in
if let strongSelf = self, let itemLayer = strongSelf.itemGrid.item(at: point)?.layer as? ItemLayer, let messageId = itemLayer.item?.message.id {
return (messageId, strongSelf.chatControllerInteraction.selectionState?.selectedIds.contains(messageId) ?? false)
} else {
return nil
}
}
selectionGesture.updateSelection = { [weak self] messageId, selected in
if let strongSelf = self {
strongSelf.chatControllerInteraction.toggleMessagesSelection([messageId], selected)
}
}
self.itemGrid.view.addGestureRecognizer(selectionGesture)
self.gridSelectionGesture = selectionGesture
/*let isSelecting = self.chatControllerInteraction.selectionState != nil
self.itemGrid.pinchEnabled = !isSelecting
if isSelecting {
if self.gridSelectionGesture == nil {
let selectionGesture = MediaPickerGridSelectionGesture<EngineMessage.Id>()
selectionGesture.delegate = self
selectionGesture.sideInset = 44.0
selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in
self?.itemGrid.isScrollEnabled = isEnabled
}
} else if let gridSelectionGesture = self.gridSelectionGesture {
self.itemGrid.view.removeGestureRecognizer(gridSelectionGesture)
self.gridSelectionGesture = nil
selectionGesture.itemAt = { [weak self] point in
if let strongSelf = self, let itemLayer = strongSelf.itemGrid.item(at: point)?.layer as? ItemLayer, let messageId = itemLayer.item?.message.id {
return (messageId, strongSelf.chatControllerInteraction.selectionState?.selectedIds.contains(messageId) ?? false)
} else {
return nil
}
}
selectionGesture.updateSelection = { [weak self] messageId, selected in
if let strongSelf = self {
strongSelf.chatControllerInteraction.toggleMessagesSelection([messageId], selected)
}
}
self.itemGrid.view.addGestureRecognizer(selectionGesture)
self.gridSelectionGesture = selectionGesture
}
} else if let gridSelectionGesture = self.gridSelectionGesture {
self.itemGrid.view.removeGestureRecognizer(gridSelectionGesture)
self.gridSelectionGesture = nil
}*/
}
private func updateHiddenItems() {
self.itemGrid.forEachVisibleItem { item in
guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item else {
return
}
itemLayer.isHidden = self.itemInteraction.hiddenMedia.contains(item.story.id)
}
}
public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
@ -1704,8 +1841,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
let fixedItemAspect: CGFloat? = 9.0 / 16.0
let gridTopInset = topInset
self.itemGrid.update(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none)
self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none)
}
}

View File

@ -1144,7 +1144,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme
}
}
func onTap(item: SparseItemGrid.Item) {
func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint) {
guard let item = item as? VisualMediaItem else {
return
}

View File

@ -27,6 +27,7 @@ swift_library(
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/BottomButtonPanelComponent",
"//submodules/CheckNode",
"//submodules/Markdown",
"//submodules/ContextUI",

View File

@ -23,6 +23,7 @@ import TelegramAnimatedStickerNode
import TelegramStringFormatting
import GalleryData
import AnimatedTextComponent
import BottomButtonPanelComponent
#if DEBUG
import os.signpost
@ -1207,7 +1208,7 @@ final class StorageUsageScreenComponent: Component {
let selectionPanelSize = selectionPanel.update(
transition: selectionPanelTransition,
component: AnyComponent(StorageUsageScreenSelectionPanelComponent(
component: AnyComponent(BottomButtonPanelComponent(
theme: environment.theme,
title: bottomPanelSelectionData.isComplete ? environment.strings.StorageManagement_ClearCache : environment.strings.StorageManagement_ClearSelected,
label: bottomPanelSelectionData.size == 0 ? nil : dataSizeString(Int(bottomPanelSelectionData.size), formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".")),

View File

@ -29,22 +29,63 @@ func hasFirstResponder(_ view: UIView) -> Bool {
return false
}
private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
var updateIsTracking: ((Bool) -> Void)?
override var state: UIGestureRecognizer.State {
didSet {
switch self.state {
case .began, .cancelled, .ended, .failed:
if self.isTracking {
self.isTracking = false
self.updateIsTracking?(false)
}
default:
break
}
}
}
private var isTracking: Bool = false
override func reset() {
super.reset()
if self.isTracking {
self.isTracking = false
self.updateIsTracking?(false)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if !self.isTracking {
self.isTracking = true
self.updateIsTracking?(true)
}
}
}
private final class StoryContainerScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let content: StoryContentContext
let focusedItemPromise: Promise<StoryId?>
let transitionIn: StoryContainerScreen.TransitionIn?
let transitionOut: (EnginePeer.Id, AnyHashable) -> StoryContainerScreen.TransitionOut?
init(
context: AccountContext,
content: StoryContentContext,
focusedItemPromise: Promise<StoryId?>,
transitionIn: StoryContainerScreen.TransitionIn?,
transitionOut: @escaping (EnginePeer.Id, AnyHashable) -> StoryContainerScreen.TransitionOut?
) {
self.context = context
self.content = content
self.focusedItemPromise = focusedItemPromise
self.transitionIn = transitionIn
self.transitionOut = transitionOut
}
@ -118,12 +159,14 @@ private final class StoryContainerScreenComponent: Component {
private let backgroundLayer: SimpleLayer
private let backgroundEffectView: BlurredBackgroundView
private let focusedItem = ValuePromise<StoryId?>(nil, ignoreRepeated: true)
private var contentUpdatedDisposable: Disposable?
private var visibleItemSetViews: [EnginePeer.Id: ItemSetView] = [:]
private var itemSetPanState: ItemSetPanState?
private var dismissPanState: ItemSetPanState?
private var isHoldingTouch: Bool = false
private var isAnimatingOut: Bool = false
private var didAnimateOut: Bool = false
@ -163,8 +206,15 @@ private final class StoryContainerScreenComponent: Component {
})
self.addGestureRecognizer(verticalPanRecognizer)
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:)))
let longPressRecognizer = StoryLongPressRecognizer(target: self, action: #selector(self.longPressGesture(_:)))
longPressRecognizer.delegate = self
longPressRecognizer.updateIsTracking = { [weak self] isTracking in
guard let self else {
return
}
self.isHoldingTouch = isTracking
self.state?.updated(transition: .immediate)
}
self.addGestureRecognizer(longPressRecognizer)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
@ -316,7 +366,7 @@ private final class StoryContainerScreenComponent: Component {
}
}
@objc private func longPressGesture(_ recognizer: UILongPressGestureRecognizer) {
@objc private func longPressGesture(_ recognizer: StoryLongPressRecognizer) {
switch recognizer.state {
case .began:
if self.itemSetPanState == nil {
@ -381,6 +431,10 @@ private final class StoryContainerScreenComponent: Component {
}
func animateIn() {
if let component = self.component {
component.focusedItemPromise.set(self.focusedItem.get())
}
if let transitionIn = self.component?.transitionIn, transitionIn.sourceView != nil {
self.backgroundLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.28, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
self.backgroundEffectView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.28, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
@ -409,17 +463,22 @@ private final class StoryContainerScreenComponent: Component {
transition.setAlpha(view: self.backgroundEffectView, alpha: 0.0)
let transitionOutCompleted = transitionOut.completed
let focusedItemPromise = component.focusedItemPromise
itemSetComponentView.animateOut(transitionOut: transitionOut, completion: {
completion()
transitionOutCompleted()
focusedItemPromise.set(.single(nil))
})
} else {
self.dismissPanState = ItemSetPanState(fraction: 1.0, didBegin: true)
self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
let focusedItemPromise = self.component?.focusedItemPromise
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
transition.setAlpha(layer: self.backgroundLayer, alpha: 0.0, completion: { _ in
completion()
focusedItemPromise?.set(.single(nil))
})
transition.setAlpha(view: self.backgroundEffectView, alpha: 0.0)
}
@ -475,6 +534,12 @@ private final class StoryContainerScreenComponent: Component {
return
}
if update {
var focusedItemId: StoryId?
if let slice = component.content.stateValue?.slice {
focusedItemId = StoryId(peerId: slice.peer.id, id: slice.item.storyItem.id)
}
self.focusedItem.set(focusedItemId)
if component.content.stateValue?.slice == nil {
self.environment?.controller()?.dismiss()
} else {
@ -511,6 +576,9 @@ private final class StoryContainerScreenComponent: Component {
if self.isAnimatingOut {
isProgressPaused = true
}
if self.isHoldingTouch {
isProgressPaused = true
}
var dismissPanOffset: CGFloat = 0.0
var dismissPanScale: CGFloat = 1.0
@ -684,9 +752,15 @@ private final class StoryContainerScreenComponent: Component {
environment.controller()?.dismiss()
}
let _ = component.context.engine.messages.deleteStory(id: slice.item.storyItem.id).start()
let _ = component.context.engine.messages.deleteStories(ids: [slice.item.storyItem.id]).start()
}
},
markAsSeen: { [weak self] id in
guard let self, let component = self.component else {
return
}
component.content.markAsSeen(id: id)
},
controller: { [weak self] in
return self?.environment?.controller()
}
@ -865,6 +939,35 @@ private final class StoryContainerScreenComponent: Component {
}
public class StoryContainerScreen: ViewControllerComponentContainer {
public struct TransitionState: Equatable {
public var sourceSize: CGSize
public var destinationSize: CGSize
public var progress: CGFloat
public init(
sourceSize: CGSize,
destinationSize: CGSize,
progress: CGFloat
) {
self.sourceSize = sourceSize
self.destinationSize = destinationSize
self.progress = progress
}
}
public final class TransitionView {
public let makeView: () -> UIView
public let updateView: (UIView, TransitionState, Transition) -> Void
public init(
makeView: @escaping () -> UIView,
updateView: @escaping (UIView, TransitionState, Transition) -> Void
) {
self.makeView = makeView
self.updateView = updateView
}
}
public final class TransitionIn {
public weak var sourceView: UIView?
public let sourceRect: CGRect
@ -883,6 +986,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
public final class TransitionOut {
public weak var destinationView: UIView?
public let transitionView: TransitionView?
public let destinationRect: CGRect
public let destinationCornerRadius: CGFloat
public let destinationIsAvatar: Bool
@ -890,12 +994,14 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
public init(
destinationView: UIView,
transitionView: TransitionView?,
destinationRect: CGRect,
destinationCornerRadius: CGFloat,
destinationIsAvatar: Bool,
completed: @escaping () -> Void
) {
self.destinationView = destinationView
self.transitionView = transitionView
self.destinationRect = destinationRect
self.destinationCornerRadius = destinationCornerRadius
self.destinationIsAvatar = destinationIsAvatar
@ -906,6 +1012,11 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
private let context: AccountContext
private var isDismissed: Bool = false
private let focusedItemPromise = Promise<StoryId?>(nil)
public var focusedItem: Signal<StoryId?, NoError> {
return self.focusedItemPromise.get()
}
public init(
context: AccountContext,
content: StoryContentContext,
@ -917,6 +1028,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
super.init(context: context, component: StoryContainerScreenComponent(
context: context,
content: content,
focusedItemPromise: self.focusedItemPromise,
transitionIn: transitionIn,
transitionOut: transitionOut
), navigationBarAppearance: .none, theme: .dark)
@ -925,6 +1037,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
self.automaticallyControlPresentationContextLayout = false
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: [.portrait])
self.context.sharedContext.hasPreloadBlockingContent.set(.single(true))
}

View File

@ -4,6 +4,7 @@ import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import Postbox
public final class StoryContentItem {
public final class ExternalState {
@ -22,13 +23,16 @@ public final class StoryContentItem {
public final class Environment: Equatable {
public let externalState: ExternalState
public let presentationProgressUpdated: (Double, Bool) -> Void
public let markAsSeen: (StoryId) -> Void
public init(
externalState: ExternalState,
presentationProgressUpdated: @escaping (Double, Bool) -> Void
presentationProgressUpdated: @escaping (Double, Bool) -> Void,
markAsSeen: @escaping (StoryId) -> Void
) {
self.externalState = externalState
self.presentationProgressUpdated = presentationProgressUpdated
self.markAsSeen = markAsSeen
}
public static func ==(lhs: Environment, rhs: Environment) -> Bool {
@ -46,10 +50,6 @@ public final class StoryContentItem {
public let rightInfoComponent: AnyComponent<Empty>?
public let peerId: EnginePeer.Id?
public let storyItem: EngineStoryItem
public let preload: Signal<Never, NoError>?
public let delete: (() -> Void)?
public let markAsSeen: (() -> Void)?
public let hasLike: Bool
public let isMy: Bool
public init(
@ -60,10 +60,6 @@ public final class StoryContentItem {
rightInfoComponent: AnyComponent<Empty>?,
peerId: EnginePeer.Id?,
storyItem: EngineStoryItem,
preload: Signal<Never, NoError>?,
delete: (() -> Void)?,
markAsSeen: (() -> Void)?,
hasLike: Bool,
isMy: Bool
) {
self.id = id
@ -73,10 +69,6 @@ public final class StoryContentItem {
self.rightInfoComponent = rightInfoComponent
self.peerId = peerId
self.storyItem = storyItem
self.preload = preload
self.delete = delete
self.markAsSeen = markAsSeen
self.hasLike = hasLike
self.isMy = isMy
}
}
@ -183,4 +175,5 @@ public protocol StoryContentContext: AnyObject {
func resetSideStates()
func navigate(navigation: StoryContentContextNavigation)
func markAsSeen(id: StoryId)
}

View File

@ -51,6 +51,7 @@ public final class StoryItemSetContainerComponent: Component {
public let close: () -> Void
public let navigate: (NavigationDirection) -> Void
public let delete: () -> Void
public let markAsSeen: (StoryId) -> Void
public let controller: () -> ViewController?
public init(
@ -71,6 +72,7 @@ public final class StoryItemSetContainerComponent: Component {
close: @escaping () -> Void,
navigate: @escaping (NavigationDirection) -> Void,
delete: @escaping () -> Void,
markAsSeen: @escaping (StoryId) -> Void,
controller: @escaping () -> ViewController?
) {
self.context = context
@ -90,6 +92,7 @@ public final class StoryItemSetContainerComponent: Component {
self.close = close
self.navigate = navigate
self.delete = delete
self.markAsSeen = markAsSeen
self.controller = controller
}
@ -489,31 +492,38 @@ public final class StoryItemSetContainerComponent: Component {
self.visibleItems[focusedItem.id] = visibleItem
}
let itemEnvironment = StoryContentItem.Environment(
externalState: visibleItem.externalState,
presentationProgressUpdated: { [weak self, weak visibleItem] progress, canSwitch in
guard let self = self, let component = self.component else {
return
}
guard let visibleItem else {
return
}
visibleItem.currentProgress = progress
if let navigationStripView = self.navigationStrip.view as? MediaNavigationStripComponent.View {
navigationStripView.updateCurrentItemProgress(value: progress, transition: .immediate)
}
if progress >= 1.0 && canSwitch && !visibleItem.requestedNext {
visibleItem.requestedNext = true
component.navigate(.next)
}
},
markAsSeen: { [weak self] id in
guard let self, let component = self.component else {
return
}
component.markAsSeen(id)
}
)
let _ = visibleItem.view.update(
transition: itemTransition,
component: focusedItem.component,
environment: {
StoryContentItem.Environment(
externalState: visibleItem.externalState,
presentationProgressUpdated: { [weak self, weak visibleItem] progress, canSwitch in
guard let self = self, let component = self.component else {
return
}
guard let visibleItem else {
return
}
visibleItem.currentProgress = progress
if let navigationStripView = self.navigationStrip.view as? MediaNavigationStripComponent.View {
navigationStripView.updateCurrentItemProgress(value: progress, transition: .immediate)
}
if progress >= 1.0 && canSwitch && !visibleItem.requestedNext {
visibleItem.requestedNext = true
component.navigate(.next)
}
}
)
itemEnvironment
},
containerSize: itemLayout.size
)
@ -678,6 +688,8 @@ public final class StoryItemSetContainerComponent: Component {
let sourceLocalFrame = sourceView.convert(transitionOut.destinationRect, to: self)
let innerSourceLocalFrame = CGRect(origin: CGPoint(x: sourceLocalFrame.minX - self.contentContainerView.frame.minX, y: sourceLocalFrame.minY - self.contentContainerView.frame.minY), size: sourceLocalFrame.size)
let contentSourceFrame = self.contentContainerView.frame
if let centerInfoView = self.centerInfoItem?.view.view {
centerInfoView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
}
@ -703,6 +715,35 @@ public final class StoryItemSetContainerComponent: Component {
removeOnCompletion: false
)
let transitionView = transitionOut.transitionView
let transitionViewImpl = transitionView?.makeView()
if let transitionViewImpl {
self.insertSubview(transitionViewImpl, belowSubview: self.contentContainerView)
transitionViewImpl.frame = contentSourceFrame
transitionViewImpl.alpha = 0.0
transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState(
sourceSize: contentSourceFrame.size,
destinationSize: sourceLocalFrame.size,
progress: 0.0
), .immediate)
}
if let transitionViewImpl {
let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
transitionViewImpl.alpha = 1.0
transitionViewImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
self.contentContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame)
transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState(
sourceSize: contentSourceFrame.size,
destinationSize: sourceLocalFrame.size,
progress: 1.0
), transition)
}
if let component = self.component, let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view {
let innerScale = innerSourceLocalFrame.width / visibleItemView.bounds.width
@ -755,7 +796,7 @@ public final class StoryItemSetContainerComponent: Component {
}
if self.component?.slice.item.storyItem.id != component.slice.item.storyItem.id {
let _ = component.context.engine.messages.markStoryAsSeen(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id).start()
component.markAsSeen(StoryId(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id))
}
if self.topContentGradientLayer.colors == nil {
@ -1069,7 +1110,7 @@ public final class StoryItemSetContainerComponent: Component {
return
}
let _ = component.context.engine.messages.updateStoryIsPinned(id: component.slice.item.storyItem.id, isPinned: !component.slice.item.storyItem.isPinned).start()
let _ = component.context.engine.messages.updateStoriesArePinned(ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).start()
if component.slice.item.storyItem.isPinned {
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)

View File

@ -197,20 +197,6 @@ public final class StoryContentContextImpl: StoryContentContext {
)),
peerId: peer.id,
storyItem: mappedItem,
preload: nil,
delete: { [weak context] in
guard let context else {
return
}
let _ = context
},
markAsSeen: { [weak context] in
guard let context else {
return
}
let _ = context.engine.messages.markStoryAsSeen(peerId: peerId, id: item.id).start()
},
hasLike: false,
isMy: peerId == context.account.peerId
),
totalCount: itemsView.items.count,
@ -727,6 +713,10 @@ public final class StoryContentContextImpl: StoryContentContext {
}
}
}
public func markAsSeen(id: StoryId) {
let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: false).start()
}
}
public final class SingleStoryContentContextImpl: StoryContentContext {
@ -818,20 +808,6 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
)),
peerId: peer.id,
storyItem: mappedItem,
preload: nil,
delete: { [weak context] in
guard let context else {
return
}
let _ = context
},
markAsSeen: { [weak context] in
guard let context else {
return
}
let _ = context.engine.messages.markStoryAsSeen(peerId: peer.id, id: item.id).start()
},
hasLike: false,
isMy: peer.id == context.account.peerId
),
totalCount: 1,
@ -873,6 +849,9 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
public func navigate(navigation: StoryContentContextNavigation) {
}
public func markAsSeen(id: StoryId) {
}
}
public final class PeerStoryListContentContextImpl: StoryContentContext {
@ -899,6 +878,9 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
private var focusedId: Int32?
private var focusedIdUpdated = Promise<Void>(Void())
private var preloadStoryResourceDisposables: [MediaResourceId: Disposable] = [:]
private var pollStoryMetadataDisposables = DisposableSet()
public init(context: AccountContext, peerId: EnginePeer.Id, listContext: PeerStoryListContext, initialId: Int32?) {
self.context = context
@ -968,12 +950,6 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
)),
peerId: peer.id,
storyItem: item,
preload: nil,
delete: {
},
markAsSeen: {
},
hasLike: false,
isMy: peerId == self.context.account.peerId
),
totalCount: state.totalCount,
@ -997,6 +973,97 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
self.stateValue = stateValue
self.statePromise.set(.single(stateValue))
self.updatedPromise.set(.single(Void()))
var resultResources: [EngineMediaResource.Id: StoryPreloadInfo] = [:]
var pollItems: [StoryKey] = []
if let peer, let focusedIndex, let slice = stateValue.slice {
var possibleItems: [(EnginePeer, EngineStoryItem)] = []
if peer.id == self.context.account.peerId {
pollItems.append(StoryKey(peerId: peer.id, id: slice.item.storyItem.id))
}
for i in focusedIndex ..< min(focusedIndex + 4, state.items.count) {
if i != focusedIndex {
possibleItems.append((slice.peer, state.items[i]))
}
if slice.peer.id == self.context.account.peerId {
pollItems.append(StoryKey(peerId: slice.peer.id, id: state.items[i].id))
}
}
var nextPriority = 0
for i in 0 ..< min(possibleItems.count, 3) {
let peer = possibleItems[i].0
let item = possibleItems[i].1
if let peerReference = PeerReference(peer._asPeer()) {
if let image = item.media._asMedia() as? TelegramMediaImage, let resource = image.representations.last?.resource {
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: image), resource: resource)
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
resource: resource,
size: nil,
priority: .top(position: nextPriority)
)
nextPriority += 1
} else if let file = item.media._asMedia() as? TelegramMediaFile {
if let preview = file.previewRepresentations.last {
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: preview.resource)
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
resource: resource,
size: nil,
priority: .top(position: nextPriority)
)
nextPriority += 1
}
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: file.resource)
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
resource: resource,
size: file.preloadSize,
priority: .top(position: nextPriority)
)
nextPriority += 1
}
}
}
}
var validIds: [MediaResourceId] = []
for (_, info) in resultResources.sorted(by: { $0.value.priority < $1.value.priority }) {
let resource = info.resource
validIds.append(resource.resource.id)
if self.preloadStoryResourceDisposables[resource.resource.id] == nil {
var fetchRange: (Range<Int64>, MediaBoxFetchPriority)?
if let size = info.size {
fetchRange = (0 ..< Int64(size), .default)
}
self.preloadStoryResourceDisposables[resource.resource.id] = fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resource, range: fetchRange).start()
}
}
var removeIds: [MediaResourceId] = []
for (id, disposable) in self.preloadStoryResourceDisposables {
if !validIds.contains(id) {
removeIds.append(id)
disposable.dispose()
}
}
for id in removeIds {
self.preloadStoryResourceDisposables.removeValue(forKey: id)
}
var pollIdByPeerId: [EnginePeer.Id: [Int32]] = [:]
for storyKey in pollItems.prefix(3) {
if pollIdByPeerId[storyKey.peerId] == nil {
pollIdByPeerId[storyKey.peerId] = [storyKey.id]
} else {
pollIdByPeerId[storyKey.peerId]?.append(storyKey.id)
}
}
for (peerId, ids) in pollIdByPeerId {
self.pollStoryMetadataDisposables.add(self.context.engine.messages.refreshStoryViews(peerId: peerId, ids: ids).start())
}
}
})
}
@ -1004,6 +1071,11 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
deinit {
self.storyDisposable?.dispose()
self.requestStoryDisposables.dispose()
for (_, disposable) in self.preloadStoryResourceDisposables {
disposable.dispose()
}
self.pollStoryMetadataDisposables.dispose()
}
public func resetSideStates() {
@ -1039,4 +1111,8 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
}
}
}
public func markAsSeen(id: StoryId) {
let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: true).start()
}
}

View File

@ -4,6 +4,7 @@ import Display
import ComponentFlow
import AccountContext
import TelegramCore
import Postbox
import AsyncDisplayKit
import PhotoResources
import SwiftSignalKit
@ -240,7 +241,7 @@ final class StoryItemContentComponent: Component {
if !self.markedAsSeen {
self.markedAsSeen = true
if let component = self.component {
let _ = component.context.engine.messages.markStoryAsSeen(peerId: component.peer.id, id: component.item.id).start()
self.environment?.markAsSeen(StoryId(peerId: component.peer.id, id: component.item.id))
}
}
@ -319,7 +320,7 @@ final class StoryItemContentComponent: Component {
if !self.markedAsSeen {
self.markedAsSeen = true
if let component = self.component {
let _ = component.context.engine.messages.markStoryAsSeen(peerId: component.peer.id, id: component.item.id).start()
self.environment?.markAsSeen(StoryId(peerId: component.peer.id, id: component.item.id))
}
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Stories.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,5 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="30" height="30" rx="7" fill="#FF2D55"/>
<path d="M15 23C10.5817 23 7 19.4183 7 15C7 10.5817 10.5817 7 15 7" stroke="white" stroke-width="2" stroke-linecap="round"/>
<circle cx="15" cy="15" r="8" stroke="white" stroke-width="2" stroke-linecap="round" stroke-dasharray="1.32 4.32"/>
</svg>

After

Width:  |  Height:  |  Size: 397 B

View File

@ -4564,6 +4564,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let result = itemNode.targetForStoryTransition(id: storyId) {
transitionOut = StoryContainerScreen.TransitionOut(
destinationView: result,
transitionView: nil,
destinationRect: result.bounds,
destinationCornerRadius: 2.0,
destinationIsAvatar: false,

View File

@ -368,7 +368,7 @@ private final class PeerInfoPendingPane {
let paneNode: PeerInfoPaneNode
switch key {
case .stories:
let visualPaneNode = PeerInfoStoryPaneNode(context: context, peerId: peerId, chatLocation: chatLocation, contentType: .photoOrVideo, captureProtected: captureProtected, isArchive: false, navigationController: chatControllerInteraction.navigationController)
let visualPaneNode = PeerInfoStoryPaneNode(context: context, peerId: peerId, chatLocation: chatLocation, contentType: .photoOrVideo, captureProtected: captureProtected, isSaved: false, isArchive: false, navigationController: chatControllerInteraction.navigationController)
paneNode = visualPaneNode
visualPaneNode.openCurrentDate = {
openMediaCalendar()

View File

@ -789,7 +789,7 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
}
//TODO:localize
items[.stories]!.append(PeerInfoScreenDisclosureItem(id: 0, text: "My Stories", icon: PresentationResourcesSettings.stickers, action: {
items[.stories]!.append(PeerInfoScreenDisclosureItem(id: 0, text: "My Stories", icon: PresentationResourcesSettings.stories, action: {
interaction.openSettings(.stories)
}))