[WIP] Stories

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 397 B

View File

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

View File

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

View File

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