[WIP] Stories

This commit is contained in:
Ali 2023-05-05 21:08:04 +04:00
parent 8ff2da5d00
commit 1ca5546adc
33 changed files with 1761 additions and 674 deletions

View File

@ -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";

View File

@ -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",

View File

@ -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?()

View File

@ -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)
}

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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:

View File

@ -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
}

View File

@ -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
}

View File

@ -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])

View File

@ -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() {

View File

@ -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
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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)?

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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(

View File

@ -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)
}
}

View File

@ -15,6 +15,9 @@ swift_library(
"//submodules/AppBundle",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
"//submodules/AnimatedAvatarSetNode",
"//submodules/AccountContext",
"//submodules/TelegramCore",
],
visibility = [
"//visibility:public",

View File

@ -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

View File

@ -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",
],
)

View File

@ -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)
}
}

View File

@ -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)
}
}