mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +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) {
|
||||
return StoryContainerScreen.TransitionOut(
|
||||
destinationView: transitionView,
|
||||
transitionView: nil,
|
||||
destinationRect: transitionView.bounds,
|
||||
destinationCornerRadius: transitionView.bounds.height * 0.5,
|
||||
destinationIsAvatar: true,
|
||||
|
@ -515,7 +515,7 @@ public class ContactsController: ViewController {
|
||||
return
|
||||
}
|
||||
|
||||
let storyContent = StoryContentContextImpl(context: self.context, includeHidden: false, focusedPeerId: peer?.id)
|
||||
let storyContent = StoryContentContextImpl(context: self.context, includeHidden: true, focusedPeerId: peer?.id)
|
||||
let _ = (storyContent.state
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] storyContentState in
|
||||
@ -551,6 +551,7 @@ public class ContactsController: ViewController {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
|
||||
return StoryContainerScreen.TransitionOut(
|
||||
destinationView: transitionView,
|
||||
transitionView: nil,
|
||||
destinationRect: transitionView.bounds,
|
||||
destinationCornerRadius: transitionView.bounds.height * 0.5,
|
||||
destinationIsAvatar: true,
|
||||
|
@ -62,7 +62,7 @@ public final class GridMessageSelectionNode: ASDisplayNode {
|
||||
|
||||
public final class GridMessageSelectionLayer: CALayer {
|
||||
private var selected = false
|
||||
private let checkLayer: CheckLayer
|
||||
public let checkLayer: CheckLayer
|
||||
|
||||
public init(theme: CheckNodeTheme) {
|
||||
self.checkLayer = CheckLayer(theme: theme, content: .check)
|
||||
|
@ -17,7 +17,7 @@ public enum MediaTrackFrameResult {
|
||||
}
|
||||
|
||||
private let traceEvents: Bool = {
|
||||
#if DEBUG
|
||||
#if DEBUG && false
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
|
@ -35,7 +35,7 @@ public protocol SparseItemGridBinding: AnyObject {
|
||||
func unbindLayer(layer: SparseItemGridLayer)
|
||||
func scrollerTextForTag(tag: Int32) -> String?
|
||||
func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError>
|
||||
func onTap(item: SparseItemGrid.Item)
|
||||
func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint)
|
||||
func onTagTap()
|
||||
func didScroll()
|
||||
func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition)
|
||||
@ -667,6 +667,27 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func itemHitTest(at point: CGPoint) -> (Item, CALayer, CGPoint)? {
|
||||
guard let items = self.items, !items.items.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let localPoint = self.scrollView.convert(point, from: self.view)
|
||||
|
||||
for (id, visibleItem) in self.visibleItems {
|
||||
if visibleItem.frame.contains(localPoint) {
|
||||
for item in items.items {
|
||||
if item.id == id {
|
||||
return (item, visibleItem.displayLayer, self.view.layer.convert(point, to: visibleItem.displayLayer))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func anchorItem(at point: CGPoint, orLower: Bool = false) -> (Item, Int)? {
|
||||
guard let items = self.items, !items.items.isEmpty, let layout = self.layout else {
|
||||
@ -862,7 +883,12 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
return
|
||||
}
|
||||
|
||||
let contentHeight = layout.contentHeight(count: items.count)
|
||||
let contentHeight: CGFloat
|
||||
if items.items.isEmpty {
|
||||
contentHeight = 0.0
|
||||
} else {
|
||||
contentHeight = layout.contentHeight(count: items.count)
|
||||
}
|
||||
let shimmerColors = items.itemBinding.getShimmerColors()
|
||||
|
||||
if resetScrolling {
|
||||
@ -904,83 +930,82 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
|
||||
var validIds = Set<AnyHashable>()
|
||||
var usedPlaceholderCount = 0
|
||||
if !items.items.isEmpty {
|
||||
var bindItems: [Item] = []
|
||||
var bindLayers: [SparseItemGridDisplayItem] = []
|
||||
var updateLayers: [SparseItemGridDisplayItem] = []
|
||||
|
||||
var bindItems: [Item] = []
|
||||
var bindLayers: [SparseItemGridDisplayItem] = []
|
||||
var updateLayers: [SparseItemGridDisplayItem] = []
|
||||
|
||||
let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count)
|
||||
for index in visibleRange.minIndex ... visibleRange.maxIndex {
|
||||
if let item = items.item(at: index) {
|
||||
let itemFrame = layout.frame(at: index)
|
||||
let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count)
|
||||
for index in visibleRange.minIndex ... visibleRange.maxIndex {
|
||||
if let item = items.item(at: index) {
|
||||
let itemFrame = layout.frame(at: index)
|
||||
|
||||
let itemLayer: VisibleItem
|
||||
if let current = self.visibleItems[item.id] {
|
||||
itemLayer = current
|
||||
updateLayers.append(itemLayer)
|
||||
} else {
|
||||
itemLayer = VisibleItem(layer: items.itemBinding.createLayer(), view: items.itemBinding.createView())
|
||||
self.visibleItems[item.id] = itemLayer
|
||||
|
||||
bindItems.append(item)
|
||||
bindLayers.append(itemLayer)
|
||||
|
||||
if let layer = itemLayer.layer {
|
||||
self.scrollView.layer.addSublayer(layer)
|
||||
} else if let view = itemLayer.view {
|
||||
self.scrollView.addSubview(view)
|
||||
}
|
||||
}
|
||||
|
||||
if itemLayer.needsShimmer {
|
||||
let placeholderLayer: SparseItemGridShimmerLayer
|
||||
if let current = itemLayer.shimmerLayer {
|
||||
placeholderLayer = current
|
||||
} else {
|
||||
placeholderLayer = items.itemBinding.createShimmerLayer() ?? Shimmer.Layer()
|
||||
self.scrollView.layer.insertSublayer(placeholderLayer, at: 0)
|
||||
itemLayer.shimmerLayer = placeholderLayer
|
||||
}
|
||||
|
||||
placeholderLayer.frame = itemFrame
|
||||
self.shimmer.update(colors: shimmerColors, layer: placeholderLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY))
|
||||
placeholderLayer.update(size: itemFrame.size)
|
||||
} else if let placeholderLayer = itemLayer.shimmerLayer {
|
||||
itemLayer.shimmerLayer = nil
|
||||
placeholderLayer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
validIds.insert(item.id)
|
||||
|
||||
itemLayer.frame = itemFrame
|
||||
let itemLayer: VisibleItem
|
||||
if let current = self.visibleItems[item.id] {
|
||||
itemLayer = current
|
||||
updateLayers.append(itemLayer)
|
||||
} else {
|
||||
itemLayer = VisibleItem(layer: items.itemBinding.createLayer(), view: items.itemBinding.createView())
|
||||
self.visibleItems[item.id] = itemLayer
|
||||
|
||||
bindItems.append(item)
|
||||
bindLayers.append(itemLayer)
|
||||
|
||||
if let layer = itemLayer.layer {
|
||||
self.scrollView.layer.addSublayer(layer)
|
||||
} else if let view = itemLayer.view {
|
||||
self.scrollView.addSubview(view)
|
||||
}
|
||||
}
|
||||
|
||||
if itemLayer.needsShimmer {
|
||||
let placeholderLayer: SparseItemGridShimmerLayer
|
||||
if self.visiblePlaceholders.count > usedPlaceholderCount {
|
||||
placeholderLayer = self.visiblePlaceholders[usedPlaceholderCount]
|
||||
if let current = itemLayer.shimmerLayer {
|
||||
placeholderLayer = current
|
||||
} else {
|
||||
placeholderLayer = items.itemBinding.createShimmerLayer() ?? Shimmer.Layer()
|
||||
self.scrollView.layer.addSublayer(placeholderLayer)
|
||||
self.visiblePlaceholders.append(placeholderLayer)
|
||||
self.scrollView.layer.insertSublayer(placeholderLayer, at: 0)
|
||||
itemLayer.shimmerLayer = placeholderLayer
|
||||
}
|
||||
let itemFrame = layout.frame(at: index)
|
||||
|
||||
placeholderLayer.frame = itemFrame
|
||||
self.shimmer.update(colors: shimmerColors, layer: placeholderLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY))
|
||||
placeholderLayer.update(size: itemFrame.size)
|
||||
usedPlaceholderCount += 1
|
||||
} else if let placeholderLayer = itemLayer.shimmerLayer {
|
||||
itemLayer.shimmerLayer = nil
|
||||
placeholderLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
|
||||
if !bindItems.isEmpty {
|
||||
items.itemBinding.bindLayers(items: bindItems, layers: bindLayers, size: layout.containerLayout.size, insets: layout.containerLayout.insets, synchronous: synchronous)
|
||||
}
|
||||
validIds.insert(item.id)
|
||||
|
||||
for item in updateLayers {
|
||||
let item = item as! VisibleItem
|
||||
if let layer = item.layer {
|
||||
layer.update(size: layer.frame.size)
|
||||
} else if let view = item.view {
|
||||
view.update(size: layer.frame.size, insets: layout.containerLayout.insets)
|
||||
itemLayer.frame = itemFrame
|
||||
} else {
|
||||
let placeholderLayer: SparseItemGridShimmerLayer
|
||||
if self.visiblePlaceholders.count > usedPlaceholderCount {
|
||||
placeholderLayer = self.visiblePlaceholders[usedPlaceholderCount]
|
||||
} else {
|
||||
placeholderLayer = items.itemBinding.createShimmerLayer() ?? Shimmer.Layer()
|
||||
self.scrollView.layer.addSublayer(placeholderLayer)
|
||||
self.visiblePlaceholders.append(placeholderLayer)
|
||||
}
|
||||
let itemFrame = layout.frame(at: index)
|
||||
placeholderLayer.frame = itemFrame
|
||||
self.shimmer.update(colors: shimmerColors, layer: placeholderLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY))
|
||||
placeholderLayer.update(size: itemFrame.size)
|
||||
usedPlaceholderCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
if !bindItems.isEmpty {
|
||||
items.itemBinding.bindLayers(items: bindItems, layers: bindLayers, size: layout.containerLayout.size, insets: layout.containerLayout.insets, synchronous: synchronous)
|
||||
}
|
||||
|
||||
for item in updateLayers {
|
||||
let item = item as! VisibleItem
|
||||
if let layer = item.layer {
|
||||
layer.update(size: layer.frame.size)
|
||||
} else if let view = item.view {
|
||||
view.update(size: layer.frame.size, insets: layout.containerLayout.insets)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1398,8 +1423,8 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
}
|
||||
if case .ended = recognizer.state {
|
||||
let location = recognizer.location(in: self.view)
|
||||
if let item = currentViewport.item(at: self.view.convert(location, to: currentViewport.view)) {
|
||||
items.itemBinding.onTap(item: item)
|
||||
if let (item, itemLayer, point) = currentViewport.itemHitTest(at: self.view.convert(location, to: currentViewport.view)) {
|
||||
items.itemBinding.onTap(item: item, itemLayer: itemLayer, point: point)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -780,12 +780,6 @@ public final class ManagedAudioSession {
|
||||
managedAudioSessionLog("ManagedAudioSession resetting options")
|
||||
try AVAudioSession.sharedInstance().setCategory(nativeCategory, options: options)
|
||||
}
|
||||
/*if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
try AVAudioSession.sharedInstance().setCategory(nativeCategory, mode: mode, policy: .default, options: options)
|
||||
} else {
|
||||
AVAudioSession.sharedInstance().perform(NSSelectorFromString("setCategory:error:"), with: nativeCategory)
|
||||
try AVAudioSession.sharedInstance().setMode(mode)
|
||||
}*/
|
||||
} catch let error {
|
||||
managedAudioSessionLog("ManagedAudioSession setup error \(error)")
|
||||
}
|
||||
|
@ -810,16 +810,25 @@ func _internal_editStory(account: Account, media: EngineStoryInputMedia?, id: In
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_deleteStory(account: Account, id: Int32) -> Signal<Never, NoError> {
|
||||
func _internal_deleteStories(account: Account, ids: [Int32]) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Void in
|
||||
var items = transaction.getStoryItems(peerId: account.peerId)
|
||||
if let index = items.firstIndex(where: { $0.id == id }) {
|
||||
items.remove(at: index)
|
||||
var updated = false
|
||||
for id in ids {
|
||||
if let index = items.firstIndex(where: { $0.id == id }) {
|
||||
items.remove(at: index)
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
if updated {
|
||||
transaction.setStoryItems(peerId: account.peerId, items: items)
|
||||
}
|
||||
account.stateManager.injectStoryUpdates(updates: ids.map { id in
|
||||
return .deleted(peerId: account.peerId, id: id)
|
||||
})
|
||||
}
|
||||
|> mapToSignal { _ -> Signal<Never, NoError> in
|
||||
return account.network.request(Api.functions.stories.deleteStories(id: [id]))
|
||||
return account.network.request(Api.functions.stories.deleteStories(id: ids))
|
||||
|> `catch` { _ -> Signal<[Int32], NoError> in
|
||||
return .single([])
|
||||
}
|
||||
@ -829,67 +838,114 @@ func _internal_deleteStory(account: Account, id: Int32) -> Signal<Never, NoError
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Api.InputUser? in
|
||||
if let peerStoryState = transaction.getPeerStoryState(peerId: peerId)?.get(Stories.PeerState.self) {
|
||||
transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState(
|
||||
subscriptionsOpaqueState: peerStoryState.subscriptionsOpaqueState,
|
||||
maxReadId: max(peerStoryState.maxReadId, id)
|
||||
)))
|
||||
func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32, asPinned: Bool) -> Signal<Never, NoError> {
|
||||
if asPinned {
|
||||
return account.postbox.transaction { transaction -> Api.InputUser? in
|
||||
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||
}
|
||||
|
||||
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||
}
|
||||
|> mapToSignal { inputUser -> Signal<Never, NoError> in
|
||||
guard let inputUser = inputUser else {
|
||||
return .complete()
|
||||
|> mapToSignal { inputUser -> Signal<Never, NoError> in
|
||||
guard let inputUser = inputUser else {
|
||||
return .complete()
|
||||
}
|
||||
|
||||
#if DEBUG && false
|
||||
if "".isEmpty {
|
||||
return .complete()
|
||||
}
|
||||
#endif
|
||||
|
||||
return account.network.request(Api.functions.stories.incrementStoryViews(userId: inputUser, id: [id]))
|
||||
|> `catch` { _ -> Signal<Api.Bool, NoError> in
|
||||
return .single(.boolFalse)
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
|
||||
account.stateManager.injectStoryUpdates(updates: [.read(peerId: peerId, maxId: id)])
|
||||
|
||||
#if DEBUG
|
||||
if "".isEmpty {
|
||||
return .complete()
|
||||
} else {
|
||||
return account.postbox.transaction { transaction -> Api.InputUser? in
|
||||
if let peerStoryState = transaction.getPeerStoryState(peerId: peerId)?.get(Stories.PeerState.self) {
|
||||
transaction.setPeerStoryState(peerId: peerId, state: CodableEntry(Stories.PeerState(
|
||||
subscriptionsOpaqueState: peerStoryState.subscriptionsOpaqueState,
|
||||
maxReadId: max(peerStoryState.maxReadId, id)
|
||||
)))
|
||||
}
|
||||
|
||||
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||
}
|
||||
#endif
|
||||
|
||||
return account.network.request(Api.functions.stories.readStories(userId: inputUser, maxId: id))
|
||||
|> `catch` { _ -> Signal<[Int32], NoError> in
|
||||
return .single([])
|
||||
|> mapToSignal { inputUser -> Signal<Never, NoError> in
|
||||
guard let inputUser = inputUser else {
|
||||
return .complete()
|
||||
}
|
||||
|
||||
account.stateManager.injectStoryUpdates(updates: [.read(peerId: peerId, maxId: id)])
|
||||
|
||||
#if DEBUG && false
|
||||
if "".isEmpty {
|
||||
return .complete()
|
||||
}
|
||||
#endif
|
||||
|
||||
return account.network.request(Api.functions.stories.readStories(userId: inputUser, maxId: id))
|
||||
|> `catch` { _ -> Signal<[Int32], NoError> in
|
||||
return .single([])
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_updateStoryIsPinned(account: Account, id: Int32, isPinned: Bool) -> Signal<Never, NoError> {
|
||||
func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStoryItem], isPinned: Bool) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Void in
|
||||
var items = transaction.getStoryItems(peerId: account.peerId)
|
||||
if let index = items.firstIndex(where: { $0.id == id }), case let .item(item) = items[index].value.get(Stories.StoredItem.self) {
|
||||
let updatedItem = Stories.Item(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: item.expirationTimestamp,
|
||||
media: item.media,
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views,
|
||||
privacy: item.privacy,
|
||||
isPinned: isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
items[index] = StoryItemsTableEntry(value: entry, id: item.id)
|
||||
transaction.setStoryItems(peerId: account.peerId, items: items)
|
||||
var updatedItems: [Stories.Item] = []
|
||||
for (id, referenceItem) in ids {
|
||||
if let index = items.firstIndex(where: { $0.id == id }), case let .item(item) = items[index].value.get(Stories.StoredItem.self) {
|
||||
let updatedItem = Stories.Item(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: item.expirationTimestamp,
|
||||
media: item.media,
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views,
|
||||
privacy: item.privacy,
|
||||
isPinned: isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
items[index] = StoryItemsTableEntry(value: entry, id: item.id)
|
||||
}
|
||||
|
||||
updatedItems.append(updatedItem)
|
||||
} else {
|
||||
let item = referenceItem.asStoryItem()
|
||||
let updatedItem = Stories.Item(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: item.expirationTimestamp,
|
||||
media: item.media,
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views,
|
||||
privacy: item.privacy,
|
||||
isPinned: isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
updatedItems.append(updatedItem)
|
||||
}
|
||||
|
||||
}
|
||||
transaction.setStoryItems(peerId: account.peerId, items: items)
|
||||
if !updatedItems.isEmpty {
|
||||
DispatchQueue.main.async {
|
||||
account.stateManager.injectStoryUpdates(updates: [.added(peerId: account.peerId, item: Stories.StoredItem.item(updatedItem))])
|
||||
account.stateManager.injectStoryUpdates(updates: updatedItems.map { updatedItem in
|
||||
return .added(peerId: account.peerId, item: Stories.StoredItem.item(updatedItem))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|> mapToSignal { _ -> Signal<Never, NoError> in
|
||||
return account.network.request(Api.functions.stories.togglePinned(id: [id], pinned: isPinned ? .boolTrue : .boolFalse))
|
||||
return account.network.request(Api.functions.stories.togglePinned(id: ids.keys.sorted(), pinned: isPinned ? .boolTrue : .boolFalse))
|
||||
|> `catch` { _ -> Signal<[Int32], NoError> in
|
||||
return .single([])
|
||||
}
|
||||
|
@ -95,6 +95,34 @@ public final class EngineStoryItem: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
extension EngineStoryItem {
|
||||
func asStoryItem() -> Stories.Item {
|
||||
return Stories.Item(
|
||||
id: self.id,
|
||||
timestamp: self.timestamp,
|
||||
expirationTimestamp: self.expirationTimestamp,
|
||||
media: self.media._asMedia(),
|
||||
text: self.text,
|
||||
entities: self.entities,
|
||||
views: self.views.flatMap { views in
|
||||
return Stories.Item.Views(
|
||||
seenCount: views.seenCount,
|
||||
seenPeerIds: views.seenPeers.map(\.id)
|
||||
)
|
||||
},
|
||||
privacy: self.privacy.flatMap { privacy in
|
||||
return Stories.Item.Privacy(
|
||||
base: privacy.base,
|
||||
additionallyIncludePeers: privacy.additionallyIncludePeers
|
||||
)
|
||||
},
|
||||
isPinned: self.isPinned,
|
||||
isExpired: self.isExpired,
|
||||
isPublic: self.isPublic
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public final class StorySubscriptionsContext {
|
||||
private enum OpaqueStateMark: Equatable {
|
||||
case empty
|
||||
@ -599,15 +627,17 @@ public final class PeerStoryListContext {
|
||||
return
|
||||
}
|
||||
|
||||
var finalUpdatedState: State?
|
||||
|
||||
for update in updates {
|
||||
switch update {
|
||||
case let .deleted(peerId, id):
|
||||
if self.peerId == peerId {
|
||||
if let index = self.stateValue.items.firstIndex(where: { $0.id == id }) {
|
||||
var updatedState = self.stateValue
|
||||
var updatedState = finalUpdatedState ?? self.stateValue
|
||||
updatedState.items.remove(at: index)
|
||||
updatedState.totalCount = max(0, updatedState.totalCount - 1)
|
||||
self.stateValue = updatedState
|
||||
finalUpdatedState = updatedState
|
||||
}
|
||||
}
|
||||
case let .added(peerId, item):
|
||||
@ -617,7 +647,7 @@ public final class PeerStoryListContext {
|
||||
if case let .item(item) = item {
|
||||
if item.isPinned {
|
||||
if let media = item.media {
|
||||
var updatedState = self.stateValue
|
||||
var updatedState = finalUpdatedState ?? self.stateValue
|
||||
updatedState.items[index] = EngineStoryItem(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
@ -638,13 +668,47 @@ public final class PeerStoryListContext {
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
self.stateValue = updatedState
|
||||
finalUpdatedState = updatedState
|
||||
}
|
||||
} else {
|
||||
var updatedState = self.stateValue
|
||||
var updatedState = finalUpdatedState ?? self.stateValue
|
||||
updatedState.items.remove(at: index)
|
||||
updatedState.totalCount = max(0, updatedState.totalCount - 1)
|
||||
self.stateValue = updatedState
|
||||
finalUpdatedState = updatedState
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !self.isArchived {
|
||||
if case let .item(item) = item {
|
||||
if item.isPinned {
|
||||
if let media = item.media {
|
||||
var updatedState = finalUpdatedState ?? self.stateValue
|
||||
updatedState.items.append(EngineStoryItem(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: item.expirationTimestamp,
|
||||
media: EngineMedia(media),
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views.flatMap { views in
|
||||
return EngineStoryItem.Views(
|
||||
seenCount: views.seenCount,
|
||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||
return peers[id].flatMap(EnginePeer.init)
|
||||
}
|
||||
)
|
||||
},
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
))
|
||||
updatedState.items.sort(by: { lhs, rhs in
|
||||
return lhs.timestamp > rhs.timestamp
|
||||
})
|
||||
finalUpdatedState = updatedState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -654,6 +718,10 @@ public final class PeerStoryListContext {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let finalUpdatedState = finalUpdatedState {
|
||||
self.stateValue = finalUpdatedState
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -926,16 +926,16 @@ public extension TelegramEngine {
|
||||
return _internal_editStory(account: self.account, media: media, id: id, text: text, entities: entities, privacy: privacy)
|
||||
}
|
||||
|
||||
public func deleteStory(id: Int32) -> Signal<Never, NoError> {
|
||||
return _internal_deleteStory(account: self.account, id: id)
|
||||
public func deleteStories(ids: [Int32]) -> Signal<Never, NoError> {
|
||||
return _internal_deleteStories(account: self.account, ids: ids)
|
||||
}
|
||||
|
||||
public func markStoryAsSeen(peerId: EnginePeer.Id, id: Int32) -> Signal<Never, NoError> {
|
||||
return _internal_markStoryAsSeen(account: self.account, peerId: peerId, id: id)
|
||||
public func markStoryAsSeen(peerId: EnginePeer.Id, id: Int32, asPinned: Bool) -> Signal<Never, NoError> {
|
||||
return _internal_markStoryAsSeen(account: self.account, peerId: peerId, id: id, asPinned: asPinned)
|
||||
}
|
||||
|
||||
public func updateStoryIsPinned(id: Int32, isPinned: Bool) -> Signal<Never, NoError> {
|
||||
return _internal_updateStoryIsPinned(account: self.account, id: id, isPinned: isPinned)
|
||||
public func updateStoriesArePinned(ids: [Int32: EngineStoryItem], isPinned: Bool) -> Signal<Never, NoError> {
|
||||
return _internal_updateStoriesArePinned(account: self.account, ids: ids, isPinned: isPinned)
|
||||
}
|
||||
|
||||
public func getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal<StoryViewList?, NoError> {
|
||||
|
@ -29,16 +29,14 @@ public struct PresentationResourcesSettings {
|
||||
public static let devices = renderIcon(name: "Settings/Menu/Sessions")
|
||||
public static let chatFolders = renderIcon(name: "Settings/Menu/ChatListFilters")
|
||||
public static let stickers = renderIcon(name: "Settings/Menu/Stickers")
|
||||
|
||||
public static let notifications = renderIcon(name: "Settings/Menu/Notifications")
|
||||
public static let security = renderIcon(name: "Settings/Menu/Security")
|
||||
public static let dataAndStorage = renderIcon(name: "Settings/Menu/DataAndStorage")
|
||||
public static let appearance = renderIcon(name: "Settings/Menu/Appearance")
|
||||
public static let language = renderIcon(name: "Settings/Menu/Language")
|
||||
|
||||
public static let deleteAccount = renderIcon(name: "Chat/Info/GroupRemovedIcon")
|
||||
|
||||
public static let powerSaving = renderIcon(name: "Settings/Menu/PowerSaving")
|
||||
public static let stories = renderIcon(name: "Settings/Menu/Stories")
|
||||
|
||||
public static let premium = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
|
@ -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 AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import ViewControllerComponent
|
||||
import ComponentDisplayAdapters
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import MultilineTextComponent
|
||||
import EmojiStatusComponent
|
||||
import TelegramStringFormatting
|
||||
import CheckNode
|
||||
import SolidRoundedButtonComponent
|
||||
|
||||
final class StorageUsageScreenSelectionPanelComponent: Component {
|
||||
public final class BottomButtonPanelComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let title: String
|
||||
let label: String?
|
||||
@ -23,7 +15,7 @@ final class StorageUsageScreenSelectionPanelComponent: Component {
|
||||
let insets: UIEdgeInsets
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
public init(
|
||||
theme: PresentationTheme,
|
||||
title: String,
|
||||
label: String?,
|
||||
@ -39,7 +31,7 @@ final class StorageUsageScreenSelectionPanelComponent: Component {
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: StorageUsageScreenSelectionPanelComponent, rhs: StorageUsageScreenSelectionPanelComponent) -> Bool {
|
||||
public static func ==(lhs: BottomButtonPanelComponent, rhs: BottomButtonPanelComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
@ -58,14 +50,14 @@ final class StorageUsageScreenSelectionPanelComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
class View: UIView {
|
||||
public class View: UIView {
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private let separatorLayer: SimpleLayer
|
||||
private let actionButton = ComponentView<Empty>()
|
||||
|
||||
private var component: StorageUsageScreenSelectionPanelComponent?
|
||||
private var component: BottomButtonPanelComponent?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
override public init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
|
||||
self.separatorLayer = SimpleLayer()
|
||||
|
||||
@ -75,11 +67,11 @@ final class StorageUsageScreenSelectionPanelComponent: Component {
|
||||
self.layer.addSublayer(self.separatorLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: StorageUsageScreenSelectionPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
func update(component: BottomButtonPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
self.component = component
|
||||
|
||||
@ -146,11 +138,11 @@ final class StorageUsageScreenSelectionPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -117,6 +117,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
|
||||
private let button: HighlightTrackingButtonNode
|
||||
|
||||
public var disableAnimations: Bool = false
|
||||
|
||||
var manualLayout: Bool = false
|
||||
private var validLayout: (CGSize, CGRect)?
|
||||
|
||||
@ -356,7 +358,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
if !self.updateStatus() {
|
||||
if updated {
|
||||
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: self.disableAnimations ? .immediate : .animated(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,10 @@ swift_library(
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode",
|
||||
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
|
||||
"//submodules/TelegramUI/Components/ChatTitleView",
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TelegramUI/Components/BottomButtonPanelComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -10,6 +10,9 @@ import PeerInfoVisualMediaPaneNode
|
||||
import ViewControllerComponent
|
||||
import ChatListHeaderComponent
|
||||
import ContextUI
|
||||
import ChatTitleView
|
||||
import BottomButtonPanelComponent
|
||||
import UndoUI
|
||||
|
||||
final class PeerInfoStoryGridScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -48,6 +51,13 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
private var paneNode: PeerInfoStoryPaneNode?
|
||||
private var paneStatusDisposable: Disposable?
|
||||
private(set) var paneStatusText: String?
|
||||
|
||||
private(set) var selectedCount: Int = 0
|
||||
private var selectionStateDisposable: Disposable?
|
||||
|
||||
private var selectionPanel: ComponentView<Empty>?
|
||||
|
||||
private weak var mediaGalleryContextMenu: ContextController?
|
||||
|
||||
@ -59,6 +69,11 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.paneStatusDisposable?.dispose()
|
||||
self.selectionStateDisposable?.dispose()
|
||||
}
|
||||
|
||||
func morePressed(source: ContextReferenceContentNode) {
|
||||
guard let component = self.component, let controller = self.environment?.controller(), let pane = self.paneNode else {
|
||||
return
|
||||
@ -68,120 +83,168 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let strings = presentationData.strings
|
||||
|
||||
var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)?
|
||||
let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in
|
||||
let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement
|
||||
let canZoom: Bool = nextZoomLevel != nil
|
||||
|
||||
return ContextMenuActionItem(id: isZoomIn ? 0 : 1, text: isZoomIn ? strings.SharedMedia_ZoomIn : strings.SharedMedia_ZoomOut, textColor: canZoom ? .primary : .disabled, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4))
|
||||
}, action: canZoom ? { action in
|
||||
guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else {
|
||||
return
|
||||
}
|
||||
pane.updateZoomLevel(level: zoomLevel)
|
||||
if let recurseGenerateAction = recurseGenerateAction {
|
||||
action.updateAction(0, recurseGenerateAction(true))
|
||||
action.updateAction(1, recurseGenerateAction(false))
|
||||
}
|
||||
} : nil)
|
||||
}
|
||||
recurseGenerateAction = { isZoomIn in
|
||||
return generateAction(isZoomIn)
|
||||
}
|
||||
|
||||
items.append(.action(generateAction(true)))
|
||||
items.append(.action(generateAction(false)))
|
||||
|
||||
if component.peerId == component.context.account.peerId, case .saved = component.scope {
|
||||
var ignoreNextActions = false
|
||||
if self.selectedCount != 0 {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Show Archive", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/StoryArchive"), color: theme.contextMenu.primaryColor)
|
||||
//TODO:update icon
|
||||
items.append(.action(ContextMenuActionItem(text: "Save to Photos", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
if ignoreNextActions {
|
||||
return
|
||||
}
|
||||
ignoreNextActions = true
|
||||
a(.default)
|
||||
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
self.environment?.controller()?.push(PeerInfoStoryGridScreen(context: component.context, peerId: component.peerId, scope: .archive))
|
||||
let _ = component
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: strings.Common_Delete, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
||||
}, action: { [weak self] _, a in
|
||||
a(.default)
|
||||
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
guard let paneNode = self.paneNode, !paneNode.selectedIds.isEmpty else {
|
||||
return
|
||||
}
|
||||
let _ = component.context.engine.messages.deleteStories(ids: Array(paneNode.selectedIds)).start()
|
||||
|
||||
//TODO:localize
|
||||
let text: String
|
||||
if paneNode.selectedIds.count == 1 {
|
||||
text = "1 story deleted."
|
||||
} else {
|
||||
text = "\(paneNode.selectedIds.count) stories deleted."
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
|
||||
environment.controller()?.present(UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .info(title: nil, text: text, timeout: nil),
|
||||
elevatedLayout: false,
|
||||
animateInAsReplacement: false,
|
||||
action: { _ in return false }
|
||||
), in: .current)
|
||||
|
||||
paneNode.clearSelection()
|
||||
})))
|
||||
} else {
|
||||
var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)?
|
||||
let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in
|
||||
let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement
|
||||
let canZoom: Bool = nextZoomLevel != nil
|
||||
|
||||
return ContextMenuActionItem(id: isZoomIn ? 0 : 1, text: isZoomIn ? strings.SharedMedia_ZoomIn : strings.SharedMedia_ZoomOut, textColor: canZoom ? .primary : .disabled, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4))
|
||||
}, action: canZoom ? { action in
|
||||
guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else {
|
||||
return
|
||||
}
|
||||
pane.updateZoomLevel(level: zoomLevel)
|
||||
if let recurseGenerateAction = recurseGenerateAction {
|
||||
action.updateAction(0, recurseGenerateAction(true))
|
||||
action.updateAction(1, recurseGenerateAction(false))
|
||||
}
|
||||
} : nil)
|
||||
}
|
||||
recurseGenerateAction = { isZoomIn in
|
||||
return generateAction(isZoomIn)
|
||||
}
|
||||
|
||||
items.append(.action(generateAction(true)))
|
||||
items.append(.action(generateAction(false)))
|
||||
|
||||
if component.peerId == component.context.account.peerId, case .saved = component.scope {
|
||||
var ignoreNextActions = false
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Show Archive", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/StoryArchive"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
if ignoreNextActions {
|
||||
return
|
||||
}
|
||||
ignoreNextActions = true
|
||||
a(.default)
|
||||
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
self.environment?.controller()?.push(PeerInfoStoryGridScreen(context: component.context, peerId: component.peerId, scope: .archive))
|
||||
})))
|
||||
}
|
||||
|
||||
/*if photoCount != 0 && videoCount != 0 {
|
||||
items.append(.separator)
|
||||
|
||||
let showPhotos: Bool
|
||||
switch pane.contentType {
|
||||
case .photo, .photoOrVideo:
|
||||
showPhotos = true
|
||||
default:
|
||||
showPhotos = false
|
||||
}
|
||||
let showVideos: Bool
|
||||
switch pane.contentType {
|
||||
case .video, .photoOrVideo:
|
||||
showVideos = true
|
||||
default:
|
||||
showVideos = false
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowPhotos, icon: { theme in
|
||||
if !showPhotos {
|
||||
return nil
|
||||
}
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak pane] _, a in
|
||||
a(.default)
|
||||
|
||||
guard let pane = pane else {
|
||||
return
|
||||
}
|
||||
let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType
|
||||
switch pane.contentType {
|
||||
case .photoOrVideo:
|
||||
updatedContentType = .video
|
||||
case .photo:
|
||||
updatedContentType = .photo
|
||||
case .video:
|
||||
updatedContentType = .photoOrVideo
|
||||
default:
|
||||
updatedContentType = pane.contentType
|
||||
}
|
||||
pane.updateContentType(contentType: updatedContentType)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowVideos, icon: { theme in
|
||||
if !showVideos {
|
||||
return nil
|
||||
}
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak pane] _, a in
|
||||
a(.default)
|
||||
|
||||
guard let pane = pane else {
|
||||
return
|
||||
}
|
||||
let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType
|
||||
switch pane.contentType {
|
||||
case .photoOrVideo:
|
||||
updatedContentType = .photo
|
||||
case .photo:
|
||||
updatedContentType = .photoOrVideo
|
||||
case .video:
|
||||
updatedContentType = .video
|
||||
default:
|
||||
updatedContentType = pane.contentType
|
||||
}
|
||||
pane.updateContentType(contentType: updatedContentType)
|
||||
})))
|
||||
}*/
|
||||
}
|
||||
|
||||
/*if photoCount != 0 && videoCount != 0 {
|
||||
items.append(.separator)
|
||||
|
||||
let showPhotos: Bool
|
||||
switch pane.contentType {
|
||||
case .photo, .photoOrVideo:
|
||||
showPhotos = true
|
||||
default:
|
||||
showPhotos = false
|
||||
}
|
||||
let showVideos: Bool
|
||||
switch pane.contentType {
|
||||
case .video, .photoOrVideo:
|
||||
showVideos = true
|
||||
default:
|
||||
showVideos = false
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowPhotos, icon: { theme in
|
||||
if !showPhotos {
|
||||
return nil
|
||||
}
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak pane] _, a in
|
||||
a(.default)
|
||||
|
||||
guard let pane = pane else {
|
||||
return
|
||||
}
|
||||
let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType
|
||||
switch pane.contentType {
|
||||
case .photoOrVideo:
|
||||
updatedContentType = .video
|
||||
case .photo:
|
||||
updatedContentType = .photo
|
||||
case .video:
|
||||
updatedContentType = .photoOrVideo
|
||||
default:
|
||||
updatedContentType = pane.contentType
|
||||
}
|
||||
pane.updateContentType(contentType: updatedContentType)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ShowVideos, icon: { theme in
|
||||
if !showVideos {
|
||||
return nil
|
||||
}
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak pane] _, a in
|
||||
a(.default)
|
||||
|
||||
guard let pane = pane else {
|
||||
return
|
||||
}
|
||||
let updatedContentType: PeerInfoVisualMediaPaneNode.ContentType
|
||||
switch pane.contentType {
|
||||
case .photoOrVideo:
|
||||
updatedContentType = .photo
|
||||
case .photo:
|
||||
updatedContentType = .photoOrVideo
|
||||
case .video:
|
||||
updatedContentType = .video
|
||||
default:
|
||||
updatedContentType = pane.contentType
|
||||
}
|
||||
pane.updateContentType(contentType: updatedContentType)
|
||||
})))
|
||||
}*/
|
||||
|
||||
let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
|
||||
contextController.passthroughTouchEvent = { [weak self] sourceView, point in
|
||||
guard let self else {
|
||||
@ -217,6 +280,8 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let sideInset: CGFloat = 14.0
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
|
||||
let themeUpdated = self.environment?.theme !== environment.theme
|
||||
@ -227,6 +292,82 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
||||
self.backgroundColor = environment.theme.list.plainBackgroundColor
|
||||
}
|
||||
|
||||
var bottomInset: CGFloat = environment.safeInsets.bottom
|
||||
|
||||
if self.selectedCount != 0 {
|
||||
let selectionPanel: ComponentView<Empty>
|
||||
var selectionPanelTransition = transition
|
||||
if let current = self.selectionPanel {
|
||||
selectionPanel = current
|
||||
} else {
|
||||
selectionPanelTransition = .immediate
|
||||
selectionPanel = ComponentView()
|
||||
self.selectionPanel = selectionPanel
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let selectionPanelSize = selectionPanel.update(
|
||||
transition: selectionPanelTransition,
|
||||
component: AnyComponent(BottomButtonPanelComponent(
|
||||
theme: environment.theme,
|
||||
title: "Save to Profile",
|
||||
label: nil,
|
||||
isEnabled: true,
|
||||
insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: environment.safeInsets.bottom, right: sideInset),
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
guard let paneNode = self.paneNode, !paneNode.selectedIds.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = component.context.engine.messages.updateStoriesArePinned(ids: paneNode.selectedItems, isPinned: true).start()
|
||||
|
||||
//TODO:localize
|
||||
let title: String
|
||||
if paneNode.selectedIds.count == 1 {
|
||||
title = "Story saved to your profile"
|
||||
} else {
|
||||
title = "\(paneNode.selectedIds.count) saved to your profile"
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
|
||||
environment.controller()?.present(UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .info(title: title, text: "Saved stories can be viewed by others on your profile until you remove them.", timeout: nil),
|
||||
elevatedLayout: false,
|
||||
animateInAsReplacement: false,
|
||||
action: { _ in return false }
|
||||
), in: .current)
|
||||
|
||||
paneNode.clearSelection()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let selectionPanelView = selectionPanel.view {
|
||||
var animateIn = false
|
||||
if selectionPanelView.superview == nil {
|
||||
self.addSubview(selectionPanelView)
|
||||
animateIn = true
|
||||
}
|
||||
selectionPanelTransition.setFrame(view: selectionPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - selectionPanelSize.height), size: selectionPanelSize))
|
||||
if animateIn {
|
||||
transition.animatePosition(view: selectionPanelView, from: CGPoint(x: 0.0, y: selectionPanelSize.height), to: CGPoint(), additive: true)
|
||||
}
|
||||
}
|
||||
bottomInset = selectionPanelSize.height
|
||||
} else if let selectionPanel = self.selectionPanel {
|
||||
self.selectionPanel = nil
|
||||
if let selectionPanelView = selectionPanel.view {
|
||||
transition.setPosition(view: selectionPanelView, position: CGPoint(x: selectionPanelView.center.x, y: availableSize.height + selectionPanelView.bounds.height * 0.5), completion: { [weak selectionPanelView] _ in
|
||||
selectionPanelView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let paneNode: PeerInfoStoryPaneNode
|
||||
if let current = self.paneNode {
|
||||
paneNode = current
|
||||
@ -237,6 +378,7 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
||||
chatLocation: .peer(id: component.peerId),
|
||||
contentType: .photoOrVideo,
|
||||
captureProtected: false,
|
||||
isSaved: true,
|
||||
isArchive: component.scope == .archive,
|
||||
navigationController: { [weak self] in
|
||||
guard let self else {
|
||||
@ -247,13 +389,41 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
||||
)
|
||||
self.paneNode = paneNode
|
||||
self.addSubview(paneNode.view)
|
||||
|
||||
self.paneStatusDisposable = (paneNode.status
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.paneStatusText != status?.text {
|
||||
self.paneStatusText = status?.text
|
||||
(self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle()
|
||||
}
|
||||
})
|
||||
|
||||
var applyState = false
|
||||
self.selectionStateDisposable = (paneNode.updatedSelectedIds
|
||||
|> distinctUntilChanged
|
||||
|> deliverOnMainQueue).start(next: { [weak self] selectedIds in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.selectedCount = selectedIds.count
|
||||
|
||||
if applyState {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
}
|
||||
(self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle()
|
||||
})
|
||||
applyState = true
|
||||
}
|
||||
|
||||
paneNode.update(
|
||||
size: availableSize,
|
||||
topInset: environment.navigationHeight,
|
||||
sideInset: environment.safeInsets.left,
|
||||
bottomInset: environment.safeInsets.bottom,
|
||||
bottomInset: bottomInset,
|
||||
visibleHeight: availableSize.height,
|
||||
isScrollingLockedAtTop: false,
|
||||
expandProgress: 1.0,
|
||||
@ -283,8 +453,11 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let scope: Scope
|
||||
private var isDismissed: Bool = false
|
||||
|
||||
private var titleView: ChatTitleView?
|
||||
|
||||
private var moreBarButton: MoreHeaderButton?
|
||||
private var moreBarButtonItem: UIBarButtonItem?
|
||||
|
||||
@ -294,6 +467,7 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
|
||||
scope: Scope
|
||||
) {
|
||||
self.context = context
|
||||
self.scope = scope
|
||||
|
||||
super.init(context: context, component: PeerInfoStoryGridScreenComponent(
|
||||
context: context,
|
||||
@ -301,9 +475,6 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
|
||||
scope: scope
|
||||
), navigationBarAppearance: .default, theme: .default)
|
||||
|
||||
//TODO:localize
|
||||
self.navigationItem.title = "My Stories"
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
let moreBarButton = MoreHeaderButton(color: presentationData.theme.rootController.navigationBar.buttonColor)
|
||||
moreBarButton.isUserInteractionEnabled = true
|
||||
@ -321,6 +492,21 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
|
||||
moreBarButton.addTarget(self, action: #selector(self.morePressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.navigationItem.setRightBarButton(moreBarButtonItem, animated: false)
|
||||
|
||||
self.titleView = ChatTitleView(
|
||||
context: context, theme:
|
||||
presentationData.theme,
|
||||
strings: presentationData.strings,
|
||||
dateTimeFormat: presentationData.dateTimeFormat,
|
||||
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||
animationCache: context.animationCache,
|
||||
animationRenderer: context.animationRenderer
|
||||
)
|
||||
self.titleView?.disableAnimations = true
|
||||
|
||||
self.navigationItem.titleView = self.titleView
|
||||
|
||||
self.updateTitle()
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
@ -330,6 +516,34 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
|
||||
deinit {
|
||||
}
|
||||
|
||||
func updateTitle() {
|
||||
//TODO:localize
|
||||
switch self.scope {
|
||||
case .saved:
|
||||
guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else {
|
||||
return
|
||||
}
|
||||
let title: String?
|
||||
if let paneStatusText = componentView.paneStatusText, !paneStatusText.isEmpty {
|
||||
title = paneStatusText
|
||||
} else {
|
||||
title = nil
|
||||
}
|
||||
self.titleView?.titleContent = .custom("My Stories", title, false)
|
||||
case .archive:
|
||||
guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else {
|
||||
return
|
||||
}
|
||||
let title: String
|
||||
if componentView.selectedCount != 0 {
|
||||
title = "\(componentView.selectedCount) Selected"
|
||||
} else {
|
||||
title = "Stories Archive"
|
||||
}
|
||||
self.titleView?.titleContent = .custom(title, nil, false)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func morePressed() {
|
||||
guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else {
|
||||
return
|
||||
@ -342,6 +556,8 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.titleView?.layout = layout
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -476,6 +476,49 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL
|
||||
}
|
||||
}
|
||||
|
||||
private final class ItemTransitionView: UIView {
|
||||
private weak var itemLayer: ItemLayer?
|
||||
private var copyDurationLayer: SimpleLayer?
|
||||
|
||||
private var durationLayerBottomLeftPosition: CGPoint?
|
||||
|
||||
init(itemLayer: ItemLayer?) {
|
||||
self.itemLayer = itemLayer
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
if let itemLayer {
|
||||
self.layer.contents = itemLayer.contents
|
||||
self.layer.contentsRect = itemLayer.contentsRect
|
||||
|
||||
if let durationLayer = itemLayer.durationLayer {
|
||||
let copyDurationLayer = SimpleLayer()
|
||||
copyDurationLayer.contents = durationLayer.contents
|
||||
copyDurationLayer.contentsRect = durationLayer.contentsRect
|
||||
copyDurationLayer.contentsGravity = durationLayer.contentsGravity
|
||||
copyDurationLayer.contentsScale = durationLayer.contentsScale
|
||||
copyDurationLayer.frame = durationLayer.frame
|
||||
self.layer.addSublayer(copyDurationLayer)
|
||||
self.copyDurationLayer = copyDurationLayer
|
||||
|
||||
self.durationLayerBottomLeftPosition = CGPoint(x: itemLayer.bounds.width - durationLayer.frame.maxX, y: itemLayer.bounds.height - durationLayer.frame.maxY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(state: StoryContainerScreen.TransitionState, transition: Transition) {
|
||||
let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)
|
||||
|
||||
if let copyDurationLayer = self.copyDurationLayer, let durationLayerBottomLeftPosition = self.durationLayerBottomLeftPosition {
|
||||
transition.setFrame(layer: copyDurationLayer, frame: CGRect(origin: CGPoint(x: size.width - durationLayerBottomLeftPosition.x - copyDurationLayer.bounds.width, y: size.height - durationLayerBottomLeftPosition.y - copyDurationLayer.bounds.height), size: copyDurationLayer.bounds.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
||||
let context: AccountContext
|
||||
let chatLocation: ChatLocation
|
||||
@ -485,8 +528,9 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
||||
var chatPresentationData: ChatPresentationData
|
||||
var checkNodeTheme: CheckNodeTheme
|
||||
|
||||
var itemInteraction: VisualMediaItemInteraction?
|
||||
var loadHoleImpl: ((SparseItemGrid.HoleAnchor, SparseItemGrid.HoleLocation) -> Signal<Never, NoError>)?
|
||||
var onTapImpl: ((VisualMediaItem) -> Void)?
|
||||
var onTapImpl: ((VisualMediaItem, CALayer, CGPoint) -> Void)?
|
||||
var onTagTapImpl: (() -> Void)?
|
||||
var didScrollImpl: (() -> Void)?
|
||||
var coveringInsetOffsetUpdatedImpl: ((ContainedViewLayoutTransition) -> Void)?
|
||||
@ -671,8 +715,11 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
||||
layer.updateDuration(duration: duration, isMin: isMin, minFactor: min(1.0, layer.bounds.height / 74.0))
|
||||
}
|
||||
|
||||
//TODO:selection
|
||||
layer.updateSelection(theme: self.checkNodeTheme, isSelected: nil, animated: false)
|
||||
var isSelected: Bool?
|
||||
if let selectedIds = self.itemInteraction?.selectedIds {
|
||||
isSelected = selectedIds.contains(story.id)
|
||||
}
|
||||
layer.updateSelection(theme: self.checkNodeTheme, isSelected: isSelected, animated: false)
|
||||
|
||||
layer.bind(item: item)
|
||||
}
|
||||
@ -698,11 +745,11 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
||||
}
|
||||
}
|
||||
|
||||
func onTap(item: SparseItemGrid.Item) {
|
||||
func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint) {
|
||||
guard let item = item as? VisualMediaItem else {
|
||||
return
|
||||
}
|
||||
self.onTapImpl?(item)
|
||||
self.onTapImpl?(item, itemLayer, point)
|
||||
}
|
||||
|
||||
func onTagTap() {
|
||||
@ -756,6 +803,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
private let context: AccountContext
|
||||
private let peerId: PeerId
|
||||
private let chatLocation: ChatLocation
|
||||
private let isSaved: Bool
|
||||
private let isArchive: Bool
|
||||
public private(set) var contentType: ContentType
|
||||
private var contentTypePromise: ValuePromise<ContentType>
|
||||
@ -778,6 +826,30 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
return self._itemInteraction!
|
||||
}
|
||||
|
||||
public var selectedIds: Set<Int32> {
|
||||
return self.itemInteraction.selectedIds ?? Set()
|
||||
}
|
||||
private let selectedIdsPromise = ValuePromise<Set<Int32>>(Set())
|
||||
public var updatedSelectedIds: Signal<Set<Int32>, NoError> {
|
||||
return self.selectedIdsPromise.get()
|
||||
}
|
||||
|
||||
public var selectedItems: [Int32: EngineStoryItem] {
|
||||
var result: [Int32: EngineStoryItem] = [:]
|
||||
for id in self.selectedIds {
|
||||
if let items = self.items {
|
||||
for item in items.items {
|
||||
if let item = item as? VisualMediaItem {
|
||||
if item.story.id == id {
|
||||
result[id] = item.story
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)?
|
||||
|
||||
private let ready = Promise<Bool>()
|
||||
@ -818,13 +890,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
public init(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, contentType: ContentType, captureProtected: Bool, isArchive: Bool, navigationController: @escaping () -> NavigationController?) {
|
||||
public init(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, contentType: ContentType, captureProtected: Bool, isSaved: Bool, isArchive: Bool, navigationController: @escaping () -> NavigationController?) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.chatLocation = chatLocation
|
||||
self.contentType = contentType
|
||||
self.contentTypePromise = ValuePromise<ContentType>(contentType)
|
||||
self.navigationController = navigationController
|
||||
self.isSaved = isSaved
|
||||
self.isArchive = isArchive
|
||||
|
||||
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
@ -867,10 +940,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
return strongSelf.loadHole(anchor: hole, at: location)
|
||||
}
|
||||
|
||||
self.itemGridBinding.onTapImpl = { [weak self] item in
|
||||
self.itemGridBinding.onTapImpl = { [weak self] item, itemLayer, point in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let selectedIds = self.itemInteraction.selectedIds, let itemLayer = itemLayer as? ItemLayer, let selectionLayer = itemLayer.selectionLayer {
|
||||
if selectionLayer.checkLayer.frame.insetBy(dx: -4.0, dy: -4.0).contains(point) {
|
||||
self.itemInteraction.toggleSelection(item.story.id, !selectedIds.contains(item.story.id))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//TODO:selection
|
||||
let listContext = PeerStoryListContentContextImpl(
|
||||
context: self.context,
|
||||
@ -928,6 +1009,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
let itemRect = self.itemGrid.frameForItem(layer: foundItemLayer)
|
||||
return StoryContainerScreen.TransitionOut(
|
||||
destinationView: self.view,
|
||||
transitionView: StoryContainerScreen.TransitionView(
|
||||
makeView: { [weak foundItemLayer] in
|
||||
return ItemTransitionView(itemLayer: foundItemLayer as? ItemLayer)
|
||||
},
|
||||
updateView: { view, state, transition in
|
||||
(view as? ItemTransitionView)?.update(state: state, transition: transition)
|
||||
}
|
||||
),
|
||||
destinationRect: self.itemGrid.view.convert(itemRect, to: self.view),
|
||||
destinationCornerRadius: 0.0,
|
||||
destinationIsAvatar: false,
|
||||
@ -938,6 +1027,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
return nil
|
||||
}
|
||||
)
|
||||
|
||||
self.hiddenMediaDisposable?.dispose()
|
||||
self.hiddenMediaDisposable = (storyContainerScreen.focusedItem
|
||||
|> deliverOnMainQueue).start(next: { [weak self] itemId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let itemId {
|
||||
self.itemInteraction.hiddenMedia = Set([itemId.id])
|
||||
} else {
|
||||
self.itemInteraction.hiddenMedia = Set()
|
||||
}
|
||||
self.updateHiddenItems()
|
||||
})
|
||||
|
||||
navigationController.pushViewController(storyContainerScreen)
|
||||
})
|
||||
}
|
||||
@ -1043,14 +1147,26 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
let _ = self
|
||||
},
|
||||
toggleSelection: { [weak self] id, value in
|
||||
guard let self else {
|
||||
guard let self, let itemInteraction = self._itemInteraction else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
if var selectedIds = itemInteraction.selectedIds {
|
||||
if value {
|
||||
selectedIds.insert(id)
|
||||
} else {
|
||||
selectedIds.remove(id)
|
||||
}
|
||||
itemInteraction.selectedIds = selectedIds
|
||||
self.selectedIdsPromise.set(selectedIds)
|
||||
self.updateSelectedItems(animated: true)
|
||||
}
|
||||
}
|
||||
)
|
||||
//TODO:selection
|
||||
//self.itemInteraction.selectedItemIds =
|
||||
if isArchive {
|
||||
self._itemInteraction?.selectedIds = Set()
|
||||
}
|
||||
self.itemGridBinding.itemInteraction = self._itemInteraction
|
||||
|
||||
self.contextGestureContainerNode.isGestureEnabled = true
|
||||
self.contextGestureContainerNode.addSubnode(self.itemGrid)
|
||||
@ -1136,6 +1252,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
|
||||
strongSelf.itemGrid.cancelGestures()
|
||||
}
|
||||
|
||||
self.statusPromise.set(.single(PeerInfoStatusData(text: "", isActivity: false, key: .stories)))
|
||||
|
||||
/*self.storedStateDisposable = (visualMediaStoredState(engine: context.engine, peerId: peerId, messageTag: self.stateTag)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
@ -1385,6 +1503,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
return
|
||||
}
|
||||
|
||||
let title: String
|
||||
if state.totalCount == 0 {
|
||||
title = ""
|
||||
} else if state.totalCount == 1 {
|
||||
if self.isSaved {
|
||||
title = "1 saved story"
|
||||
} else {
|
||||
title = "1 story"
|
||||
}
|
||||
} else {
|
||||
if self.isSaved {
|
||||
title = "\(state.totalCount) saved stories"
|
||||
} else {
|
||||
title = "\(state.totalCount) stories"
|
||||
}
|
||||
}
|
||||
self.statusPromise.set(.single(PeerInfoStatusData(text: title, isActivity: false, key: .stories)))
|
||||
|
||||
let timezoneOffset = Int32(TimeZone.current.secondsFromGMT())
|
||||
|
||||
var mappedItems: [SparseItemGrid.Item] = []
|
||||
@ -1402,6 +1538,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
}
|
||||
totalCount = state.totalCount
|
||||
totalCount = max(mappedItems.count, totalCount)
|
||||
|
||||
if totalCount == 0 {
|
||||
totalCount = 100
|
||||
}
|
||||
|
||||
Queue.mainQueue().async { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
@ -1626,67 +1766,64 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
}
|
||||
}
|
||||
|
||||
public func clearSelection() {
|
||||
self.itemInteraction.selectedIds = Set()
|
||||
self.selectedIdsPromise.set(Set())
|
||||
self.updateSelectedItems(animated: true)
|
||||
}
|
||||
|
||||
public func updateSelectedMessages(animated: Bool) {
|
||||
/*switch self.contentType {
|
||||
case .files, .music, .voiceAndVideoMessages:
|
||||
self.itemGrid.forEachVisibleItem { item in
|
||||
guard let itemView = item.view as? ItemView, let (size, topInset, sideInset, bottomInset, _, _, _, _) = self.currentParams else {
|
||||
return
|
||||
}
|
||||
if let item = itemView.item {
|
||||
itemView.bind(
|
||||
item: item,
|
||||
presentationData: self.itemGridBinding.chatPresentationData,
|
||||
context: self.itemGridBinding.context,
|
||||
chatLocation: self.itemGridBinding.chatLocation,
|
||||
interaction: self.itemGridBinding.listItemInteraction,
|
||||
isSelected: self.chatControllerInteraction.selectionState?.selectedIds.contains(item.message.id),
|
||||
size: CGSize(width: size.width, height: itemView.bounds.height),
|
||||
insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset)
|
||||
)
|
||||
}
|
||||
}
|
||||
case .photo, .video, .photoOrVideo, .gifs:
|
||||
self.itemGrid.forEachVisibleItem { item in
|
||||
guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item else {
|
||||
return
|
||||
}
|
||||
itemLayer.updateSelection(theme: self.itemGridBinding.checkNodeTheme, isSelected: self.chatControllerInteraction.selectionState?.selectedIds.contains(item.message.id), animated: animated)
|
||||
}
|
||||
|
||||
private func updateSelectedItems(animated: Bool) {
|
||||
self.itemGrid.forEachVisibleItem { item in
|
||||
guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item else {
|
||||
return
|
||||
}
|
||||
itemLayer.updateSelection(theme: self.itemGridBinding.checkNodeTheme, isSelected: self.itemInteraction.selectedIds?.contains(item.story.id), animated: animated)
|
||||
}
|
||||
|
||||
let isSelecting = self.chatControllerInteraction.selectionState != nil
|
||||
self.itemGrid.pinchEnabled = !isSelecting
|
||||
|
||||
if isSelecting {
|
||||
if self.gridSelectionGesture == nil {
|
||||
let selectionGesture = MediaPickerGridSelectionGesture<EngineMessage.Id>()
|
||||
selectionGesture.delegate = self
|
||||
selectionGesture.sideInset = 44.0
|
||||
selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in
|
||||
self?.itemGrid.isScrollEnabled = isEnabled
|
||||
}
|
||||
selectionGesture.itemAt = { [weak self] point in
|
||||
if let strongSelf = self, let itemLayer = strongSelf.itemGrid.item(at: point)?.layer as? ItemLayer, let messageId = itemLayer.item?.message.id {
|
||||
return (messageId, strongSelf.chatControllerInteraction.selectionState?.selectedIds.contains(messageId) ?? false)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
selectionGesture.updateSelection = { [weak self] messageId, selected in
|
||||
if let strongSelf = self {
|
||||
strongSelf.chatControllerInteraction.toggleMessagesSelection([messageId], selected)
|
||||
}
|
||||
}
|
||||
self.itemGrid.view.addGestureRecognizer(selectionGesture)
|
||||
self.gridSelectionGesture = selectionGesture
|
||||
/*let isSelecting = self.chatControllerInteraction.selectionState != nil
|
||||
self.itemGrid.pinchEnabled = !isSelecting
|
||||
|
||||
if isSelecting {
|
||||
if self.gridSelectionGesture == nil {
|
||||
let selectionGesture = MediaPickerGridSelectionGesture<EngineMessage.Id>()
|
||||
selectionGesture.delegate = self
|
||||
selectionGesture.sideInset = 44.0
|
||||
selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in
|
||||
self?.itemGrid.isScrollEnabled = isEnabled
|
||||
}
|
||||
} else if let gridSelectionGesture = self.gridSelectionGesture {
|
||||
self.itemGrid.view.removeGestureRecognizer(gridSelectionGesture)
|
||||
self.gridSelectionGesture = nil
|
||||
selectionGesture.itemAt = { [weak self] point in
|
||||
if let strongSelf = self, let itemLayer = strongSelf.itemGrid.item(at: point)?.layer as? ItemLayer, let messageId = itemLayer.item?.message.id {
|
||||
return (messageId, strongSelf.chatControllerInteraction.selectionState?.selectedIds.contains(messageId) ?? false)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
selectionGesture.updateSelection = { [weak self] messageId, selected in
|
||||
if let strongSelf = self {
|
||||
strongSelf.chatControllerInteraction.toggleMessagesSelection([messageId], selected)
|
||||
}
|
||||
}
|
||||
self.itemGrid.view.addGestureRecognizer(selectionGesture)
|
||||
self.gridSelectionGesture = selectionGesture
|
||||
}
|
||||
} else if let gridSelectionGesture = self.gridSelectionGesture {
|
||||
self.itemGrid.view.removeGestureRecognizer(gridSelectionGesture)
|
||||
self.gridSelectionGesture = nil
|
||||
}*/
|
||||
}
|
||||
|
||||
private func updateHiddenItems() {
|
||||
self.itemGrid.forEachVisibleItem { item in
|
||||
guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item else {
|
||||
return
|
||||
}
|
||||
itemLayer.isHidden = self.itemInteraction.hiddenMedia.contains(item.story.id)
|
||||
}
|
||||
}
|
||||
|
||||
public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||
self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
|
||||
|
||||
@ -1704,8 +1841,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
}
|
||||
|
||||
let fixedItemAspect: CGFloat? = 9.0 / 16.0
|
||||
|
||||
let gridTopInset = topInset
|
||||
|
||||
self.itemGrid.update(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none)
|
||||
self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1144,7 +1144,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme
|
||||
}
|
||||
}
|
||||
|
||||
func onTap(item: SparseItemGrid.Item) {
|
||||
func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint) {
|
||||
guard let item = item as? VisualMediaItem else {
|
||||
return
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ swift_library(
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
||||
"//submodules/TelegramUI/Components/BottomButtonPanelComponent",
|
||||
"//submodules/CheckNode",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/ContextUI",
|
||||
|
@ -23,6 +23,7 @@ import TelegramAnimatedStickerNode
|
||||
import TelegramStringFormatting
|
||||
import GalleryData
|
||||
import AnimatedTextComponent
|
||||
import BottomButtonPanelComponent
|
||||
|
||||
#if DEBUG
|
||||
import os.signpost
|
||||
@ -1207,7 +1208,7 @@ final class StorageUsageScreenComponent: Component {
|
||||
|
||||
let selectionPanelSize = selectionPanel.update(
|
||||
transition: selectionPanelTransition,
|
||||
component: AnyComponent(StorageUsageScreenSelectionPanelComponent(
|
||||
component: AnyComponent(BottomButtonPanelComponent(
|
||||
theme: environment.theme,
|
||||
title: bottomPanelSelectionData.isComplete ? environment.strings.StorageManagement_ClearCache : environment.strings.StorageManagement_ClearSelected,
|
||||
label: bottomPanelSelectionData.size == 0 ? nil : dataSizeString(Int(bottomPanelSelectionData.size), formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".")),
|
||||
|
@ -29,22 +29,63 @@ func hasFirstResponder(_ view: UIView) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
|
||||
var updateIsTracking: ((Bool) -> Void)?
|
||||
|
||||
override var state: UIGestureRecognizer.State {
|
||||
didSet {
|
||||
switch self.state {
|
||||
case .began, .cancelled, .ended, .failed:
|
||||
if self.isTracking {
|
||||
self.isTracking = false
|
||||
self.updateIsTracking?(false)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isTracking: Bool = false
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
|
||||
if self.isTracking {
|
||||
self.isTracking = false
|
||||
self.updateIsTracking?(false)
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
if !self.isTracking {
|
||||
self.isTracking = true
|
||||
self.updateIsTracking?(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class StoryContainerScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let content: StoryContentContext
|
||||
let focusedItemPromise: Promise<StoryId?>
|
||||
let transitionIn: StoryContainerScreen.TransitionIn?
|
||||
let transitionOut: (EnginePeer.Id, AnyHashable) -> StoryContainerScreen.TransitionOut?
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
content: StoryContentContext,
|
||||
focusedItemPromise: Promise<StoryId?>,
|
||||
transitionIn: StoryContainerScreen.TransitionIn?,
|
||||
transitionOut: @escaping (EnginePeer.Id, AnyHashable) -> StoryContainerScreen.TransitionOut?
|
||||
) {
|
||||
self.context = context
|
||||
self.content = content
|
||||
self.focusedItemPromise = focusedItemPromise
|
||||
self.transitionIn = transitionIn
|
||||
self.transitionOut = transitionOut
|
||||
}
|
||||
@ -118,12 +159,14 @@ private final class StoryContainerScreenComponent: Component {
|
||||
private let backgroundLayer: SimpleLayer
|
||||
private let backgroundEffectView: BlurredBackgroundView
|
||||
|
||||
private let focusedItem = ValuePromise<StoryId?>(nil, ignoreRepeated: true)
|
||||
private var contentUpdatedDisposable: Disposable?
|
||||
|
||||
private var visibleItemSetViews: [EnginePeer.Id: ItemSetView] = [:]
|
||||
|
||||
private var itemSetPanState: ItemSetPanState?
|
||||
private var dismissPanState: ItemSetPanState?
|
||||
private var isHoldingTouch: Bool = false
|
||||
|
||||
private var isAnimatingOut: Bool = false
|
||||
private var didAnimateOut: Bool = false
|
||||
@ -163,8 +206,15 @@ private final class StoryContainerScreenComponent: Component {
|
||||
})
|
||||
self.addGestureRecognizer(verticalPanRecognizer)
|
||||
|
||||
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:)))
|
||||
let longPressRecognizer = StoryLongPressRecognizer(target: self, action: #selector(self.longPressGesture(_:)))
|
||||
longPressRecognizer.delegate = self
|
||||
longPressRecognizer.updateIsTracking = { [weak self] isTracking in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isHoldingTouch = isTracking
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
self.addGestureRecognizer(longPressRecognizer)
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||
@ -316,7 +366,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func longPressGesture(_ recognizer: UILongPressGestureRecognizer) {
|
||||
@objc private func longPressGesture(_ recognizer: StoryLongPressRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
if self.itemSetPanState == nil {
|
||||
@ -381,6 +431,10 @@ private final class StoryContainerScreenComponent: Component {
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
if let component = self.component {
|
||||
component.focusedItemPromise.set(self.focusedItem.get())
|
||||
}
|
||||
|
||||
if let transitionIn = self.component?.transitionIn, transitionIn.sourceView != nil {
|
||||
self.backgroundLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.28, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
self.backgroundEffectView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.28, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
@ -409,17 +463,22 @@ private final class StoryContainerScreenComponent: Component {
|
||||
transition.setAlpha(view: self.backgroundEffectView, alpha: 0.0)
|
||||
|
||||
let transitionOutCompleted = transitionOut.completed
|
||||
let focusedItemPromise = component.focusedItemPromise
|
||||
itemSetComponentView.animateOut(transitionOut: transitionOut, completion: {
|
||||
completion()
|
||||
transitionOutCompleted()
|
||||
focusedItemPromise.set(.single(nil))
|
||||
})
|
||||
} else {
|
||||
self.dismissPanState = ItemSetPanState(fraction: 1.0, didBegin: true)
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
|
||||
|
||||
let focusedItemPromise = self.component?.focusedItemPromise
|
||||
|
||||
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
transition.setAlpha(layer: self.backgroundLayer, alpha: 0.0, completion: { _ in
|
||||
completion()
|
||||
focusedItemPromise?.set(.single(nil))
|
||||
})
|
||||
transition.setAlpha(view: self.backgroundEffectView, alpha: 0.0)
|
||||
}
|
||||
@ -475,6 +534,12 @@ private final class StoryContainerScreenComponent: Component {
|
||||
return
|
||||
}
|
||||
if update {
|
||||
var focusedItemId: StoryId?
|
||||
if let slice = component.content.stateValue?.slice {
|
||||
focusedItemId = StoryId(peerId: slice.peer.id, id: slice.item.storyItem.id)
|
||||
}
|
||||
self.focusedItem.set(focusedItemId)
|
||||
|
||||
if component.content.stateValue?.slice == nil {
|
||||
self.environment?.controller()?.dismiss()
|
||||
} else {
|
||||
@ -511,6 +576,9 @@ private final class StoryContainerScreenComponent: Component {
|
||||
if self.isAnimatingOut {
|
||||
isProgressPaused = true
|
||||
}
|
||||
if self.isHoldingTouch {
|
||||
isProgressPaused = true
|
||||
}
|
||||
|
||||
var dismissPanOffset: CGFloat = 0.0
|
||||
var dismissPanScale: CGFloat = 1.0
|
||||
@ -684,9 +752,15 @@ private final class StoryContainerScreenComponent: Component {
|
||||
environment.controller()?.dismiss()
|
||||
}
|
||||
|
||||
let _ = component.context.engine.messages.deleteStory(id: slice.item.storyItem.id).start()
|
||||
let _ = component.context.engine.messages.deleteStories(ids: [slice.item.storyItem.id]).start()
|
||||
}
|
||||
},
|
||||
markAsSeen: { [weak self] id in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.content.markAsSeen(id: id)
|
||||
},
|
||||
controller: { [weak self] in
|
||||
return self?.environment?.controller()
|
||||
}
|
||||
@ -865,6 +939,35 @@ private final class StoryContainerScreenComponent: Component {
|
||||
}
|
||||
|
||||
public class StoryContainerScreen: ViewControllerComponentContainer {
|
||||
public struct TransitionState: Equatable {
|
||||
public var sourceSize: CGSize
|
||||
public var destinationSize: CGSize
|
||||
public var progress: CGFloat
|
||||
|
||||
public init(
|
||||
sourceSize: CGSize,
|
||||
destinationSize: CGSize,
|
||||
progress: CGFloat
|
||||
) {
|
||||
self.sourceSize = sourceSize
|
||||
self.destinationSize = destinationSize
|
||||
self.progress = progress
|
||||
}
|
||||
}
|
||||
|
||||
public final class TransitionView {
|
||||
public let makeView: () -> UIView
|
||||
public let updateView: (UIView, TransitionState, Transition) -> Void
|
||||
|
||||
public init(
|
||||
makeView: @escaping () -> UIView,
|
||||
updateView: @escaping (UIView, TransitionState, Transition) -> Void
|
||||
) {
|
||||
self.makeView = makeView
|
||||
self.updateView = updateView
|
||||
}
|
||||
}
|
||||
|
||||
public final class TransitionIn {
|
||||
public weak var sourceView: UIView?
|
||||
public let sourceRect: CGRect
|
||||
@ -883,6 +986,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
|
||||
|
||||
public final class TransitionOut {
|
||||
public weak var destinationView: UIView?
|
||||
public let transitionView: TransitionView?
|
||||
public let destinationRect: CGRect
|
||||
public let destinationCornerRadius: CGFloat
|
||||
public let destinationIsAvatar: Bool
|
||||
@ -890,12 +994,14 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
|
||||
|
||||
public init(
|
||||
destinationView: UIView,
|
||||
transitionView: TransitionView?,
|
||||
destinationRect: CGRect,
|
||||
destinationCornerRadius: CGFloat,
|
||||
destinationIsAvatar: Bool,
|
||||
completed: @escaping () -> Void
|
||||
) {
|
||||
self.destinationView = destinationView
|
||||
self.transitionView = transitionView
|
||||
self.destinationRect = destinationRect
|
||||
self.destinationCornerRadius = destinationCornerRadius
|
||||
self.destinationIsAvatar = destinationIsAvatar
|
||||
@ -906,6 +1012,11 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
|
||||
private let context: AccountContext
|
||||
private var isDismissed: Bool = false
|
||||
|
||||
private let focusedItemPromise = Promise<StoryId?>(nil)
|
||||
public var focusedItem: Signal<StoryId?, NoError> {
|
||||
return self.focusedItemPromise.get()
|
||||
}
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
content: StoryContentContext,
|
||||
@ -917,6 +1028,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
|
||||
super.init(context: context, component: StoryContainerScreenComponent(
|
||||
context: context,
|
||||
content: content,
|
||||
focusedItemPromise: self.focusedItemPromise,
|
||||
transitionIn: transitionIn,
|
||||
transitionOut: transitionOut
|
||||
), navigationBarAppearance: .none, theme: .dark)
|
||||
@ -925,6 +1037,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
|
||||
self.navigationPresentation = .flatModal
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
self.automaticallyControlPresentationContextLayout = false
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: [.portrait])
|
||||
|
||||
self.context.sharedContext.hasPreloadBlockingContent.set(.single(true))
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
|
||||
public final class StoryContentItem {
|
||||
public final class ExternalState {
|
||||
@ -22,13 +23,16 @@ public final class StoryContentItem {
|
||||
public final class Environment: Equatable {
|
||||
public let externalState: ExternalState
|
||||
public let presentationProgressUpdated: (Double, Bool) -> Void
|
||||
public let markAsSeen: (StoryId) -> Void
|
||||
|
||||
public init(
|
||||
externalState: ExternalState,
|
||||
presentationProgressUpdated: @escaping (Double, Bool) -> Void
|
||||
presentationProgressUpdated: @escaping (Double, Bool) -> Void,
|
||||
markAsSeen: @escaping (StoryId) -> Void
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.presentationProgressUpdated = presentationProgressUpdated
|
||||
self.markAsSeen = markAsSeen
|
||||
}
|
||||
|
||||
public static func ==(lhs: Environment, rhs: Environment) -> Bool {
|
||||
@ -46,10 +50,6 @@ public final class StoryContentItem {
|
||||
public let rightInfoComponent: AnyComponent<Empty>?
|
||||
public let peerId: EnginePeer.Id?
|
||||
public let storyItem: EngineStoryItem
|
||||
public let preload: Signal<Never, NoError>?
|
||||
public let delete: (() -> Void)?
|
||||
public let markAsSeen: (() -> Void)?
|
||||
public let hasLike: Bool
|
||||
public let isMy: Bool
|
||||
|
||||
public init(
|
||||
@ -60,10 +60,6 @@ public final class StoryContentItem {
|
||||
rightInfoComponent: AnyComponent<Empty>?,
|
||||
peerId: EnginePeer.Id?,
|
||||
storyItem: EngineStoryItem,
|
||||
preload: Signal<Never, NoError>?,
|
||||
delete: (() -> Void)?,
|
||||
markAsSeen: (() -> Void)?,
|
||||
hasLike: Bool,
|
||||
isMy: Bool
|
||||
) {
|
||||
self.id = id
|
||||
@ -73,10 +69,6 @@ public final class StoryContentItem {
|
||||
self.rightInfoComponent = rightInfoComponent
|
||||
self.peerId = peerId
|
||||
self.storyItem = storyItem
|
||||
self.preload = preload
|
||||
self.delete = delete
|
||||
self.markAsSeen = markAsSeen
|
||||
self.hasLike = hasLike
|
||||
self.isMy = isMy
|
||||
}
|
||||
}
|
||||
@ -183,4 +175,5 @@ public protocol StoryContentContext: AnyObject {
|
||||
|
||||
func resetSideStates()
|
||||
func navigate(navigation: StoryContentContextNavigation)
|
||||
func markAsSeen(id: StoryId)
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
public let close: () -> Void
|
||||
public let navigate: (NavigationDirection) -> Void
|
||||
public let delete: () -> Void
|
||||
public let markAsSeen: (StoryId) -> Void
|
||||
public let controller: () -> ViewController?
|
||||
|
||||
public init(
|
||||
@ -71,6 +72,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
close: @escaping () -> Void,
|
||||
navigate: @escaping (NavigationDirection) -> Void,
|
||||
delete: @escaping () -> Void,
|
||||
markAsSeen: @escaping (StoryId) -> Void,
|
||||
controller: @escaping () -> ViewController?
|
||||
) {
|
||||
self.context = context
|
||||
@ -90,6 +92,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.close = close
|
||||
self.navigate = navigate
|
||||
self.delete = delete
|
||||
self.markAsSeen = markAsSeen
|
||||
self.controller = controller
|
||||
}
|
||||
|
||||
@ -489,31 +492,38 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.visibleItems[focusedItem.id] = visibleItem
|
||||
}
|
||||
|
||||
let itemEnvironment = StoryContentItem.Environment(
|
||||
externalState: visibleItem.externalState,
|
||||
presentationProgressUpdated: { [weak self, weak visibleItem] progress, canSwitch in
|
||||
guard let self = self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
guard let visibleItem else {
|
||||
return
|
||||
}
|
||||
visibleItem.currentProgress = progress
|
||||
|
||||
if let navigationStripView = self.navigationStrip.view as? MediaNavigationStripComponent.View {
|
||||
navigationStripView.updateCurrentItemProgress(value: progress, transition: .immediate)
|
||||
}
|
||||
if progress >= 1.0 && canSwitch && !visibleItem.requestedNext {
|
||||
visibleItem.requestedNext = true
|
||||
|
||||
component.navigate(.next)
|
||||
}
|
||||
},
|
||||
markAsSeen: { [weak self] id in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.markAsSeen(id)
|
||||
}
|
||||
)
|
||||
let _ = visibleItem.view.update(
|
||||
transition: itemTransition,
|
||||
component: focusedItem.component,
|
||||
environment: {
|
||||
StoryContentItem.Environment(
|
||||
externalState: visibleItem.externalState,
|
||||
presentationProgressUpdated: { [weak self, weak visibleItem] progress, canSwitch in
|
||||
guard let self = self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
guard let visibleItem else {
|
||||
return
|
||||
}
|
||||
visibleItem.currentProgress = progress
|
||||
|
||||
if let navigationStripView = self.navigationStrip.view as? MediaNavigationStripComponent.View {
|
||||
navigationStripView.updateCurrentItemProgress(value: progress, transition: .immediate)
|
||||
}
|
||||
if progress >= 1.0 && canSwitch && !visibleItem.requestedNext {
|
||||
visibleItem.requestedNext = true
|
||||
|
||||
component.navigate(.next)
|
||||
}
|
||||
}
|
||||
)
|
||||
itemEnvironment
|
||||
},
|
||||
containerSize: itemLayout.size
|
||||
)
|
||||
@ -678,6 +688,8 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
let sourceLocalFrame = sourceView.convert(transitionOut.destinationRect, to: self)
|
||||
let innerSourceLocalFrame = CGRect(origin: CGPoint(x: sourceLocalFrame.minX - self.contentContainerView.frame.minX, y: sourceLocalFrame.minY - self.contentContainerView.frame.minY), size: sourceLocalFrame.size)
|
||||
|
||||
let contentSourceFrame = self.contentContainerView.frame
|
||||
|
||||
if let centerInfoView = self.centerInfoItem?.view.view {
|
||||
centerInfoView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
|
||||
}
|
||||
@ -703,6 +715,35 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
removeOnCompletion: false
|
||||
)
|
||||
|
||||
let transitionView = transitionOut.transitionView
|
||||
let transitionViewImpl = transitionView?.makeView()
|
||||
if let transitionViewImpl {
|
||||
self.insertSubview(transitionViewImpl, belowSubview: self.contentContainerView)
|
||||
|
||||
transitionViewImpl.frame = contentSourceFrame
|
||||
transitionViewImpl.alpha = 0.0
|
||||
transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState(
|
||||
sourceSize: contentSourceFrame.size,
|
||||
destinationSize: sourceLocalFrame.size,
|
||||
progress: 0.0
|
||||
), .immediate)
|
||||
}
|
||||
|
||||
if let transitionViewImpl {
|
||||
let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
|
||||
|
||||
transitionViewImpl.alpha = 1.0
|
||||
transitionViewImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
||||
self.contentContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
|
||||
transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame)
|
||||
transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState(
|
||||
sourceSize: contentSourceFrame.size,
|
||||
destinationSize: sourceLocalFrame.size,
|
||||
progress: 1.0
|
||||
), transition)
|
||||
}
|
||||
|
||||
if let component = self.component, let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view {
|
||||
let innerScale = innerSourceLocalFrame.width / visibleItemView.bounds.width
|
||||
|
||||
@ -755,7 +796,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
|
||||
if self.component?.slice.item.storyItem.id != component.slice.item.storyItem.id {
|
||||
let _ = component.context.engine.messages.markStoryAsSeen(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id).start()
|
||||
component.markAsSeen(StoryId(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id))
|
||||
}
|
||||
|
||||
if self.topContentGradientLayer.colors == nil {
|
||||
@ -1069,7 +1110,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = component.context.engine.messages.updateStoryIsPinned(id: component.slice.item.storyItem.id, isPinned: !component.slice.item.storyItem.isPinned).start()
|
||||
let _ = component.context.engine.messages.updateStoriesArePinned(ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).start()
|
||||
|
||||
if component.slice.item.storyItem.isPinned {
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||
|
@ -197,20 +197,6 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
)),
|
||||
peerId: peer.id,
|
||||
storyItem: mappedItem,
|
||||
preload: nil,
|
||||
delete: { [weak context] in
|
||||
guard let context else {
|
||||
return
|
||||
}
|
||||
let _ = context
|
||||
},
|
||||
markAsSeen: { [weak context] in
|
||||
guard let context else {
|
||||
return
|
||||
}
|
||||
let _ = context.engine.messages.markStoryAsSeen(peerId: peerId, id: item.id).start()
|
||||
},
|
||||
hasLike: false,
|
||||
isMy: peerId == context.account.peerId
|
||||
),
|
||||
totalCount: itemsView.items.count,
|
||||
@ -727,6 +713,10 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func markAsSeen(id: StoryId) {
|
||||
let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: false).start()
|
||||
}
|
||||
}
|
||||
|
||||
public final class SingleStoryContentContextImpl: StoryContentContext {
|
||||
@ -818,20 +808,6 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
||||
)),
|
||||
peerId: peer.id,
|
||||
storyItem: mappedItem,
|
||||
preload: nil,
|
||||
delete: { [weak context] in
|
||||
guard let context else {
|
||||
return
|
||||
}
|
||||
let _ = context
|
||||
},
|
||||
markAsSeen: { [weak context] in
|
||||
guard let context else {
|
||||
return
|
||||
}
|
||||
let _ = context.engine.messages.markStoryAsSeen(peerId: peer.id, id: item.id).start()
|
||||
},
|
||||
hasLike: false,
|
||||
isMy: peer.id == context.account.peerId
|
||||
),
|
||||
totalCount: 1,
|
||||
@ -873,6 +849,9 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
||||
|
||||
public func navigate(navigation: StoryContentContextNavigation) {
|
||||
}
|
||||
|
||||
public func markAsSeen(id: StoryId) {
|
||||
}
|
||||
}
|
||||
|
||||
public final class PeerStoryListContentContextImpl: StoryContentContext {
|
||||
@ -899,6 +878,9 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
|
||||
private var focusedId: Int32?
|
||||
private var focusedIdUpdated = Promise<Void>(Void())
|
||||
|
||||
private var preloadStoryResourceDisposables: [MediaResourceId: Disposable] = [:]
|
||||
private var pollStoryMetadataDisposables = DisposableSet()
|
||||
|
||||
public init(context: AccountContext, peerId: EnginePeer.Id, listContext: PeerStoryListContext, initialId: Int32?) {
|
||||
self.context = context
|
||||
|
||||
@ -968,12 +950,6 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
|
||||
)),
|
||||
peerId: peer.id,
|
||||
storyItem: item,
|
||||
preload: nil,
|
||||
delete: {
|
||||
},
|
||||
markAsSeen: {
|
||||
},
|
||||
hasLike: false,
|
||||
isMy: peerId == self.context.account.peerId
|
||||
),
|
||||
totalCount: state.totalCount,
|
||||
@ -997,6 +973,97 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
|
||||
self.stateValue = stateValue
|
||||
self.statePromise.set(.single(stateValue))
|
||||
self.updatedPromise.set(.single(Void()))
|
||||
|
||||
var resultResources: [EngineMediaResource.Id: StoryPreloadInfo] = [:]
|
||||
var pollItems: [StoryKey] = []
|
||||
|
||||
if let peer, let focusedIndex, let slice = stateValue.slice {
|
||||
var possibleItems: [(EnginePeer, EngineStoryItem)] = []
|
||||
if peer.id == self.context.account.peerId {
|
||||
pollItems.append(StoryKey(peerId: peer.id, id: slice.item.storyItem.id))
|
||||
}
|
||||
|
||||
for i in focusedIndex ..< min(focusedIndex + 4, state.items.count) {
|
||||
if i != focusedIndex {
|
||||
possibleItems.append((slice.peer, state.items[i]))
|
||||
}
|
||||
|
||||
if slice.peer.id == self.context.account.peerId {
|
||||
pollItems.append(StoryKey(peerId: slice.peer.id, id: state.items[i].id))
|
||||
}
|
||||
}
|
||||
|
||||
var nextPriority = 0
|
||||
for i in 0 ..< min(possibleItems.count, 3) {
|
||||
let peer = possibleItems[i].0
|
||||
let item = possibleItems[i].1
|
||||
if let peerReference = PeerReference(peer._asPeer()) {
|
||||
if let image = item.media._asMedia() as? TelegramMediaImage, let resource = image.representations.last?.resource {
|
||||
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: image), resource: resource)
|
||||
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
|
||||
resource: resource,
|
||||
size: nil,
|
||||
priority: .top(position: nextPriority)
|
||||
)
|
||||
nextPriority += 1
|
||||
} else if let file = item.media._asMedia() as? TelegramMediaFile {
|
||||
if let preview = file.previewRepresentations.last {
|
||||
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: preview.resource)
|
||||
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
|
||||
resource: resource,
|
||||
size: nil,
|
||||
priority: .top(position: nextPriority)
|
||||
)
|
||||
nextPriority += 1
|
||||
}
|
||||
|
||||
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: file.resource)
|
||||
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
|
||||
resource: resource,
|
||||
size: file.preloadSize,
|
||||
priority: .top(position: nextPriority)
|
||||
)
|
||||
nextPriority += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var validIds: [MediaResourceId] = []
|
||||
for (_, info) in resultResources.sorted(by: { $0.value.priority < $1.value.priority }) {
|
||||
let resource = info.resource
|
||||
validIds.append(resource.resource.id)
|
||||
if self.preloadStoryResourceDisposables[resource.resource.id] == nil {
|
||||
var fetchRange: (Range<Int64>, MediaBoxFetchPriority)?
|
||||
if let size = info.size {
|
||||
fetchRange = (0 ..< Int64(size), .default)
|
||||
}
|
||||
self.preloadStoryResourceDisposables[resource.resource.id] = fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resource, range: fetchRange).start()
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [MediaResourceId] = []
|
||||
for (id, disposable) in self.preloadStoryResourceDisposables {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.preloadStoryResourceDisposables.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
var pollIdByPeerId: [EnginePeer.Id: [Int32]] = [:]
|
||||
for storyKey in pollItems.prefix(3) {
|
||||
if pollIdByPeerId[storyKey.peerId] == nil {
|
||||
pollIdByPeerId[storyKey.peerId] = [storyKey.id]
|
||||
} else {
|
||||
pollIdByPeerId[storyKey.peerId]?.append(storyKey.id)
|
||||
}
|
||||
}
|
||||
for (peerId, ids) in pollIdByPeerId {
|
||||
self.pollStoryMetadataDisposables.add(self.context.engine.messages.refreshStoryViews(peerId: peerId, ids: ids).start())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1004,6 +1071,11 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
|
||||
deinit {
|
||||
self.storyDisposable?.dispose()
|
||||
self.requestStoryDisposables.dispose()
|
||||
|
||||
for (_, disposable) in self.preloadStoryResourceDisposables {
|
||||
disposable.dispose()
|
||||
}
|
||||
self.pollStoryMetadataDisposables.dispose()
|
||||
}
|
||||
|
||||
public func resetSideStates() {
|
||||
@ -1039,4 +1111,8 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func markAsSeen(id: StoryId) {
|
||||
let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: true).start()
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import Display
|
||||
import ComponentFlow
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import AsyncDisplayKit
|
||||
import PhotoResources
|
||||
import SwiftSignalKit
|
||||
@ -240,7 +241,7 @@ final class StoryItemContentComponent: Component {
|
||||
if !self.markedAsSeen {
|
||||
self.markedAsSeen = true
|
||||
if let component = self.component {
|
||||
let _ = component.context.engine.messages.markStoryAsSeen(peerId: component.peer.id, id: component.item.id).start()
|
||||
self.environment?.markAsSeen(StoryId(peerId: component.peer.id, id: component.item.id))
|
||||
}
|
||||
}
|
||||
|
||||
@ -319,7 +320,7 @@ final class StoryItemContentComponent: Component {
|
||||
if !self.markedAsSeen {
|
||||
self.markedAsSeen = true
|
||||
if let component = self.component {
|
||||
let _ = component.context.engine.messages.markStoryAsSeen(peerId: component.peer.id, id: component.item.id).start()
|
||||
self.environment?.markAsSeen(StoryId(peerId: component.peer.id, id: component.item.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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) {
|
||||
transitionOut = StoryContainerScreen.TransitionOut(
|
||||
destinationView: result,
|
||||
transitionView: nil,
|
||||
destinationRect: result.bounds,
|
||||
destinationCornerRadius: 2.0,
|
||||
destinationIsAvatar: false,
|
||||
|
@ -368,7 +368,7 @@ private final class PeerInfoPendingPane {
|
||||
let paneNode: PeerInfoPaneNode
|
||||
switch key {
|
||||
case .stories:
|
||||
let visualPaneNode = PeerInfoStoryPaneNode(context: context, peerId: peerId, chatLocation: chatLocation, contentType: .photoOrVideo, captureProtected: captureProtected, isArchive: false, navigationController: chatControllerInteraction.navigationController)
|
||||
let visualPaneNode = PeerInfoStoryPaneNode(context: context, peerId: peerId, chatLocation: chatLocation, contentType: .photoOrVideo, captureProtected: captureProtected, isSaved: false, isArchive: false, navigationController: chatControllerInteraction.navigationController)
|
||||
paneNode = visualPaneNode
|
||||
visualPaneNode.openCurrentDate = {
|
||||
openMediaCalendar()
|
||||
|
@ -789,7 +789,7 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
items[.stories]!.append(PeerInfoScreenDisclosureItem(id: 0, text: "My Stories", icon: PresentationResourcesSettings.stickers, action: {
|
||||
items[.stories]!.append(PeerInfoScreenDisclosureItem(id: 0, text: "My Stories", icon: PresentationResourcesSettings.stories, action: {
|
||||
interaction.openSettings(.stories)
|
||||
}))
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user