mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-08 19:10:53 +00:00
[WIP] Stories
This commit is contained in:
parent
780168d30b
commit
de8c3f055f
@ -2362,6 +2362,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
|
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
|
||||||
return StoryContainerScreen.TransitionOut(
|
return StoryContainerScreen.TransitionOut(
|
||||||
destinationView: transitionView,
|
destinationView: transitionView,
|
||||||
|
transitionView: nil,
|
||||||
destinationRect: transitionView.bounds,
|
destinationRect: transitionView.bounds,
|
||||||
destinationCornerRadius: transitionView.bounds.height * 0.5,
|
destinationCornerRadius: transitionView.bounds.height * 0.5,
|
||||||
destinationIsAvatar: true,
|
destinationIsAvatar: true,
|
||||||
|
@ -515,7 +515,7 @@ public class ContactsController: ViewController {
|
|||||||
return
|
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
|
let _ = (storyContent.state
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] storyContentState in
|
|> deliverOnMainQueue).start(next: { [weak self] storyContentState in
|
||||||
@ -551,6 +551,7 @@ public class ContactsController: ViewController {
|
|||||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
|
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
|
||||||
return StoryContainerScreen.TransitionOut(
|
return StoryContainerScreen.TransitionOut(
|
||||||
destinationView: transitionView,
|
destinationView: transitionView,
|
||||||
|
transitionView: nil,
|
||||||
destinationRect: transitionView.bounds,
|
destinationRect: transitionView.bounds,
|
||||||
destinationCornerRadius: transitionView.bounds.height * 0.5,
|
destinationCornerRadius: transitionView.bounds.height * 0.5,
|
||||||
destinationIsAvatar: true,
|
destinationIsAvatar: true,
|
||||||
|
@ -62,7 +62,7 @@ public final class GridMessageSelectionNode: ASDisplayNode {
|
|||||||
|
|
||||||
public final class GridMessageSelectionLayer: CALayer {
|
public final class GridMessageSelectionLayer: CALayer {
|
||||||
private var selected = false
|
private var selected = false
|
||||||
private let checkLayer: CheckLayer
|
public let checkLayer: CheckLayer
|
||||||
|
|
||||||
public init(theme: CheckNodeTheme) {
|
public init(theme: CheckNodeTheme) {
|
||||||
self.checkLayer = CheckLayer(theme: theme, content: .check)
|
self.checkLayer = CheckLayer(theme: theme, content: .check)
|
||||||
|
@ -17,7 +17,7 @@ public enum MediaTrackFrameResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private let traceEvents: Bool = {
|
private let traceEvents: Bool = {
|
||||||
#if DEBUG
|
#if DEBUG && false
|
||||||
return true
|
return true
|
||||||
#else
|
#else
|
||||||
return false
|
return false
|
||||||
|
@ -35,7 +35,7 @@ public protocol SparseItemGridBinding: AnyObject {
|
|||||||
func unbindLayer(layer: SparseItemGridLayer)
|
func unbindLayer(layer: SparseItemGridLayer)
|
||||||
func scrollerTextForTag(tag: Int32) -> String?
|
func scrollerTextForTag(tag: Int32) -> String?
|
||||||
func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError>
|
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 onTagTap()
|
||||||
func didScroll()
|
func didScroll()
|
||||||
func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition)
|
func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition)
|
||||||
@ -667,6 +667,27 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
|
|
||||||
return nil
|
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)? {
|
func anchorItem(at point: CGPoint, orLower: Bool = false) -> (Item, Int)? {
|
||||||
guard let items = self.items, !items.items.isEmpty, let layout = self.layout else {
|
guard let items = self.items, !items.items.isEmpty, let layout = self.layout else {
|
||||||
@ -862,7 +883,12 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
return
|
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()
|
let shimmerColors = items.itemBinding.getShimmerColors()
|
||||||
|
|
||||||
if resetScrolling {
|
if resetScrolling {
|
||||||
@ -904,83 +930,82 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
|
|
||||||
var validIds = Set<AnyHashable>()
|
var validIds = Set<AnyHashable>()
|
||||||
var usedPlaceholderCount = 0
|
var usedPlaceholderCount = 0
|
||||||
if !items.items.isEmpty {
|
|
||||||
var bindItems: [Item] = []
|
var bindItems: [Item] = []
|
||||||
var bindLayers: [SparseItemGridDisplayItem] = []
|
var bindLayers: [SparseItemGridDisplayItem] = []
|
||||||
var updateLayers: [SparseItemGridDisplayItem] = []
|
var updateLayers: [SparseItemGridDisplayItem] = []
|
||||||
|
|
||||||
let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count)
|
let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count)
|
||||||
for index in visibleRange.minIndex ... visibleRange.maxIndex {
|
for index in visibleRange.minIndex ... visibleRange.maxIndex {
|
||||||
if let item = items.item(at: index) {
|
if let item = items.item(at: index) {
|
||||||
let itemFrame = layout.frame(at: index)
|
let itemFrame = layout.frame(at: index)
|
||||||
|
|
||||||
let itemLayer: VisibleItem
|
let itemLayer: VisibleItem
|
||||||
if let current = self.visibleItems[item.id] {
|
if let current = self.visibleItems[item.id] {
|
||||||
itemLayer = current
|
itemLayer = current
|
||||||
updateLayers.append(itemLayer)
|
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
|
|
||||||
} else {
|
} 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
|
let placeholderLayer: SparseItemGridShimmerLayer
|
||||||
if self.visiblePlaceholders.count > usedPlaceholderCount {
|
if let current = itemLayer.shimmerLayer {
|
||||||
placeholderLayer = self.visiblePlaceholders[usedPlaceholderCount]
|
placeholderLayer = current
|
||||||
} else {
|
} else {
|
||||||
placeholderLayer = items.itemBinding.createShimmerLayer() ?? Shimmer.Layer()
|
placeholderLayer = items.itemBinding.createShimmerLayer() ?? Shimmer.Layer()
|
||||||
self.scrollView.layer.addSublayer(placeholderLayer)
|
self.scrollView.layer.insertSublayer(placeholderLayer, at: 0)
|
||||||
self.visiblePlaceholders.append(placeholderLayer)
|
itemLayer.shimmerLayer = placeholderLayer
|
||||||
}
|
}
|
||||||
let itemFrame = layout.frame(at: index)
|
|
||||||
placeholderLayer.frame = itemFrame
|
placeholderLayer.frame = itemFrame
|
||||||
self.shimmer.update(colors: shimmerColors, layer: placeholderLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY))
|
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)
|
placeholderLayer.update(size: itemFrame.size)
|
||||||
usedPlaceholderCount += 1
|
} else if let placeholderLayer = itemLayer.shimmerLayer {
|
||||||
|
itemLayer.shimmerLayer = nil
|
||||||
|
placeholderLayer.removeFromSuperlayer()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if !bindItems.isEmpty {
|
validIds.insert(item.id)
|
||||||
items.itemBinding.bindLayers(items: bindItems, layers: bindLayers, size: layout.containerLayout.size, insets: layout.containerLayout.insets, synchronous: synchronous)
|
|
||||||
}
|
|
||||||
|
|
||||||
for item in updateLayers {
|
itemLayer.frame = itemFrame
|
||||||
let item = item as! VisibleItem
|
} else {
|
||||||
if let layer = item.layer {
|
let placeholderLayer: SparseItemGridShimmerLayer
|
||||||
layer.update(size: layer.frame.size)
|
if self.visiblePlaceholders.count > usedPlaceholderCount {
|
||||||
} else if let view = item.view {
|
placeholderLayer = self.visiblePlaceholders[usedPlaceholderCount]
|
||||||
view.update(size: layer.frame.size, insets: layout.containerLayout.insets)
|
} 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 {
|
if case .ended = recognizer.state {
|
||||||
let location = recognizer.location(in: self.view)
|
let location = recognizer.location(in: self.view)
|
||||||
if let item = currentViewport.item(at: self.view.convert(location, to: currentViewport.view)) {
|
if let (item, itemLayer, point) = currentViewport.itemHitTest(at: self.view.convert(location, to: currentViewport.view)) {
|
||||||
items.itemBinding.onTap(item: item)
|
items.itemBinding.onTap(item: item, itemLayer: itemLayer, point: point)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -780,12 +780,6 @@ public final class ManagedAudioSession {
|
|||||||
managedAudioSessionLog("ManagedAudioSession resetting options")
|
managedAudioSessionLog("ManagedAudioSession resetting options")
|
||||||
try AVAudioSession.sharedInstance().setCategory(nativeCategory, options: 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 {
|
} catch let error {
|
||||||
managedAudioSessionLog("ManagedAudioSession setup error \(error)")
|
managedAudioSessionLog("ManagedAudioSession setup error \(error)")
|
||||||
}
|
}
|
||||||
|
@ -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
|
return account.postbox.transaction { transaction -> Void in
|
||||||
var items = transaction.getStoryItems(peerId: account.peerId)
|
var items = transaction.getStoryItems(peerId: account.peerId)
|
||||||
if let index = items.firstIndex(where: { $0.id == id }) {
|
var updated = false
|
||||||
items.remove(at: index)
|
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)
|
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
|
|> 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
|
|> `catch` { _ -> Signal<[Int32], NoError> in
|
||||||
return .single([])
|
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> {
|
func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32, asPinned: Bool) -> Signal<Never, NoError> {
|
||||||
return account.postbox.transaction { transaction -> Api.InputUser? in
|
if asPinned {
|
||||||
if let peerStoryState = transaction.getPeerStoryState(peerId: peerId)?.get(Stories.PeerState.self) {
|
return account.postbox.transaction { transaction -> Api.InputUser? in
|
||||||
transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState(
|
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||||
subscriptionsOpaqueState: peerStoryState.subscriptionsOpaqueState,
|
|
||||||
maxReadId: max(peerStoryState.maxReadId, id)
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
|
|> mapToSignal { inputUser -> Signal<Never, NoError> in
|
||||||
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
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
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
account.stateManager.injectStoryUpdates(updates: [.read(peerId: peerId, maxId: id)])
|
return account.postbox.transaction { transaction -> Api.InputUser? in
|
||||||
|
if let peerStoryState = transaction.getPeerStoryState(peerId: peerId)?.get(Stories.PeerState.self) {
|
||||||
#if DEBUG
|
transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState(
|
||||||
if "".isEmpty {
|
subscriptionsOpaqueState: peerStoryState.subscriptionsOpaqueState,
|
||||||
return .complete()
|
maxReadId: max(peerStoryState.maxReadId, id)
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||||
}
|
}
|
||||||
#endif
|
|> mapToSignal { inputUser -> Signal<Never, NoError> in
|
||||||
|
guard let inputUser = inputUser else {
|
||||||
return account.network.request(Api.functions.stories.readStories(userId: inputUser, maxId: id))
|
return .complete()
|
||||||
|> `catch` { _ -> Signal<[Int32], NoError> in
|
}
|
||||||
return .single([])
|
|
||||||
|
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
|
return account.postbox.transaction { transaction -> Void in
|
||||||
var items = transaction.getStoryItems(peerId: account.peerId)
|
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) {
|
var updatedItems: [Stories.Item] = []
|
||||||
let updatedItem = Stories.Item(
|
for (id, referenceItem) in ids {
|
||||||
id: item.id,
|
if let index = items.firstIndex(where: { $0.id == id }), case let .item(item) = items[index].value.get(Stories.StoredItem.self) {
|
||||||
timestamp: item.timestamp,
|
let updatedItem = Stories.Item(
|
||||||
expirationTimestamp: item.expirationTimestamp,
|
id: item.id,
|
||||||
media: item.media,
|
timestamp: item.timestamp,
|
||||||
text: item.text,
|
expirationTimestamp: item.expirationTimestamp,
|
||||||
entities: item.entities,
|
media: item.media,
|
||||||
views: item.views,
|
text: item.text,
|
||||||
privacy: item.privacy,
|
entities: item.entities,
|
||||||
isPinned: isPinned,
|
views: item.views,
|
||||||
isExpired: item.isExpired,
|
privacy: item.privacy,
|
||||||
isPublic: item.isPublic
|
isPinned: isPinned,
|
||||||
)
|
isExpired: item.isExpired,
|
||||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
isPublic: item.isPublic
|
||||||
items[index] = StoryItemsTableEntry(value: entry, id: item.id)
|
)
|
||||||
transaction.setStoryItems(peerId: account.peerId, items: items)
|
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 {
|
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
|
|> 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
|
|> `catch` { _ -> Signal<[Int32], NoError> in
|
||||||
return .single([])
|
return .single([])
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
public final class StorySubscriptionsContext {
|
||||||
private enum OpaqueStateMark: Equatable {
|
private enum OpaqueStateMark: Equatable {
|
||||||
case empty
|
case empty
|
||||||
@ -599,15 +627,17 @@ public final class PeerStoryListContext {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var finalUpdatedState: State?
|
||||||
|
|
||||||
for update in updates {
|
for update in updates {
|
||||||
switch update {
|
switch update {
|
||||||
case let .deleted(peerId, id):
|
case let .deleted(peerId, id):
|
||||||
if self.peerId == peerId {
|
if self.peerId == peerId {
|
||||||
if let index = self.stateValue.items.firstIndex(where: { $0.id == id }) {
|
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.items.remove(at: index)
|
||||||
updatedState.totalCount = max(0, updatedState.totalCount - 1)
|
updatedState.totalCount = max(0, updatedState.totalCount - 1)
|
||||||
self.stateValue = updatedState
|
finalUpdatedState = updatedState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .added(peerId, item):
|
case let .added(peerId, item):
|
||||||
@ -617,7 +647,7 @@ public final class PeerStoryListContext {
|
|||||||
if case let .item(item) = item {
|
if case let .item(item) = item {
|
||||||
if item.isPinned {
|
if item.isPinned {
|
||||||
if let media = item.media {
|
if let media = item.media {
|
||||||
var updatedState = self.stateValue
|
var updatedState = finalUpdatedState ?? self.stateValue
|
||||||
updatedState.items[index] = EngineStoryItem(
|
updatedState.items[index] = EngineStoryItem(
|
||||||
id: item.id,
|
id: item.id,
|
||||||
timestamp: item.timestamp,
|
timestamp: item.timestamp,
|
||||||
@ -638,13 +668,47 @@ public final class PeerStoryListContext {
|
|||||||
isExpired: item.isExpired,
|
isExpired: item.isExpired,
|
||||||
isPublic: item.isPublic
|
isPublic: item.isPublic
|
||||||
)
|
)
|
||||||
self.stateValue = updatedState
|
finalUpdatedState = updatedState
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var updatedState = self.stateValue
|
var updatedState = finalUpdatedState ?? self.stateValue
|
||||||
updatedState.items.remove(at: index)
|
updatedState.items.remove(at: index)
|
||||||
updatedState.totalCount = max(0, updatedState.totalCount - 1)
|
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
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let finalUpdatedState = finalUpdatedState {
|
||||||
|
self.stateValue = finalUpdatedState
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -926,16 +926,16 @@ public extension TelegramEngine {
|
|||||||
return _internal_editStory(account: self.account, media: media, id: id, text: text, entities: entities, privacy: privacy)
|
return _internal_editStory(account: self.account, media: media, id: id, text: text, entities: entities, privacy: privacy)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func deleteStory(id: Int32) -> Signal<Never, NoError> {
|
public func deleteStories(ids: [Int32]) -> Signal<Never, NoError> {
|
||||||
return _internal_deleteStory(account: self.account, id: id)
|
return _internal_deleteStories(account: self.account, ids: ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func markStoryAsSeen(peerId: EnginePeer.Id, id: Int32) -> Signal<Never, NoError> {
|
public func markStoryAsSeen(peerId: EnginePeer.Id, id: Int32, asPinned: Bool) -> Signal<Never, NoError> {
|
||||||
return _internal_markStoryAsSeen(account: self.account, peerId: peerId, id: id)
|
return _internal_markStoryAsSeen(account: self.account, peerId: peerId, id: id, asPinned: asPinned)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateStoryIsPinned(id: Int32, isPinned: Bool) -> Signal<Never, NoError> {
|
public func updateStoriesArePinned(ids: [Int32: EngineStoryItem], isPinned: Bool) -> Signal<Never, NoError> {
|
||||||
return _internal_updateStoryIsPinned(account: self.account, id: id, isPinned: isPinned)
|
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> {
|
public func getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal<StoryViewList?, NoError> {
|
||||||
|
@ -29,16 +29,14 @@ public struct PresentationResourcesSettings {
|
|||||||
public static let devices = renderIcon(name: "Settings/Menu/Sessions")
|
public static let devices = renderIcon(name: "Settings/Menu/Sessions")
|
||||||
public static let chatFolders = renderIcon(name: "Settings/Menu/ChatListFilters")
|
public static let chatFolders = renderIcon(name: "Settings/Menu/ChatListFilters")
|
||||||
public static let stickers = renderIcon(name: "Settings/Menu/Stickers")
|
public static let stickers = renderIcon(name: "Settings/Menu/Stickers")
|
||||||
|
|
||||||
public static let notifications = renderIcon(name: "Settings/Menu/Notifications")
|
public static let notifications = renderIcon(name: "Settings/Menu/Notifications")
|
||||||
public static let security = renderIcon(name: "Settings/Menu/Security")
|
public static let security = renderIcon(name: "Settings/Menu/Security")
|
||||||
public static let dataAndStorage = renderIcon(name: "Settings/Menu/DataAndStorage")
|
public static let dataAndStorage = renderIcon(name: "Settings/Menu/DataAndStorage")
|
||||||
public static let appearance = renderIcon(name: "Settings/Menu/Appearance")
|
public static let appearance = renderIcon(name: "Settings/Menu/Appearance")
|
||||||
public static let language = renderIcon(name: "Settings/Menu/Language")
|
public static let language = renderIcon(name: "Settings/Menu/Language")
|
||||||
|
|
||||||
public static let deleteAccount = renderIcon(name: "Chat/Info/GroupRemovedIcon")
|
public static let deleteAccount = renderIcon(name: "Chat/Info/GroupRemovedIcon")
|
||||||
|
|
||||||
public static let powerSaving = renderIcon(name: "Settings/Menu/PowerSaving")
|
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
|
public static let premium = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
|
||||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||||
|
@ -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",
|
||||||
|
],
|
||||||
|
)
|
@ -3,19 +3,11 @@ import UIKit
|
|||||||
import Display
|
import Display
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
import SwiftSignalKit
|
|
||||||
import ViewControllerComponent
|
|
||||||
import ComponentDisplayAdapters
|
import ComponentDisplayAdapters
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import AccountContext
|
|
||||||
import TelegramCore
|
|
||||||
import MultilineTextComponent
|
|
||||||
import EmojiStatusComponent
|
|
||||||
import TelegramStringFormatting
|
|
||||||
import CheckNode
|
|
||||||
import SolidRoundedButtonComponent
|
import SolidRoundedButtonComponent
|
||||||
|
|
||||||
final class StorageUsageScreenSelectionPanelComponent: Component {
|
public final class BottomButtonPanelComponent: Component {
|
||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
let title: String
|
let title: String
|
||||||
let label: String?
|
let label: String?
|
||||||
@ -23,7 +15,7 @@ final class StorageUsageScreenSelectionPanelComponent: Component {
|
|||||||
let insets: UIEdgeInsets
|
let insets: UIEdgeInsets
|
||||||
let action: () -> Void
|
let action: () -> Void
|
||||||
|
|
||||||
init(
|
public init(
|
||||||
theme: PresentationTheme,
|
theme: PresentationTheme,
|
||||||
title: String,
|
title: String,
|
||||||
label: String?,
|
label: String?,
|
||||||
@ -39,7 +31,7 @@ final class StorageUsageScreenSelectionPanelComponent: Component {
|
|||||||
self.action = action
|
self.action = action
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: StorageUsageScreenSelectionPanelComponent, rhs: StorageUsageScreenSelectionPanelComponent) -> Bool {
|
public static func ==(lhs: BottomButtonPanelComponent, rhs: BottomButtonPanelComponent) -> Bool {
|
||||||
if lhs.theme !== rhs.theme {
|
if lhs.theme !== rhs.theme {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -58,14 +50,14 @@ final class StorageUsageScreenSelectionPanelComponent: Component {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
class View: UIView {
|
public class View: UIView {
|
||||||
private let backgroundView: BlurredBackgroundView
|
private let backgroundView: BlurredBackgroundView
|
||||||
private let separatorLayer: SimpleLayer
|
private let separatorLayer: SimpleLayer
|
||||||
private let actionButton = ComponentView<Empty>()
|
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.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
|
||||||
self.separatorLayer = SimpleLayer()
|
self.separatorLayer = SimpleLayer()
|
||||||
|
|
||||||
@ -75,11 +67,11 @@ final class StorageUsageScreenSelectionPanelComponent: Component {
|
|||||||
self.layer.addSublayer(self.separatorLayer)
|
self.layer.addSublayer(self.separatorLayer)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required public init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
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
|
let themeUpdated = self.component?.theme !== component.theme
|
||||||
self.component = component
|
self.component = component
|
||||||
|
|
||||||
@ -146,11 +138,11 @@ final class StorageUsageScreenSelectionPanelComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeView() -> View {
|
public func makeView() -> View {
|
||||||
return View(frame: CGRect())
|
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)
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -117,6 +117,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
|
|
||||||
private let button: HighlightTrackingButtonNode
|
private let button: HighlightTrackingButtonNode
|
||||||
|
|
||||||
|
public var disableAnimations: Bool = false
|
||||||
|
|
||||||
var manualLayout: Bool = false
|
var manualLayout: Bool = false
|
||||||
private var validLayout: (CGSize, CGRect)?
|
private var validLayout: (CGSize, CGRect)?
|
||||||
|
|
||||||
@ -356,7 +358,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
if !self.updateStatus() {
|
if !self.updateStatus() {
|
||||||
if updated {
|
if updated {
|
||||||
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,10 @@ swift_library(
|
|||||||
"//submodules/Components/ViewControllerComponent",
|
"//submodules/Components/ViewControllerComponent",
|
||||||
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode",
|
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode",
|
||||||
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
|
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
|
||||||
|
"//submodules/TelegramUI/Components/ChatTitleView",
|
||||||
"//submodules/ContextUI",
|
"//submodules/ContextUI",
|
||||||
|
"//submodules/UndoUI",
|
||||||
|
"//submodules/TelegramUI/Components/BottomButtonPanelComponent",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -10,6 +10,9 @@ import PeerInfoVisualMediaPaneNode
|
|||||||
import ViewControllerComponent
|
import ViewControllerComponent
|
||||||
import ChatListHeaderComponent
|
import ChatListHeaderComponent
|
||||||
import ContextUI
|
import ContextUI
|
||||||
|
import ChatTitleView
|
||||||
|
import BottomButtonPanelComponent
|
||||||
|
import UndoUI
|
||||||
|
|
||||||
final class PeerInfoStoryGridScreenComponent: Component {
|
final class PeerInfoStoryGridScreenComponent: Component {
|
||||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||||
@ -48,6 +51,13 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
|||||||
private var environment: EnvironmentType?
|
private var environment: EnvironmentType?
|
||||||
|
|
||||||
private var paneNode: PeerInfoStoryPaneNode?
|
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?
|
private weak var mediaGalleryContextMenu: ContextController?
|
||||||
|
|
||||||
@ -59,6 +69,11 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.paneStatusDisposable?.dispose()
|
||||||
|
self.selectionStateDisposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
func morePressed(source: ContextReferenceContentNode) {
|
func morePressed(source: ContextReferenceContentNode) {
|
||||||
guard let component = self.component, let controller = self.environment?.controller(), let pane = self.paneNode else {
|
guard let component = self.component, let controller = self.environment?.controller(), let pane = self.paneNode else {
|
||||||
return
|
return
|
||||||
@ -68,120 +83,168 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
|||||||
|
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
let strings = presentationData.strings
|
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 {
|
if self.selectedCount != 0 {
|
||||||
var ignoreNextActions = false
|
|
||||||
//TODO:localize
|
//TODO:localize
|
||||||
items.append(.action(ContextMenuActionItem(text: "Show Archive", icon: { theme in
|
//TODO:update icon
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/StoryArchive"), color: theme.contextMenu.primaryColor)
|
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
|
}, action: { [weak self] _, a in
|
||||||
if ignoreNextActions {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ignoreNextActions = true
|
|
||||||
a(.default)
|
a(.default)
|
||||||
|
|
||||||
guard let self, let component = self.component else {
|
guard let self, let component = self.component else {
|
||||||
return
|
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)
|
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
|
contextController.passthroughTouchEvent = { [weak self] sourceView, point in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
@ -217,6 +280,8 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
|||||||
self.component = component
|
self.component = component
|
||||||
self.state = state
|
self.state = state
|
||||||
|
|
||||||
|
let sideInset: CGFloat = 14.0
|
||||||
|
|
||||||
let environment = environment[EnvironmentType.self].value
|
let environment = environment[EnvironmentType.self].value
|
||||||
|
|
||||||
let themeUpdated = self.environment?.theme !== environment.theme
|
let themeUpdated = self.environment?.theme !== environment.theme
|
||||||
@ -227,6 +292,82 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
|||||||
self.backgroundColor = environment.theme.list.plainBackgroundColor
|
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
|
let paneNode: PeerInfoStoryPaneNode
|
||||||
if let current = self.paneNode {
|
if let current = self.paneNode {
|
||||||
paneNode = current
|
paneNode = current
|
||||||
@ -237,6 +378,7 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
|||||||
chatLocation: .peer(id: component.peerId),
|
chatLocation: .peer(id: component.peerId),
|
||||||
contentType: .photoOrVideo,
|
contentType: .photoOrVideo,
|
||||||
captureProtected: false,
|
captureProtected: false,
|
||||||
|
isSaved: true,
|
||||||
isArchive: component.scope == .archive,
|
isArchive: component.scope == .archive,
|
||||||
navigationController: { [weak self] in
|
navigationController: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
@ -247,13 +389,41 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
|||||||
)
|
)
|
||||||
self.paneNode = paneNode
|
self.paneNode = paneNode
|
||||||
self.addSubview(paneNode.view)
|
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(
|
paneNode.update(
|
||||||
size: availableSize,
|
size: availableSize,
|
||||||
topInset: environment.navigationHeight,
|
topInset: environment.navigationHeight,
|
||||||
sideInset: environment.safeInsets.left,
|
sideInset: environment.safeInsets.left,
|
||||||
bottomInset: environment.safeInsets.bottom,
|
bottomInset: bottomInset,
|
||||||
visibleHeight: availableSize.height,
|
visibleHeight: availableSize.height,
|
||||||
isScrollingLockedAtTop: false,
|
isScrollingLockedAtTop: false,
|
||||||
expandProgress: 1.0,
|
expandProgress: 1.0,
|
||||||
@ -283,8 +453,11 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
|
private let scope: Scope
|
||||||
private var isDismissed: Bool = false
|
private var isDismissed: Bool = false
|
||||||
|
|
||||||
|
private var titleView: ChatTitleView?
|
||||||
|
|
||||||
private var moreBarButton: MoreHeaderButton?
|
private var moreBarButton: MoreHeaderButton?
|
||||||
private var moreBarButtonItem: UIBarButtonItem?
|
private var moreBarButtonItem: UIBarButtonItem?
|
||||||
|
|
||||||
@ -294,6 +467,7 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
|
|||||||
scope: Scope
|
scope: Scope
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
self.scope = scope
|
||||||
|
|
||||||
super.init(context: context, component: PeerInfoStoryGridScreenComponent(
|
super.init(context: context, component: PeerInfoStoryGridScreenComponent(
|
||||||
context: context,
|
context: context,
|
||||||
@ -301,9 +475,6 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
|
|||||||
scope: scope
|
scope: scope
|
||||||
), navigationBarAppearance: .default, theme: .default)
|
), navigationBarAppearance: .default, theme: .default)
|
||||||
|
|
||||||
//TODO:localize
|
|
||||||
self.navigationItem.title = "My Stories"
|
|
||||||
|
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||||
let moreBarButton = MoreHeaderButton(color: presentationData.theme.rootController.navigationBar.buttonColor)
|
let moreBarButton = MoreHeaderButton(color: presentationData.theme.rootController.navigationBar.buttonColor)
|
||||||
moreBarButton.isUserInteractionEnabled = true
|
moreBarButton.isUserInteractionEnabled = true
|
||||||
@ -321,6 +492,21 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
|
|||||||
moreBarButton.addTarget(self, action: #selector(self.morePressed), forControlEvents: .touchUpInside)
|
moreBarButton.addTarget(self, action: #selector(self.morePressed), forControlEvents: .touchUpInside)
|
||||||
|
|
||||||
self.navigationItem.setRightBarButton(moreBarButtonItem, animated: false)
|
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) {
|
required public init(coder aDecoder: NSCoder) {
|
||||||
@ -330,6 +516,34 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
|
|||||||
deinit {
|
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() {
|
@objc private func morePressed() {
|
||||||
guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else {
|
guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else {
|
||||||
return
|
return
|
||||||
@ -342,6 +556,8 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
|
|||||||
|
|
||||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||||
super.containerLayoutUpdated(layout, transition: transition)
|
super.containerLayoutUpdated(layout, transition: transition)
|
||||||
|
|
||||||
|
self.titleView?.layout = layout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let chatLocation: ChatLocation
|
let chatLocation: ChatLocation
|
||||||
@ -485,8 +528,9 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
|||||||
var chatPresentationData: ChatPresentationData
|
var chatPresentationData: ChatPresentationData
|
||||||
var checkNodeTheme: CheckNodeTheme
|
var checkNodeTheme: CheckNodeTheme
|
||||||
|
|
||||||
|
var itemInteraction: VisualMediaItemInteraction?
|
||||||
var loadHoleImpl: ((SparseItemGrid.HoleAnchor, SparseItemGrid.HoleLocation) -> Signal<Never, NoError>)?
|
var loadHoleImpl: ((SparseItemGrid.HoleAnchor, SparseItemGrid.HoleLocation) -> Signal<Never, NoError>)?
|
||||||
var onTapImpl: ((VisualMediaItem) -> Void)?
|
var onTapImpl: ((VisualMediaItem, CALayer, CGPoint) -> Void)?
|
||||||
var onTagTapImpl: (() -> Void)?
|
var onTagTapImpl: (() -> Void)?
|
||||||
var didScrollImpl: (() -> Void)?
|
var didScrollImpl: (() -> Void)?
|
||||||
var coveringInsetOffsetUpdatedImpl: ((ContainedViewLayoutTransition) -> 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))
|
layer.updateDuration(duration: duration, isMin: isMin, minFactor: min(1.0, layer.bounds.height / 74.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO:selection
|
var isSelected: Bool?
|
||||||
layer.updateSelection(theme: self.checkNodeTheme, isSelected: nil, animated: false)
|
if let selectedIds = self.itemInteraction?.selectedIds {
|
||||||
|
isSelected = selectedIds.contains(story.id)
|
||||||
|
}
|
||||||
|
layer.updateSelection(theme: self.checkNodeTheme, isSelected: isSelected, animated: false)
|
||||||
|
|
||||||
layer.bind(item: item)
|
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 {
|
guard let item = item as? VisualMediaItem else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.onTapImpl?(item)
|
self.onTapImpl?(item, itemLayer, point)
|
||||||
}
|
}
|
||||||
|
|
||||||
func onTagTap() {
|
func onTagTap() {
|
||||||
@ -756,6 +803,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
|||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let peerId: PeerId
|
private let peerId: PeerId
|
||||||
private let chatLocation: ChatLocation
|
private let chatLocation: ChatLocation
|
||||||
|
private let isSaved: Bool
|
||||||
private let isArchive: Bool
|
private let isArchive: Bool
|
||||||
public private(set) var contentType: ContentType
|
public private(set) var contentType: ContentType
|
||||||
private var contentTypePromise: ValuePromise<ContentType>
|
private var contentTypePromise: ValuePromise<ContentType>
|
||||||
@ -778,6 +826,30 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
|||||||
return self._itemInteraction!
|
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 var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)?
|
||||||
|
|
||||||
private let ready = Promise<Bool>()
|
private let ready = Promise<Bool>()
|
||||||
@ -818,13 +890,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
|||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
private var presentationDataDisposable: Disposable?
|
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.context = context
|
||||||
self.peerId = peerId
|
self.peerId = peerId
|
||||||
self.chatLocation = chatLocation
|
self.chatLocation = chatLocation
|
||||||
self.contentType = contentType
|
self.contentType = contentType
|
||||||
self.contentTypePromise = ValuePromise<ContentType>(contentType)
|
self.contentTypePromise = ValuePromise<ContentType>(contentType)
|
||||||
self.navigationController = navigationController
|
self.navigationController = navigationController
|
||||||
|
self.isSaved = isSaved
|
||||||
self.isArchive = isArchive
|
self.isArchive = isArchive
|
||||||
|
|
||||||
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
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)
|
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 {
|
guard let self else {
|
||||||
return
|
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
|
//TODO:selection
|
||||||
let listContext = PeerStoryListContentContextImpl(
|
let listContext = PeerStoryListContentContextImpl(
|
||||||
context: self.context,
|
context: self.context,
|
||||||
@ -928,6 +1009,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
|||||||
let itemRect = self.itemGrid.frameForItem(layer: foundItemLayer)
|
let itemRect = self.itemGrid.frameForItem(layer: foundItemLayer)
|
||||||
return StoryContainerScreen.TransitionOut(
|
return StoryContainerScreen.TransitionOut(
|
||||||
destinationView: self.view,
|
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),
|
destinationRect: self.itemGrid.view.convert(itemRect, to: self.view),
|
||||||
destinationCornerRadius: 0.0,
|
destinationCornerRadius: 0.0,
|
||||||
destinationIsAvatar: false,
|
destinationIsAvatar: false,
|
||||||
@ -938,6 +1027,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
|||||||
return nil
|
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)
|
navigationController.pushViewController(storyContainerScreen)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1043,14 +1147,26 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
|||||||
let _ = self
|
let _ = self
|
||||||
},
|
},
|
||||||
toggleSelection: { [weak self] id, value in
|
toggleSelection: { [weak self] id, value in
|
||||||
guard let self else {
|
guard let self, let itemInteraction = self._itemInteraction else {
|
||||||
return
|
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
|
//TODO:selection
|
||||||
//self.itemInteraction.selectedItemIds =
|
if isArchive {
|
||||||
|
self._itemInteraction?.selectedIds = Set()
|
||||||
|
}
|
||||||
|
self.itemGridBinding.itemInteraction = self._itemInteraction
|
||||||
|
|
||||||
self.contextGestureContainerNode.isGestureEnabled = true
|
self.contextGestureContainerNode.isGestureEnabled = true
|
||||||
self.contextGestureContainerNode.addSubnode(self.itemGrid)
|
self.contextGestureContainerNode.addSubnode(self.itemGrid)
|
||||||
@ -1136,6 +1252,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
|||||||
|
|
||||||
strongSelf.itemGrid.cancelGestures()
|
strongSelf.itemGrid.cancelGestures()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.statusPromise.set(.single(PeerInfoStatusData(text: "", isActivity: false, key: .stories)))
|
||||||
|
|
||||||
/*self.storedStateDisposable = (visualMediaStoredState(engine: context.engine, peerId: peerId, messageTag: self.stateTag)
|
/*self.storedStateDisposable = (visualMediaStoredState(engine: context.engine, peerId: peerId, messageTag: self.stateTag)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||||
@ -1385,6 +1503,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
|||||||
return
|
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())
|
let timezoneOffset = Int32(TimeZone.current.secondsFromGMT())
|
||||||
|
|
||||||
var mappedItems: [SparseItemGrid.Item] = []
|
var mappedItems: [SparseItemGrid.Item] = []
|
||||||
@ -1402,6 +1538,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
|||||||
}
|
}
|
||||||
totalCount = state.totalCount
|
totalCount = state.totalCount
|
||||||
totalCount = max(mappedItems.count, totalCount)
|
totalCount = max(mappedItems.count, totalCount)
|
||||||
|
|
||||||
|
if totalCount == 0 {
|
||||||
|
totalCount = 100
|
||||||
|
}
|
||||||
|
|
||||||
Queue.mainQueue().async { [weak self] in
|
Queue.mainQueue().async { [weak self] in
|
||||||
guard let strongSelf = self else {
|
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) {
|
public func updateSelectedMessages(animated: Bool) {
|
||||||
/*switch self.contentType {
|
}
|
||||||
case .files, .music, .voiceAndVideoMessages:
|
|
||||||
self.itemGrid.forEachVisibleItem { item in
|
private func updateSelectedItems(animated: Bool) {
|
||||||
guard let itemView = item.view as? ItemView, let (size, topInset, sideInset, bottomInset, _, _, _, _) = self.currentParams else {
|
self.itemGrid.forEachVisibleItem { item in
|
||||||
return
|
guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item 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)
|
|
||||||
}
|
}
|
||||||
|
itemLayer.updateSelection(theme: self.itemGridBinding.checkNodeTheme, isSelected: self.itemInteraction.selectedIds?.contains(item.story.id), animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
let isSelecting = self.chatControllerInteraction.selectionState != nil
|
/*let isSelecting = self.chatControllerInteraction.selectionState != nil
|
||||||
self.itemGrid.pinchEnabled = !isSelecting
|
self.itemGrid.pinchEnabled = !isSelecting
|
||||||
|
|
||||||
if isSelecting {
|
if isSelecting {
|
||||||
if self.gridSelectionGesture == nil {
|
if self.gridSelectionGesture == nil {
|
||||||
let selectionGesture = MediaPickerGridSelectionGesture<EngineMessage.Id>()
|
let selectionGesture = MediaPickerGridSelectionGesture<EngineMessage.Id>()
|
||||||
selectionGesture.delegate = self
|
selectionGesture.delegate = self
|
||||||
selectionGesture.sideInset = 44.0
|
selectionGesture.sideInset = 44.0
|
||||||
selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in
|
selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in
|
||||||
self?.itemGrid.isScrollEnabled = isEnabled
|
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
|
|
||||||
}
|
}
|
||||||
} else if let gridSelectionGesture = self.gridSelectionGesture {
|
selectionGesture.itemAt = { [weak self] point in
|
||||||
self.itemGrid.view.removeGestureRecognizer(gridSelectionGesture)
|
if let strongSelf = self, let itemLayer = strongSelf.itemGrid.item(at: point)?.layer as? ItemLayer, let messageId = itemLayer.item?.message.id {
|
||||||
self.gridSelectionGesture = nil
|
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) {
|
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)
|
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 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
guard let item = item as? VisualMediaItem else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ swift_library(
|
|||||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||||
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
||||||
|
"//submodules/TelegramUI/Components/BottomButtonPanelComponent",
|
||||||
"//submodules/CheckNode",
|
"//submodules/CheckNode",
|
||||||
"//submodules/Markdown",
|
"//submodules/Markdown",
|
||||||
"//submodules/ContextUI",
|
"//submodules/ContextUI",
|
||||||
|
@ -23,6 +23,7 @@ import TelegramAnimatedStickerNode
|
|||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
import GalleryData
|
import GalleryData
|
||||||
import AnimatedTextComponent
|
import AnimatedTextComponent
|
||||||
|
import BottomButtonPanelComponent
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
import os.signpost
|
import os.signpost
|
||||||
@ -1207,7 +1208,7 @@ final class StorageUsageScreenComponent: Component {
|
|||||||
|
|
||||||
let selectionPanelSize = selectionPanel.update(
|
let selectionPanelSize = selectionPanel.update(
|
||||||
transition: selectionPanelTransition,
|
transition: selectionPanelTransition,
|
||||||
component: AnyComponent(StorageUsageScreenSelectionPanelComponent(
|
component: AnyComponent(BottomButtonPanelComponent(
|
||||||
theme: environment.theme,
|
theme: environment.theme,
|
||||||
title: bottomPanelSelectionData.isComplete ? environment.strings.StorageManagement_ClearCache : environment.strings.StorageManagement_ClearSelected,
|
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: ".")),
|
label: bottomPanelSelectionData.size == 0 ? nil : dataSizeString(Int(bottomPanelSelectionData.size), formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".")),
|
||||||
|
@ -29,22 +29,63 @@ func hasFirstResponder(_ view: UIView) -> Bool {
|
|||||||
return false
|
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 {
|
private final class StoryContainerScreenComponent: Component {
|
||||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let content: StoryContentContext
|
let content: StoryContentContext
|
||||||
|
let focusedItemPromise: Promise<StoryId?>
|
||||||
let transitionIn: StoryContainerScreen.TransitionIn?
|
let transitionIn: StoryContainerScreen.TransitionIn?
|
||||||
let transitionOut: (EnginePeer.Id, AnyHashable) -> StoryContainerScreen.TransitionOut?
|
let transitionOut: (EnginePeer.Id, AnyHashable) -> StoryContainerScreen.TransitionOut?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
content: StoryContentContext,
|
content: StoryContentContext,
|
||||||
|
focusedItemPromise: Promise<StoryId?>,
|
||||||
transitionIn: StoryContainerScreen.TransitionIn?,
|
transitionIn: StoryContainerScreen.TransitionIn?,
|
||||||
transitionOut: @escaping (EnginePeer.Id, AnyHashable) -> StoryContainerScreen.TransitionOut?
|
transitionOut: @escaping (EnginePeer.Id, AnyHashable) -> StoryContainerScreen.TransitionOut?
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.content = content
|
self.content = content
|
||||||
|
self.focusedItemPromise = focusedItemPromise
|
||||||
self.transitionIn = transitionIn
|
self.transitionIn = transitionIn
|
||||||
self.transitionOut = transitionOut
|
self.transitionOut = transitionOut
|
||||||
}
|
}
|
||||||
@ -118,12 +159,14 @@ private final class StoryContainerScreenComponent: Component {
|
|||||||
private let backgroundLayer: SimpleLayer
|
private let backgroundLayer: SimpleLayer
|
||||||
private let backgroundEffectView: BlurredBackgroundView
|
private let backgroundEffectView: BlurredBackgroundView
|
||||||
|
|
||||||
|
private let focusedItem = ValuePromise<StoryId?>(nil, ignoreRepeated: true)
|
||||||
private var contentUpdatedDisposable: Disposable?
|
private var contentUpdatedDisposable: Disposable?
|
||||||
|
|
||||||
private var visibleItemSetViews: [EnginePeer.Id: ItemSetView] = [:]
|
private var visibleItemSetViews: [EnginePeer.Id: ItemSetView] = [:]
|
||||||
|
|
||||||
private var itemSetPanState: ItemSetPanState?
|
private var itemSetPanState: ItemSetPanState?
|
||||||
private var dismissPanState: ItemSetPanState?
|
private var dismissPanState: ItemSetPanState?
|
||||||
|
private var isHoldingTouch: Bool = false
|
||||||
|
|
||||||
private var isAnimatingOut: Bool = false
|
private var isAnimatingOut: Bool = false
|
||||||
private var didAnimateOut: Bool = false
|
private var didAnimateOut: Bool = false
|
||||||
@ -163,8 +206,15 @@ private final class StoryContainerScreenComponent: Component {
|
|||||||
})
|
})
|
||||||
self.addGestureRecognizer(verticalPanRecognizer)
|
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.delegate = self
|
||||||
|
longPressRecognizer.updateIsTracking = { [weak self] isTracking in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.isHoldingTouch = isTracking
|
||||||
|
self.state?.updated(transition: .immediate)
|
||||||
|
}
|
||||||
self.addGestureRecognizer(longPressRecognizer)
|
self.addGestureRecognizer(longPressRecognizer)
|
||||||
|
|
||||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
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 {
|
switch recognizer.state {
|
||||||
case .began:
|
case .began:
|
||||||
if self.itemSetPanState == nil {
|
if self.itemSetPanState == nil {
|
||||||
@ -381,6 +431,10 @@ private final class StoryContainerScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func animateIn() {
|
func animateIn() {
|
||||||
|
if let component = self.component {
|
||||||
|
component.focusedItemPromise.set(self.focusedItem.get())
|
||||||
|
}
|
||||||
|
|
||||||
if let transitionIn = self.component?.transitionIn, transitionIn.sourceView != nil {
|
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.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)
|
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)
|
transition.setAlpha(view: self.backgroundEffectView, alpha: 0.0)
|
||||||
|
|
||||||
let transitionOutCompleted = transitionOut.completed
|
let transitionOutCompleted = transitionOut.completed
|
||||||
|
let focusedItemPromise = component.focusedItemPromise
|
||||||
itemSetComponentView.animateOut(transitionOut: transitionOut, completion: {
|
itemSetComponentView.animateOut(transitionOut: transitionOut, completion: {
|
||||||
completion()
|
completion()
|
||||||
transitionOutCompleted()
|
transitionOutCompleted()
|
||||||
|
focusedItemPromise.set(.single(nil))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
self.dismissPanState = ItemSetPanState(fraction: 1.0, didBegin: true)
|
self.dismissPanState = ItemSetPanState(fraction: 1.0, didBegin: true)
|
||||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
|
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))
|
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||||
transition.setAlpha(layer: self.backgroundLayer, alpha: 0.0, completion: { _ in
|
transition.setAlpha(layer: self.backgroundLayer, alpha: 0.0, completion: { _ in
|
||||||
completion()
|
completion()
|
||||||
|
focusedItemPromise?.set(.single(nil))
|
||||||
})
|
})
|
||||||
transition.setAlpha(view: self.backgroundEffectView, alpha: 0.0)
|
transition.setAlpha(view: self.backgroundEffectView, alpha: 0.0)
|
||||||
}
|
}
|
||||||
@ -475,6 +534,12 @@ private final class StoryContainerScreenComponent: Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if update {
|
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 {
|
if component.content.stateValue?.slice == nil {
|
||||||
self.environment?.controller()?.dismiss()
|
self.environment?.controller()?.dismiss()
|
||||||
} else {
|
} else {
|
||||||
@ -511,6 +576,9 @@ private final class StoryContainerScreenComponent: Component {
|
|||||||
if self.isAnimatingOut {
|
if self.isAnimatingOut {
|
||||||
isProgressPaused = true
|
isProgressPaused = true
|
||||||
}
|
}
|
||||||
|
if self.isHoldingTouch {
|
||||||
|
isProgressPaused = true
|
||||||
|
}
|
||||||
|
|
||||||
var dismissPanOffset: CGFloat = 0.0
|
var dismissPanOffset: CGFloat = 0.0
|
||||||
var dismissPanScale: CGFloat = 1.0
|
var dismissPanScale: CGFloat = 1.0
|
||||||
@ -684,9 +752,15 @@ private final class StoryContainerScreenComponent: Component {
|
|||||||
environment.controller()?.dismiss()
|
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
|
controller: { [weak self] in
|
||||||
return self?.environment?.controller()
|
return self?.environment?.controller()
|
||||||
}
|
}
|
||||||
@ -865,6 +939,35 @@ private final class StoryContainerScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class StoryContainerScreen: ViewControllerComponentContainer {
|
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 final class TransitionIn {
|
||||||
public weak var sourceView: UIView?
|
public weak var sourceView: UIView?
|
||||||
public let sourceRect: CGRect
|
public let sourceRect: CGRect
|
||||||
@ -883,6 +986,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
|
|||||||
|
|
||||||
public final class TransitionOut {
|
public final class TransitionOut {
|
||||||
public weak var destinationView: UIView?
|
public weak var destinationView: UIView?
|
||||||
|
public let transitionView: TransitionView?
|
||||||
public let destinationRect: CGRect
|
public let destinationRect: CGRect
|
||||||
public let destinationCornerRadius: CGFloat
|
public let destinationCornerRadius: CGFloat
|
||||||
public let destinationIsAvatar: Bool
|
public let destinationIsAvatar: Bool
|
||||||
@ -890,12 +994,14 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
|
|||||||
|
|
||||||
public init(
|
public init(
|
||||||
destinationView: UIView,
|
destinationView: UIView,
|
||||||
|
transitionView: TransitionView?,
|
||||||
destinationRect: CGRect,
|
destinationRect: CGRect,
|
||||||
destinationCornerRadius: CGFloat,
|
destinationCornerRadius: CGFloat,
|
||||||
destinationIsAvatar: Bool,
|
destinationIsAvatar: Bool,
|
||||||
completed: @escaping () -> Void
|
completed: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.destinationView = destinationView
|
self.destinationView = destinationView
|
||||||
|
self.transitionView = transitionView
|
||||||
self.destinationRect = destinationRect
|
self.destinationRect = destinationRect
|
||||||
self.destinationCornerRadius = destinationCornerRadius
|
self.destinationCornerRadius = destinationCornerRadius
|
||||||
self.destinationIsAvatar = destinationIsAvatar
|
self.destinationIsAvatar = destinationIsAvatar
|
||||||
@ -906,6 +1012,11 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
|
|||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private var isDismissed: Bool = false
|
private var isDismissed: Bool = false
|
||||||
|
|
||||||
|
private let focusedItemPromise = Promise<StoryId?>(nil)
|
||||||
|
public var focusedItem: Signal<StoryId?, NoError> {
|
||||||
|
return self.focusedItemPromise.get()
|
||||||
|
}
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
content: StoryContentContext,
|
content: StoryContentContext,
|
||||||
@ -917,6 +1028,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
|
|||||||
super.init(context: context, component: StoryContainerScreenComponent(
|
super.init(context: context, component: StoryContainerScreenComponent(
|
||||||
context: context,
|
context: context,
|
||||||
content: content,
|
content: content,
|
||||||
|
focusedItemPromise: self.focusedItemPromise,
|
||||||
transitionIn: transitionIn,
|
transitionIn: transitionIn,
|
||||||
transitionOut: transitionOut
|
transitionOut: transitionOut
|
||||||
), navigationBarAppearance: .none, theme: .dark)
|
), navigationBarAppearance: .none, theme: .dark)
|
||||||
@ -925,6 +1037,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
|
|||||||
self.navigationPresentation = .flatModal
|
self.navigationPresentation = .flatModal
|
||||||
self.blocksBackgroundWhenInOverlay = true
|
self.blocksBackgroundWhenInOverlay = true
|
||||||
self.automaticallyControlPresentationContextLayout = false
|
self.automaticallyControlPresentationContextLayout = false
|
||||||
|
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: [.portrait])
|
||||||
|
|
||||||
self.context.sharedContext.hasPreloadBlockingContent.set(.single(true))
|
self.context.sharedContext.hasPreloadBlockingContent.set(.single(true))
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import Display
|
|||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
import Postbox
|
||||||
|
|
||||||
public final class StoryContentItem {
|
public final class StoryContentItem {
|
||||||
public final class ExternalState {
|
public final class ExternalState {
|
||||||
@ -22,13 +23,16 @@ public final class StoryContentItem {
|
|||||||
public final class Environment: Equatable {
|
public final class Environment: Equatable {
|
||||||
public let externalState: ExternalState
|
public let externalState: ExternalState
|
||||||
public let presentationProgressUpdated: (Double, Bool) -> Void
|
public let presentationProgressUpdated: (Double, Bool) -> Void
|
||||||
|
public let markAsSeen: (StoryId) -> Void
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
externalState: ExternalState,
|
externalState: ExternalState,
|
||||||
presentationProgressUpdated: @escaping (Double, Bool) -> Void
|
presentationProgressUpdated: @escaping (Double, Bool) -> Void,
|
||||||
|
markAsSeen: @escaping (StoryId) -> Void
|
||||||
) {
|
) {
|
||||||
self.externalState = externalState
|
self.externalState = externalState
|
||||||
self.presentationProgressUpdated = presentationProgressUpdated
|
self.presentationProgressUpdated = presentationProgressUpdated
|
||||||
|
self.markAsSeen = markAsSeen
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: Environment, rhs: Environment) -> Bool {
|
public static func ==(lhs: Environment, rhs: Environment) -> Bool {
|
||||||
@ -46,10 +50,6 @@ public final class StoryContentItem {
|
|||||||
public let rightInfoComponent: AnyComponent<Empty>?
|
public let rightInfoComponent: AnyComponent<Empty>?
|
||||||
public let peerId: EnginePeer.Id?
|
public let peerId: EnginePeer.Id?
|
||||||
public let storyItem: EngineStoryItem
|
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 let isMy: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@ -60,10 +60,6 @@ public final class StoryContentItem {
|
|||||||
rightInfoComponent: AnyComponent<Empty>?,
|
rightInfoComponent: AnyComponent<Empty>?,
|
||||||
peerId: EnginePeer.Id?,
|
peerId: EnginePeer.Id?,
|
||||||
storyItem: EngineStoryItem,
|
storyItem: EngineStoryItem,
|
||||||
preload: Signal<Never, NoError>?,
|
|
||||||
delete: (() -> Void)?,
|
|
||||||
markAsSeen: (() -> Void)?,
|
|
||||||
hasLike: Bool,
|
|
||||||
isMy: Bool
|
isMy: Bool
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
@ -73,10 +69,6 @@ public final class StoryContentItem {
|
|||||||
self.rightInfoComponent = rightInfoComponent
|
self.rightInfoComponent = rightInfoComponent
|
||||||
self.peerId = peerId
|
self.peerId = peerId
|
||||||
self.storyItem = storyItem
|
self.storyItem = storyItem
|
||||||
self.preload = preload
|
|
||||||
self.delete = delete
|
|
||||||
self.markAsSeen = markAsSeen
|
|
||||||
self.hasLike = hasLike
|
|
||||||
self.isMy = isMy
|
self.isMy = isMy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,4 +175,5 @@ public protocol StoryContentContext: AnyObject {
|
|||||||
|
|
||||||
func resetSideStates()
|
func resetSideStates()
|
||||||
func navigate(navigation: StoryContentContextNavigation)
|
func navigate(navigation: StoryContentContextNavigation)
|
||||||
|
func markAsSeen(id: StoryId)
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
public let close: () -> Void
|
public let close: () -> Void
|
||||||
public let navigate: (NavigationDirection) -> Void
|
public let navigate: (NavigationDirection) -> Void
|
||||||
public let delete: () -> Void
|
public let delete: () -> Void
|
||||||
|
public let markAsSeen: (StoryId) -> Void
|
||||||
public let controller: () -> ViewController?
|
public let controller: () -> ViewController?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@ -71,6 +72,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
close: @escaping () -> Void,
|
close: @escaping () -> Void,
|
||||||
navigate: @escaping (NavigationDirection) -> Void,
|
navigate: @escaping (NavigationDirection) -> Void,
|
||||||
delete: @escaping () -> Void,
|
delete: @escaping () -> Void,
|
||||||
|
markAsSeen: @escaping (StoryId) -> Void,
|
||||||
controller: @escaping () -> ViewController?
|
controller: @escaping () -> ViewController?
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
@ -90,6 +92,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.close = close
|
self.close = close
|
||||||
self.navigate = navigate
|
self.navigate = navigate
|
||||||
self.delete = delete
|
self.delete = delete
|
||||||
|
self.markAsSeen = markAsSeen
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -489,31 +492,38 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.visibleItems[focusedItem.id] = visibleItem
|
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(
|
let _ = visibleItem.view.update(
|
||||||
transition: itemTransition,
|
transition: itemTransition,
|
||||||
component: focusedItem.component,
|
component: focusedItem.component,
|
||||||
environment: {
|
environment: {
|
||||||
StoryContentItem.Environment(
|
itemEnvironment
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
containerSize: itemLayout.size
|
containerSize: itemLayout.size
|
||||||
)
|
)
|
||||||
@ -678,6 +688,8 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
let sourceLocalFrame = sourceView.convert(transitionOut.destinationRect, to: self)
|
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 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 {
|
if let centerInfoView = self.centerInfoItem?.view.view {
|
||||||
centerInfoView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
|
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
|
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 {
|
if let component = self.component, let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view {
|
||||||
let innerScale = innerSourceLocalFrame.width / visibleItemView.bounds.width
|
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 {
|
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 {
|
if self.topContentGradientLayer.colors == nil {
|
||||||
@ -1069,7 +1110,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
return
|
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 {
|
if component.slice.item.storyItem.isPinned {
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||||
|
@ -197,20 +197,6 @@ public final class StoryContentContextImpl: StoryContentContext {
|
|||||||
)),
|
)),
|
||||||
peerId: peer.id,
|
peerId: peer.id,
|
||||||
storyItem: mappedItem,
|
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
|
isMy: peerId == context.account.peerId
|
||||||
),
|
),
|
||||||
totalCount: itemsView.items.count,
|
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 {
|
public final class SingleStoryContentContextImpl: StoryContentContext {
|
||||||
@ -818,20 +808,6 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
|||||||
)),
|
)),
|
||||||
peerId: peer.id,
|
peerId: peer.id,
|
||||||
storyItem: mappedItem,
|
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
|
isMy: peer.id == context.account.peerId
|
||||||
),
|
),
|
||||||
totalCount: 1,
|
totalCount: 1,
|
||||||
@ -873,6 +849,9 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
|||||||
|
|
||||||
public func navigate(navigation: StoryContentContextNavigation) {
|
public func navigate(navigation: StoryContentContextNavigation) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func markAsSeen(id: StoryId) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class PeerStoryListContentContextImpl: StoryContentContext {
|
public final class PeerStoryListContentContextImpl: StoryContentContext {
|
||||||
@ -899,6 +878,9 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
|
|||||||
private var focusedId: Int32?
|
private var focusedId: Int32?
|
||||||
private var focusedIdUpdated = Promise<Void>(Void())
|
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?) {
|
public init(context: AccountContext, peerId: EnginePeer.Id, listContext: PeerStoryListContext, initialId: Int32?) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
@ -968,12 +950,6 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
|
|||||||
)),
|
)),
|
||||||
peerId: peer.id,
|
peerId: peer.id,
|
||||||
storyItem: item,
|
storyItem: item,
|
||||||
preload: nil,
|
|
||||||
delete: {
|
|
||||||
},
|
|
||||||
markAsSeen: {
|
|
||||||
},
|
|
||||||
hasLike: false,
|
|
||||||
isMy: peerId == self.context.account.peerId
|
isMy: peerId == self.context.account.peerId
|
||||||
),
|
),
|
||||||
totalCount: state.totalCount,
|
totalCount: state.totalCount,
|
||||||
@ -997,6 +973,97 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
|
|||||||
self.stateValue = stateValue
|
self.stateValue = stateValue
|
||||||
self.statePromise.set(.single(stateValue))
|
self.statePromise.set(.single(stateValue))
|
||||||
self.updatedPromise.set(.single(Void()))
|
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 {
|
deinit {
|
||||||
self.storyDisposable?.dispose()
|
self.storyDisposable?.dispose()
|
||||||
self.requestStoryDisposables.dispose()
|
self.requestStoryDisposables.dispose()
|
||||||
|
|
||||||
|
for (_, disposable) in self.preloadStoryResourceDisposables {
|
||||||
|
disposable.dispose()
|
||||||
|
}
|
||||||
|
self.pollStoryMetadataDisposables.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func resetSideStates() {
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import Display
|
|||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
import Postbox
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import PhotoResources
|
import PhotoResources
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
@ -240,7 +241,7 @@ final class StoryItemContentComponent: Component {
|
|||||||
if !self.markedAsSeen {
|
if !self.markedAsSeen {
|
||||||
self.markedAsSeen = true
|
self.markedAsSeen = true
|
||||||
if let component = self.component {
|
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 {
|
if !self.markedAsSeen {
|
||||||
self.markedAsSeen = true
|
self.markedAsSeen = true
|
||||||
if let component = self.component {
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
submodules/TelegramUI/Images.xcassets/Settings/Menu/Stories.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Settings/Menu/Stories.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Stories.svg",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
5
submodules/TelegramUI/Images.xcassets/Settings/Menu/Stories.imageset/Stories.svg
vendored
Normal file
5
submodules/TelegramUI/Images.xcassets/Settings/Menu/Stories.imageset/Stories.svg
vendored
Normal 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 |
@ -4564,6 +4564,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
if let result = itemNode.targetForStoryTransition(id: storyId) {
|
if let result = itemNode.targetForStoryTransition(id: storyId) {
|
||||||
transitionOut = StoryContainerScreen.TransitionOut(
|
transitionOut = StoryContainerScreen.TransitionOut(
|
||||||
destinationView: result,
|
destinationView: result,
|
||||||
|
transitionView: nil,
|
||||||
destinationRect: result.bounds,
|
destinationRect: result.bounds,
|
||||||
destinationCornerRadius: 2.0,
|
destinationCornerRadius: 2.0,
|
||||||
destinationIsAvatar: false,
|
destinationIsAvatar: false,
|
||||||
|
@ -368,7 +368,7 @@ private final class PeerInfoPendingPane {
|
|||||||
let paneNode: PeerInfoPaneNode
|
let paneNode: PeerInfoPaneNode
|
||||||
switch key {
|
switch key {
|
||||||
case .stories:
|
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
|
paneNode = visualPaneNode
|
||||||
visualPaneNode.openCurrentDate = {
|
visualPaneNode.openCurrentDate = {
|
||||||
openMediaCalendar()
|
openMediaCalendar()
|
||||||
|
@ -789,7 +789,7 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
|
|||||||
}
|
}
|
||||||
|
|
||||||
//TODO:localize
|
//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)
|
interaction.openSettings(.stories)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user