mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Stories
This commit is contained in:
parent
8ff2da5d00
commit
1ca5546adc
@ -963,6 +963,8 @@
|
||||
"PrivacySettings.LastSeenContactsMinus" = "My Contacts (-%@)";
|
||||
"PrivacySettings.LastSeenContactsMinusPlus" = "My Contacts (-%@, +%@)";
|
||||
"PrivacySettings.LastSeenNobodyPlus" = "Nobody (+%@)";
|
||||
"PrivacySettings.LastSeenCloseFriendsPlus" = "Close Friends (+%@)";
|
||||
"PrivacySettings.LastSeenCloseFriends" = "Close Friends";
|
||||
|
||||
"PrivacySettings.SecurityTitle" = "SECURITY";
|
||||
|
||||
|
@ -95,6 +95,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/ActionPanelComponent",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryContentComponent",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryPeerListComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -47,6 +47,7 @@ import InviteLinksUI
|
||||
import ChatFolderLinkPreviewScreen
|
||||
import StoryContainerScreen
|
||||
import StoryContentComponent
|
||||
import StoryPeerListComponent
|
||||
|
||||
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
|
||||
if listNode.scroller.isDragging {
|
||||
@ -183,6 +184,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
|
||||
private var searchContentNode: NavigationBarSearchContentNode?
|
||||
|
||||
private let navigationSecondaryContentNode: ASDisplayNode
|
||||
private var storyPeerListView: ComponentView<Empty>?
|
||||
private let tabContainerNode: ChatListFilterTabContainerNode
|
||||
private var tabContainerData: ([ChatListFilterTabEntry], Bool, Int32?)?
|
||||
|
||||
@ -208,8 +211,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
private var powerSavingMonitoringDisposable: Disposable?
|
||||
|
||||
private var storyListContext: StoryListContext?
|
||||
private var storyListState: StoryListContext.State?
|
||||
private var storyListStateDisposable: Disposable?
|
||||
|
||||
private var storyListHeight: CGFloat
|
||||
|
||||
public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
if self.isNodeLoaded {
|
||||
self.chatListDisplayNode.effectiveContainerNode.updateSelectedChatLocation(data: data as? ChatLocation, progress: progress, transition: transition)
|
||||
@ -238,7 +244,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
groupCallPanelSource = .peer(peerId)
|
||||
}
|
||||
|
||||
self.navigationSecondaryContentNode = SparseNode()
|
||||
self.tabContainerNode = ChatListFilterTabContainerNode()
|
||||
self.navigationSecondaryContentNode.addSubnode(self.tabContainerNode)
|
||||
|
||||
self.storyListHeight = 0.0
|
||||
|
||||
super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .always, locationBroadcastPanelSource: .summary, groupCallPanelSource: groupCallPanelSource)
|
||||
|
||||
@ -427,6 +437,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
self.searchContentNode?.updateExpansionProgress(0.0)
|
||||
self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
|
||||
|
||||
let tabsIsEmpty: Bool
|
||||
if let (resolvedItems, displayTabsAtBottom, _) = self.tabContainerData {
|
||||
tabsIsEmpty = resolvedItems.count <= 1 || displayTabsAtBottom
|
||||
} else {
|
||||
tabsIsEmpty = true
|
||||
}
|
||||
|
||||
self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + self.storyListHeight
|
||||
|
||||
enum State: Equatable {
|
||||
case empty(hasDownloads: Bool)
|
||||
case downloading(progress: Double)
|
||||
@ -1338,7 +1357,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
let storyContainerScreen = StoryContainerScreen(
|
||||
context: self.context,
|
||||
initialFocusedId: AnyHashable(peerId),
|
||||
initialContent: initialContent
|
||||
initialContent: initialContent,
|
||||
transitionIn: nil
|
||||
)
|
||||
self.push(storyContainerScreen)
|
||||
})
|
||||
@ -2067,20 +2087,46 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
let _ = state
|
||||
var wasEmpty = true
|
||||
if let storyListState = self.storyListState, !storyListState.itemSets.isEmpty {
|
||||
wasEmpty = false
|
||||
}
|
||||
self.storyListState = state
|
||||
let isEmpty = state.itemSets.isEmpty
|
||||
|
||||
self.chatListDisplayNode.mainContainerNode.currentItemNode.updateState { chatListState in
|
||||
var chatListState = chatListState
|
||||
|
||||
var peersWithNewStories = Set<EnginePeer.Id>()
|
||||
for itemSet in state.itemSets {
|
||||
peersWithNewStories.insert(itemSet.peerId)
|
||||
if itemSet.peerId == self.context.account.peerId {
|
||||
continue
|
||||
}
|
||||
if itemSet.items.contains(where: { !$0.isSeen }) {
|
||||
peersWithNewStories.insert(itemSet.peerId)
|
||||
}
|
||||
}
|
||||
chatListState.peersWithNewStories = peersWithNewStories
|
||||
|
||||
return chatListState
|
||||
}
|
||||
|
||||
self.storyListHeight = isEmpty ? 0.0 : 94.0
|
||||
|
||||
let tabsIsEmpty: Bool
|
||||
if let (resolvedItems, displayTabsAtBottom, _) = self.tabContainerData {
|
||||
tabsIsEmpty = resolvedItems.count <= 1 || displayTabsAtBottom
|
||||
} else {
|
||||
tabsIsEmpty = true
|
||||
}
|
||||
|
||||
self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + self.storyListHeight
|
||||
|
||||
if wasEmpty != isEmpty || self.storyPeerListView == nil {
|
||||
self.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
|
||||
} else if let componentView = self.storyPeerListView?.view, !componentView.bounds.isEmpty {
|
||||
self.updateStoryPeerListView(size: componentView.bounds.size, transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -2304,20 +2350,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
|
||||
override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
/*if self.chatListDisplayNode.searchDisplayController?.contentNode != nil {
|
||||
self.navigationBar?.secondaryContentNodeDisplayFraction = 1.0
|
||||
} else {
|
||||
self.navigationBar?.secondaryContentNodeDisplayFraction = 1.0 - self.chatListDisplayNode.inlineStackContainerTransitionFraction
|
||||
}*/
|
||||
|
||||
self.updateHeaderContent(layout: layout, transition: transition)
|
||||
|
||||
super.updateNavigationBarLayout(layout, transition: transition)
|
||||
|
||||
if let inlineStackContainerNode = self.chatListDisplayNode.inlineStackContainerNode {
|
||||
let _ = inlineStackContainerNode
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
@ -2335,6 +2370,59 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
}
|
||||
|
||||
private func updateStoryPeerListView(size: CGSize, transition: Transition) {
|
||||
guard let storyPeerListView = self.storyPeerListView else {
|
||||
return
|
||||
}
|
||||
let _ = storyPeerListView.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(StoryPeerListComponent(
|
||||
context: self.context,
|
||||
theme: self.presentationData.theme,
|
||||
strings: self.presentationData.strings,
|
||||
state: self.storyListState,
|
||||
peerAction: { [weak self] peer in
|
||||
guard let self, let storyListContext = self.storyListContext else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (StoryChatContent.stories(
|
||||
context: self.context,
|
||||
storyList: storyListContext,
|
||||
focusItem: nil
|
||||
)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] initialContent in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var transitionIn: StoryContainerScreen.TransitionIn?
|
||||
if let storyPeerListView = self.storyPeerListView?.view as? StoryPeerListComponent.View {
|
||||
if let transitionView = storyPeerListView.transitionViewForItem(peerId: peer.id) {
|
||||
transitionIn = StoryContainerScreen.TransitionIn(
|
||||
sourceView: transitionView,
|
||||
sourceRect: transitionView.bounds,
|
||||
sourceCornerRadius: transitionView.bounds.height * 0.5
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let storyContainerScreen = StoryContainerScreen(
|
||||
context: self.context,
|
||||
initialFocusedId: AnyHashable(peer.id),
|
||||
initialContent: initialContent,
|
||||
transitionIn: transitionIn
|
||||
)
|
||||
self.push(storyContainerScreen)
|
||||
})
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: size
|
||||
)
|
||||
}
|
||||
|
||||
private func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
var tabContainerOffset: CGFloat = 0.0
|
||||
if !self.displayNavigationBar {
|
||||
@ -2343,10 +2431,45 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
|
||||
let navigationBarHeight = self.navigationBar?.frame.maxY ?? 0.0
|
||||
transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight - self.additionalNavigationBarHeight - 46.0 + tabContainerOffset), size: CGSize(width: layout.size.width, height: 46.0)))
|
||||
let secondaryContentHeight = self.navigationBar?.secondaryContentHeight ?? 0.0
|
||||
|
||||
transition.updateFrame(node: self.navigationSecondaryContentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight - self.additionalNavigationBarHeight - secondaryContentHeight + tabContainerOffset), size: CGSize(width: layout.size.width, height: secondaryContentHeight)))
|
||||
|
||||
transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: layout.size.width, height: 46.0)))
|
||||
|
||||
if let storyListState = self.storyListState, !storyListState.itemSets.isEmpty {
|
||||
var storyPeerListTransition = Transition(transition)
|
||||
let storyPeerListView: ComponentView<Empty>
|
||||
if let current = self.storyPeerListView {
|
||||
storyPeerListView = current
|
||||
} else {
|
||||
storyPeerListTransition = .immediate
|
||||
storyPeerListView = ComponentView()
|
||||
self.storyPeerListView = storyPeerListView
|
||||
}
|
||||
let storyListFrame = CGRect(origin: CGPoint(x: 0.0, y: 46.0 - 0.0), size: CGSize(width: layout.size.width, height: self.storyListHeight + 0.0))
|
||||
self.updateStoryPeerListView(size: storyListFrame.size, transition: storyPeerListTransition)
|
||||
if let componentView = storyPeerListView.view {
|
||||
if componentView.superview == nil {
|
||||
componentView.alpha = 0.0
|
||||
self.navigationSecondaryContentNode.view.addSubview(componentView)
|
||||
}
|
||||
storyPeerListTransition.setFrame(view: componentView, frame: storyListFrame)
|
||||
transition.updateAlpha(layer: componentView.layer, alpha: 1.0)
|
||||
}
|
||||
} else if let storyPeerListView = self.storyPeerListView {
|
||||
self.storyPeerListView = nil
|
||||
if let componentView = storyPeerListView.view {
|
||||
transition.updateAlpha(layer: componentView.layer, alpha: 0.0, completion: { [weak componentView] _ in
|
||||
componentView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !skipTabContainerUpdate {
|
||||
self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.mainContainerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, filtersLimit: self.tabContainerData?.2, transitionFraction: self.chatListDisplayNode.effectiveContainerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
|
||||
self.chatListDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, visualNavigationHeight: navigationBarHeight, cleanNavigationBarHeight: self.cleanNavigationHeight, transition: transition)
|
||||
}
|
||||
|
||||
@ -2786,16 +2909,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
let animated = strongSelf.didSetupTabs
|
||||
strongSelf.didSetupTabs = true
|
||||
|
||||
if wasEmpty != isEmpty, strongSelf.displayNavigationBar {
|
||||
strongSelf.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode, animated: false)
|
||||
if let parentController = strongSelf.parent as? TabBarController {
|
||||
parentController.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode, animated: animated)
|
||||
}
|
||||
if strongSelf.displayNavigationBar {
|
||||
strongSelf.navigationBar?.secondaryContentHeight = (!isEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + strongSelf.storyListHeight
|
||||
strongSelf.navigationBar?.setSecondaryContentNode(strongSelf.navigationSecondaryContentNode, animated: false)
|
||||
}
|
||||
|
||||
if let layout = strongSelf.validLayout {
|
||||
if wasEmpty != isEmpty {
|
||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate
|
||||
transition.updateAlpha(node: strongSelf.tabContainerNode, alpha: isEmpty ? 0.0 : 1.0)
|
||||
strongSelf.containerLayoutUpdated(layout, transition: transition)
|
||||
(strongSelf.parent as? TabBarController)?.updateLayout(transition: transition)
|
||||
} else {
|
||||
@ -3164,10 +3286,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
if let filterContainerNodeAndActivate = strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, displaySearchFilters: displaySearchFilters, hasDownloads: strongSelf.hasDownloads, initialFilter: filter, navigationController: strongSelf.navigationController as? NavigationController) {
|
||||
let (filterContainerNode, activate) = filterContainerNodeAndActivate
|
||||
if displaySearchFilters {
|
||||
strongSelf.navigationBar?.secondaryContentHeight = NavigationBar.defaultSecondaryContentHeight
|
||||
strongSelf.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: false)
|
||||
if let parentController = strongSelf.parent as? TabBarController {
|
||||
parentController.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
activate(filter != .downloads)
|
||||
@ -3242,13 +3362,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
completion = self.chatListDisplayNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode, animated: animated)
|
||||
}
|
||||
|
||||
|
||||
self.navigationBar?.setSecondaryContentNode(tabsIsEmpty ? nil : self.tabContainerNode, animated: false)
|
||||
if let parentController = self.parent as? TabBarController {
|
||||
parentController.navigationBar?.setSecondaryContentNode(tabsIsEmpty ? nil : self.tabContainerNode, animated: animated)
|
||||
}
|
||||
self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + self.storyListHeight
|
||||
self.navigationBar?.setSecondaryContentNode(self.navigationSecondaryContentNode, animated: false)
|
||||
|
||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate
|
||||
transition.updateAlpha(node: self.tabContainerNode, alpha: tabsIsEmpty ? 0.0 : 1.0)
|
||||
self.setDisplayNavigationBar(true, transition: transition)
|
||||
|
||||
completion?()
|
||||
|
@ -73,6 +73,17 @@ public struct Transition {
|
||||
case easeInOut
|
||||
case spring
|
||||
case custom(Float, Float, Float, Float)
|
||||
|
||||
public func solve(at offset: CGFloat) -> CGFloat {
|
||||
switch self {
|
||||
case .easeInOut:
|
||||
return listViewAnimationCurveEaseInOut(offset)
|
||||
case .spring:
|
||||
return listViewAnimationCurveSystem(offset)
|
||||
case let .custom(c1x, c1y, c2x, c2y):
|
||||
return bezierPoint(CGFloat(c1x), CGFloat(c1y), CGFloat(c2x), CGFloat(c2y), offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case none
|
||||
@ -421,7 +432,24 @@ public struct Transition {
|
||||
self.setTransform(layer: view.layer, transform: transform, completion: completion)
|
||||
}
|
||||
|
||||
public func setTransformAsKeyframes(view: UIView, transform: (CGFloat) -> CATransform3D, completion: ((Bool) -> Void)? = nil) {
|
||||
self.setTransformAsKeyframes(layer: view.layer, transform: transform, completion: completion)
|
||||
}
|
||||
|
||||
public func setTransform(layer: CALayer, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) {
|
||||
let t = layer.presentation()?.transform ?? layer.transform
|
||||
if CATransform3DEqualToTransform(t, transform) {
|
||||
if let animation = layer.animation(forKey: "transform") as? CABasicAnimation, let toValue = animation.toValue as? NSValue {
|
||||
if CATransform3DEqualToTransform(toValue.caTransform3DValue, transform) {
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch self.animation {
|
||||
case .none:
|
||||
layer.transform = transform
|
||||
@ -433,6 +461,7 @@ public struct Transition {
|
||||
} else {
|
||||
previousValue = layer.transform
|
||||
}
|
||||
|
||||
layer.transform = transform
|
||||
layer.animate(
|
||||
from: NSValue(caTransform3D: previousValue),
|
||||
@ -448,6 +477,59 @@ public struct Transition {
|
||||
}
|
||||
}
|
||||
|
||||
public func setTransformAsKeyframes(layer: CALayer, transform: (CGFloat) -> CATransform3D, completion: ((Bool) -> Void)? = nil) {
|
||||
let finalTransform = transform(1.0)
|
||||
|
||||
let t = layer.presentation()?.transform ?? layer.transform
|
||||
if CATransform3DEqualToTransform(t, finalTransform) {
|
||||
if let animation = layer.animation(forKey: "transform") as? CABasicAnimation, let toValue = animation.toValue as? NSValue {
|
||||
if CATransform3DEqualToTransform(toValue.caTransform3DValue, finalTransform) {
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch self.animation {
|
||||
case .none:
|
||||
layer.transform = transform(1.0)
|
||||
completion?(true)
|
||||
case let .curve(duration, curve):
|
||||
let framesPerSecond: CGFloat
|
||||
if #available(iOS 15.0, *) {
|
||||
framesPerSecond = duration * CGFloat(UIScreen.main.maximumFramesPerSecond)
|
||||
} else {
|
||||
framesPerSecond = 60.0
|
||||
}
|
||||
|
||||
let numValues = Int(framesPerSecond * duration)
|
||||
if numValues == 0 {
|
||||
layer.transform = transform(1.0)
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
|
||||
var values: [AnyObject] = []
|
||||
|
||||
for i in 0 ... numValues {
|
||||
let t = curve.solve(at: CGFloat(i) / CGFloat(numValues))
|
||||
values.append(NSValue(caTransform3D: transform(t)))
|
||||
}
|
||||
|
||||
layer.transform = transform(1.0)
|
||||
layer.animateKeyframes(
|
||||
values: values,
|
||||
duration: duration,
|
||||
keyPath: "transform",
|
||||
removeOnCompletion: true,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func setSublayerTransform(view: UIView, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) {
|
||||
self.setSublayerTransform(layer: view.layer, transform: transform, completion: completion)
|
||||
}
|
||||
|
@ -1066,6 +1066,8 @@ open class NavigationBar: ASDisplayNode {
|
||||
private var transitionBackArrowNode: ASDisplayNode?
|
||||
private var transitionBadgeNode: ASDisplayNode?
|
||||
|
||||
public var secondaryContentHeight: CGFloat
|
||||
|
||||
public init(presentationData: NavigationBarPresentationData) {
|
||||
self.presentationData = presentationData
|
||||
self.stripeNode = ASDisplayNode()
|
||||
@ -1111,6 +1113,8 @@ open class NavigationBar: ASDisplayNode {
|
||||
self.backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.backgroundColor, enableBlur: self.presentationData.theme.enableBackgroundBlur)
|
||||
self.additionalContentNode = SparseNode()
|
||||
|
||||
self.secondaryContentHeight = NavigationBar.defaultSecondaryContentHeight
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
@ -1235,7 +1239,7 @@ open class NavigationBar: ASDisplayNode {
|
||||
self.backgroundNode.update(size: backgroundFrame.size, transition: transition)
|
||||
}
|
||||
|
||||
let apparentAdditionalHeight: CGFloat = self.secondaryContentNode != nil ? (NavigationBar.defaultSecondaryContentHeight * self.secondaryContentNodeDisplayFraction) : 0.0
|
||||
let apparentAdditionalHeight: CGFloat = self.secondaryContentNode != nil ? (self.secondaryContentHeight * self.secondaryContentNodeDisplayFraction) : 0.0
|
||||
|
||||
let leftButtonInset: CGFloat = leftInset + 16.0
|
||||
let backButtonInset: CGFloat = leftInset + 27.0
|
||||
@ -1253,11 +1257,11 @@ open class NavigationBar: ASDisplayNode {
|
||||
case .expansion:
|
||||
expansionHeight = contentNode.height
|
||||
|
||||
let additionalExpansionHeight: CGFloat = self.secondaryContentNode != nil && appearsHidden ? (NavigationBar.defaultSecondaryContentHeight * self.secondaryContentNodeDisplayFraction) : 0.0
|
||||
let additionalExpansionHeight: CGFloat = self.secondaryContentNode != nil && appearsHidden ? (self.secondaryContentHeight * self.secondaryContentNodeDisplayFraction) : 0.0
|
||||
contentNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - (appearsHidden ? 0.0 : additionalContentHeight) - expansionHeight - apparentAdditionalHeight - additionalExpansionHeight), size: CGSize(width: size.width, height: expansionHeight))
|
||||
if appearsHidden {
|
||||
if self.secondaryContentNode != nil {
|
||||
contentNodeFrame.origin.y += NavigationBar.defaultSecondaryContentHeight * self.secondaryContentNodeDisplayFraction
|
||||
contentNodeFrame.origin.y += self.secondaryContentHeight * self.secondaryContentNodeDisplayFraction
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1619,7 +1623,7 @@ open class NavigationBar: ASDisplayNode {
|
||||
}
|
||||
|
||||
if let _ = self.secondaryContentNode {
|
||||
result += NavigationBar.defaultSecondaryContentHeight * self.secondaryContentNodeDisplayFraction
|
||||
result += self.secondaryContentHeight * self.secondaryContentNodeDisplayFraction
|
||||
}
|
||||
|
||||
return result
|
||||
|
@ -416,11 +416,9 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject {
|
||||
if let contentNode = navigationBar.contentNode, case .expansion = contentNode.mode, !self.displayNavigationBar {
|
||||
navigationBarFrame.origin.y -= navigationLayout.defaultContentHeight
|
||||
navigationBarFrame.size.height += contentNode.height + navigationLayout.defaultContentHeight + statusBarHeight
|
||||
//navigationBarFrame.origin.y += contentNode.height + statusBarHeight
|
||||
}
|
||||
if let _ = navigationBar.contentNode, let _ = navigationBar.secondaryContentNode, !self.displayNavigationBar {
|
||||
navigationBarFrame.size.height += NavigationBar.defaultSecondaryContentHeight
|
||||
//navigationBarFrame.origin.y += NavigationBar.defaultSecondaryContentHeight
|
||||
navigationBarFrame.size.height += navigationBar.secondaryContentHeight
|
||||
}
|
||||
|
||||
navigationBar.updateLayout(size: navigationBarFrame.size, defaultHeight: navigationLayout.defaultContentHeight, additionalTopHeight: statusBarHeight, additionalContentHeight: self.additionalNavigationBarHeight, additionalBackgroundHeight: additionalBackgroundHeight, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, appearsHidden: !self.displayNavigationBar, isLandscape: isLandscape, transition: transition)
|
||||
|
@ -444,11 +444,23 @@ private func countForSelectivePeers(_ peers: [PeerId: SelectivePrivacyPeer]) ->
|
||||
|
||||
private func stringForSelectiveSettings(strings: PresentationStrings, settings: SelectivePrivacySettings) -> String {
|
||||
switch settings {
|
||||
case let .disableEveryone(enableFor):
|
||||
if enableFor.isEmpty {
|
||||
case let .disableEveryone(enableFor, enableForCloseFriends):
|
||||
if enableFor.isEmpty && !enableForCloseFriends {
|
||||
return strings.PrivacySettings_LastSeenNobody
|
||||
} else {
|
||||
return strings.PrivacySettings_LastSeenNobodyPlus("\(countForSelectivePeers(enableFor))").string
|
||||
if enableForCloseFriends {
|
||||
if enableFor.isEmpty {
|
||||
return strings.PrivacySettings_LastSeenCloseFriendsPlus("\(countForSelectivePeers(enableFor))").string
|
||||
} else {
|
||||
return strings.PrivacySettings_LastSeenCloseFriends
|
||||
}
|
||||
} else {
|
||||
if enableFor.isEmpty {
|
||||
return strings.PrivacySettings_LastSeenNobody
|
||||
} else {
|
||||
return strings.PrivacySettings_LastSeenNobodyPlus("\(countForSelectivePeers(enableFor))").string
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .enableEveryone(disableFor):
|
||||
if disableFor.isEmpty {
|
||||
|
@ -487,6 +487,7 @@ private struct SelectivePrivacySettingsControllerState: Equatable {
|
||||
let setting: SelectivePrivacySettingType
|
||||
let enableFor: [EnginePeer.Id: SelectivePrivacyPeer]
|
||||
let disableFor: [EnginePeer.Id: SelectivePrivacyPeer]
|
||||
let enableForCloseFriends: Bool
|
||||
|
||||
let saving: Bool
|
||||
|
||||
@ -494,21 +495,24 @@ private struct SelectivePrivacySettingsControllerState: Equatable {
|
||||
let callP2PMode: SelectivePrivacySettingType?
|
||||
let callP2PEnableFor: [EnginePeer.Id: SelectivePrivacyPeer]?
|
||||
let callP2PDisableFor: [EnginePeer.Id: SelectivePrivacyPeer]?
|
||||
let callP2PEnableForCloseFriends: Bool?
|
||||
let callIntegrationAvailable: Bool?
|
||||
let callIntegrationEnabled: Bool?
|
||||
let phoneDiscoveryEnabled: Bool?
|
||||
|
||||
let uploadedPhoto: UIImage?
|
||||
|
||||
init(setting: SelectivePrivacySettingType, enableFor: [EnginePeer.Id: SelectivePrivacyPeer], disableFor: [EnginePeer.Id: SelectivePrivacyPeer], saving: Bool, callDataSaving: VoiceCallDataSaving?, callP2PMode: SelectivePrivacySettingType?, callP2PEnableFor: [EnginePeer.Id: SelectivePrivacyPeer]?, callP2PDisableFor: [EnginePeer.Id: SelectivePrivacyPeer]?, callIntegrationAvailable: Bool?, callIntegrationEnabled: Bool?, phoneDiscoveryEnabled: Bool?, uploadedPhoto: UIImage?) {
|
||||
init(setting: SelectivePrivacySettingType, enableFor: [EnginePeer.Id: SelectivePrivacyPeer], disableFor: [EnginePeer.Id: SelectivePrivacyPeer], enableForCloseFriends: Bool, saving: Bool, callDataSaving: VoiceCallDataSaving?, callP2PMode: SelectivePrivacySettingType?, callP2PEnableFor: [EnginePeer.Id: SelectivePrivacyPeer]?, callP2PDisableFor: [EnginePeer.Id: SelectivePrivacyPeer]?, callP2PEnableForCloseFriends: Bool?, callIntegrationAvailable: Bool?, callIntegrationEnabled: Bool?, phoneDiscoveryEnabled: Bool?, uploadedPhoto: UIImage?) {
|
||||
self.setting = setting
|
||||
self.enableFor = enableFor
|
||||
self.disableFor = disableFor
|
||||
self.enableForCloseFriends = enableForCloseFriends
|
||||
self.saving = saving
|
||||
self.callDataSaving = callDataSaving
|
||||
self.callP2PMode = callP2PMode
|
||||
self.callP2PEnableFor = callP2PEnableFor
|
||||
self.callP2PDisableFor = callP2PDisableFor
|
||||
self.callP2PEnableForCloseFriends = callP2PEnableForCloseFriends
|
||||
self.callIntegrationAvailable = callIntegrationAvailable
|
||||
self.callIntegrationEnabled = callIntegrationEnabled
|
||||
self.phoneDiscoveryEnabled = phoneDiscoveryEnabled
|
||||
@ -525,6 +529,9 @@ private struct SelectivePrivacySettingsControllerState: Equatable {
|
||||
if lhs.disableFor != rhs.disableFor {
|
||||
return false
|
||||
}
|
||||
if lhs.enableForCloseFriends != rhs.enableForCloseFriends {
|
||||
return false
|
||||
}
|
||||
if lhs.saving != rhs.saving {
|
||||
return false
|
||||
}
|
||||
@ -540,6 +547,9 @@ private struct SelectivePrivacySettingsControllerState: Equatable {
|
||||
if lhs.callP2PDisableFor != rhs.callP2PDisableFor {
|
||||
return false
|
||||
}
|
||||
if lhs.callP2PEnableForCloseFriends != rhs.callP2PEnableForCloseFriends {
|
||||
return false
|
||||
}
|
||||
if lhs.callIntegrationAvailable != rhs.callIntegrationAvailable {
|
||||
return false
|
||||
}
|
||||
@ -557,43 +567,51 @@ private struct SelectivePrivacySettingsControllerState: Equatable {
|
||||
}
|
||||
|
||||
func withUpdatedSetting(_ setting: SelectivePrivacySettingType) -> SelectivePrivacySettingsControllerState {
|
||||
return SelectivePrivacySettingsControllerState(setting: setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
return SelectivePrivacySettingsControllerState(setting: setting, enableFor: self.enableFor, disableFor: self.disableFor, enableForCloseFriends: self.enableForCloseFriends, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callP2PEnableForCloseFriends: self.callP2PEnableForCloseFriends, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
}
|
||||
|
||||
func withUpdatedEnableFor(_ enableFor: [EnginePeer.Id: SelectivePrivacyPeer]) -> SelectivePrivacySettingsControllerState {
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: enableFor, disableFor: self.disableFor, enableForCloseFriends: self.enableForCloseFriends, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callP2PEnableForCloseFriends: self.callP2PEnableForCloseFriends, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
}
|
||||
|
||||
func withUpdatedDisableFor(_ disableFor: [EnginePeer.Id: SelectivePrivacyPeer]) -> SelectivePrivacySettingsControllerState {
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: disableFor, enableForCloseFriends: self.enableForCloseFriends, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callP2PEnableForCloseFriends: self.callP2PEnableForCloseFriends, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
}
|
||||
|
||||
func withUpdatedEnableForCloseFriends(_ enableForCloseFriends: Bool) -> SelectivePrivacySettingsControllerState {
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, enableForCloseFriends: enableForCloseFriends, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callP2PEnableForCloseFriends: self.callP2PEnableForCloseFriends, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
}
|
||||
|
||||
func withUpdatedSaving(_ saving: Bool) -> SelectivePrivacySettingsControllerState {
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, enableForCloseFriends: self.enableForCloseFriends, saving: saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callP2PEnableForCloseFriends: self.callP2PEnableForCloseFriends, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
}
|
||||
|
||||
func withUpdatedCallP2PMode(_ mode: SelectivePrivacySettingType) -> SelectivePrivacySettingsControllerState {
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: mode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, enableForCloseFriends: self.enableForCloseFriends, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: mode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callP2PEnableForCloseFriends: self.callP2PEnableForCloseFriends, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
}
|
||||
|
||||
func withUpdatedCallP2PEnableFor(_ enableFor: [EnginePeer.Id: SelectivePrivacyPeer]) -> SelectivePrivacySettingsControllerState {
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: enableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, enableForCloseFriends: self.enableForCloseFriends, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: enableFor, callP2PDisableFor: self.callP2PDisableFor, callP2PEnableForCloseFriends: self.callP2PEnableForCloseFriends, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
}
|
||||
|
||||
func withUpdatedCallP2PDisableFor(_ disableFor: [EnginePeer.Id: SelectivePrivacyPeer]) -> SelectivePrivacySettingsControllerState {
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: disableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, enableForCloseFriends: self.enableForCloseFriends, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: disableFor, callP2PEnableForCloseFriends: self.callP2PEnableForCloseFriends, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
}
|
||||
|
||||
func withUpdatedCallP2PEnableForCloseFriends(_ callP2PEnableForCloseFriends: Bool) -> SelectivePrivacySettingsControllerState {
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, enableForCloseFriends: self.enableForCloseFriends, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callP2PEnableForCloseFriends: callP2PEnableForCloseFriends, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
}
|
||||
|
||||
func withUpdatedCallsIntegrationEnabled(_ enabled: Bool) -> SelectivePrivacySettingsControllerState {
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: enabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, enableForCloseFriends: self.enableForCloseFriends, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callP2PEnableForCloseFriends: self.callP2PEnableForCloseFriends, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: enabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
}
|
||||
|
||||
func withUpdatedPhoneDiscoveryEnabled(_ phoneDiscoveryEnabled: Bool) -> SelectivePrivacySettingsControllerState {
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, enableForCloseFriends: self.enableForCloseFriends, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callP2PEnableForCloseFriends: self.callP2PEnableForCloseFriends, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto)
|
||||
}
|
||||
|
||||
func withUpdatedUploadedPhoto(_ uploadedPhoto: UIImage?) -> SelectivePrivacySettingsControllerState {
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: uploadedPhoto)
|
||||
return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, enableForCloseFriends: self.enableForCloseFriends, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callP2PEnableForCloseFriends: self.callP2PEnableForCloseFriends, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: uploadedPhoto)
|
||||
}
|
||||
}
|
||||
|
||||
@ -764,9 +782,11 @@ func selectivePrivacySettingsController(
|
||||
|
||||
var initialEnableFor: [EnginePeer.Id: SelectivePrivacyPeer] = [:]
|
||||
var initialDisableFor: [EnginePeer.Id: SelectivePrivacyPeer] = [:]
|
||||
var initialEnableForCloseFriends = false
|
||||
switch current {
|
||||
case let .disableEveryone(enableFor):
|
||||
case let .disableEveryone(enableFor, enableForCloseFriends):
|
||||
initialEnableFor = enableFor
|
||||
initialEnableForCloseFriends = enableForCloseFriends
|
||||
case let .enableContacts(enableFor, disableFor):
|
||||
initialEnableFor = enableFor
|
||||
initialDisableFor = disableFor
|
||||
@ -775,11 +795,13 @@ func selectivePrivacySettingsController(
|
||||
}
|
||||
var initialCallP2PEnableFor: [EnginePeer.Id: SelectivePrivacyPeer]?
|
||||
var initialCallP2PDisableFor: [EnginePeer.Id: SelectivePrivacyPeer]?
|
||||
var initialCallEnableForCloseFriends = false
|
||||
if let callCurrent = callSettings?.0 {
|
||||
switch callCurrent {
|
||||
case let .disableEveryone(enableFor):
|
||||
case let .disableEveryone(enableFor, enableForCloseFriends):
|
||||
initialCallP2PEnableFor = enableFor
|
||||
initialCallP2PDisableFor = [:]
|
||||
initialCallEnableForCloseFriends = enableForCloseFriends
|
||||
case let .enableContacts(enableFor, disableFor):
|
||||
initialCallP2PEnableFor = enableFor
|
||||
initialCallP2PDisableFor = disableFor
|
||||
@ -789,7 +811,7 @@ func selectivePrivacySettingsController(
|
||||
}
|
||||
}
|
||||
|
||||
let initialState = SelectivePrivacySettingsControllerState(setting: SelectivePrivacySettingType(current), enableFor: initialEnableFor, disableFor: initialDisableFor, saving: false, callDataSaving: callSettings?.1.dataSaving, callP2PMode: callSettings != nil ? SelectivePrivacySettingType(callSettings!.0) : nil, callP2PEnableFor: initialCallP2PEnableFor, callP2PDisableFor: initialCallP2PDisableFor, callIntegrationAvailable: callIntegrationAvailable, callIntegrationEnabled: callSettings?.1.enableSystemIntegration, phoneDiscoveryEnabled: phoneDiscoveryEnabled, uploadedPhoto: nil)
|
||||
let initialState = SelectivePrivacySettingsControllerState(setting: SelectivePrivacySettingType(current), enableFor: initialEnableFor, disableFor: initialDisableFor, enableForCloseFriends: initialEnableForCloseFriends, saving: false, callDataSaving: callSettings?.1.dataSaving, callP2PMode: callSettings != nil ? SelectivePrivacySettingType(callSettings!.0) : nil, callP2PEnableFor: initialCallP2PEnableFor, callP2PDisableFor: initialCallP2PDisableFor, callP2PEnableForCloseFriends: initialCallEnableForCloseFriends, callIntegrationAvailable: callIntegrationAvailable, callIntegrationEnabled: callSettings?.1.enableSystemIntegration, phoneDiscoveryEnabled: phoneDiscoveryEnabled, uploadedPhoto: nil)
|
||||
|
||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: initialState)
|
||||
@ -1116,21 +1138,21 @@ func selectivePrivacySettingsController(
|
||||
case .contacts:
|
||||
settings = SelectivePrivacySettings.enableContacts(enableFor: state.enableFor, disableFor: state.disableFor)
|
||||
case .nobody:
|
||||
settings = SelectivePrivacySettings.disableEveryone(enableFor: state.enableFor)
|
||||
settings = SelectivePrivacySettings.disableEveryone(enableFor: state.enableFor, enableForCloseFriends: state.enableForCloseFriends)
|
||||
}
|
||||
|
||||
if case .phoneNumber = kind, let value = state.phoneDiscoveryEnabled {
|
||||
phoneDiscoveryEnabled = value
|
||||
}
|
||||
|
||||
if case .voiceCalls = kind, let callP2PMode = state.callP2PMode, let disableFor = state.callP2PDisableFor, let enableFor = state.callP2PEnableFor {
|
||||
if case .voiceCalls = kind, let callP2PMode = state.callP2PMode, let disableFor = state.callP2PDisableFor, let enableFor = state.callP2PEnableFor, let enableForCloseFriends = state.callP2PEnableForCloseFriends {
|
||||
switch callP2PMode {
|
||||
case .everybody:
|
||||
callP2PSettings = SelectivePrivacySettings.enableEveryone(disableFor: disableFor)
|
||||
case .contacts:
|
||||
callP2PSettings = SelectivePrivacySettings.enableContacts(enableFor: enableFor, disableFor: disableFor)
|
||||
case .nobody:
|
||||
callP2PSettings = SelectivePrivacySettings.disableEveryone(enableFor: enableFor)
|
||||
callP2PSettings = SelectivePrivacySettings.disableEveryone(enableFor: enableFor, enableForCloseFriends: enableForCloseFriends)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -363,6 +363,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[506920429] = { return Api.InputPhoneCall.parse_inputPhoneCall($0) }
|
||||
dict[1001634122] = { return Api.InputPhoto.parse_inputPhoto($0) }
|
||||
dict[483901197] = { return Api.InputPhoto.parse_inputPhotoEmpty($0) }
|
||||
dict[941870144] = { return Api.InputPrivacyKey.parse_inputPrivacyKeyAbout($0) }
|
||||
dict[-786326563] = { return Api.InputPrivacyKey.parse_inputPrivacyKeyAddedByPhone($0) }
|
||||
dict[-1107622874] = { return Api.InputPrivacyKey.parse_inputPrivacyKeyChatInvite($0) }
|
||||
dict[-1529000952] = { return Api.InputPrivacyKey.parse_inputPrivacyKeyForwards($0) }
|
||||
@ -374,6 +375,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-1360618136] = { return Api.InputPrivacyKey.parse_inputPrivacyKeyVoiceMessages($0) }
|
||||
dict[407582158] = { return Api.InputPrivacyRule.parse_inputPrivacyValueAllowAll($0) }
|
||||
dict[-2079962673] = { return Api.InputPrivacyRule.parse_inputPrivacyValueAllowChatParticipants($0) }
|
||||
dict[793067081] = { return Api.InputPrivacyRule.parse_inputPrivacyValueAllowCloseFriends($0) }
|
||||
dict[218751099] = { return Api.InputPrivacyRule.parse_inputPrivacyValueAllowContacts($0) }
|
||||
dict[320652927] = { return Api.InputPrivacyRule.parse_inputPrivacyValueAllowUsers($0) }
|
||||
dict[-697604407] = { return Api.InputPrivacyRule.parse_inputPrivacyValueDisallowAll($0) }
|
||||
@ -642,6 +644,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[512535275] = { return Api.PostAddress.parse_postAddress($0) }
|
||||
dict[1958953753] = { return Api.PremiumGiftOption.parse_premiumGiftOption($0) }
|
||||
dict[1596792306] = { return Api.PremiumSubscriptionOption.parse_premiumSubscriptionOption($0) }
|
||||
dict[-1534675103] = { return Api.PrivacyKey.parse_privacyKeyAbout($0) }
|
||||
dict[1124062251] = { return Api.PrivacyKey.parse_privacyKeyAddedByPhone($0) }
|
||||
dict[1343122938] = { return Api.PrivacyKey.parse_privacyKeyChatInvite($0) }
|
||||
dict[1777096355] = { return Api.PrivacyKey.parse_privacyKeyForwards($0) }
|
||||
@ -653,6 +656,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[110621716] = { return Api.PrivacyKey.parse_privacyKeyVoiceMessages($0) }
|
||||
dict[1698855810] = { return Api.PrivacyRule.parse_privacyValueAllowAll($0) }
|
||||
dict[1796427406] = { return Api.PrivacyRule.parse_privacyValueAllowChatParticipants($0) }
|
||||
dict[-135735141] = { return Api.PrivacyRule.parse_privacyValueAllowCloseFriends($0) }
|
||||
dict[-123988] = { return Api.PrivacyRule.parse_privacyValueAllowContacts($0) }
|
||||
dict[-1198497870] = { return Api.PrivacyRule.parse_privacyValueAllowUsers($0) }
|
||||
dict[-1955338397] = { return Api.PrivacyRule.parse_privacyValueDisallowAll($0) }
|
||||
@ -782,8 +786,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[1087454222] = { return Api.StickerSetCovered.parse_stickerSetFullCovered($0) }
|
||||
dict[872932635] = { return Api.StickerSetCovered.parse_stickerSetMultiCovered($0) }
|
||||
dict[2008112412] = { return Api.StickerSetCovered.parse_stickerSetNoCovered($0) }
|
||||
dict[-1230510430] = { return Api.StoryItem.parse_storyItem($0) }
|
||||
dict[271121336] = { return Api.StoryItem.parse_storyItem($0) }
|
||||
dict[-2020380585] = { return Api.StoryItem.parse_storyItemDeleted($0) }
|
||||
dict[90474706] = { return Api.StoryView.parse_storyView($0) }
|
||||
dict[1964978502] = { return Api.TextWithEntities.parse_textWithEntities($0) }
|
||||
dict[-1609668650] = { return Api.Theme.parse_theme($0) }
|
||||
dict[-94849324] = { return Api.ThemeSettings.parse_themeSettings($0) }
|
||||
@ -890,6 +895,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-1667805217] = { return Api.Update.parse_updateReadHistoryInbox($0) }
|
||||
dict[791617983] = { return Api.Update.parse_updateReadHistoryOutbox($0) }
|
||||
dict[1757493555] = { return Api.Update.parse_updateReadMessagesContents($0) }
|
||||
dict[-1653870963] = { return Api.Update.parse_updateReadStories($0) }
|
||||
dict[821314523] = { return Api.Update.parse_updateRecentEmojiStatuses($0) }
|
||||
dict[1870160884] = { return Api.Update.parse_updateRecentReactions($0) }
|
||||
dict[-1706939360] = { return Api.Update.parse_updateRecentStickers($0) }
|
||||
@ -1149,6 +1155,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-1432995067] = { return Api.storage.FileType.parse_fileUnknown($0) }
|
||||
dict[276907596] = { return Api.storage.FileType.parse_fileWebp($0) }
|
||||
dict[1214632796] = { return Api.stories.AllStories.parse_allStories($0) }
|
||||
dict[-79726676] = { return Api.stories.StoryViewsList.parse_storyViewsList($0) }
|
||||
dict[543450958] = { return Api.updates.ChannelDifference.parse_channelDifference($0) }
|
||||
dict[1041346555] = { return Api.updates.ChannelDifference.parse_channelDifferenceEmpty($0) }
|
||||
dict[-1531132162] = { return Api.updates.ChannelDifference.parse_channelDifferenceTooLong($0) }
|
||||
@ -1702,6 +1709,8 @@ public extension Api {
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.StoryItem:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.StoryView:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.TextWithEntities:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.Theme:
|
||||
@ -2008,6 +2017,8 @@ public extension Api {
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.stories.AllStories:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.stories.StoryViewsList:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.updates.ChannelDifference:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.updates.Difference:
|
||||
|
@ -714,6 +714,7 @@ public extension Api {
|
||||
}
|
||||
public extension Api {
|
||||
enum PrivacyKey: TypeConstructorDescription {
|
||||
case privacyKeyAbout
|
||||
case privacyKeyAddedByPhone
|
||||
case privacyKeyChatInvite
|
||||
case privacyKeyForwards
|
||||
@ -726,6 +727,12 @@ public extension Api {
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .privacyKeyAbout:
|
||||
if boxed {
|
||||
buffer.appendInt32(-1534675103)
|
||||
}
|
||||
|
||||
break
|
||||
case .privacyKeyAddedByPhone:
|
||||
if boxed {
|
||||
buffer.appendInt32(1124062251)
|
||||
@ -785,6 +792,8 @@ public extension Api {
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .privacyKeyAbout:
|
||||
return ("privacyKeyAbout", [])
|
||||
case .privacyKeyAddedByPhone:
|
||||
return ("privacyKeyAddedByPhone", [])
|
||||
case .privacyKeyChatInvite:
|
||||
@ -806,6 +815,9 @@ public extension Api {
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_privacyKeyAbout(_ reader: BufferReader) -> PrivacyKey? {
|
||||
return Api.PrivacyKey.privacyKeyAbout
|
||||
}
|
||||
public static func parse_privacyKeyAddedByPhone(_ reader: BufferReader) -> PrivacyKey? {
|
||||
return Api.PrivacyKey.privacyKeyAddedByPhone
|
||||
}
|
||||
@ -840,6 +852,7 @@ public extension Api {
|
||||
enum PrivacyRule: TypeConstructorDescription {
|
||||
case privacyValueAllowAll
|
||||
case privacyValueAllowChatParticipants(chats: [Int64])
|
||||
case privacyValueAllowCloseFriends
|
||||
case privacyValueAllowContacts
|
||||
case privacyValueAllowUsers(users: [Int64])
|
||||
case privacyValueDisallowAll
|
||||
@ -864,6 +877,12 @@ public extension Api {
|
||||
for item in chats {
|
||||
serializeInt64(item, buffer: buffer, boxed: false)
|
||||
}
|
||||
break
|
||||
case .privacyValueAllowCloseFriends:
|
||||
if boxed {
|
||||
buffer.appendInt32(-135735141)
|
||||
}
|
||||
|
||||
break
|
||||
case .privacyValueAllowContacts:
|
||||
if boxed {
|
||||
@ -922,6 +941,8 @@ public extension Api {
|
||||
return ("privacyValueAllowAll", [])
|
||||
case .privacyValueAllowChatParticipants(let chats):
|
||||
return ("privacyValueAllowChatParticipants", [("chats", chats as Any)])
|
||||
case .privacyValueAllowCloseFriends:
|
||||
return ("privacyValueAllowCloseFriends", [])
|
||||
case .privacyValueAllowContacts:
|
||||
return ("privacyValueAllowContacts", [])
|
||||
case .privacyValueAllowUsers(let users):
|
||||
@ -953,6 +974,9 @@ public extension Api {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_privacyValueAllowCloseFriends(_ reader: BufferReader) -> PrivacyRule? {
|
||||
return Api.PrivacyRule.privacyValueAllowCloseFriends
|
||||
}
|
||||
public static func parse_privacyValueAllowContacts(_ reader: BufferReader) -> PrivacyRule? {
|
||||
return Api.PrivacyRule.privacyValueAllowContacts
|
||||
}
|
||||
|
@ -328,18 +328,36 @@ public extension Api {
|
||||
}
|
||||
public extension Api {
|
||||
indirect enum StoryItem: TypeConstructorDescription {
|
||||
case storyItem(id: Int64, date: Int32, media: Api.MessageMedia)
|
||||
case storyItem(flags: Int32, id: Int64, date: Int32, caption: String?, entities: [Api.MessageEntity]?, media: Api.MessageMedia, privacy: [Api.PrivacyRule]?, recentViewers: [Int64]?, viewsCount: Int32?)
|
||||
case storyItemDeleted(id: Int64)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .storyItem(let id, let date, let media):
|
||||
case .storyItem(let flags, let id, let date, let caption, let entities, let media, let privacy, let recentViewers, let viewsCount):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1230510430)
|
||||
buffer.appendInt32(271121336)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeInt64(id, buffer: buffer, boxed: false)
|
||||
serializeInt32(date, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 0) != 0 {serializeString(caption!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(entities!.count))
|
||||
for item in entities! {
|
||||
item.serialize(buffer, true)
|
||||
}}
|
||||
media.serialize(buffer, true)
|
||||
if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(privacy!.count))
|
||||
for item in privacy! {
|
||||
item.serialize(buffer, true)
|
||||
}}
|
||||
if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(recentViewers!.count))
|
||||
for item in recentViewers! {
|
||||
serializeInt64(item, buffer: buffer, boxed: false)
|
||||
}}
|
||||
if Int(flags) & Int(1 << 3) != 0 {serializeInt32(viewsCount!, buffer: buffer, boxed: false)}
|
||||
break
|
||||
case .storyItemDeleted(let id):
|
||||
if boxed {
|
||||
@ -352,27 +370,51 @@ public extension Api {
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .storyItem(let id, let date, let media):
|
||||
return ("storyItem", [("id", id as Any), ("date", date as Any), ("media", media as Any)])
|
||||
case .storyItem(let flags, let id, let date, let caption, let entities, let media, let privacy, let recentViewers, let viewsCount):
|
||||
return ("storyItem", [("flags", flags as Any), ("id", id as Any), ("date", date as Any), ("caption", caption as Any), ("entities", entities as Any), ("media", media as Any), ("privacy", privacy as Any), ("recentViewers", recentViewers as Any), ("viewsCount", viewsCount as Any)])
|
||||
case .storyItemDeleted(let id):
|
||||
return ("storyItemDeleted", [("id", id as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_storyItem(_ reader: BufferReader) -> StoryItem? {
|
||||
var _1: Int64?
|
||||
_1 = reader.readInt64()
|
||||
var _2: Int32?
|
||||
_2 = reader.readInt32()
|
||||
var _3: Api.MessageMedia?
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int64?
|
||||
_2 = reader.readInt64()
|
||||
var _3: Int32?
|
||||
_3 = reader.readInt32()
|
||||
var _4: String?
|
||||
if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) }
|
||||
var _5: [Api.MessageEntity]?
|
||||
if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() {
|
||||
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self)
|
||||
} }
|
||||
var _6: Api.MessageMedia?
|
||||
if let signature = reader.readInt32() {
|
||||
_3 = Api.parse(reader, signature: signature) as? Api.MessageMedia
|
||||
_6 = Api.parse(reader, signature: signature) as? Api.MessageMedia
|
||||
}
|
||||
var _7: [Api.PrivacyRule]?
|
||||
if Int(_1!) & Int(1 << 2) != 0 {if let _ = reader.readInt32() {
|
||||
_7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PrivacyRule.self)
|
||||
} }
|
||||
var _8: [Int64]?
|
||||
if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() {
|
||||
_8 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self)
|
||||
} }
|
||||
var _9: Int32?
|
||||
if Int(_1!) & Int(1 << 3) != 0 {_9 = reader.readInt32() }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
if _c1 && _c2 && _c3 {
|
||||
return Api.StoryItem.storyItem(id: _1!, date: _2!, media: _3!)
|
||||
let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil
|
||||
let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil
|
||||
let _c6 = _6 != nil
|
||||
let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil
|
||||
let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil
|
||||
let _c9 = (Int(_1!) & Int(1 << 3) == 0) || _9 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 {
|
||||
return Api.StoryItem.storyItem(flags: _1!, id: _2!, date: _3!, caption: _4, entities: _5, media: _6!, privacy: _7, recentViewers: _8, viewsCount: _9)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
@ -392,6 +434,46 @@ public extension Api {
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum StoryView: TypeConstructorDescription {
|
||||
case storyView(userId: Int64, date: Int64)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .storyView(let userId, let date):
|
||||
if boxed {
|
||||
buffer.appendInt32(90474706)
|
||||
}
|
||||
serializeInt64(userId, buffer: buffer, boxed: false)
|
||||
serializeInt64(date, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .storyView(let userId, let date):
|
||||
return ("storyView", [("userId", userId as Any), ("date", date as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_storyView(_ reader: BufferReader) -> StoryView? {
|
||||
var _1: Int64?
|
||||
_1 = reader.readInt64()
|
||||
var _2: Int64?
|
||||
_2 = reader.readInt64()
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.StoryView.storyView(userId: _1!, date: _2!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum TextWithEntities: TypeConstructorDescription {
|
||||
case textWithEntities(text: String, entities: [Api.MessageEntity])
|
||||
@ -881,6 +963,7 @@ public extension Api {
|
||||
case updateReadHistoryInbox(flags: Int32, folderId: Int32?, peer: Api.Peer, maxId: Int32, stillUnreadCount: Int32, pts: Int32, ptsCount: Int32)
|
||||
case updateReadHistoryOutbox(peer: Api.Peer, maxId: Int32, pts: Int32, ptsCount: Int32)
|
||||
case updateReadMessagesContents(messages: [Int32], pts: Int32, ptsCount: Int32)
|
||||
case updateReadStories(userId: Int64, id: [Int64])
|
||||
case updateRecentEmojiStatuses
|
||||
case updateRecentReactions
|
||||
case updateRecentStickers
|
||||
@ -1720,6 +1803,17 @@ public extension Api {
|
||||
serializeInt32(pts, buffer: buffer, boxed: false)
|
||||
serializeInt32(ptsCount, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .updateReadStories(let userId, let id):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1653870963)
|
||||
}
|
||||
serializeInt64(userId, buffer: buffer, boxed: false)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(id.count))
|
||||
for item in id {
|
||||
serializeInt64(item, buffer: buffer, boxed: false)
|
||||
}
|
||||
break
|
||||
case .updateRecentEmojiStatuses:
|
||||
if boxed {
|
||||
buffer.appendInt32(821314523)
|
||||
@ -2056,6 +2150,8 @@ public extension Api {
|
||||
return ("updateReadHistoryOutbox", [("peer", peer as Any), ("maxId", maxId as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)])
|
||||
case .updateReadMessagesContents(let messages, let pts, let ptsCount):
|
||||
return ("updateReadMessagesContents", [("messages", messages as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)])
|
||||
case .updateReadStories(let userId, let id):
|
||||
return ("updateReadStories", [("userId", userId as Any), ("id", id as Any)])
|
||||
case .updateRecentEmojiStatuses:
|
||||
return ("updateRecentEmojiStatuses", [])
|
||||
case .updateRecentReactions:
|
||||
@ -3775,6 +3871,22 @@ public extension Api {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_updateReadStories(_ reader: BufferReader) -> Update? {
|
||||
var _1: Int64?
|
||||
_1 = reader.readInt64()
|
||||
var _2: [Int64]?
|
||||
if let _ = reader.readInt32() {
|
||||
_2 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.Update.updateReadStories(userId: _1!, id: _2!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_updateRecentEmojiStatuses(_ reader: BufferReader) -> Update? {
|
||||
return Api.Update.updateRecentEmojiStatuses
|
||||
}
|
||||
|
@ -418,6 +418,62 @@ public extension Api.stories {
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.stories {
|
||||
enum StoryViewsList: TypeConstructorDescription {
|
||||
case storyViewsList(count: Int32, views: [Api.StoryView], users: [Api.User])
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .storyViewsList(let count, let views, let users):
|
||||
if boxed {
|
||||
buffer.appendInt32(-79726676)
|
||||
}
|
||||
serializeInt32(count, buffer: buffer, boxed: false)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(views.count))
|
||||
for item in views {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(users.count))
|
||||
for item in users {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .storyViewsList(let count, let views, let users):
|
||||
return ("storyViewsList", [("count", count as Any), ("views", views as Any), ("users", users as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_storyViewsList(_ reader: BufferReader) -> StoryViewsList? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: [Api.StoryView]?
|
||||
if let _ = reader.readInt32() {
|
||||
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryView.self)
|
||||
}
|
||||
var _3: [Api.User]?
|
||||
if let _ = reader.readInt32() {
|
||||
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
if _c1 && _c2 && _c3 {
|
||||
return Api.stories.StoryViewsList.storyViewsList(count: _1!, views: _2!, users: _3!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.updates {
|
||||
enum ChannelDifference: TypeConstructorDescription {
|
||||
case channelDifference(flags: Int32, pts: Int32, timeout: Int32?, newMessages: [Api.Message], otherUpdates: [Api.Update], chats: [Api.Chat], users: [Api.User])
|
||||
|
@ -2042,6 +2042,22 @@ public extension Api.functions.channels {
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.channels {
|
||||
static func clickSponsoredMessage(channel: Api.InputChannel, randomId: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(414170259)
|
||||
channel.serialize(buffer, true)
|
||||
serializeBytes(randomId, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "channels.clickSponsoredMessage", parameters: [("channel", String(describing: channel)), ("randomId", String(describing: randomId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Bool?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.Bool
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.channels {
|
||||
static func convertToGigagroup(channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
let buffer = Buffer()
|
||||
@ -3270,6 +3286,25 @@ public extension Api.functions.contacts {
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.contacts {
|
||||
static func editCloseFriends(id: [Int64]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1167653392)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(id.count))
|
||||
for item in id {
|
||||
serializeInt64(item, buffer: buffer, boxed: false)
|
||||
}
|
||||
return (FunctionDescription(name: "contacts.editCloseFriends", parameters: [("id", String(describing: id))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Bool?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.Bool
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.contacts {
|
||||
static func exportContactToken() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.ExportedContactToken>) {
|
||||
let buffer = Buffer()
|
||||
@ -8452,6 +8487,24 @@ public extension Api.functions.stories {
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.stories {
|
||||
static func getStoryViews(id: Int64, offsetDate: Int32, offsetId: Int64, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.stories.StoryViewsList>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1233598578)
|
||||
serializeInt64(id, buffer: buffer, boxed: false)
|
||||
serializeInt32(offsetDate, buffer: buffer, boxed: false)
|
||||
serializeInt64(offsetId, buffer: buffer, boxed: false)
|
||||
serializeInt32(limit, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "stories.getStoryViews", parameters: [("id", String(describing: id)), ("offsetDate", String(describing: offsetDate)), ("offsetId", String(describing: offsetId)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.StoryViewsList? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.stories.StoryViewsList?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.stories.StoryViewsList
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.stories {
|
||||
static func getUserStories(userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
let buffer = Buffer()
|
||||
@ -8468,16 +8521,43 @@ public extension Api.functions.stories {
|
||||
}
|
||||
}
|
||||
public extension Api.functions.stories {
|
||||
static func sendStory(media: Api.InputMedia, privacyRules: [Api.InputPrivacyRule]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
static func readStories(userId: Api.InputUser, id: [Int64]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1310573354)
|
||||
buffer.appendInt32(1026304810)
|
||||
userId.serialize(buffer, true)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(id.count))
|
||||
for item in id {
|
||||
serializeInt64(item, buffer: buffer, boxed: false)
|
||||
}
|
||||
return (FunctionDescription(name: "stories.readStories", parameters: [("userId", String(describing: userId)), ("id", String(describing: id))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Bool?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.Bool
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.stories {
|
||||
static func sendStory(flags: Int32, media: Api.InputMedia, caption: String?, entities: [Api.MessageEntity]?, privacyRules: [Api.InputPrivacyRule]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1904054435)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
media.serialize(buffer, true)
|
||||
if Int(flags) & Int(1 << 0) != 0 {serializeString(caption!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(entities!.count))
|
||||
for item in entities! {
|
||||
item.serialize(buffer, true)
|
||||
}}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(privacyRules.count))
|
||||
for item in privacyRules {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
return (FunctionDescription(name: "stories.sendStory", parameters: [("media", String(describing: media)), ("privacyRules", String(describing: privacyRules))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
|
||||
return (FunctionDescription(name: "stories.sendStory", parameters: [("flags", String(describing: flags)), ("media", String(describing: media)), ("caption", String(describing: caption)), ("entities", String(describing: entities)), ("privacyRules", String(describing: privacyRules))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Updates?
|
||||
if let signature = reader.readInt32() {
|
||||
|
@ -432,6 +432,7 @@ public extension Api {
|
||||
}
|
||||
public extension Api {
|
||||
enum InputPrivacyKey: TypeConstructorDescription {
|
||||
case inputPrivacyKeyAbout
|
||||
case inputPrivacyKeyAddedByPhone
|
||||
case inputPrivacyKeyChatInvite
|
||||
case inputPrivacyKeyForwards
|
||||
@ -444,6 +445,12 @@ public extension Api {
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .inputPrivacyKeyAbout:
|
||||
if boxed {
|
||||
buffer.appendInt32(941870144)
|
||||
}
|
||||
|
||||
break
|
||||
case .inputPrivacyKeyAddedByPhone:
|
||||
if boxed {
|
||||
buffer.appendInt32(-786326563)
|
||||
@ -503,6 +510,8 @@ public extension Api {
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .inputPrivacyKeyAbout:
|
||||
return ("inputPrivacyKeyAbout", [])
|
||||
case .inputPrivacyKeyAddedByPhone:
|
||||
return ("inputPrivacyKeyAddedByPhone", [])
|
||||
case .inputPrivacyKeyChatInvite:
|
||||
@ -524,6 +533,9 @@ public extension Api {
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_inputPrivacyKeyAbout(_ reader: BufferReader) -> InputPrivacyKey? {
|
||||
return Api.InputPrivacyKey.inputPrivacyKeyAbout
|
||||
}
|
||||
public static func parse_inputPrivacyKeyAddedByPhone(_ reader: BufferReader) -> InputPrivacyKey? {
|
||||
return Api.InputPrivacyKey.inputPrivacyKeyAddedByPhone
|
||||
}
|
||||
@ -558,6 +570,7 @@ public extension Api {
|
||||
enum InputPrivacyRule: TypeConstructorDescription {
|
||||
case inputPrivacyValueAllowAll
|
||||
case inputPrivacyValueAllowChatParticipants(chats: [Int64])
|
||||
case inputPrivacyValueAllowCloseFriends
|
||||
case inputPrivacyValueAllowContacts
|
||||
case inputPrivacyValueAllowUsers(users: [Api.InputUser])
|
||||
case inputPrivacyValueDisallowAll
|
||||
@ -582,6 +595,12 @@ public extension Api {
|
||||
for item in chats {
|
||||
serializeInt64(item, buffer: buffer, boxed: false)
|
||||
}
|
||||
break
|
||||
case .inputPrivacyValueAllowCloseFriends:
|
||||
if boxed {
|
||||
buffer.appendInt32(793067081)
|
||||
}
|
||||
|
||||
break
|
||||
case .inputPrivacyValueAllowContacts:
|
||||
if boxed {
|
||||
@ -640,6 +659,8 @@ public extension Api {
|
||||
return ("inputPrivacyValueAllowAll", [])
|
||||
case .inputPrivacyValueAllowChatParticipants(let chats):
|
||||
return ("inputPrivacyValueAllowChatParticipants", [("chats", chats as Any)])
|
||||
case .inputPrivacyValueAllowCloseFriends:
|
||||
return ("inputPrivacyValueAllowCloseFriends", [])
|
||||
case .inputPrivacyValueAllowContacts:
|
||||
return ("inputPrivacyValueAllowContacts", [])
|
||||
case .inputPrivacyValueAllowUsers(let users):
|
||||
@ -671,6 +692,9 @@ public extension Api {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_inputPrivacyValueAllowCloseFriends(_ reader: BufferReader) -> InputPrivacyRule? {
|
||||
return Api.InputPrivacyRule.inputPrivacyValueAllowCloseFriends
|
||||
}
|
||||
public static func parse_inputPrivacyValueAllowContacts(_ reader: BufferReader) -> InputPrivacyRule? {
|
||||
return Api.InputPrivacyRule.inputPrivacyValueAllowContacts
|
||||
}
|
||||
|
@ -121,6 +121,7 @@ enum AccountStateMutationOperation {
|
||||
case UpdateExtendedMedia(MessageId, Api.MessageExtendedMedia)
|
||||
case ResetForumTopic(topicId: MessageId, data: StoreMessageHistoryThreadData, pts: Int32)
|
||||
case UpdateStories(Api.UserStories)
|
||||
case UpdateReadStories(peerId: PeerId, ids: [Int64])
|
||||
}
|
||||
|
||||
struct HoleFromPreviousState {
|
||||
@ -614,9 +615,13 @@ struct AccountMutableState {
|
||||
self.addOperation(.UpdateStories(stories))
|
||||
}
|
||||
|
||||
mutating func readStories(peerId: PeerId, ids: [Int64]) {
|
||||
self.addOperation(.UpdateReadStories(peerId: peerId, ids: ids))
|
||||
}
|
||||
|
||||
mutating func addOperation(_ operation: AccountStateMutationOperation) {
|
||||
switch operation {
|
||||
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStories:
|
||||
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStories, .UpdateReadStories:
|
||||
break
|
||||
case let .AddMessages(messages, location):
|
||||
for message in messages {
|
||||
|
@ -36,7 +36,7 @@ public final class SelectivePrivacyPeer: Equatable {
|
||||
public enum SelectivePrivacySettings: Equatable {
|
||||
case enableEveryone(disableFor: [PeerId: SelectivePrivacyPeer])
|
||||
case enableContacts(enableFor: [PeerId: SelectivePrivacyPeer], disableFor: [PeerId: SelectivePrivacyPeer])
|
||||
case disableEveryone(enableFor: [PeerId: SelectivePrivacyPeer])
|
||||
case disableEveryone(enableFor: [PeerId: SelectivePrivacyPeer], enableForCloseFriends: Bool)
|
||||
|
||||
public static func ==(lhs: SelectivePrivacySettings, rhs: SelectivePrivacySettings) -> Bool {
|
||||
switch lhs {
|
||||
@ -52,8 +52,8 @@ public enum SelectivePrivacySettings: Equatable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .disableEveryone(enableFor):
|
||||
if case .disableEveryone(enableFor) = rhs {
|
||||
case let .disableEveryone(enableFor, enableForCloseFriends):
|
||||
if case .disableEveryone(enableFor, enableForCloseFriends) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -63,8 +63,8 @@ public enum SelectivePrivacySettings: Equatable {
|
||||
|
||||
func withEnabledPeers(_ peers: [PeerId: SelectivePrivacyPeer]) -> SelectivePrivacySettings {
|
||||
switch self {
|
||||
case let .disableEveryone(enableFor):
|
||||
return .disableEveryone(enableFor: enableFor.merging(peers, uniquingKeysWith: { lhs, rhs in lhs }))
|
||||
case let .disableEveryone(enableFor, enableForCloseFriends):
|
||||
return .disableEveryone(enableFor: enableFor.merging(peers, uniquingKeysWith: { lhs, rhs in lhs }), enableForCloseFriends: enableForCloseFriends)
|
||||
case let .enableContacts(enableFor, disableFor):
|
||||
return .enableContacts(enableFor: enableFor.merging(peers, uniquingKeysWith: { lhs, rhs in lhs }), disableFor: disableFor)
|
||||
case .enableEveryone:
|
||||
@ -82,6 +82,17 @@ public enum SelectivePrivacySettings: Equatable {
|
||||
return .enableEveryone(disableFor: disableFor.merging(peers, uniquingKeysWith: { lhs, rhs in lhs }))
|
||||
}
|
||||
}
|
||||
|
||||
func withEnableForCloseFriends(_ enableForCloseFriends: Bool) -> SelectivePrivacySettings {
|
||||
switch self {
|
||||
case let .disableEveryone(enableFor, _):
|
||||
return .disableEveryone(enableFor: enableFor, enableForCloseFriends: enableForCloseFriends)
|
||||
case .enableContacts:
|
||||
return self
|
||||
case .enableEveryone:
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct AccountPrivacySettings: Equatable {
|
||||
@ -158,10 +169,11 @@ public struct AccountPrivacySettings: Equatable {
|
||||
|
||||
extension SelectivePrivacySettings {
|
||||
init(apiRules: [Api.PrivacyRule], peers: [PeerId: SelectivePrivacyPeer]) {
|
||||
var current: SelectivePrivacySettings = .disableEveryone(enableFor: [:])
|
||||
var current: SelectivePrivacySettings = .disableEveryone(enableFor: [:], enableForCloseFriends: false)
|
||||
|
||||
var disableFor: [PeerId: SelectivePrivacyPeer] = [:]
|
||||
var enableFor: [PeerId: SelectivePrivacyPeer] = [:]
|
||||
var enableForCloseFriends: Bool = false
|
||||
|
||||
for rule in apiRules {
|
||||
switch rule {
|
||||
@ -201,10 +213,12 @@ extension SelectivePrivacySettings {
|
||||
}
|
||||
}
|
||||
}
|
||||
case .privacyValueAllowCloseFriends:
|
||||
enableForCloseFriends = true
|
||||
}
|
||||
}
|
||||
|
||||
self = current.withEnabledPeers(enableFor).withDisabledPeers(disableFor)
|
||||
self = current.withEnabledPeers(enableFor).withDisabledPeers(disableFor).withEnableForCloseFriends(enableForCloseFriends)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1630,6 +1630,8 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
|
||||
updatedState.updateExtendedMedia(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: msgId), extendedMedia: extendedMedia)
|
||||
case let .updateStories(stories):
|
||||
updatedState.updateStories(stories: stories)
|
||||
case let .updateReadStories(userId, id):
|
||||
updatedState.readStories(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), ids: id)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -3002,7 +3004,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation])
|
||||
var currentAddScheduledMessages: OptimizeAddMessagesState?
|
||||
for operation in operations {
|
||||
switch operation {
|
||||
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStories:
|
||||
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStories, .UpdateReadStories:
|
||||
if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty {
|
||||
result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location))
|
||||
}
|
||||
@ -4334,14 +4336,32 @@ func replayFinalState(
|
||||
switch storyItem {
|
||||
case let .storyItemDeleted(id):
|
||||
storyUpdates.append(InternalStoryUpdate.deleted(id))
|
||||
case let .storyItem(id, date, media):
|
||||
case let .storyItem(flags, id, date, _, _, media, _, recentViewers, viewCount):
|
||||
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
|
||||
if let parsedMedia = parsedMedia {
|
||||
storyUpdates.append(InternalStoryUpdate.added(peerId: peerId, item: StoryListContext.Item(id: id, timestamp: date, media: EngineMedia(parsedMedia))))
|
||||
var seenPeers: [EnginePeer] = []
|
||||
if let recentViewers = recentViewers {
|
||||
for id in recentViewers {
|
||||
if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))) {
|
||||
seenPeers.append(EnginePeer(peer))
|
||||
}
|
||||
}
|
||||
}
|
||||
storyUpdates.append(InternalStoryUpdate.added(peerId: peerId, item: StoryListContext.Item(
|
||||
id: id,
|
||||
timestamp: date,
|
||||
media: EngineMedia(parsedMedia),
|
||||
isSeen: (flags & (1 << 4)) == 0,
|
||||
seenCount: viewCount.flatMap(Int.init) ?? 0,
|
||||
seenPeers: seenPeers
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .UpdateReadStories(peerId, ids):
|
||||
let _ = peerId
|
||||
storyUpdates.append(InternalStoryUpdate.read(ids))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -224,7 +224,7 @@ public final class AccountStateManager {
|
||||
return self.deletedMessagesPipe.signal()
|
||||
}
|
||||
|
||||
private let storyUpdatesPipe = ValuePipe<[InternalStoryUpdate]>()
|
||||
fileprivate let storyUpdatesPipe = ValuePipe<[InternalStoryUpdate]>()
|
||||
public var storyUpdates: Signal<[InternalStoryUpdate], NoError> {
|
||||
return self.storyUpdatesPipe.signal()
|
||||
}
|
||||
@ -1639,6 +1639,12 @@ public final class AccountStateManager {
|
||||
}
|
||||
}
|
||||
|
||||
func injectStoryUpdates(updates: [InternalStoryUpdate]) {
|
||||
self.impl.with { impl in
|
||||
impl.storyUpdatesPipe.putNext(updates)
|
||||
}
|
||||
}
|
||||
|
||||
var updateConfigRequested: (() -> Void)?
|
||||
var isPremiumUpdated: (() -> Void)?
|
||||
|
||||
|
@ -55,7 +55,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia) -> Si
|
||||
case let .content(content):
|
||||
switch content.content {
|
||||
case let .media(inputMedia, _):
|
||||
return account.network.request(Api.functions.stories.sendStory(media: inputMedia, privacyRules: [.inputPrivacyValueAllowAll]))
|
||||
return account.network.request(Api.functions.stories.sendStory(flags: 0, media: inputMedia, caption: nil, entities: nil, privacyRules: [.inputPrivacyValueAllowAll]))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
@ -68,7 +68,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia) -> Si
|
||||
case .userStories(let userId, let apiStories), .userStoriesShort(let userId, let apiStories, _):
|
||||
if PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) == account.peerId, apiStories.count == 1 {
|
||||
switch apiStories[0] {
|
||||
case let .storyItem(_, _, media):
|
||||
case let .storyItem(_, _, _, _, _, media, _, _, _):
|
||||
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId)
|
||||
if let parsedMedia = parsedMedia {
|
||||
applyMediaResourceChanges(from: imageMedia, to: parsedMedia, postbox: account.postbox, force: false)
|
||||
@ -105,3 +105,22 @@ func _internal_deleteStory(account: Account, id: Int64) -> Signal<Never, NoError
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int64) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Api.InputUser? in
|
||||
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||
}
|
||||
|> mapToSignal { inputUser -> Signal<Never, NoError> in
|
||||
guard let inputUser = inputUser else {
|
||||
return .complete()
|
||||
}
|
||||
|
||||
account.stateManager.injectStoryUpdates(updates: [.read([id])])
|
||||
|
||||
return account.network.request(Api.functions.stories.readStories(userId: inputUser, id: [id]))
|
||||
|> `catch` { _ -> Signal<Api.Bool, NoError> in
|
||||
return .single(.boolFalse)
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import SwiftSignalKit
|
||||
enum InternalStoryUpdate {
|
||||
case deleted(Int64)
|
||||
case added(peerId: PeerId, item: StoryListContext.Item)
|
||||
case read([Int64])
|
||||
}
|
||||
|
||||
public final class StoryListContext {
|
||||
@ -18,11 +19,17 @@ public final class StoryListContext {
|
||||
public let id: Int64
|
||||
public let timestamp: Int32
|
||||
public let media: EngineMedia
|
||||
public let isSeen: Bool
|
||||
public let seenCount: Int
|
||||
public let seenPeers: [EnginePeer]
|
||||
|
||||
public init(id: Int64, timestamp: Int32, media: EngineMedia) {
|
||||
public init(id: Int64, timestamp: Int32, media: EngineMedia, isSeen: Bool, seenCount: Int, seenPeers: [EnginePeer]) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
self.media = media
|
||||
self.isSeen = isSeen
|
||||
self.seenCount = seenCount
|
||||
self.seenPeers = seenPeers
|
||||
}
|
||||
|
||||
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
@ -35,6 +42,15 @@ public final class StoryListContext {
|
||||
if lhs.media != rhs.media {
|
||||
return false
|
||||
}
|
||||
if lhs.isSeen != rhs.isSeen {
|
||||
return false
|
||||
}
|
||||
if lhs.seenCount != rhs.seenCount {
|
||||
return false
|
||||
}
|
||||
if lhs.seenPeers != rhs.seenPeers {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -137,6 +153,8 @@ public final class StoryListContext {
|
||||
}
|
||||
case .deleted:
|
||||
break
|
||||
case .read:
|
||||
break
|
||||
}
|
||||
}
|
||||
return peers
|
||||
@ -144,6 +162,9 @@ public final class StoryListContext {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.isLoadingMore {
|
||||
return
|
||||
}
|
||||
|
||||
var itemSets: [PeerItemSet] = self.stateValue.itemSets
|
||||
|
||||
@ -151,7 +172,7 @@ public final class StoryListContext {
|
||||
switch update {
|
||||
case let .deleted(id):
|
||||
for i in 0 ..< itemSets.count {
|
||||
if let index = itemSets[i].items.firstIndex(where: { $0.id != id }) {
|
||||
if let index = itemSets[i].items.firstIndex(where: { $0.id == id }) {
|
||||
var items = itemSets[i].items
|
||||
items.remove(at: index)
|
||||
itemSets[i] = PeerItemSet(
|
||||
@ -175,9 +196,9 @@ public final class StoryListContext {
|
||||
items.append(item)
|
||||
items.sort(by: { lhsItem, rhsItem in
|
||||
if lhsItem.timestamp != rhsItem.timestamp {
|
||||
return lhsItem.timestamp > rhsItem.timestamp
|
||||
return lhsItem.timestamp < rhsItem.timestamp
|
||||
}
|
||||
return lhsItem.id > rhsItem.id
|
||||
return lhsItem.id < rhsItem.id
|
||||
})
|
||||
itemSets[i] = PeerItemSet(
|
||||
peerId: itemSets[i].peerId,
|
||||
@ -195,6 +216,29 @@ public final class StoryListContext {
|
||||
totalCount: 1
|
||||
), at: 0)
|
||||
}
|
||||
case let .read(ids):
|
||||
for id in ids {
|
||||
for i in 0 ..< itemSets.count {
|
||||
if let index = itemSets[i].items.firstIndex(where: { $0.id == id }) {
|
||||
var items = itemSets[i].items
|
||||
let item = items[index]
|
||||
items[index] = Item(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
media: item.media,
|
||||
isSeen: true,
|
||||
seenCount: item.seenCount,
|
||||
seenPeers: item.seenPeers
|
||||
)
|
||||
itemSets[i] = PeerItemSet(
|
||||
peerId: itemSets[i].peerId,
|
||||
peer: itemSets[i].peer,
|
||||
items: items,
|
||||
totalCount: items.count
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -263,10 +307,25 @@ public final class StoryListContext {
|
||||
|
||||
for apiStory in apiStories {
|
||||
switch apiStory {
|
||||
case let .storyItem(id, date, media):
|
||||
case let .storyItem(flags, id, date, _, _, media, _, recentViewers, viewCount):
|
||||
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
|
||||
if let parsedMedia = parsedMedia {
|
||||
let item = StoryListContext.Item(id: id, timestamp: date, media: EngineMedia(parsedMedia))
|
||||
var seenPeers: [EnginePeer] = []
|
||||
if let recentViewers = recentViewers {
|
||||
for id in recentViewers {
|
||||
if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))) {
|
||||
seenPeers.append(EnginePeer(peer))
|
||||
}
|
||||
}
|
||||
}
|
||||
let item = StoryListContext.Item(
|
||||
id: id,
|
||||
timestamp: date,
|
||||
media: EngineMedia(parsedMedia),
|
||||
isSeen: (flags & (1 << 4)) == 0,
|
||||
seenCount: viewCount.flatMap(Int.init) ?? 0,
|
||||
seenPeers: seenPeers
|
||||
)
|
||||
if !parsedItemSets.isEmpty && parsedItemSets[parsedItemSets.count - 1].peerId == peerId {
|
||||
parsedItemSets[parsedItemSets.count - 1].items.append(item)
|
||||
parsedItemSets[parsedItemSets.count - 1].totalCount = parsedItemSets[parsedItemSets.count - 1].items.count
|
||||
@ -366,10 +425,25 @@ public final class StoryListContext {
|
||||
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(apiUserId))
|
||||
for apiStory in apiStories {
|
||||
switch apiStory {
|
||||
case let .storyItem(id, date, media):
|
||||
case let .storyItem(flags, id, date, _, _, media, _, recentViewers, viewCount):
|
||||
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
|
||||
if let parsedMedia = parsedMedia {
|
||||
let item = StoryListContext.Item(id: id, timestamp: date, media: EngineMedia(parsedMedia))
|
||||
var seenPeers: [EnginePeer] = []
|
||||
if let recentViewers = recentViewers {
|
||||
for id in recentViewers {
|
||||
if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))) {
|
||||
seenPeers.append(EnginePeer(peer))
|
||||
}
|
||||
}
|
||||
}
|
||||
let item = StoryListContext.Item(
|
||||
id: id,
|
||||
timestamp: date,
|
||||
media: EngineMedia(parsedMedia),
|
||||
isSeen: (flags & (1 << 4)) == 0,
|
||||
seenCount: viewCount.flatMap(Int.init) ?? 0,
|
||||
seenPeers: seenPeers
|
||||
)
|
||||
if !parsedItemSets.isEmpty && parsedItemSets[parsedItemSets.count - 1].peerId == peerId {
|
||||
parsedItemSets[parsedItemSets.count - 1].items.append(item)
|
||||
} else {
|
||||
@ -406,9 +480,9 @@ public final class StoryListContext {
|
||||
|
||||
items.sort(by: { lhsItem, rhsItem in
|
||||
if lhsItem.timestamp != rhsItem.timestamp {
|
||||
return lhsItem.timestamp > rhsItem.timestamp
|
||||
return lhsItem.timestamp < rhsItem.timestamp
|
||||
}
|
||||
return lhsItem.id > rhsItem.id
|
||||
return lhsItem.id < rhsItem.id
|
||||
})
|
||||
|
||||
itemSets[index] = PeerItemSet(
|
||||
@ -418,6 +492,12 @@ public final class StoryListContext {
|
||||
totalCount: items.count
|
||||
)
|
||||
} else {
|
||||
itemSet.items.sort(by: { lhsItem, rhsItem in
|
||||
if lhsItem.timestamp != rhsItem.timestamp {
|
||||
return lhsItem.timestamp < rhsItem.timestamp
|
||||
}
|
||||
return lhsItem.id < rhsItem.id
|
||||
})
|
||||
itemSets.append(itemSet)
|
||||
}
|
||||
}
|
||||
|
@ -582,7 +582,11 @@ public extension TelegramEngine {
|
||||
}
|
||||
|
||||
public func deleteStory(id: Int64) -> Signal<Never, NoError> {
|
||||
return _internal_deleteStory(account: account, id: id)
|
||||
return _internal_deleteStory(account: self.account, id: id)
|
||||
}
|
||||
|
||||
public func markStoryAsSeen(peerId: EnginePeer.Id, id: Int64) -> Signal<Never, NoError> {
|
||||
return _internal_markStoryAsSeen(account: self.account, peerId: peerId, id: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -273,7 +273,7 @@ func _internal_updateSelectiveAccountPrivacySettings(account: Account, type: Upd
|
||||
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
||||
var rules: [Api.InputPrivacyRule] = []
|
||||
switch settings {
|
||||
case let .disableEveryone(enableFor):
|
||||
case let .disableEveryone(enableFor, enableForCloseFriends):
|
||||
let enablePeers = apiUserAndGroupIds(peerIds: enableFor)
|
||||
|
||||
if !enablePeers.users.isEmpty {
|
||||
@ -284,6 +284,9 @@ func _internal_updateSelectiveAccountPrivacySettings(account: Account, type: Upd
|
||||
}
|
||||
|
||||
rules.append(Api.InputPrivacyRule.inputPrivacyValueDisallowAll)
|
||||
if enableForCloseFriends {
|
||||
rules.append(.inputPrivacyValueAllowCloseFriends)
|
||||
}
|
||||
case let .enableContacts(enableFor, disableFor):
|
||||
let enablePeers = apiUserAndGroupIds(peerIds: enableFor)
|
||||
let disablePeers = apiUserAndGroupIds(peerIds: disableFor)
|
||||
|
@ -56,15 +56,18 @@ private final class StoryContainerScreenComponent: Component {
|
||||
let context: AccountContext
|
||||
let initialFocusedId: AnyHashable?
|
||||
let initialContent: [StoryContentItemSlice]
|
||||
let transitionIn: StoryContainerScreen.TransitionIn?
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
initialFocusedId: AnyHashable?,
|
||||
initialContent: [StoryContentItemSlice]
|
||||
initialContent: [StoryContentItemSlice],
|
||||
transitionIn: StoryContainerScreen.TransitionIn?
|
||||
) {
|
||||
self.context = context
|
||||
self.initialFocusedId = initialFocusedId
|
||||
self.initialContent = initialContent
|
||||
self.transitionIn = transitionIn
|
||||
}
|
||||
|
||||
static func ==(lhs: StoryContainerScreenComponent, rhs: StoryContainerScreenComponent) -> Bool {
|
||||
@ -78,24 +81,121 @@ private final class StoryContainerScreenComponent: Component {
|
||||
let view = ComponentView<Empty>()
|
||||
let externalState = StoryItemSetContainerComponent.ExternalState()
|
||||
|
||||
let tintLayer = SimpleGradientLayer()
|
||||
|
||||
var rotationFraction: CGFloat?
|
||||
|
||||
override static var layerClass: AnyClass {
|
||||
return CATransformLayer.self
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.tintLayer.opacity = 0.0
|
||||
|
||||
let colors: [CGColor] = [
|
||||
UIColor.black.withAlphaComponent(1.0).cgColor,
|
||||
UIColor.black.withAlphaComponent(0.8).cgColor,
|
||||
UIColor.black.withAlphaComponent(0.5).cgColor
|
||||
]
|
||||
|
||||
self.tintLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
|
||||
self.tintLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
|
||||
self.tintLayer.colors = colors
|
||||
self.tintLayer.type = .axial
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
guard let componentView = self.view.view else {
|
||||
return nil
|
||||
}
|
||||
return componentView.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ItemSetPanState: Equatable {
|
||||
var fraction: CGFloat
|
||||
var didBegin: Bool
|
||||
|
||||
init(fraction: CGFloat) {
|
||||
init(fraction: CGFloat, didBegin: Bool) {
|
||||
self.fraction = fraction
|
||||
self.didBegin = didBegin
|
||||
}
|
||||
}
|
||||
|
||||
private final class StoryPanRecognizer: UIPanGestureRecognizer {
|
||||
private let updateIsActive: (Bool) -> Void
|
||||
private var isActive: Bool = false
|
||||
private var timer: Foundation.Timer?
|
||||
|
||||
init(target: Any?, action: Selector?, updateIsActive: @escaping (Bool) -> Void) {
|
||||
self.updateIsActive = updateIsActive
|
||||
|
||||
super.init(target: target, action: action)
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
|
||||
self.isActive = false
|
||||
self.timer?.invalidate()
|
||||
self.timer = nil
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
if !self.isActive {
|
||||
if self.timer == nil {
|
||||
self.timer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false, block: { [weak self] timer in
|
||||
guard let self, self.timer === timer else {
|
||||
return
|
||||
}
|
||||
self.timer = nil
|
||||
if !self.isActive {
|
||||
self.isActive = true
|
||||
self.updateIsActive(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
if self.isActive {
|
||||
self.isActive = false
|
||||
self.updateIsActive(false)
|
||||
|
||||
for touch in touches {
|
||||
if let gestureRecognizers = touch.gestureRecognizers {
|
||||
for gestureRecognizer in gestureRecognizers {
|
||||
if gestureRecognizer is UITapGestureRecognizer {
|
||||
gestureRecognizer.state = .cancelled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.timer?.invalidate()
|
||||
self.timer = nil
|
||||
|
||||
super.touchesEnded(touches, with: event)
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
if self.isActive {
|
||||
self.isActive = false
|
||||
self.updateIsActive(false)
|
||||
}
|
||||
self.timer?.invalidate()
|
||||
self.timer = nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,7 +229,22 @@ private final class StoryContainerScreenComponent: Component {
|
||||
|
||||
self.backgroundColor = .black
|
||||
|
||||
self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
|
||||
self.addGestureRecognizer(StoryPanRecognizer(target: self, action: #selector(self.panGesture(_:)), updateIsActive: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if value {
|
||||
if self.itemSetPanState == nil {
|
||||
self.itemSetPanState = ItemSetPanState(fraction: 0.0, didBegin: false)
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
|
||||
}
|
||||
} else {
|
||||
if let itemSetPanState = self.itemSetPanState, !itemSetPanState.didBegin {
|
||||
self.itemSetPanState = nil
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
self.audioRecorderDisposable = (self.audioRecorder.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] audioRecorder in
|
||||
@ -247,14 +362,34 @@ private final class StoryContainerScreenComponent: Component {
|
||||
case .began:
|
||||
self.layer.removeAnimation(forKey: "panState")
|
||||
|
||||
self.itemSetPanState = ItemSetPanState(fraction: 0.0)
|
||||
self.state?.updated(transition: .immediate)
|
||||
if let itemSetPanState = self.itemSetPanState, !itemSetPanState.didBegin {
|
||||
self.itemSetPanState = ItemSetPanState(fraction: 0.0, didBegin: true)
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
|
||||
} else {
|
||||
self.itemSetPanState = ItemSetPanState(fraction: 0.0, didBegin: true)
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
case .changed:
|
||||
if var itemSetPanState = self.itemSetPanState, self.bounds.width > 0.0 {
|
||||
let translation = recognizer.translation(in: self)
|
||||
if var itemSetPanState = self.itemSetPanState, self.bounds.width > 0.0, let focusedItemSet = self.focusedItemSet, let focusedIndex = self.itemSets.firstIndex(where: { $0.id == focusedItemSet }) {
|
||||
var translation = recognizer.translation(in: self)
|
||||
|
||||
let fraction = translation.x / self.bounds.width
|
||||
itemSetPanState.fraction = -max(-1.0, min(1.0, fraction))
|
||||
func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat {
|
||||
let bandedOffset = offset - bandingStart
|
||||
let range: CGFloat = 600.0
|
||||
let coefficient: CGFloat = 0.4
|
||||
return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range
|
||||
}
|
||||
|
||||
if translation.x > 0.0 && focusedIndex == 0 {
|
||||
translation.x = rubberBandingOffset(offset: translation.x, bandingStart: 0.0)
|
||||
} else if translation.x < 0.0 && focusedIndex == self.itemSets.count - 1 {
|
||||
translation.x = -rubberBandingOffset(offset: -translation.x, bandingStart: 0.0)
|
||||
}
|
||||
|
||||
var fraction = translation.x / self.bounds.width
|
||||
fraction = -max(-1.0, min(1.0, fraction))
|
||||
|
||||
itemSetPanState.fraction = fraction
|
||||
self.itemSetPanState = itemSetPanState
|
||||
|
||||
self.state?.updated(transition: .immediate)
|
||||
@ -306,14 +441,40 @@ private final class StoryContainerScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.layer.allowsGroupOpacity = true
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, completion: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
for subview in self.subviews.reversed() {
|
||||
if !subview.isUserInteractionEnabled || subview.isHidden || subview.alpha == 0.0 {
|
||||
continue
|
||||
}
|
||||
self.layer.allowsGroupOpacity = false
|
||||
})
|
||||
if subview is ItemSetView {
|
||||
if let result = subview.hitTest(point, with: event) {
|
||||
return result
|
||||
}
|
||||
} else {
|
||||
if let result = subview.hitTest(self.convert(point, to: subview), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
if let transitionIn = self.component?.transitionIn, transitionIn.sourceView != nil {
|
||||
self.layer.animate(from: UIColor.black.withAlphaComponent(0.0).cgColor, to: self.layer.backgroundColor ?? UIColor.black.cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.28)
|
||||
|
||||
if let transitionIn = self.component?.transitionIn, let focusedItemSet = self.focusedItemSet, let itemSetView = self.visibleItemSetViews[focusedItemSet] {
|
||||
if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
|
||||
itemSetComponentView.animateIn(transitionIn: transitionIn)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.layer.allowsGroupOpacity = true
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { [weak self] _ in
|
||||
self?.layer.allowsGroupOpacity = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
@ -2088,6 +2249,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
isProgressPaused: isProgressPaused || i != focusedIndex,
|
||||
audioRecorder: i == focusedIndex ? self.audioRecorderValue : nil,
|
||||
videoRecorder: i == focusedIndex ? self.videoRecorderValue : nil,
|
||||
hideUI: i == focusedIndex && self.itemSetPanState?.didBegin == false,
|
||||
presentController: { [weak self] c in
|
||||
guard let self, let environment = self.environment else {
|
||||
return
|
||||
@ -2141,18 +2303,15 @@ private final class StoryContainerScreenComponent: Component {
|
||||
contentDerivedBottomInset = itemSetView.externalState.derivedBottomInset
|
||||
}
|
||||
|
||||
var itemFrame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
itemFrame.origin.x += CGFloat(i - focusedIndex) * (availableSize.width + 10.0)
|
||||
if let itemSetPanState = self.itemSetPanState {
|
||||
itemFrame.origin.x += -itemSetPanState.fraction * (availableSize.width + 10.0)
|
||||
}
|
||||
let itemFrame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
if let itemSetComponentView = itemSetView.view.view {
|
||||
if itemSetView.superview == nil {
|
||||
self.addSubview(itemSetView)
|
||||
}
|
||||
if itemSetComponentView.superview == nil {
|
||||
//itemSetComponentView.layer.zPosition = availableSize.width * 0.5
|
||||
itemSetComponentView.layer.isDoubleSided = false
|
||||
itemSetView.addSubview(itemSetComponentView)
|
||||
itemSetView.layer.addSublayer(itemSetView.tintLayer)
|
||||
}
|
||||
|
||||
itemSetTransition.setPosition(view: itemSetView, position: itemFrame.center)
|
||||
@ -2161,35 +2320,101 @@ private final class StoryContainerScreenComponent: Component {
|
||||
itemSetTransition.setPosition(view: itemSetComponentView, position: CGRect(origin: CGPoint(), size: itemFrame.size).center)
|
||||
itemSetTransition.setBounds(view: itemSetComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
|
||||
|
||||
/*var cubeTransform: CATransform3D
|
||||
cubeTransform = CATransform3DIdentity
|
||||
itemSetTransition.setPosition(layer: itemSetView.tintLayer, position: CGRect(origin: CGPoint(), size: itemFrame.size).center)
|
||||
itemSetTransition.setBounds(layer: itemSetView.tintLayer, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
|
||||
|
||||
var perspectiveTransform: CATransform3D = CATransform3DIdentity
|
||||
let perspectiveDistance: CGFloat = 500.0
|
||||
perspectiveTransform.m34 = -1.0 / perspectiveDistance
|
||||
let _ = perspectiveTransform
|
||||
//itemSetView.layer.sublayerTransform = perspectiveTransform
|
||||
let perspectiveConstant: CGFloat = 500.0
|
||||
let width = itemFrame.width
|
||||
|
||||
let yRotation: CGFloat = (self.itemSetPanState?.fraction ?? 0.0) * CGFloat.pi * 0.5
|
||||
let sideDistance: CGFloat = 40.0
|
||||
|
||||
let rotationTransform = CATransform3DMakeRotation(yRotation, 0.0, 1.0, 0.0)
|
||||
cubeTransform = CATransform3DConcat(cubeTransform, rotationTransform)
|
||||
cubeTransform = CATransform3DTranslate(cubeTransform, (self.itemSetPanState?.fraction ?? 0.0) * availableSize.width * 0.5, 0.0, -availableSize.width * 0.5)
|
||||
let sideAngle_d: CGFloat = -pow(perspectiveConstant, 2)*pow(sideDistance, 2)
|
||||
let sideAngle_e: CGFloat = pow(perspectiveConstant, 2)*pow(width, 2)
|
||||
let sideAngle_f: CGFloat = pow(sideDistance, 2)*pow(width, 2)
|
||||
let sideAngle_c: CGFloat = sqrt(sideAngle_d + sideAngle_e + sideAngle_f + sideDistance*pow(width, 3) + 0.25*pow(width, 4))
|
||||
let sideAngle_a: CGFloat = (2.0*perspectiveConstant*width - 2.0*sideAngle_c)
|
||||
let sideAngle_b: CGFloat = (-2.0*perspectiveConstant*sideDistance + 2.0*sideDistance*width + pow(width, 2))
|
||||
|
||||
//let transform = CATransform3DTranslate(CATransform3DIdentity, 0.0, 0.0, availableSize.width * 0.5)
|
||||
let sideAngle: CGFloat = 2.0*atan(sideAngle_a / sideAngle_b)
|
||||
|
||||
/*let perspectiveDistance: CGFloat = 500.0
|
||||
transform.m34 = -1.0 / perspectiveDistance
|
||||
let cubeSize: CGFloat = availableSize.width
|
||||
let faceTransform = CATransform3DMakeTranslation(0, 0, itemFrame.width * 0.5)
|
||||
|
||||
// Translate the cube to its original position.
|
||||
let halfSize = cubeSize / 2.0
|
||||
let translationTransform = CATransform3DMakeTranslation(0, 0, halfSize)
|
||||
func calculateCubeTransform(rotationFraction: CGFloat, sideAngle: CGFloat, cubeSize: CGSize) -> CATransform3D {
|
||||
let t = rotationFraction
|
||||
let absT = abs(rotationFraction)
|
||||
let currentAngle = t * (CGFloat.pi * 0.5 + sideAngle)
|
||||
let width = cubeSize.width
|
||||
|
||||
let cubeDistance_a: CGFloat = -1.4142135623731*absT*cos(sideAngle + 0.785398163397448)
|
||||
let cubeDistance_b: CGFloat = sin(sideAngle*absT + 1.5707963267949*absT + 0.785398163397448)
|
||||
var cubeDistance: CGFloat = 0.5*width*(cubeDistance_a + absT + 1.4142135623731*cubeDistance_b - 1.0)
|
||||
cubeDistance *= 1.0
|
||||
|
||||
let backDistance_a = sqrt(pow(width, 2.0))
|
||||
let backDistance_b = tan(sideAngle) / 2.0
|
||||
let backDistance_c = sqrt(pow(width, 2.0))
|
||||
let backDistance_d = (2*cos(sideAngle))
|
||||
let backDistance: CGFloat = width / 2.0 + backDistance_a * backDistance_b - backDistance_c / backDistance_d
|
||||
|
||||
var perspective = CATransform3DIdentity
|
||||
perspective.m34 = -1 / perspectiveConstant
|
||||
let initialCubeTransform = CATransform3DTranslate(perspective, 0.0, 0.0, -cubeSize.width * 0.5)
|
||||
|
||||
var targetTransform = initialCubeTransform
|
||||
targetTransform = CATransform3DTranslate(targetTransform, 0.0, 0.0, -cubeDistance + backDistance)
|
||||
targetTransform = CATransform3DConcat(CATransform3DMakeRotation(currentAngle, 0, 1, 0), targetTransform)
|
||||
targetTransform = CATransform3DTranslate(targetTransform, 0.0, 0.0, -backDistance)
|
||||
|
||||
return targetTransform
|
||||
}
|
||||
|
||||
// Apply the translation transformation.
|
||||
transform = CATransform3DConcat(transform, translationTransform)*/
|
||||
//itemSetTransition.setTransform(view: itemSetComponentView, transform: transform)
|
||||
itemSetTransition.setTransform(view: itemSetView, transform: cubeTransform)*/
|
||||
let cubeAdditionalRotationFraction: CGFloat
|
||||
if i == focusedIndex {
|
||||
cubeAdditionalRotationFraction = 0.0
|
||||
} else if i < focusedIndex {
|
||||
cubeAdditionalRotationFraction = -1.0
|
||||
} else {
|
||||
cubeAdditionalRotationFraction = 1.0
|
||||
}
|
||||
|
||||
var panFraction: CGFloat = 0.0
|
||||
if let itemSetPanState = self.itemSetPanState {
|
||||
panFraction = -itemSetPanState.fraction
|
||||
}
|
||||
|
||||
Transition.immediate.setTransform(view: itemSetComponentView, transform: faceTransform)
|
||||
Transition.immediate.setTransform(layer: itemSetView.tintLayer, transform: faceTransform)
|
||||
|
||||
if let previousRotationFraction = itemSetView.rotationFraction, "".isEmpty {
|
||||
let fromT = previousRotationFraction
|
||||
let toT = panFraction
|
||||
itemSetTransition.setTransformAsKeyframes(view: itemSetView, transform: { sourceT in
|
||||
let t = fromT * (1.0 - sourceT) + toT * sourceT
|
||||
|
||||
return calculateCubeTransform(rotationFraction: t + cubeAdditionalRotationFraction, sideAngle: sideAngle, cubeSize: itemFrame.size)
|
||||
})
|
||||
} else {
|
||||
itemSetTransition.setTransform(view: itemSetView, transform: calculateCubeTransform(rotationFraction: panFraction + cubeAdditionalRotationFraction, sideAngle: sideAngle, cubeSize: itemFrame.size))
|
||||
}
|
||||
itemSetView.rotationFraction = panFraction
|
||||
|
||||
var alphaFraction = panFraction + cubeAdditionalRotationFraction
|
||||
|
||||
if alphaFraction != 0.0 {
|
||||
if alphaFraction < 0.0 {
|
||||
itemSetView.tintLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
|
||||
itemSetView.tintLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
|
||||
} else {
|
||||
itemSetView.tintLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
|
||||
itemSetView.tintLayer.endPoint = CGPoint(x: 0.0, y: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
alphaFraction *= 1.3
|
||||
alphaFraction = max(-1.0, min(1.0, alphaFraction))
|
||||
alphaFraction = abs(alphaFraction)
|
||||
|
||||
itemSetTransition.setAlpha(layer: itemSetView.tintLayer, alpha: alphaFraction)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2235,20 +2460,38 @@ private final class StoryContainerScreenComponent: Component {
|
||||
}
|
||||
|
||||
public class StoryContainerScreen: ViewControllerComponentContainer {
|
||||
public final class TransitionIn {
|
||||
public weak var sourceView: UIView?
|
||||
public let sourceRect: CGRect
|
||||
public let sourceCornerRadius: CGFloat
|
||||
|
||||
public init(
|
||||
sourceView: UIView,
|
||||
sourceRect: CGRect,
|
||||
sourceCornerRadius: CGFloat
|
||||
) {
|
||||
self.sourceView = sourceView
|
||||
self.sourceRect = sourceRect
|
||||
self.sourceCornerRadius = sourceCornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private var isDismissed: Bool = false
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
initialFocusedId: AnyHashable?,
|
||||
initialContent: [StoryContentItemSlice]
|
||||
initialContent: [StoryContentItemSlice],
|
||||
transitionIn: TransitionIn?
|
||||
) {
|
||||
self.context = context
|
||||
|
||||
super.init(context: context, component: StoryContainerScreenComponent(
|
||||
context: context,
|
||||
initialFocusedId: initialFocusedId,
|
||||
initialContent: initialContent
|
||||
initialContent: initialContent,
|
||||
transitionIn: transitionIn
|
||||
), navigationBarAppearance: .none, theme: .dark)
|
||||
|
||||
self.statusBar.statusBarStyle = .White
|
||||
|
@ -42,8 +42,10 @@ public final class StoryContentItem {
|
||||
public let centerInfoComponent: AnyComponent<Empty>?
|
||||
public let rightInfoComponent: AnyComponent<Empty>?
|
||||
public let targetMessageId: EngineMessage.Id?
|
||||
public let storyItem: StoryListContext.Item?
|
||||
public let preload: Signal<Never, NoError>?
|
||||
public let delete: (() -> Void)?
|
||||
public let markAsSeen: (() -> Void)?
|
||||
public let hasLike: Bool
|
||||
public let isMy: Bool
|
||||
|
||||
@ -54,8 +56,10 @@ public final class StoryContentItem {
|
||||
centerInfoComponent: AnyComponent<Empty>?,
|
||||
rightInfoComponent: AnyComponent<Empty>?,
|
||||
targetMessageId: EngineMessage.Id?,
|
||||
storyItem: StoryListContext.Item?,
|
||||
preload: Signal<Never, NoError>?,
|
||||
delete: (() -> Void)?,
|
||||
markAsSeen: (() -> Void)?,
|
||||
hasLike: Bool,
|
||||
isMy: Bool
|
||||
) {
|
||||
@ -65,8 +69,10 @@ public final class StoryContentItem {
|
||||
self.centerInfoComponent = centerInfoComponent
|
||||
self.rightInfoComponent = rightInfoComponent
|
||||
self.targetMessageId = targetMessageId
|
||||
self.storyItem = storyItem
|
||||
self.preload = preload
|
||||
self.delete = delete
|
||||
self.markAsSeen = markAsSeen
|
||||
self.hasLike = hasLike
|
||||
self.isMy = isMy
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
public let isProgressPaused: Bool
|
||||
public let audioRecorder: ManagedAudioRecorder?
|
||||
public let videoRecorder: InstantVideoController?
|
||||
public let hideUI: Bool
|
||||
public let presentController: (ViewController) -> Void
|
||||
public let close: () -> Void
|
||||
public let navigateToItemSet: (NavigationDirection) -> Void
|
||||
@ -56,6 +57,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
isProgressPaused: Bool,
|
||||
audioRecorder: ManagedAudioRecorder?,
|
||||
videoRecorder: InstantVideoController?,
|
||||
hideUI: Bool,
|
||||
presentController: @escaping (ViewController) -> Void,
|
||||
close: @escaping () -> Void,
|
||||
navigateToItemSet: @escaping (NavigationDirection) -> Void,
|
||||
@ -72,6 +74,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.isProgressPaused = isProgressPaused
|
||||
self.audioRecorder = audioRecorder
|
||||
self.videoRecorder = videoRecorder
|
||||
self.hideUI = hideUI
|
||||
self.presentController = presentController
|
||||
self.close = close
|
||||
self.navigateToItemSet = navigateToItemSet
|
||||
@ -109,6 +112,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
if lhs.videoRecorder !== rhs.videoRecorder {
|
||||
return false
|
||||
}
|
||||
if lhs.hideUI != rhs.hideUI {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -149,7 +155,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView, UIScrollViewDelegate {
|
||||
public final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||
private let scrollView: ScrollView
|
||||
|
||||
private let contentContainerView: UIView
|
||||
@ -205,8 +211,6 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = .black
|
||||
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.clipsToBounds = false
|
||||
@ -225,17 +229,17 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.scrollView.clipsToBounds = true
|
||||
|
||||
self.addSubview(self.contentContainerView)
|
||||
self.layer.addSublayer(self.contentDimLayer)
|
||||
self.layer.addSublayer(self.topContentGradientLayer)
|
||||
self.contentContainerView.layer.addSublayer(self.contentDimLayer)
|
||||
self.contentContainerView.layer.addSublayer(self.topContentGradientLayer)
|
||||
self.layer.addSublayer(self.bottomContentGradientLayer)
|
||||
|
||||
self.closeButton.addSubview(self.closeButtonIconView)
|
||||
self.addSubview(self.closeButton)
|
||||
self.contentContainerView.addSubview(self.closeButton)
|
||||
self.closeButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside)
|
||||
|
||||
self.contentContainerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
|
||||
|
||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||
tapRecognizer.delegate = self
|
||||
self.contentContainerView.addGestureRecognizer(tapRecognizer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -246,6 +250,13 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.currentSliceDisposable?.dispose()
|
||||
}
|
||||
|
||||
@objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if otherGestureRecognizer is UIPanGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state, let currentSlice = self.currentSlice, let focusedItemId = self.focusedItemId, let currentIndex = currentSlice.items.firstIndex(where: { $0.id == focusedItemId }), let itemLayout = self.itemLayout {
|
||||
if hasFirstResponder(self) {
|
||||
@ -267,6 +278,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
if nextIndex != currentIndex {
|
||||
let focusedItemId = currentSlice.items[nextIndex].id
|
||||
self.focusedItemId = focusedItemId
|
||||
|
||||
currentSlice.items[nextIndex].markAsSeen?()
|
||||
|
||||
self.state?.updated(transition: .immediate)
|
||||
|
||||
self.currentSliceDisposable?.dispose()
|
||||
@ -358,6 +372,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
if nextIndex != currentIndex {
|
||||
let focusedItemId = currentSlice.items[nextIndex].id
|
||||
self.focusedItemId = focusedItemId
|
||||
|
||||
currentSlice.items[nextIndex].markAsSeen?()
|
||||
|
||||
self.state?.updated(transition: .immediate)
|
||||
|
||||
self.currentSliceDisposable?.dispose()
|
||||
@ -385,7 +402,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
if let view = visibleItem.view.view {
|
||||
if view.superview == nil {
|
||||
view.isUserInteractionEnabled = false
|
||||
self.contentContainerView.addSubview(view)
|
||||
self.contentContainerView.insertSubview(view, at: 0)
|
||||
}
|
||||
itemTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(), size: itemLayout.size))
|
||||
|
||||
@ -422,6 +439,69 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn(transitionIn: StoryContainerScreen.TransitionIn) {
|
||||
self.closeButton.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2, delay: 0.12, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
if let inputPanelView = self.inputPanel.view {
|
||||
inputPanelView.layer.animatePosition(
|
||||
from: CGPoint(x: 0.0, y: self.bounds.height - inputPanelView.frame.minY),
|
||||
to: CGPoint(),
|
||||
duration: 0.48,
|
||||
timingFunction: kCAMediaTimingFunctionSpring,
|
||||
additive: true
|
||||
)
|
||||
inputPanelView.layer.animateAlpha(from: 0.0, to: inputPanelView.alpha, duration: 0.28)
|
||||
}
|
||||
if let footerPanelView = self.footerPanel.view {
|
||||
footerPanelView.layer.animatePosition(
|
||||
from: CGPoint(x: 0.0, y: self.bounds.height - footerPanelView.frame.minY),
|
||||
to: CGPoint(),
|
||||
duration: 0.3,
|
||||
timingFunction: kCAMediaTimingFunctionSpring,
|
||||
additive: true
|
||||
)
|
||||
footerPanelView.layer.animateAlpha(from: 0.0, to: footerPanelView.alpha, duration: 0.28)
|
||||
}
|
||||
|
||||
if let sourceView = transitionIn.sourceView {
|
||||
let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self)
|
||||
let innerSourceLocalFrame = CGRect(origin: CGPoint(x: sourceLocalFrame.minX - self.contentContainerView.frame.minX, y: sourceLocalFrame.minY - self.contentContainerView.frame.minY), size: sourceLocalFrame.size)
|
||||
|
||||
if let rightInfoView = self.rightInfoItem?.view.view {
|
||||
rightInfoView.layer.animatePosition(from: CGPoint(x: innerSourceLocalFrame.center.x - rightInfoView.layer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
rightInfoView.layer.animatePosition(from: CGPoint(x: 0.0, y: innerSourceLocalFrame.center.y - rightInfoView.layer.position.y), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
|
||||
rightInfoView.layer.animateScale(from: innerSourceLocalFrame.width / rightInfoView.bounds.width, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
self.contentContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.contentContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.contentContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), to: self.contentContainerView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.contentContainerView.layer.animate(
|
||||
from: transitionIn.sourceCornerRadius as NSNumber,
|
||||
to: self.contentContainerView.layer.cornerRadius as NSNumber,
|
||||
keyPath: "cornerRadius",
|
||||
timingFunction: kCAMediaTimingFunctionSpring,
|
||||
duration: 0.3
|
||||
)
|
||||
|
||||
if let focusedItemId = self.focusedItemId, let visibleItemView = self.visibleItems[focusedItemId]?.view.view {
|
||||
let innerScale = innerSourceLocalFrame.width / visibleItemView.bounds.width
|
||||
let innerFromFrame = CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: CGSize(width: innerSourceLocalFrame.width, height: visibleItemView.bounds.height * innerScale))
|
||||
|
||||
visibleItemView.layer.animatePosition(
|
||||
from: CGPoint(
|
||||
x: innerFromFrame.midX,
|
||||
y: innerFromFrame.midY
|
||||
),
|
||||
to: visibleItemView.layer.position,
|
||||
duration: 0.3,
|
||||
timingFunction: kCAMediaTimingFunctionSpring
|
||||
)
|
||||
visibleItemView.layer.animateScale(from: innerScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: StoryItemSetContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let isFirstTime = self.component == nil
|
||||
|
||||
@ -431,6 +511,10 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
self.currentSliceDisposable?.dispose()
|
||||
if let focusedItemId = self.focusedItemId {
|
||||
if let item = self.currentSlice?.items.first(where: { $0.id == focusedItemId }) {
|
||||
item.markAsSeen?()
|
||||
}
|
||||
|
||||
self.currentSliceDisposable = (component.initialItemSlice.update(
|
||||
component.initialItemSlice,
|
||||
focusedItemId
|
||||
@ -490,6 +574,8 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
if let currentSlice = self.currentSlice {
|
||||
if !currentSlice.items.contains(where: { $0.id == focusedItemId }) {
|
||||
self.focusedItemId = currentSlice.items.first?.id
|
||||
|
||||
currentSlice.items.first?.markAsSeen?()
|
||||
}
|
||||
} else {
|
||||
self.focusedItemId = nil
|
||||
@ -572,9 +658,16 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
containerSize: CGSize(width: availableSize.width, height: 200.0)
|
||||
)
|
||||
|
||||
var currentItem: StoryContentItem?
|
||||
if let focusedItemId = self.focusedItemId, let currentSlice = self.currentSlice, let item = currentSlice.items.first(where: { $0.id == focusedItemId }) {
|
||||
currentItem = item
|
||||
}
|
||||
|
||||
let footerPanelSize = self.footerPanel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(StoryFooterPanelComponent(
|
||||
context: component.context,
|
||||
storyItem: currentItem?.storyItem,
|
||||
deleteAction: { [weak self] in
|
||||
guard let self, let component = self.component, let focusedItemId = self.focusedItemId else {
|
||||
return
|
||||
@ -604,6 +697,8 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
self.focusedItemId = currentSlice.items[nextIndex].id
|
||||
|
||||
currentSlice.items[nextIndex].markAsSeen?()
|
||||
|
||||
/*var updatedItems: [StoryContentItem] = []
|
||||
for item in currentSlice.items {
|
||||
if item.id != focusedItemId {
|
||||
@ -744,9 +839,10 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.closeButtonIconView.tintColor = .white
|
||||
}
|
||||
if let image = self.closeButtonIconView.image {
|
||||
let closeButtonFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: 50.0, height: 64.0))
|
||||
let closeButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 50.0, height: 64.0))
|
||||
transition.setFrame(view: self.closeButton, frame: closeButtonFrame)
|
||||
transition.setFrame(view: self.closeButtonIconView, frame: CGRect(origin: CGPoint(x: floor((closeButtonFrame.width - image.size.width) * 0.5), y: floor((closeButtonFrame.height - image.size.height) * 0.5)), size: image.size))
|
||||
transition.setAlpha(view: self.closeButton, alpha: component.hideUI ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
var focusedItem: StoryContentItem?
|
||||
@ -808,15 +904,17 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
if let view = currentRightInfoItem.view.view {
|
||||
var animateIn = false
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
self.contentContainerView.addSubview(view)
|
||||
animateIn = true
|
||||
}
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: contentFrame.maxX - 6.0 - rightInfoItemSize.width, y: contentFrame.minY + 14.0), size: rightInfoItemSize))
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: contentFrame.width - 6.0 - rightInfoItemSize.width, y: 14.0), size: rightInfoItemSize))
|
||||
|
||||
if animateIn, !isFirstTime {
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
view.layer.animateScale(from: 0.5, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
transition.setAlpha(view: view, alpha: component.hideUI ? 0.0 : 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -833,19 +931,22 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
var animateIn = false
|
||||
if view.superview == nil {
|
||||
view.isUserInteractionEnabled = false
|
||||
self.addSubview(view)
|
||||
self.contentContainerView.addSubview(view)
|
||||
animateIn = true
|
||||
}
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY + 10.0), size: centerInfoItemSize))
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: 0.0, y: 10.0), size: centerInfoItemSize))
|
||||
|
||||
if animateIn, !isFirstTime {
|
||||
//view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
transition.setAlpha(view: view, alpha: component.hideUI ? 0.0 : 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
let gradientHeight: CGFloat = 74.0
|
||||
transition.setFrame(layer: self.topContentGradientLayer, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: gradientHeight)))
|
||||
transition.setFrame(layer: self.topContentGradientLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentFrame.width, height: gradientHeight)))
|
||||
transition.setAlpha(layer: self.topContentGradientLayer, alpha: component.hideUI ? 0.0 : 1.0)
|
||||
|
||||
let itemLayout = ItemLayout(size: CGSize(width: contentFrame.width, height: availableSize.height - component.containerInsets.top - 44.0 - bottomContentInsetWithoutInput))
|
||||
self.itemLayout = itemLayout
|
||||
@ -971,7 +1072,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
transition.setFrame(layer: self.bottomContentGradientLayer, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: availableSize.height - component.inputHeight - bottomGradientHeight), size: CGSize(width: contentFrame.width, height: bottomGradientHeight)))
|
||||
transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: inputPanelIsOverlay ? 1.0 : 0.0)
|
||||
|
||||
transition.setFrame(layer: self.contentDimLayer, frame: contentFrame)
|
||||
transition.setFrame(layer: self.contentDimLayer, frame: CGRect(origin: CGPoint(), size: contentFrame.size))
|
||||
transition.setAlpha(layer: self.contentDimLayer, alpha: (inputPanelIsOverlay || self.inputPanelExternalState.isEditing) ? 1.0 : 0.0)
|
||||
|
||||
self.ignoreScrolling = true
|
||||
@ -1005,9 +1106,10 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
if let navigationStripView = self.navigationStrip.view {
|
||||
if navigationStripView.superview == nil {
|
||||
navigationStripView.isUserInteractionEnabled = false
|
||||
self.addSubview(navigationStripView)
|
||||
self.contentContainerView.addSubview(navigationStripView)
|
||||
}
|
||||
transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: contentFrame.minX + navigationStripSideInset, y: contentFrame.minY + navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0)))
|
||||
transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: navigationStripSideInset, y: navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0)))
|
||||
transition.setAlpha(view: navigationStripView, alpha: component.hideUI ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
if let focusedItemId = self.focusedItemId, let focusedItem = self.currentSlice?.items.first(where: { $0.id == focusedItemId }) {
|
||||
@ -1041,9 +1143,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
)
|
||||
if let inlineActionsView = self.inlineActions.view {
|
||||
if inlineActionsView.superview == nil {
|
||||
self.addSubview(inlineActionsView)
|
||||
self.contentContainerView.addSubview(inlineActionsView)
|
||||
}
|
||||
transition.setFrame(view: inlineActionsView, frame: CGRect(origin: CGPoint(x: contentFrame.maxX - 10.0 - inlineActionsSize.width, y: contentFrame.maxY - 20.0 - inlineActionsSize.height), size: inlineActionsSize))
|
||||
transition.setFrame(view: inlineActionsView, frame: CGRect(origin: CGPoint(x: contentFrame.width - 10.0 - inlineActionsSize.width, y: contentFrame.height - 20.0 - inlineActionsSize.height), size: inlineActionsSize))
|
||||
|
||||
var inlineActionsAlpha: CGFloat = inputPanelIsOverlay ? 0.0 : 1.0
|
||||
if component.audioRecorder != nil || component.videoRecorder != nil {
|
||||
@ -1052,6 +1154,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
if self.reactionItems != nil {
|
||||
inlineActionsAlpha = 0.0
|
||||
}
|
||||
if component.hideUI {
|
||||
inlineActionsAlpha = 0.0
|
||||
}
|
||||
|
||||
transition.setAlpha(view: inlineActionsView, alpha: inlineActionsAlpha)
|
||||
}
|
||||
|
@ -54,7 +54,8 @@ final class StoryAvatarInfoComponent: Component {
|
||||
self.avatarNode.setPeer(
|
||||
context: component.context,
|
||||
theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme,
|
||||
peer: component.peer
|
||||
peer: component.peer,
|
||||
synchronousLoad: true
|
||||
)
|
||||
|
||||
return size
|
||||
|
@ -8,89 +8,6 @@ import TelegramCore
|
||||
import StoryContainerScreen
|
||||
|
||||
public enum StoryChatContent {
|
||||
/*public static func messages(
|
||||
context: AccountContext,
|
||||
messageId: EngineMessage.Id
|
||||
) -> Signal<StoryContentItemSlice, NoError> {
|
||||
return context.account.postbox.aroundIdMessageHistoryViewForLocation(
|
||||
.peer(peerId: messageId.peerId, threadId: nil),
|
||||
ignoreMessagesInTimestampRange: nil,
|
||||
count: 10,
|
||||
messageId: messageId,
|
||||
topTaggedMessageIdNamespaces: Set(),
|
||||
tagMask: .photoOrVideo,
|
||||
appendMessagesFromTheSameGroup: false,
|
||||
namespaces: .not(Set([Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal])),
|
||||
orderStatistics: .combinedLocation
|
||||
)
|
||||
|> map { view -> StoryContentItemSlice in
|
||||
var items: [StoryContentItem] = []
|
||||
var totalCount = 0
|
||||
for entry in view.0.entries {
|
||||
if let location = entry.location {
|
||||
totalCount = location.count
|
||||
}
|
||||
|
||||
var hasLike = false
|
||||
if let reactions = entry.message.effectiveReactions {
|
||||
for reaction in reactions {
|
||||
if !reaction.isSelected {
|
||||
continue
|
||||
}
|
||||
if reaction.value == .builtin("❤") {
|
||||
hasLike = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var preload: Signal<Never, NoError>?
|
||||
preload = StoryMessageContentComponent.preload(context: context, message: EngineMessage(entry.message))
|
||||
|
||||
items.append(StoryContentItem(
|
||||
id: AnyHashable(entry.message.id),
|
||||
position: entry.location?.index ?? 0,
|
||||
component: AnyComponent(StoryMessageContentComponent(
|
||||
context: context,
|
||||
message: EngineMessage(entry.message)
|
||||
)),
|
||||
centerInfoComponent: AnyComponent(StoryAuthorInfoComponent(
|
||||
context: context,
|
||||
message: EngineMessage(entry.message)
|
||||
)),
|
||||
rightInfoComponent: entry.message.author.flatMap { author -> AnyComponent<Empty> in
|
||||
return AnyComponent(StoryAvatarInfoComponent(
|
||||
context: context,
|
||||
peer: EnginePeer(author)
|
||||
))
|
||||
},
|
||||
targetMessageId: entry.message.id,
|
||||
preload: preload,
|
||||
hasLike: hasLike,
|
||||
isMy: false//!entry.message.effectivelyIncoming(context.account.peerId)
|
||||
))
|
||||
}
|
||||
return StoryContentItemSlice(
|
||||
id: AnyHashable(entry.)
|
||||
focusedItemId: AnyHashable(messageId),
|
||||
items: items,
|
||||
totalCount: totalCount,
|
||||
update: { _, itemId in
|
||||
if let id = itemId.base as? EngineMessage.Id {
|
||||
return StoryChatContent.messages(
|
||||
context: context,
|
||||
messageId: id
|
||||
)
|
||||
} else {
|
||||
return StoryChatContent.messages(
|
||||
context: context,
|
||||
messageId: messageId
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}*/
|
||||
|
||||
public static func stories(context: AccountContext, storyList: StoryListContext, focusItem: Int64?) -> Signal<[StoryContentItemSlice], NoError> {
|
||||
return storyList.state
|
||||
|> map { state -> [StoryContentItemSlice] in
|
||||
@ -99,6 +16,8 @@ public enum StoryChatContent {
|
||||
for itemSet in state.itemSets {
|
||||
var items: [StoryContentItem] = []
|
||||
|
||||
let peerId = itemSet.peerId
|
||||
|
||||
for item in itemSet.items {
|
||||
items.append(StoryContentItem(
|
||||
id: AnyHashable(item.id),
|
||||
@ -119,10 +38,17 @@ public enum StoryChatContent {
|
||||
))
|
||||
},
|
||||
targetMessageId: nil,
|
||||
storyItem: item,
|
||||
preload: nil,
|
||||
delete: { [weak storyList] in
|
||||
storyList?.delete(id: item.id)
|
||||
},
|
||||
markAsSeen: { [weak context] in
|
||||
guard let context else {
|
||||
return
|
||||
}
|
||||
let _ = context.engine.messages.markStoryAsSeen(peerId: peerId, id: item.id).start()
|
||||
},
|
||||
hasLike: false,
|
||||
isMy: itemSet.peerId == context.account.peerId
|
||||
))
|
||||
@ -131,6 +57,10 @@ public enum StoryChatContent {
|
||||
var sliceFocusedItemId: AnyHashable?
|
||||
if let focusItem, items.contains(where: { ($0.id.base as? Int64) == focusItem }) {
|
||||
sliceFocusedItemId = AnyHashable(focusItem)
|
||||
} else if itemSet.peerId != context.account.peerId {
|
||||
if let id = itemSet.items.first(where: { !$0.isSeen })?.id {
|
||||
sliceFocusedItemId = AnyHashable(id)
|
||||
}
|
||||
}
|
||||
|
||||
itemSlices.append(StoryContentItemSlice(
|
||||
|
@ -1,421 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import AsyncDisplayKit
|
||||
import PhotoResources
|
||||
import SwiftSignalKit
|
||||
import UniversalMediaPlayer
|
||||
import TelegramUniversalVideoContent
|
||||
import StoryContainerScreen
|
||||
import HierarchyTrackingLayer
|
||||
|
||||
final class StoryMessageContentComponent: Component {
|
||||
typealias EnvironmentType = StoryContentItem.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let message: EngineMessage
|
||||
|
||||
init(context: AccountContext, message: EngineMessage) {
|
||||
self.context = context
|
||||
self.message = message
|
||||
}
|
||||
|
||||
static func ==(lhs: StoryMessageContentComponent, rhs: StoryMessageContentComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.message != rhs.message {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static func preload(context: AccountContext, message: EngineMessage) -> Signal<Never, NoError> {
|
||||
var messageMedia: EngineMedia?
|
||||
for media in message.media {
|
||||
switch media {
|
||||
case let image as TelegramMediaImage:
|
||||
messageMedia = .image(image)
|
||||
case let file as TelegramMediaFile:
|
||||
messageMedia = .file(file)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
guard let messageMedia else {
|
||||
return .complete()
|
||||
}
|
||||
|
||||
var fetchSignal: Signal<Never, NoError>?
|
||||
switch messageMedia {
|
||||
case let .image(image):
|
||||
if let representation = image.representations.last {
|
||||
fetchSignal = fetchedMediaResource(
|
||||
mediaBox: context.account.postbox.mediaBox,
|
||||
userLocation: .peer(message.id.peerId),
|
||||
userContentType: .image,
|
||||
reference: ImageMediaReference.message(message: MessageReference(message._asMessage()), media: image).resourceReference(representation.resource)
|
||||
)
|
||||
|> ignoreValues
|
||||
|> `catch` { _ -> Signal<Never, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
case let .file(file):
|
||||
fetchSignal = fetchedMediaResource(
|
||||
mediaBox: context.account.postbox.mediaBox,
|
||||
userLocation: .peer(message.id.peerId),
|
||||
userContentType: .image,
|
||||
reference: FileMediaReference.message(message: MessageReference(message._asMessage()), media: file).resourceReference(file.resource)
|
||||
)
|
||||
|> ignoreValues
|
||||
|> `catch` { _ -> Signal<Never, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return fetchSignal ?? .complete()
|
||||
}
|
||||
|
||||
final class View: StoryContentItem.View {
|
||||
private let imageNode: TransformImageNode
|
||||
private var videoNode: UniversalVideoNode?
|
||||
|
||||
private var currentMessageMedia: EngineMedia?
|
||||
private var fetchDisposable: Disposable?
|
||||
|
||||
private var component: StoryMessageContentComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var environment: StoryContentItem.Environment?
|
||||
|
||||
private var isProgressPaused: Bool = false
|
||||
private var currentProgressTimer: SwiftSignalKit.Timer?
|
||||
private var currentProgressTimerValue: Double = 0.0
|
||||
private var videoProgressDisposable: Disposable?
|
||||
|
||||
private var videoPlaybackStatus: MediaPlayerStatus?
|
||||
|
||||
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
|
||||
self.imageNode = TransformImageNode()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.hierarchyTrackingLayer)
|
||||
|
||||
self.addSubnode(self.imageNode)
|
||||
|
||||
self.hierarchyTrackingLayer.isInHierarchyUpdated = { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateIsProgressPaused()
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.fetchDisposable?.dispose()
|
||||
self.currentProgressTimer?.invalidate()
|
||||
self.videoProgressDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func performActionAfterImageContentLoaded(update: Bool) {
|
||||
guard let component = self.component, let currentMessageMedia = self.currentMessageMedia else {
|
||||
return
|
||||
}
|
||||
|
||||
if case let .file(file) = currentMessageMedia {
|
||||
if self.videoNode == nil {
|
||||
let videoNode = UniversalVideoNode(
|
||||
postbox: component.context.account.postbox,
|
||||
audioSession: component.context.sharedContext.mediaManager.audioSession,
|
||||
manager: component.context.sharedContext.mediaManager.universalVideoManager,
|
||||
decoration: StoryVideoDecoration(),
|
||||
content: NativeVideoContent(
|
||||
id: .message(component.message.stableId, file.fileId),
|
||||
userLocation: .peer(component.message.id.peerId),
|
||||
fileReference: .message(message: MessageReference(component.message._asMessage()), media: file),
|
||||
imageReference: nil,
|
||||
loopVideo: true,
|
||||
enableSound: true,
|
||||
tempFilePath: nil,
|
||||
captureProtected: component.message._asMessage().isCopyProtected(),
|
||||
storeAfterDownload: nil
|
||||
),
|
||||
priority: .gallery
|
||||
)
|
||||
|
||||
self.videoNode = videoNode
|
||||
self.addSubnode(videoNode)
|
||||
|
||||
videoNode.ownsContentNodeUpdated = { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if value {
|
||||
self.videoNode?.seek(0.0)
|
||||
self.videoNode?.playOnceWithSound(playAndRecord: false)
|
||||
}
|
||||
}
|
||||
videoNode.canAttachContent = true
|
||||
if update {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func setIsProgressPaused(_ isProgressPaused: Bool) {
|
||||
if self.isProgressPaused != isProgressPaused {
|
||||
self.isProgressPaused = isProgressPaused
|
||||
self.updateIsProgressPaused()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateIsProgressPaused() {
|
||||
if let videoNode = self.videoNode {
|
||||
if !self.isProgressPaused && self.hierarchyTrackingLayer.isInHierarchy {
|
||||
videoNode.play()
|
||||
} else {
|
||||
videoNode.pause()
|
||||
}
|
||||
}
|
||||
|
||||
self.updateVideoPlaybackProgress()
|
||||
self.updateProgressTimer()
|
||||
}
|
||||
|
||||
private func updateProgressTimer() {
|
||||
let needsTimer = !self.isProgressPaused && self.hierarchyTrackingLayer.isInHierarchy
|
||||
|
||||
if needsTimer {
|
||||
if self.currentProgressTimer == nil {
|
||||
self.currentProgressTimer = SwiftSignalKit.Timer(
|
||||
timeout: 1.0 / 60.0,
|
||||
repeat: true,
|
||||
completion: { [weak self] in
|
||||
guard let self, !self.isProgressPaused, self.hierarchyTrackingLayer.isInHierarchy else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.videoNode != nil {
|
||||
self.updateVideoPlaybackProgress()
|
||||
} else {
|
||||
let currentProgressTimerLimit: Double = 5.0
|
||||
var currentProgressTimerValue = self.currentProgressTimerValue + 1.0 / 60.0
|
||||
currentProgressTimerValue = max(0.0, min(currentProgressTimerLimit, currentProgressTimerValue))
|
||||
self.currentProgressTimerValue = currentProgressTimerValue
|
||||
|
||||
self.environment?.presentationProgressUpdated(currentProgressTimerValue / currentProgressTimerLimit)
|
||||
}
|
||||
}, queue: .mainQueue()
|
||||
)
|
||||
self.currentProgressTimer?.start()
|
||||
}
|
||||
} else {
|
||||
if let currentProgressTimer = self.currentProgressTimer {
|
||||
self.currentProgressTimer = nil
|
||||
currentProgressTimer.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateVideoPlaybackProgress() {
|
||||
guard let videoPlaybackStatus = self.videoPlaybackStatus else {
|
||||
return
|
||||
}
|
||||
var isPlaying = false
|
||||
var timestampAndDuration: (timestamp: Double?, duration: Double)?
|
||||
switch videoPlaybackStatus.status {
|
||||
case .playing:
|
||||
isPlaying = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
if case .buffering(true, _, _, _) = videoPlaybackStatus.status {
|
||||
timestampAndDuration = (nil, videoPlaybackStatus.duration)
|
||||
} else if Double(0.0).isLess(than: videoPlaybackStatus.duration) {
|
||||
timestampAndDuration = (videoPlaybackStatus.timestamp, videoPlaybackStatus.duration)
|
||||
}
|
||||
|
||||
var currentProgress: Double = 0.0
|
||||
|
||||
if let (maybeTimestamp, duration) = timestampAndDuration, let timestamp = maybeTimestamp, duration > 0.01, let videoPlaybackStatus = self.videoPlaybackStatus {
|
||||
var actualTimestamp: Double
|
||||
if videoPlaybackStatus.generationTimestamp.isZero || !isPlaying {
|
||||
actualTimestamp = timestamp
|
||||
} else {
|
||||
let currentTimestamp = CACurrentMediaTime()
|
||||
actualTimestamp = timestamp + (currentTimestamp - videoPlaybackStatus.generationTimestamp) * videoPlaybackStatus.baseRate
|
||||
}
|
||||
|
||||
var progress = CGFloat(actualTimestamp / duration)
|
||||
if progress.isNaN || !progress.isFinite {
|
||||
progress = 0.0
|
||||
}
|
||||
progress = min(1.0, progress)
|
||||
|
||||
currentProgress = progress
|
||||
}
|
||||
|
||||
let clippedProgress = max(0.0, min(1.0, currentProgress))
|
||||
self.environment?.presentationProgressUpdated(clippedProgress)
|
||||
}
|
||||
|
||||
func update(component: StoryMessageContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StoryContentItem.Environment>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
self.environment = environment[StoryContentItem.Environment.self].value
|
||||
|
||||
var messageMedia: EngineMedia?
|
||||
for media in component.message.media {
|
||||
switch media {
|
||||
case let image as TelegramMediaImage:
|
||||
messageMedia = .image(image)
|
||||
case let file as TelegramMediaFile:
|
||||
messageMedia = .file(file)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var reloadMedia = false
|
||||
if self.currentMessageMedia?.id != messageMedia?.id {
|
||||
self.currentMessageMedia = messageMedia
|
||||
reloadMedia = true
|
||||
}
|
||||
|
||||
if reloadMedia, let messageMedia {
|
||||
var signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
||||
var fetchSignal: Signal<Never, NoError>?
|
||||
switch messageMedia {
|
||||
case let .image(image):
|
||||
signal = chatMessagePhoto(
|
||||
postbox: component.context.account.postbox,
|
||||
userLocation: .peer(component.message.id.peerId),
|
||||
photoReference: .message(message: MessageReference(component.message._asMessage()), media: image),
|
||||
synchronousLoad: true,
|
||||
highQuality: true
|
||||
)
|
||||
if let representation = image.representations.last {
|
||||
fetchSignal = fetchedMediaResource(
|
||||
mediaBox: component.context.account.postbox.mediaBox,
|
||||
userLocation: .peer(component.message.id.peerId),
|
||||
userContentType: .image,
|
||||
reference: ImageMediaReference.message(message: MessageReference(component.message._asMessage()), media: image).resourceReference(representation.resource)
|
||||
)
|
||||
|> ignoreValues
|
||||
|> `catch` { _ -> Signal<Never, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
case let .file(file):
|
||||
signal = chatMessageVideo(
|
||||
postbox: component.context.account.postbox,
|
||||
userLocation: .peer(component.message.id.peerId),
|
||||
videoReference: .message(message: MessageReference(component.message._asMessage()), media: file),
|
||||
synchronousLoad: true
|
||||
)
|
||||
fetchSignal = fetchedMediaResource(
|
||||
mediaBox: component.context.account.postbox.mediaBox,
|
||||
userLocation: .peer(component.message.id.peerId),
|
||||
userContentType: .image,
|
||||
reference: FileMediaReference.message(message: MessageReference(component.message._asMessage()), media: file).resourceReference(file.resource)
|
||||
)
|
||||
|> ignoreValues
|
||||
|> `catch` { _ -> Signal<Never, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if let signal {
|
||||
var wasSynchronous = true
|
||||
self.imageNode.setSignal(signal |> afterCompleted { [weak self] in
|
||||
Queue.mainQueue().async {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.performActionAfterImageContentLoaded(update: !wasSynchronous)
|
||||
}
|
||||
}, attemptSynchronously: true)
|
||||
wasSynchronous = false
|
||||
}
|
||||
|
||||
self.fetchDisposable?.dispose()
|
||||
self.fetchDisposable = nil
|
||||
if let fetchSignal {
|
||||
self.fetchDisposable = fetchSignal.start()
|
||||
}
|
||||
}
|
||||
|
||||
if let messageMedia {
|
||||
var dimensions: CGSize?
|
||||
switch messageMedia {
|
||||
case let .image(image):
|
||||
dimensions = image.representations.last?.dimensions.cgSize
|
||||
case let .file(file):
|
||||
dimensions = file.dimensions?.cgSize
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if let dimensions {
|
||||
let apply = self.imageNode.asyncLayout()(TransformImageArguments(
|
||||
corners: ImageCorners(),
|
||||
imageSize: dimensions.aspectFilled(availableSize),
|
||||
boundingSize: availableSize,
|
||||
intrinsicInsets: UIEdgeInsets()
|
||||
))
|
||||
apply()
|
||||
|
||||
if let videoNode = self.videoNode {
|
||||
let videoSize = dimensions.aspectFilled(availableSize)
|
||||
videoNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) * 0.5), y: floor((availableSize.height - videoSize.height) * 0.5)), size: videoSize)
|
||||
videoNode.updateLayout(size: videoSize, transition: .immediate)
|
||||
}
|
||||
}
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
}
|
||||
|
||||
if let videoNode = self.videoNode {
|
||||
if self.videoProgressDisposable == nil {
|
||||
self.videoProgressDisposable = (videoNode.status
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
guard let self, let status else {
|
||||
return
|
||||
}
|
||||
|
||||
self.videoPlaybackStatus = status
|
||||
self.updateVideoPlaybackProgress()
|
||||
})
|
||||
}
|
||||
}
|
||||
self.updateProgressTimer()
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StoryContentItem.Environment>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -15,6 +15,9 @@ swift_library(
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
|
||||
"//submodules/AnimatedAvatarSetNode",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/TelegramCore",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -5,20 +5,35 @@ import ComponentFlow
|
||||
import AppBundle
|
||||
import BundleIconComponent
|
||||
import ChatListHeaderComponent
|
||||
import AnimatedAvatarSetNode
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
|
||||
public final class StoryFooterPanelComponent: Component {
|
||||
public let context: AccountContext
|
||||
public let storyItem: StoryListContext.Item?
|
||||
public let deleteAction: () -> Void
|
||||
public let moreAction: (UIView, ContextGesture?) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
storyItem: StoryListContext.Item?,
|
||||
deleteAction: @escaping () -> Void,
|
||||
moreAction: @escaping (UIView, ContextGesture?) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.storyItem = storyItem
|
||||
self.deleteAction = deleteAction
|
||||
self.moreAction = moreAction
|
||||
}
|
||||
|
||||
public static func ==(lhs: StoryFooterPanelComponent, rhs: StoryFooterPanelComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.storyItem != rhs.storyItem {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -27,11 +42,19 @@ public final class StoryFooterPanelComponent: Component {
|
||||
private let deleteButton = ComponentView<Empty>()
|
||||
private var moreButton: MoreHeaderButton?
|
||||
|
||||
private let avatarsContext: AnimatedAvatarSetContext
|
||||
private let avatarsNode: AnimatedAvatarSetNode
|
||||
|
||||
private var component: StoryFooterPanelComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.avatarsContext = AnimatedAvatarSetContext()
|
||||
self.avatarsNode = AnimatedAvatarSetNode()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.avatarsNode.view)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -45,13 +68,40 @@ public final class StoryFooterPanelComponent: Component {
|
||||
let baseHeight: CGFloat = 44.0
|
||||
let size = CGSize(width: availableSize.width, height: baseHeight)
|
||||
|
||||
var leftOffset: CGFloat = 16.0
|
||||
|
||||
let avatarSpacing: CGFloat = 18.0
|
||||
|
||||
var peers: [EnginePeer] = []
|
||||
if let seenPeers = component.storyItem?.seenPeers {
|
||||
peers = Array(seenPeers.prefix(3))
|
||||
}
|
||||
let avatarsContent = self.avatarsContext.update(peers: peers, animated: false)
|
||||
let avatarsSize = self.avatarsNode.update(context: component.context, content: avatarsContent, itemSize: CGSize(width: 30.0, height: 30.0), animated: false, synchronousLoad: true)
|
||||
|
||||
let avatarsNodeFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - avatarsSize.height) * 0.5)), size: avatarsSize)
|
||||
self.avatarsNode.frame = avatarsNodeFrame
|
||||
if !avatarsSize.width.isZero {
|
||||
leftOffset = avatarsNodeFrame.maxX + avatarSpacing
|
||||
}
|
||||
|
||||
let viewsText: String
|
||||
if let storyItem = component.storyItem, storyItem.seenCount != 0 {
|
||||
if storyItem.seenCount == 1 {
|
||||
viewsText = "1 view"
|
||||
} else {
|
||||
viewsText = "\(storyItem.seenCount) views"
|
||||
}
|
||||
} else {
|
||||
viewsText = "No views yet"
|
||||
}
|
||||
let viewStatsTextSize = self.viewStatsText.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: "No views yet", font: Font.regular(15.0), color: .white)),
|
||||
component: AnyComponent(Text(text: viewsText, font: Font.regular(15.0), color: .white)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: size.height)
|
||||
)
|
||||
let viewStatsTextFrame = CGRect(origin: CGPoint(x: 16.0, y: floor((size.height - viewStatsTextSize.height) * 0.5)), size: viewStatsTextSize)
|
||||
let viewStatsTextFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - viewStatsTextSize.height) * 0.5)), size: viewStatsTextSize)
|
||||
if let viewStatsTextView = self.viewStatsText.view {
|
||||
if viewStatsTextView.superview == nil {
|
||||
viewStatsTextView.layer.anchorPoint = CGPoint()
|
||||
@ -85,7 +135,7 @@ public final class StoryFooterPanelComponent: Component {
|
||||
self.addSubview(deleteButtonView)
|
||||
}
|
||||
transition.setFrame(view: deleteButtonView, frame: CGRect(origin: CGPoint(x: rightContentOffset - deleteButtonSize.width, y: floor((size.height - deleteButtonSize.height) * 0.5)), size: deleteButtonSize))
|
||||
rightContentOffset -= deleteButtonSize.width - 8.0
|
||||
rightContentOffset -= deleteButtonSize.width + 8.0
|
||||
}
|
||||
|
||||
let moreButton: MoreHeaderButton
|
||||
|
@ -0,0 +1,26 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "StoryPeerListComponent",
|
||||
module_name = "StoryPeerListComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AvatarNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,259 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import AppBundle
|
||||
import BundleIconComponent
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
|
||||
public final class StoryPeerListComponent: Component {
|
||||
public let context: AccountContext
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let state: StoryListContext.State?
|
||||
public let peerAction: (EnginePeer) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
state: StoryListContext.State?,
|
||||
peerAction: @escaping (EnginePeer) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.state = state
|
||||
self.peerAction = peerAction
|
||||
}
|
||||
|
||||
public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.state != rhs.state {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private final class ScrollView: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private final class VisibleItem {
|
||||
let view = ComponentView<Empty>()
|
||||
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
private struct ItemLayout {
|
||||
let containerSize: CGSize
|
||||
let containerInsets: UIEdgeInsets
|
||||
let itemSize: CGSize
|
||||
let itemSpacing: CGFloat
|
||||
let itemCount: Int
|
||||
|
||||
let contentSize: CGSize
|
||||
|
||||
init(
|
||||
containerSize: CGSize,
|
||||
containerInsets: UIEdgeInsets,
|
||||
itemSize: CGSize,
|
||||
itemSpacing: CGFloat,
|
||||
itemCount: Int
|
||||
) {
|
||||
self.containerSize = containerSize
|
||||
self.containerInsets = containerInsets
|
||||
self.itemSize = itemSize
|
||||
self.itemSpacing = itemSpacing
|
||||
self.itemCount = itemCount
|
||||
|
||||
self.contentSize = CGSize(width: containerInsets.left + containerInsets.right + CGFloat(itemCount) * itemSize.width + CGFloat(max(0, itemCount - 1)) * itemSpacing, height: containerSize.height)
|
||||
}
|
||||
|
||||
func frame(at index: Int) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: self.containerInsets.left + (self.itemSize.width + self.itemSpacing) * CGFloat(index), y: self.containerInsets.top), size: self.itemSize)
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView, UIScrollViewDelegate {
|
||||
private let scrollView: ScrollView
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
private var sortedItemSets: [StoryListContext.PeerItemSet] = []
|
||||
private var visibleItems: [EnginePeer.Id: VisibleItem] = [:]
|
||||
|
||||
private var component: StoryPeerListComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.scrollView = ScrollView()
|
||||
self.scrollView.delaysContentTouches = false
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.alwaysBounceVertical = false
|
||||
self.scrollView.alwaysBounceHorizontal = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.scrollView.delegate = self
|
||||
self.addSubview(self.scrollView)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
public func transitionViewForItem(peerId: EnginePeer.Id) -> UIView? {
|
||||
if let visibleItem = self.visibleItems[peerId], let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View {
|
||||
return itemView.transitionView()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition) {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
var validIds: [EnginePeer.Id] = []
|
||||
for i in 0 ..< self.sortedItemSets.count {
|
||||
let itemSet = self.sortedItemSets[i]
|
||||
guard let peer = itemSet.peer else {
|
||||
continue
|
||||
}
|
||||
validIds.append(itemSet.peerId)
|
||||
|
||||
let visibleItem: VisibleItem
|
||||
var itemTransition = transition
|
||||
if let current = self.visibleItems[itemSet.peerId] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
itemTransition = .immediate
|
||||
visibleItem = VisibleItem()
|
||||
self.visibleItems[itemSet.peerId] = visibleItem
|
||||
}
|
||||
|
||||
var hasUnseen = false
|
||||
if peer.id != component.context.account.peerId {
|
||||
for item in itemSet.items {
|
||||
if !item.isSeen {
|
||||
hasUnseen = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = visibleItem.view.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(StoryPeerListItemComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
peer: peer,
|
||||
hasUnseen: hasUnseen,
|
||||
action: component.peerAction
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: itemLayout.itemSize
|
||||
)
|
||||
|
||||
let itemFrame = itemLayout.frame(at: i)
|
||||
|
||||
if let itemView = visibleItem.view.view {
|
||||
if itemView.superview == nil {
|
||||
self.scrollView.addSubview(itemView)
|
||||
}
|
||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||
}
|
||||
}
|
||||
|
||||
var removedIds: [EnginePeer.Id] = []
|
||||
for (id, visibleItem) in self.visibleItems {
|
||||
if !validIds.contains(id) {
|
||||
removedIds.append(id)
|
||||
if let itemView = visibleItem.view.view {
|
||||
itemView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removedIds {
|
||||
self.visibleItems.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
func update(component: StoryPeerListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
self.sortedItemSets.removeAll(keepingCapacity: true)
|
||||
if let state = component.state {
|
||||
if let myIndex = state.itemSets.firstIndex(where: { $0.peerId == component.context.account.peerId }) {
|
||||
self.sortedItemSets.append(state.itemSets[myIndex])
|
||||
}
|
||||
for itemSet in state.itemSets {
|
||||
if itemSet.peerId == component.context.account.peerId {
|
||||
continue
|
||||
}
|
||||
self.sortedItemSets.append(itemSet)
|
||||
}
|
||||
}
|
||||
|
||||
let itemLayout = ItemLayout(
|
||||
containerSize: availableSize,
|
||||
containerInsets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0),
|
||||
itemSize: CGSize(width: 60.0, height: 77.0),
|
||||
itemSpacing: 24.0,
|
||||
itemCount: self.sortedItemSets.count
|
||||
)
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
self.ignoreScrolling = true
|
||||
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
if self.scrollView.contentSize != itemLayout.contentSize {
|
||||
self.scrollView.contentSize = itemLayout.contentSize
|
||||
}
|
||||
|
||||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import AppBundle
|
||||
import BundleIconComponent
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AvatarNode
|
||||
|
||||
public final class StoryPeerListItemComponent: Component {
|
||||
public let context: AccountContext
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let peer: EnginePeer
|
||||
public let hasUnseen: Bool
|
||||
public let action: (EnginePeer) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
peer: EnginePeer,
|
||||
hasUnseen: Bool,
|
||||
action: @escaping (EnginePeer) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.peer = peer
|
||||
self.hasUnseen = hasUnseen
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public static func ==(lhs: StoryPeerListItemComponent, rhs: StoryPeerListItemComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.hasUnseen != rhs.hasUnseen {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: HighlightTrackingButton {
|
||||
private var avatarNode: AvatarNode?
|
||||
private let indicatorCircleView: UIImageView
|
||||
private let title = ComponentView<Empty>()
|
||||
|
||||
private var component: StoryPeerListItemComponent?
|
||||
private weak var componentState: EmptyComponentState?
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.indicatorCircleView = UIImageView()
|
||||
self.indicatorCircleView.isUserInteractionEnabled = false
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.indicatorCircleView)
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if highlighted {
|
||||
self.alpha = 0.7
|
||||
} else {
|
||||
let previousAlpha = self.alpha
|
||||
self.alpha = 1.0
|
||||
self.layer.animateAlpha(from: previousAlpha, to: self.alpha, duration: 0.25)
|
||||
}
|
||||
}
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.action(component.peer)
|
||||
}
|
||||
|
||||
public func transitionView() -> UIView? {
|
||||
return self.avatarNode?.view
|
||||
}
|
||||
|
||||
func update(component: StoryPeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let hadUnseen = self.component?.hasUnseen ?? false
|
||||
|
||||
self.component = component
|
||||
self.componentState = state
|
||||
|
||||
let avatarNode: AvatarNode
|
||||
if let current = self.avatarNode {
|
||||
avatarNode = current
|
||||
} else {
|
||||
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
|
||||
self.avatarNode = avatarNode
|
||||
avatarNode.isUserInteractionEnabled = false
|
||||
self.addSubview(avatarNode.view)
|
||||
}
|
||||
|
||||
let avatarSize = CGSize(width: 52.0, height: 52.0)
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - avatarSize.width) * 0.5), y: 4.0), size: avatarSize)
|
||||
let indicatorFrame = avatarFrame.insetBy(dx: -4.0, dy: -4.0)
|
||||
|
||||
avatarNode.setPeer(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
peer: component.peer
|
||||
)
|
||||
avatarNode.updateSize(size: avatarSize)
|
||||
transition.setFrame(view: avatarNode.view, frame: avatarFrame)
|
||||
|
||||
if component.peer.id == component.context.account.peerId && !component.hasUnseen {
|
||||
self.indicatorCircleView.image = nil
|
||||
} else if self.indicatorCircleView.image == nil || hadUnseen != component.hasUnseen {
|
||||
self.indicatorCircleView.image = generateImage(indicatorFrame.size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let lineWidth: CGFloat = 2.0
|
||||
context.setLineWidth(lineWidth)
|
||||
context.addEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5))
|
||||
context.replacePathWithStrokedPath()
|
||||
context.clip()
|
||||
|
||||
var locations: [CGFloat] = [1.0, 0.0]
|
||||
let colors: [CGColor]
|
||||
|
||||
if component.hasUnseen {
|
||||
colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor]
|
||||
} else {
|
||||
colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor]
|
||||
}
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
}
|
||||
transition.setFrame(view: self.indicatorCircleView, frame: indicatorFrame)
|
||||
|
||||
//TODO:localize
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: component.peer.id == component.context.account.peerId ? "My story" : component.peer.compactDisplayTitle, font: Font.regular(11.0), color: component.theme.list.itemPrimaryTextColor)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width + 4.0, height: 100.0)
|
||||
)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: indicatorFrame.maxY + 3.0), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.layer.anchorPoint = CGPoint()
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setPosition(view: titleView, position: titleFrame.origin)
|
||||
transition.setBounds(view: titleView, bounds: CGRect(origin: CGPoint(), size: titleFrame.size))
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user