mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Message statistics
This commit is contained in:
parent
e28ec6b7ca
commit
c67feac3ce
@ -5710,3 +5710,8 @@ Any member of this group will be able to see messages in the channel.";
|
|||||||
"Cache.MaximumCacheSize" = "Maximum Cache Size";
|
"Cache.MaximumCacheSize" = "Maximum Cache Size";
|
||||||
"Cache.NoLimit" = "No Limit";
|
"Cache.NoLimit" = "No Limit";
|
||||||
"Cache.MaximumCacheSizeHelp" = "If your cache size exceeds this limit, the oldest media will be deleted.\n\nAll media will stay in the Telegram cloud and can be re-downloaded if you need it again.";
|
"Cache.MaximumCacheSizeHelp" = "If your cache size exceeds this limit, the oldest media will be deleted.\n\nAll media will stay in the Telegram cloud and can be re-downloaded if you need it again.";
|
||||||
|
|
||||||
|
"Stats.MessageTitle" = "Message Statistics";
|
||||||
|
"Stats.MessageOverview" = "Overview";
|
||||||
|
"Stats.MessageInteractionsTitle" = "Interactions";
|
||||||
|
"Stats.MessagePublicForwardsTitle" = "Public Shares";
|
||||||
|
@ -761,11 +761,6 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// items.append(ActionSheetButtonItem(title: self.presentationData.strings.ProfilePhoto_OpenInEditor, color: .accent, action: { [weak self] in
|
|
||||||
// dismissAction()
|
|
||||||
// self?.openEntryEdit(rawEntry)
|
|
||||||
// }))
|
|
||||||
|
|
||||||
let deleteTitle: String
|
let deleteTitle: String
|
||||||
if let _ = rawEntry.videoRepresentations.last {
|
if let _ = rawEntry.videoRepresentations.last {
|
||||||
deleteTitle = self.presentationData.strings.Settings_RemoveVideo
|
deleteTitle = self.presentationData.strings.Settings_RemoveVideo
|
||||||
|
@ -290,6 +290,8 @@ public final class ShareController: ViewController {
|
|||||||
private let presetText: String?
|
private let presetText: String?
|
||||||
private let switchableAccounts: [AccountWithInfo]
|
private let switchableAccounts: [AccountWithInfo]
|
||||||
private let immediatePeerId: PeerId?
|
private let immediatePeerId: PeerId?
|
||||||
|
private let openStats: (() -> Void)?
|
||||||
|
private let shares: Int?
|
||||||
|
|
||||||
private let peers = Promise<([(RenderedPeer, PeerPresence?)], Peer)>()
|
private let peers = Promise<([(RenderedPeer, PeerPresence?)], Peer)>()
|
||||||
private let peersDisposable = MetaDisposable()
|
private let peersDisposable = MetaDisposable()
|
||||||
@ -300,11 +302,11 @@ public final class ShareController: ViewController {
|
|||||||
|
|
||||||
public var dismissed: ((Bool) -> Void)?
|
public var dismissed: ((Bool) -> Void)?
|
||||||
|
|
||||||
public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) {
|
public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, openStats: (() -> Void)? = nil, shares: Int? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) {
|
||||||
self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, presetText: presetText, preferredAction: preferredAction, showInChat: showInChat, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId)
|
self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, presetText: presetText, preferredAction: preferredAction, showInChat: showInChat, openStats: openStats, shares: shares, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(sharedContext: SharedAccountContext, currentContext: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) {
|
public init(sharedContext: SharedAccountContext, currentContext: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, openStats: (() -> Void)? = nil, shares: Int? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) {
|
||||||
self.sharedContext = sharedContext
|
self.sharedContext = sharedContext
|
||||||
self.currentContext = currentContext
|
self.currentContext = currentContext
|
||||||
self.currentAccount = currentContext.account
|
self.currentAccount = currentContext.account
|
||||||
@ -314,6 +316,8 @@ public final class ShareController: ViewController {
|
|||||||
self.immediateExternalShare = immediateExternalShare
|
self.immediateExternalShare = immediateExternalShare
|
||||||
self.switchableAccounts = switchableAccounts
|
self.switchableAccounts = switchableAccounts
|
||||||
self.immediatePeerId = immediatePeerId
|
self.immediatePeerId = immediatePeerId
|
||||||
|
self.openStats = openStats
|
||||||
|
self.shares = shares
|
||||||
|
|
||||||
self.presentationData = self.sharedContext.currentPresentationData.with { $0 }
|
self.presentationData = self.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
@ -437,7 +441,7 @@ public final class ShareController: ViewController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||||
}, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId)
|
}, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, shares: self.shares)
|
||||||
self.controllerNode.dismiss = { [weak self] shared in
|
self.controllerNode.dismiss = { [weak self] shared in
|
||||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||||
self?.dismissed?(shared)
|
self?.dismissed?(shared)
|
||||||
@ -714,6 +718,11 @@ public final class ShareController: ViewController {
|
|||||||
strongSelf.view.endEditing(true)
|
strongSelf.view.endEditing(true)
|
||||||
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||||
}
|
}
|
||||||
|
if case .messages = self.subject, let openStats = self.openStats {
|
||||||
|
self.controllerNode.openStats = {
|
||||||
|
openStats()
|
||||||
|
}
|
||||||
|
}
|
||||||
self.displayNodeDidLoad()
|
self.displayNodeDidLoad()
|
||||||
|
|
||||||
self.peersDisposable.set((self.peers.get()
|
self.peersDisposable.set((self.peers.get()
|
||||||
|
@ -32,6 +32,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||||||
private let externalShare: Bool
|
private let externalShare: Bool
|
||||||
private let immediateExternalShare: Bool
|
private let immediateExternalShare: Bool
|
||||||
private var immediatePeerId: PeerId?
|
private var immediatePeerId: PeerId?
|
||||||
|
private let shares: Int?
|
||||||
|
|
||||||
private let defaultAction: ShareControllerAction?
|
private let defaultAction: ShareControllerAction?
|
||||||
private let requestLayout: (ContainedViewLayoutTransition) -> Void
|
private let requestLayout: (ContainedViewLayoutTransition) -> Void
|
||||||
@ -61,6 +62,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||||||
var share: ((String, [PeerId]) -> Signal<ShareState, NoError>)?
|
var share: ((String, [PeerId]) -> Signal<ShareState, NoError>)?
|
||||||
var shareExternal: (() -> Signal<ShareExternalState, NoError>)?
|
var shareExternal: (() -> Signal<ShareExternalState, NoError>)?
|
||||||
var switchToAnotherAccount: (() -> Void)?
|
var switchToAnotherAccount: (() -> Void)?
|
||||||
|
var openStats: (() -> Void)?
|
||||||
|
|
||||||
let ready = Promise<Bool>()
|
let ready = Promise<Bool>()
|
||||||
private var didSetReady = false
|
private var didSetReady = false
|
||||||
@ -78,12 +80,13 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||||||
|
|
||||||
private let presetText: String?
|
private let presetText: String?
|
||||||
|
|
||||||
init(sharedContext: SharedAccountContext, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?) {
|
init(sharedContext: SharedAccountContext, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, shares: Int?) {
|
||||||
self.sharedContext = sharedContext
|
self.sharedContext = sharedContext
|
||||||
self.presentationData = sharedContext.currentPresentationData.with { $0 }
|
self.presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||||
self.externalShare = externalShare
|
self.externalShare = externalShare
|
||||||
self.immediateExternalShare = immediateExternalShare
|
self.immediateExternalShare = immediateExternalShare
|
||||||
self.immediatePeerId = immediatePeerId
|
self.immediatePeerId = immediatePeerId
|
||||||
|
self.shares = shares
|
||||||
self.presentError = presentError
|
self.presentError = presentError
|
||||||
|
|
||||||
self.presetText = presetText
|
self.presetText = presetText
|
||||||
@ -670,7 +673,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||||||
let animated = self.peersContentNode == nil
|
let animated = self.peersContentNode == nil
|
||||||
let peersContentNode = SharePeersContainerNode(sharedContext: self.sharedContext, context: context, switchableAccounts: switchableAccounts, theme: self.presentationData.theme, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, peers: peers, accountPeer: accountPeer, controllerInteraction: self.controllerInteraction!, externalShare: self.externalShare, switchToAnotherAccount: { [weak self] in
|
let peersContentNode = SharePeersContainerNode(sharedContext: self.sharedContext, context: context, switchableAccounts: switchableAccounts, theme: self.presentationData.theme, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, peers: peers, accountPeer: accountPeer, controllerInteraction: self.controllerInteraction!, externalShare: self.externalShare, switchToAnotherAccount: { [weak self] in
|
||||||
self?.switchToAnotherAccount?()
|
self?.switchToAnotherAccount?()
|
||||||
}, extendedInitialReveal: self.presetText != nil)
|
}, extendedInitialReveal: self.presetText != nil, statsCount: self.shares ?? 0)
|
||||||
self.peersContentNode = peersContentNode
|
self.peersContentNode = peersContentNode
|
||||||
peersContentNode.openSearch = { [weak self] in
|
peersContentNode.openSearch = { [weak self] in
|
||||||
let _ = (recentlySearchedPeers(postbox: context.account.postbox)
|
let _ = (recentlySearchedPeers(postbox: context.account.postbox)
|
||||||
@ -736,6 +739,14 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||||||
peersContentNode.openShare = {
|
peersContentNode.openShare = {
|
||||||
openShare(false)
|
openShare(false)
|
||||||
}
|
}
|
||||||
|
if let openStats = self.openStats {
|
||||||
|
peersContentNode.openStats = { [weak self] in
|
||||||
|
openStats()
|
||||||
|
self?.animateOut(shared: true, completion: {
|
||||||
|
self?.dismiss?(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
if self.immediateExternalShare {
|
if self.immediateExternalShare {
|
||||||
openShare(true)
|
openShare(true)
|
||||||
} else {
|
} else {
|
||||||
|
@ -81,6 +81,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||||||
private let controllerInteraction: ShareControllerInteraction
|
private let controllerInteraction: ShareControllerInteraction
|
||||||
private let switchToAnotherAccount: () -> Void
|
private let switchToAnotherAccount: () -> Void
|
||||||
private let extendedInitialReveal: Bool
|
private let extendedInitialReveal: Bool
|
||||||
|
private let statsCount: Int?
|
||||||
|
|
||||||
let accountPeer: Peer
|
let accountPeer: Peer
|
||||||
private let foundPeers = Promise<[RenderedPeer]>([])
|
private let foundPeers = Promise<[RenderedPeer]>([])
|
||||||
@ -96,11 +97,13 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||||||
private let contentSeparatorNode: ASDisplayNode
|
private let contentSeparatorNode: ASDisplayNode
|
||||||
private let searchButtonNode: HighlightableButtonNode
|
private let searchButtonNode: HighlightableButtonNode
|
||||||
private let shareButtonNode: HighlightableButtonNode
|
private let shareButtonNode: HighlightableButtonNode
|
||||||
|
private let statsButtonNode: HighlightableButtonNode
|
||||||
|
|
||||||
private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||||
|
|
||||||
var openSearch: (() -> Void)?
|
var openSearch: (() -> Void)?
|
||||||
var openShare: (() -> Void)?
|
var openShare: (() -> Void)?
|
||||||
|
var openStats: (() -> Void)?
|
||||||
|
|
||||||
private var ensurePeerVisibleOnLayout: PeerId?
|
private var ensurePeerVisibleOnLayout: PeerId?
|
||||||
private var validLayout: (CGSize, CGFloat)?
|
private var validLayout: (CGSize, CGFloat)?
|
||||||
@ -108,7 +111,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||||||
|
|
||||||
let peersValue = Promise<[(RenderedPeer, PeerPresence?)]>()
|
let peersValue = Promise<[(RenderedPeer, PeerPresence?)]>()
|
||||||
|
|
||||||
init(sharedContext: SharedAccountContext, context: AccountContext, switchableAccounts: [AccountWithInfo], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(RenderedPeer, PeerPresence?)], accountPeer: Peer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, extendedInitialReveal: Bool) {
|
init(sharedContext: SharedAccountContext, context: AccountContext, switchableAccounts: [AccountWithInfo], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(RenderedPeer, PeerPresence?)], accountPeer: Peer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, extendedInitialReveal: Bool, statsCount: Int?) {
|
||||||
self.sharedContext = sharedContext
|
self.sharedContext = sharedContext
|
||||||
self.context = context
|
self.context = context
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
@ -118,6 +121,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||||||
self.accountPeer = accountPeer
|
self.accountPeer = accountPeer
|
||||||
self.switchToAnotherAccount = switchToAnotherAccount
|
self.switchToAnotherAccount = switchToAnotherAccount
|
||||||
self.extendedInitialReveal = extendedInitialReveal
|
self.extendedInitialReveal = extendedInitialReveal
|
||||||
|
self.statsCount = statsCount
|
||||||
|
|
||||||
self.peersValue.set(.single(peers))
|
self.peersValue.set(.single(peers))
|
||||||
|
|
||||||
@ -174,6 +178,13 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||||||
self.shareButtonNode = HighlightableButtonNode()
|
self.shareButtonNode = HighlightableButtonNode()
|
||||||
self.shareButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Share/ShareIcon"), color: self.theme.actionSheet.controlAccentColor), for: [])
|
self.shareButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Share/ShareIcon"), color: self.theme.actionSheet.controlAccentColor), for: [])
|
||||||
|
|
||||||
|
self.statsButtonNode = HighlightableButtonNode()
|
||||||
|
self.statsButtonNode.setAttributedTitle(NSAttributedString(string: "\(statsCount ?? 0) Shares", font: Font.regular(17.0), textColor: self.theme.actionSheet.controlAccentColor), for: .normal)
|
||||||
|
self.statsButtonNode.isHidden = statsCount == nil
|
||||||
|
|
||||||
|
self.contentTitleNode.isHidden = !self.statsButtonNode.isHidden
|
||||||
|
self.contentSubtitleNode.isHidden = !self.statsButtonNode.isHidden
|
||||||
|
|
||||||
self.contentSeparatorNode = ASDisplayNode()
|
self.contentSeparatorNode = ASDisplayNode()
|
||||||
self.contentSeparatorNode.isLayerBacked = true
|
self.contentSeparatorNode.isLayerBacked = true
|
||||||
self.contentSeparatorNode.displaysAsynchronously = false
|
self.contentSeparatorNode.displaysAsynchronously = false
|
||||||
@ -192,6 +203,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||||||
self.addSubnode(self.contentTitleAccountNode)
|
self.addSubnode(self.contentTitleAccountNode)
|
||||||
self.addSubnode(self.searchButtonNode)
|
self.addSubnode(self.searchButtonNode)
|
||||||
self.addSubnode(self.shareButtonNode)
|
self.addSubnode(self.shareButtonNode)
|
||||||
|
self.addSubnode(self.statsButtonNode)
|
||||||
self.addSubnode(self.contentSeparatorNode)
|
self.addSubnode(self.contentSeparatorNode)
|
||||||
|
|
||||||
let previousItems = Atomic<[SharePeerEntry]?>(value: [])
|
let previousItems = Atomic<[SharePeerEntry]?>(value: [])
|
||||||
@ -213,6 +225,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||||||
|
|
||||||
self.searchButtonNode.addTarget(self, action: #selector(self.searchPressed), forControlEvents: .touchUpInside)
|
self.searchButtonNode.addTarget(self, action: #selector(self.searchPressed), forControlEvents: .touchUpInside)
|
||||||
self.shareButtonNode.addTarget(self, action: #selector(self.sharePressed), forControlEvents: .touchUpInside)
|
self.shareButtonNode.addTarget(self, action: #selector(self.sharePressed), forControlEvents: .touchUpInside)
|
||||||
|
self.statsButtonNode.addTarget(self, action: #selector(self.statsPressed), forControlEvents: .touchUpInside)
|
||||||
self.contentTitleAccountNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.accountTapGesture(_:))))
|
self.contentTitleAccountNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.accountTapGesture(_:))))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,6 +354,9 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||||||
self.contentSubtitleNode.frame = originalSubtitleFrame
|
self.contentSubtitleNode.frame = originalSubtitleFrame
|
||||||
transition.updateFrame(node: self.contentSubtitleNode, frame: subtitleFrame)
|
transition.updateFrame(node: self.contentSubtitleNode, frame: subtitleFrame)
|
||||||
|
|
||||||
|
let statsSize = self.statsButtonNode.measure(CGSize(width: size.width - 44.0 * 2.0 - 8.0 * 2.0, height: titleAreaHeight))
|
||||||
|
transition.updateFrame(node: self.statsButtonNode, frame: CGRect(origin: CGPoint(x: floor((size.width - statsSize.width) / 2.0), y: titleOffset + 22.0), size: statsSize))
|
||||||
|
|
||||||
let titleButtonSize = CGSize(width: 44.0, height: 44.0)
|
let titleButtonSize = CGSize(width: 44.0, height: 44.0)
|
||||||
let searchButtonFrame = CGRect(origin: CGPoint(x: 12.0, y: titleOffset + 12.0), size: titleButtonSize)
|
let searchButtonFrame = CGRect(origin: CGPoint(x: 12.0, y: titleOffset + 12.0), size: titleButtonSize)
|
||||||
transition.updateFrame(node: self.searchButtonNode, frame: searchButtonFrame)
|
transition.updateFrame(node: self.searchButtonNode, frame: searchButtonFrame)
|
||||||
@ -376,25 +392,34 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateSelectedPeers() {
|
func updateSelectedPeers() {
|
||||||
var subtitleText = self.strings.ShareMenu_SelectChats
|
if let _ = self.openStats, self.controllerInteraction.selectedPeers.isEmpty {
|
||||||
if !self.controllerInteraction.selectedPeers.isEmpty {
|
self.statsButtonNode.isHidden = false
|
||||||
subtitleText = self.controllerInteraction.selectedPeers.reduce("", { string, peer in
|
self.contentTitleNode.isHidden = true
|
||||||
let text: String
|
self.contentSubtitleNode.isHidden = true
|
||||||
if peer.peerId == self.accountPeer.id {
|
} else {
|
||||||
text = self.strings.DialogList_SavedMessages
|
self.statsButtonNode.isHidden = true
|
||||||
} else {
|
self.contentTitleNode.isHidden = false
|
||||||
text = peer.chatMainPeer?.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder) ?? ""
|
self.contentSubtitleNode.isHidden = false
|
||||||
}
|
|
||||||
|
var subtitleText = self.strings.ShareMenu_SelectChats
|
||||||
if !string.isEmpty {
|
if !self.controllerInteraction.selectedPeers.isEmpty {
|
||||||
return string + ", " + text
|
subtitleText = self.controllerInteraction.selectedPeers.reduce("", { string, peer in
|
||||||
} else {
|
let text: String
|
||||||
return string + text
|
if peer.peerId == self.accountPeer.id {
|
||||||
}
|
text = self.strings.DialogList_SavedMessages
|
||||||
})
|
} else {
|
||||||
|
text = peer.chatMainPeer?.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder) ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if !string.isEmpty {
|
||||||
|
return string + ", " + text
|
||||||
|
} else {
|
||||||
|
return string + text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
self.contentSubtitleNode.attributedText = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: self.theme.actionSheet.secondaryTextColor)
|
||||||
}
|
}
|
||||||
self.contentSubtitleNode.attributedText = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: self.theme.actionSheet.secondaryTextColor)
|
|
||||||
|
|
||||||
self.contentGridNode.forEachItemNode { itemNode in
|
self.contentGridNode.forEachItemNode { itemNode in
|
||||||
if let itemNode = itemNode as? ShareControllerPeerGridItemNode {
|
if let itemNode = itemNode as? ShareControllerPeerGridItemNode {
|
||||||
itemNode.updateSelection(animated: true)
|
itemNode.updateSelection(animated: true)
|
||||||
@ -410,6 +435,10 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||||||
self.openShare?()
|
self.openShare?()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func statsPressed() {
|
||||||
|
self.openStats?()
|
||||||
|
}
|
||||||
|
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
let nodes: [ASDisplayNode] = [self.searchButtonNode, self.shareButtonNode, self.contentTitleAccountNode]
|
let nodes: [ASDisplayNode] = [self.searchButtonNode, self.shareButtonNode, self.contentTitleAccountNode]
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
|
@ -5,7 +5,6 @@ import SwiftSignalKit
|
|||||||
import Postbox
|
import Postbox
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SyncCore
|
import SyncCore
|
||||||
import MapKit
|
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
|
@ -5,7 +5,6 @@ import SwiftSignalKit
|
|||||||
import Postbox
|
import Postbox
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SyncCore
|
import SyncCore
|
||||||
import MapKit
|
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
|
265
submodules/StatisticsUI/Sources/MessageStatsController.swift
Normal file
265
submodules/StatisticsUI/Sources/MessageStatsController.swift
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import SwiftSignalKit
|
||||||
|
import Postbox
|
||||||
|
import TelegramCore
|
||||||
|
import SyncCore
|
||||||
|
import TelegramPresentationData
|
||||||
|
import TelegramUIPreferences
|
||||||
|
import TelegramStringFormatting
|
||||||
|
import ItemListUI
|
||||||
|
import ItemListPeerItem
|
||||||
|
import PresentationDataUtils
|
||||||
|
import AccountContext
|
||||||
|
import PresentationDataUtils
|
||||||
|
import AppBundle
|
||||||
|
import GraphUI
|
||||||
|
|
||||||
|
private final class MessageStatsControllerArguments {
|
||||||
|
let context: AccountContext
|
||||||
|
let loadDetailedGraph: (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>
|
||||||
|
let openMessage: (MessageId) -> Void
|
||||||
|
|
||||||
|
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openMessage: @escaping (MessageId) -> Void) {
|
||||||
|
self.context = context
|
||||||
|
self.loadDetailedGraph = loadDetailedGraph
|
||||||
|
self.openMessage = openMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum StatsSection: Int32 {
|
||||||
|
case overview
|
||||||
|
case interactions
|
||||||
|
case publicForwards
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum StatsEntry: ItemListNodeEntry {
|
||||||
|
case overviewTitle(PresentationTheme, String)
|
||||||
|
case overview(PresentationTheme, MessageStats, Int32?)
|
||||||
|
|
||||||
|
case interactionsTitle(PresentationTheme, String)
|
||||||
|
case interactionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType)
|
||||||
|
|
||||||
|
case publicForwardsTitle(PresentationTheme, String)
|
||||||
|
case publicForward(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Message)
|
||||||
|
|
||||||
|
var section: ItemListSectionId {
|
||||||
|
switch self {
|
||||||
|
case .overviewTitle, .overview:
|
||||||
|
return StatsSection.overview.rawValue
|
||||||
|
case .interactionsTitle, .interactionsGraph:
|
||||||
|
return StatsSection.interactions.rawValue
|
||||||
|
case .publicForwardsTitle, .publicForward:
|
||||||
|
return StatsSection.publicForwards.rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var stableId: Int32 {
|
||||||
|
switch self {
|
||||||
|
case .overviewTitle:
|
||||||
|
return 0
|
||||||
|
case .overview:
|
||||||
|
return 1
|
||||||
|
case .interactionsTitle:
|
||||||
|
return 2
|
||||||
|
case .interactionsGraph:
|
||||||
|
return 3
|
||||||
|
case .publicForwardsTitle:
|
||||||
|
return 4
|
||||||
|
case let .publicForward(index, _, _, _, _):
|
||||||
|
return 5 + index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: StatsEntry, rhs: StatsEntry) -> Bool {
|
||||||
|
switch lhs {
|
||||||
|
case let .overviewTitle(lhsTheme, lhsText):
|
||||||
|
if case let .overviewTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText{
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .overview(lhsTheme, lhsStats, lhsPublicShares):
|
||||||
|
if case let .overview(rhsTheme, rhsStats, rhsPublicShares) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsPublicShares == rhsPublicShares {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .interactionsTitle(lhsTheme, lhsText):
|
||||||
|
if case let .interactionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .interactionsGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType):
|
||||||
|
if case let .interactionsGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .publicForwardsTitle(lhsTheme, lhsText):
|
||||||
|
if case let .publicForwardsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .publicForward(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsMessage):
|
||||||
|
if case let .publicForward(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsMessage) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsMessage.id == rhsMessage.id {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func <(lhs: StatsEntry, rhs: StatsEntry) -> Bool {
|
||||||
|
return lhs.stableId < rhs.stableId
|
||||||
|
}
|
||||||
|
|
||||||
|
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||||
|
let arguments = arguments as! MessageStatsControllerArguments
|
||||||
|
switch self {
|
||||||
|
case let .overviewTitle(_, text),
|
||||||
|
let .interactionsTitle(_, text),
|
||||||
|
let .publicForwardsTitle(_, text):
|
||||||
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||||
|
case let .overview(_, stats, publicShares):
|
||||||
|
return MessageStatsOverviewItem(presentationData: presentationData, stats: stats, publicShares: publicShares, sectionId: self.section, style: .blocks)
|
||||||
|
case let .interactionsGraph(_, _, _, graph, type):
|
||||||
|
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
|
||||||
|
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
|
||||||
|
if let graph = graph, case let .Loaded(_, data) = graph {
|
||||||
|
completion(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, sectionId: self.section, style: .blocks)
|
||||||
|
case let .publicForward(_, _, _, _, message):
|
||||||
|
var views: Int = 0
|
||||||
|
for attribute in message.attributes {
|
||||||
|
if let viewsAttribute = attribute as? ViewCountMessageAttribute {
|
||||||
|
views = viewsAttribute.count
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var text: String = ""
|
||||||
|
text += "\(views) views"
|
||||||
|
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", decimalSeparator: ",", groupingSeparator: ""), nameDisplayOrder: .firstLast, context: arguments.context, peer: message.peers[message.id.peerId]!, height: .generic, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .text(text), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil), revealOptions: nil, switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: {
|
||||||
|
arguments.openMessage(message.id)
|
||||||
|
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, contextAction: nil)
|
||||||
|
// return StatsMessageItem(context: arguments.context, presentationData: presentationData, message: message, views: 0, forwards: 0, sectionId: self.section, style: .blocks, action: {
|
||||||
|
// arguments.openMessage(message.id)
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func messageStatsControllerEntries(data: MessageStats?, messages: SearchMessagesResult?, presentationData: PresentationData) -> [StatsEntry] {
|
||||||
|
var entries: [StatsEntry] = []
|
||||||
|
|
||||||
|
if let data = data {
|
||||||
|
entries.append(.overviewTitle(presentationData.theme, presentationData.strings.Stats_MessageOverview.uppercased()))
|
||||||
|
entries.append(.overview(presentationData.theme, data, messages?.totalCount))
|
||||||
|
|
||||||
|
if !data.interactionsGraph.isEmpty {
|
||||||
|
entries.append(.interactionsTitle(presentationData.theme, presentationData.strings.Stats_MessageInteractionsTitle.uppercased()))
|
||||||
|
entries.append(.interactionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, .twoAxisStep))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let messages = messages, !messages.messages.isEmpty {
|
||||||
|
entries.append(.publicForwardsTitle(presentationData.theme, presentationData.strings.Stats_MessagePublicForwardsTitle.uppercased()))
|
||||||
|
var index: Int32 = 0
|
||||||
|
for message in messages.messages {
|
||||||
|
entries.append(.publicForward(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, message))
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
public func messageStatsController(context: AccountContext, messageId: MessageId, cachedPeerData: CachedPeerData) -> ViewController {
|
||||||
|
var navigateToMessageImpl: ((MessageId) -> Void)?
|
||||||
|
|
||||||
|
let actionsDisposable = DisposableSet()
|
||||||
|
let dataPromise = Promise<MessageStats?>(nil)
|
||||||
|
let messagesPromise = Promise<(SearchMessagesResult, SearchMessagesState)?>(nil)
|
||||||
|
|
||||||
|
var datacenterId: Int32 = 0
|
||||||
|
if let cachedData = cachedPeerData as? CachedChannelData {
|
||||||
|
datacenterId = cachedData.statsDatacenterId
|
||||||
|
}
|
||||||
|
|
||||||
|
let statsContext = MessageStatsContext(postbox: context.account.postbox, network: context.account.network, datacenterId: datacenterId, messageId: messageId)
|
||||||
|
let dataSignal: Signal<MessageStats?, NoError> = statsContext.state
|
||||||
|
|> map { state in
|
||||||
|
return state.stats
|
||||||
|
}
|
||||||
|
dataPromise.set(.single(nil) |> then(dataSignal))
|
||||||
|
|
||||||
|
let arguments = MessageStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal<StatsGraph?, NoError> in
|
||||||
|
return statsContext.loadDetailedGraph(graph, x: x)
|
||||||
|
}, openMessage: { messageId in
|
||||||
|
navigateToMessageImpl?(messageId)
|
||||||
|
})
|
||||||
|
|
||||||
|
let longLoadingSignal: Signal<Bool, NoError> = .single(false) |> then(.single(true) |> delay(2.0, queue: Queue.mainQueue()))
|
||||||
|
|
||||||
|
let previousData = Atomic<MessageStats?>(value: nil)
|
||||||
|
|
||||||
|
let searchSignal = searchMessages(account: context.account, location: .publicForwards(messageId), query: "", state: nil)
|
||||||
|
|> map(Optional.init)
|
||||||
|
|> afterNext { result in
|
||||||
|
if let result = result {
|
||||||
|
for message in result.0.messages {
|
||||||
|
if let peer = message.peers[message.id.peerId], let peerReference = PeerReference(peer) {
|
||||||
|
let _ = updatedRemotePeer(postbox: context.account.postbox, network: context.account.network, peer: peerReference).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messagesPromise.set(.single(nil) |> then(searchSignal))
|
||||||
|
|
||||||
|
let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), messagesPromise.get(), longLoadingSignal)
|
||||||
|
|> deliverOnMainQueue
|
||||||
|
|> map { presentationData, data, search, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||||
|
let previous = previousData.swap(data)
|
||||||
|
var emptyStateItem: ItemListControllerEmptyStateItem?
|
||||||
|
if data == nil {
|
||||||
|
if longLoading {
|
||||||
|
emptyStateItem = StatsEmptyStateItem(theme: presentationData.theme, strings: presentationData.strings)
|
||||||
|
} else {
|
||||||
|
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Stats_MessageTitle), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
|
||||||
|
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: messageStatsControllerEntries(data: data, messages: search?.0, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false)
|
||||||
|
|
||||||
|
return (controllerState, (listState, arguments))
|
||||||
|
}
|
||||||
|
|> afterDisposed {
|
||||||
|
actionsDisposable.dispose()
|
||||||
|
let _ = statsContext.state
|
||||||
|
}
|
||||||
|
|
||||||
|
let controller = ItemListController(context: context, state: signal)
|
||||||
|
controller.contentOffsetChanged = { [weak controller] _, _ in
|
||||||
|
controller?.forEachItemNode({ itemNode in
|
||||||
|
if let itemNode = itemNode as? StatsGraphItemNode {
|
||||||
|
itemNode.resetInteraction()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
controller.didDisappear = { [weak controller] _ in
|
||||||
|
controller?.clearItemNodesHighlight(animated: true)
|
||||||
|
}
|
||||||
|
navigateToMessageImpl = { [weak controller] messageId in
|
||||||
|
if let navigationController = controller?.navigationController as? NavigationController {
|
||||||
|
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(messageId.peerId), subject: .message(messageId), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return controller
|
||||||
|
}
|
289
submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift
Normal file
289
submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramCore
|
||||||
|
import SyncCore
|
||||||
|
import TelegramPresentationData
|
||||||
|
import ItemListUI
|
||||||
|
import PresentationDataUtils
|
||||||
|
|
||||||
|
class MessageStatsOverviewItem: ListViewItem, ItemListItem {
|
||||||
|
let presentationData: ItemListPresentationData
|
||||||
|
let stats: MessageStats
|
||||||
|
let publicShares: Int32?
|
||||||
|
let sectionId: ItemListSectionId
|
||||||
|
let style: ItemListStyle
|
||||||
|
|
||||||
|
init(presentationData: ItemListPresentationData, stats: MessageStats, publicShares: Int32?, sectionId: ItemListSectionId, style: ItemListStyle) {
|
||||||
|
self.presentationData = presentationData
|
||||||
|
self.stats = stats
|
||||||
|
self.publicShares = publicShares
|
||||||
|
self.sectionId = sectionId
|
||||||
|
self.style = style
|
||||||
|
}
|
||||||
|
|
||||||
|
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||||
|
async {
|
||||||
|
let node = MessageStatsOverviewItemNode()
|
||||||
|
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||||
|
|
||||||
|
node.contentSize = layout.contentSize
|
||||||
|
node.insets = layout.insets
|
||||||
|
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
completion(node, {
|
||||||
|
return (nil, { _ in apply() })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
if let nodeValue = node() as? MessageStatsOverviewItemNode {
|
||||||
|
let makeLayout = nodeValue.asyncLayout()
|
||||||
|
|
||||||
|
async {
|
||||||
|
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
completion(layout, { _ in
|
||||||
|
apply()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectable: Bool = false
|
||||||
|
}
|
||||||
|
|
||||||
|
class MessageStatsOverviewItemNode: ListViewItemNode {
|
||||||
|
private let backgroundNode: ASDisplayNode
|
||||||
|
private let topStripeNode: ASDisplayNode
|
||||||
|
private let bottomStripeNode: ASDisplayNode
|
||||||
|
private let maskNode: ASImageNode
|
||||||
|
|
||||||
|
private let leftValueLabel: ImmediateTextNode
|
||||||
|
private let centerValueLabel: ImmediateTextNode
|
||||||
|
private let rightValueLabel: ImmediateTextNode
|
||||||
|
|
||||||
|
private let leftTitleLabel: ImmediateTextNode
|
||||||
|
private let centerTitleLabel: ImmediateTextNode
|
||||||
|
private let rightTitleLabel: ImmediateTextNode
|
||||||
|
|
||||||
|
private var item: MessageStatsOverviewItem?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.backgroundNode = ASDisplayNode()
|
||||||
|
self.backgroundNode.isLayerBacked = true
|
||||||
|
self.backgroundNode.backgroundColor = .white
|
||||||
|
|
||||||
|
self.topStripeNode = ASDisplayNode()
|
||||||
|
self.topStripeNode.isLayerBacked = true
|
||||||
|
|
||||||
|
self.bottomStripeNode = ASDisplayNode()
|
||||||
|
self.bottomStripeNode.isLayerBacked = true
|
||||||
|
|
||||||
|
self.maskNode = ASImageNode()
|
||||||
|
|
||||||
|
self.leftValueLabel = ImmediateTextNode()
|
||||||
|
self.centerValueLabel = ImmediateTextNode()
|
||||||
|
self.rightValueLabel = ImmediateTextNode()
|
||||||
|
|
||||||
|
self.leftTitleLabel = ImmediateTextNode()
|
||||||
|
self.centerTitleLabel = ImmediateTextNode()
|
||||||
|
self.rightTitleLabel = ImmediateTextNode()
|
||||||
|
|
||||||
|
super.init(layerBacked: false, dynamicBounce: false)
|
||||||
|
|
||||||
|
self.clipsToBounds = true
|
||||||
|
|
||||||
|
self.addSubnode(self.leftValueLabel)
|
||||||
|
self.addSubnode(self.centerValueLabel)
|
||||||
|
self.addSubnode(self.rightValueLabel)
|
||||||
|
|
||||||
|
self.addSubnode(self.leftTitleLabel)
|
||||||
|
self.addSubnode(self.centerTitleLabel)
|
||||||
|
self.addSubnode(self.rightTitleLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func asyncLayout() -> (_ item: MessageStatsOverviewItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||||
|
let makeLeftValueLabelLayout = TextNode.asyncLayout(self.leftValueLabel)
|
||||||
|
let makeRightValueLabelLayout = TextNode.asyncLayout(self.rightValueLabel)
|
||||||
|
let makeCenterValueLabelLayout = TextNode.asyncLayout(self.centerValueLabel)
|
||||||
|
|
||||||
|
let makeLeftTitleLabelLayout = TextNode.asyncLayout(self.leftTitleLabel)
|
||||||
|
let makeRightTitleLabelLayout = TextNode.asyncLayout(self.rightTitleLabel)
|
||||||
|
let makeCenterTitleLabelLayout = TextNode.asyncLayout(self.centerTitleLabel)
|
||||||
|
|
||||||
|
let currentItem = self.item
|
||||||
|
|
||||||
|
return { item, params, neighbors in
|
||||||
|
let insets: UIEdgeInsets
|
||||||
|
let separatorHeight = UIScreenPixel
|
||||||
|
let itemBackgroundColor: UIColor
|
||||||
|
let itemSeparatorColor: UIColor
|
||||||
|
|
||||||
|
let horizontalSpacing: CGFloat = 62.0
|
||||||
|
let topInset: CGFloat = 14.0
|
||||||
|
let sideInset: CGFloat = 16.0
|
||||||
|
|
||||||
|
var height: CGFloat = topInset * 2.0
|
||||||
|
|
||||||
|
let leftInset = params.leftInset
|
||||||
|
let rightInset: CGFloat = params.rightInset
|
||||||
|
var updatedTheme: PresentationTheme?
|
||||||
|
|
||||||
|
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||||||
|
updatedTheme = item.presentationData.theme
|
||||||
|
}
|
||||||
|
|
||||||
|
switch item.style {
|
||||||
|
case .plain:
|
||||||
|
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||||
|
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
||||||
|
insets = itemListNeighborsPlainInsets(neighbors)
|
||||||
|
case .blocks:
|
||||||
|
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
||||||
|
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||||
|
insets = itemListNeighborsGroupedInsets(neighbors)
|
||||||
|
}
|
||||||
|
|
||||||
|
let valueFont = Font.semibold(item.presentationData.fontSize.itemListBaseFontSize)
|
||||||
|
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
|
||||||
|
|
||||||
|
let leftValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||||
|
let rightValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||||
|
let centerValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||||
|
|
||||||
|
let leftTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||||
|
let rightTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||||
|
let centerTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||||
|
|
||||||
|
leftValueLabelLayoutAndApply = makeLeftValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(item.stats.views), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
|
centerValueLabelLayoutAndApply = makeCenterValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
|
rightValueLabelLayoutAndApply = makeRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { "≈\( compactNumericCountString(item.stats.privateForwards - Int($0)))" } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
|
leftTitleLabelLayoutAndApply = makeLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Views", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
|
centerTitleLabelLayoutAndApply = makeCenterTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Public Shares", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
|
rightTitleLabelLayoutAndApply = makeRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Private Shares", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
|
height += rightValueLabelLayoutAndApply!.0.size.height + rightTitleLabelLayoutAndApply!.0.size.height
|
||||||
|
|
||||||
|
let contentSize = CGSize(width: params.width, height: height)
|
||||||
|
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.item = item
|
||||||
|
|
||||||
|
let _ = leftValueLabelLayoutAndApply?.1()
|
||||||
|
let _ = centerValueLabelLayoutAndApply?.1()
|
||||||
|
let _ = rightValueLabelLayoutAndApply?.1()
|
||||||
|
let _ = leftTitleLabelLayoutAndApply?.1()
|
||||||
|
let _ = centerTitleLabelLayoutAndApply?.1()
|
||||||
|
let _ = rightTitleLabelLayoutAndApply?.1()
|
||||||
|
|
||||||
|
if let _ = updatedTheme {
|
||||||
|
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||||
|
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
||||||
|
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
switch item.style {
|
||||||
|
case .plain:
|
||||||
|
if strongSelf.backgroundNode.supernode != nil {
|
||||||
|
strongSelf.backgroundNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
if strongSelf.topStripeNode.supernode != nil {
|
||||||
|
strongSelf.topStripeNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
if strongSelf.bottomStripeNode.supernode == nil {
|
||||||
|
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
||||||
|
}
|
||||||
|
if strongSelf.maskNode.supernode != nil {
|
||||||
|
strongSelf.maskNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
|
||||||
|
case .blocks:
|
||||||
|
if strongSelf.backgroundNode.supernode == nil {
|
||||||
|
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||||
|
}
|
||||||
|
if strongSelf.topStripeNode.supernode == nil {
|
||||||
|
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||||
|
}
|
||||||
|
if strongSelf.bottomStripeNode.supernode == nil {
|
||||||
|
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||||
|
}
|
||||||
|
if strongSelf.maskNode.supernode == nil {
|
||||||
|
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||||
|
var hasTopCorners = false
|
||||||
|
var hasBottomCorners = false
|
||||||
|
switch neighbors.top {
|
||||||
|
case .sameSection(false):
|
||||||
|
strongSelf.topStripeNode.isHidden = true
|
||||||
|
default:
|
||||||
|
hasTopCorners = true
|
||||||
|
strongSelf.topStripeNode.isHidden = hasCorners
|
||||||
|
}
|
||||||
|
let bottomStripeInset: CGFloat
|
||||||
|
switch neighbors.bottom {
|
||||||
|
case .sameSection(false):
|
||||||
|
bottomStripeInset = leftInset
|
||||||
|
default:
|
||||||
|
bottomStripeInset = 0.0
|
||||||
|
hasBottomCorners = true
|
||||||
|
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||||
|
|
||||||
|
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||||
|
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||||
|
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
|
||||||
|
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
||||||
|
}
|
||||||
|
|
||||||
|
var x: CGFloat = sideInset + leftInset
|
||||||
|
if let leftValueLabelLayout = leftValueLabelLayoutAndApply?.0, let leftTitleLabelLayout = leftTitleLabelLayoutAndApply?.0 {
|
||||||
|
strongSelf.leftValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: leftValueLabelLayout.size)
|
||||||
|
strongSelf.leftTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.leftValueLabel.frame.maxY), size: leftTitleLabelLayout.size)
|
||||||
|
x += max(leftValueLabelLayout.size.width, leftTitleLabelLayout.size.width) + horizontalSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
if let centerValueLabelLayout = centerValueLabelLayoutAndApply?.0, let centerTitleLabelLayout = centerTitleLabelLayoutAndApply?.0 {
|
||||||
|
strongSelf.centerValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: centerValueLabelLayout.size)
|
||||||
|
strongSelf.centerTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.centerValueLabel.frame.maxY), size: centerTitleLabelLayout.size)
|
||||||
|
x += max(centerValueLabelLayout.size.width, centerTitleLabelLayout.size.width) + horizontalSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
if let rightValueLabelLayout = rightValueLabelLayoutAndApply?.0, let rightTitleLabelLayout = rightTitleLabelLayoutAndApply?.0 {
|
||||||
|
strongSelf.rightValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: rightValueLabelLayout.size)
|
||||||
|
strongSelf.rightTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.rightValueLabel.frame.maxY), size: rightTitleLabelLayout.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||||
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||||
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||||
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
|||||||
|
import Foundation
|
||||||
|
import Postbox
|
||||||
|
|
||||||
|
public class ForwardCountMessageAttribute: MessageAttribute {
|
||||||
|
public let count: Int
|
||||||
|
|
||||||
|
public var associatedMessageIds: [MessageId] = []
|
||||||
|
|
||||||
|
public init(count: Int) {
|
||||||
|
self.count = count
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init(decoder: PostboxDecoder) {
|
||||||
|
self.count = Int(decoder.decodeInt32ForKey("c", orElse: 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(_ encoder: PostboxEncoder) {
|
||||||
|
encoder.encodeInt32(Int32(self.count), forKey: "c")
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Postbox
|
import Postbox
|
||||||
|
|
||||||
|
|
||||||
public class ViewCountMessageAttribute: MessageAttribute {
|
public class ViewCountMessageAttribute: MessageAttribute {
|
||||||
public let count: Int
|
public let count: Int
|
||||||
|
|
||||||
|
@ -254,6 +254,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[-1512627963] = { return Api.Update.parse_updateDialogFilterOrder($0) }
|
dict[-1512627963] = { return Api.Update.parse_updateDialogFilterOrder($0) }
|
||||||
dict[889491791] = { return Api.Update.parse_updateDialogFilters($0) }
|
dict[889491791] = { return Api.Update.parse_updateDialogFilters($0) }
|
||||||
dict[643940105] = { return Api.Update.parse_updatePhoneCallSignalingData($0) }
|
dict[643940105] = { return Api.Update.parse_updatePhoneCallSignalingData($0) }
|
||||||
|
dict[1854571743] = { return Api.Update.parse_updateChannelMessageForwards($0) }
|
||||||
dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) }
|
dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) }
|
||||||
dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) }
|
dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) }
|
||||||
dict[-373643672] = { return Api.FolderPeer.parse_folderPeer($0) }
|
dict[-373643672] = { return Api.FolderPeer.parse_folderPeer($0) }
|
||||||
@ -385,6 +386,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[1694474197] = { return Api.messages.Chats.parse_chats($0) }
|
dict[1694474197] = { return Api.messages.Chats.parse_chats($0) }
|
||||||
dict[-1663561404] = { return Api.messages.Chats.parse_chatsSlice($0) }
|
dict[-1663561404] = { return Api.messages.Chats.parse_chatsSlice($0) }
|
||||||
dict[482797855] = { return Api.InputSingleMedia.parse_inputSingleMedia($0) }
|
dict[482797855] = { return Api.InputSingleMedia.parse_inputSingleMedia($0) }
|
||||||
|
dict[1831138451] = { return Api.MessageViews.parse_messageViews($0) }
|
||||||
dict[218751099] = { return Api.InputPrivacyRule.parse_inputPrivacyValueAllowContacts($0) }
|
dict[218751099] = { return Api.InputPrivacyRule.parse_inputPrivacyValueAllowContacts($0) }
|
||||||
dict[407582158] = { return Api.InputPrivacyRule.parse_inputPrivacyValueAllowAll($0) }
|
dict[407582158] = { return Api.InputPrivacyRule.parse_inputPrivacyValueAllowAll($0) }
|
||||||
dict[320652927] = { return Api.InputPrivacyRule.parse_inputPrivacyValueAllowUsers($0) }
|
dict[320652927] = { return Api.InputPrivacyRule.parse_inputPrivacyValueAllowUsers($0) }
|
||||||
@ -596,7 +598,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[-1820043071] = { return Api.User.parse_user($0) }
|
dict[-1820043071] = { return Api.User.parse_user($0) }
|
||||||
dict[-2082087340] = { return Api.Message.parse_messageEmpty($0) }
|
dict[-2082087340] = { return Api.Message.parse_messageEmpty($0) }
|
||||||
dict[-1642487306] = { return Api.Message.parse_messageService($0) }
|
dict[-1642487306] = { return Api.Message.parse_messageService($0) }
|
||||||
dict[1160515173] = { return Api.Message.parse_message($0) }
|
dict[-181507201] = { return Api.Message.parse_message($0) }
|
||||||
dict[831924812] = { return Api.StatsGroupTopInviter.parse_statsGroupTopInviter($0) }
|
dict[831924812] = { return Api.StatsGroupTopInviter.parse_statsGroupTopInviter($0) }
|
||||||
dict[186120336] = { return Api.messages.RecentStickers.parse_recentStickersNotModified($0) }
|
dict[186120336] = { return Api.messages.RecentStickers.parse_recentStickersNotModified($0) }
|
||||||
dict[586395571] = { return Api.messages.RecentStickers.parse_recentStickers($0) }
|
dict[586395571] = { return Api.messages.RecentStickers.parse_recentStickers($0) }
|
||||||
@ -681,6 +683,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[364538944] = { return Api.messages.Dialogs.parse_dialogs($0) }
|
dict[364538944] = { return Api.messages.Dialogs.parse_dialogs($0) }
|
||||||
dict[1910543603] = { return Api.messages.Dialogs.parse_dialogsSlice($0) }
|
dict[1910543603] = { return Api.messages.Dialogs.parse_dialogsSlice($0) }
|
||||||
dict[-253500010] = { return Api.messages.Dialogs.parse_dialogsNotModified($0) }
|
dict[-253500010] = { return Api.messages.Dialogs.parse_dialogsNotModified($0) }
|
||||||
|
dict[-1986399595] = { return Api.stats.MessageStats.parse_messageStats($0) }
|
||||||
dict[-709641735] = { return Api.EmojiKeyword.parse_emojiKeyword($0) }
|
dict[-709641735] = { return Api.EmojiKeyword.parse_emojiKeyword($0) }
|
||||||
dict[594408994] = { return Api.EmojiKeyword.parse_emojiKeywordDeleted($0) }
|
dict[594408994] = { return Api.EmojiKeyword.parse_emojiKeywordDeleted($0) }
|
||||||
dict[-290921362] = { return Api.upload.CdnFile.parse_cdnFileReuploadNeeded($0) }
|
dict[-290921362] = { return Api.upload.CdnFile.parse_cdnFileReuploadNeeded($0) }
|
||||||
@ -1103,6 +1106,8 @@ public struct Api {
|
|||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.InputSingleMedia:
|
case let _1 as Api.InputSingleMedia:
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
|
case let _1 as Api.MessageViews:
|
||||||
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.InputPrivacyRule:
|
case let _1 as Api.InputPrivacyRule:
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.messages.DhConfig:
|
case let _1 as Api.messages.DhConfig:
|
||||||
@ -1363,6 +1368,8 @@ public struct Api {
|
|||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.messages.Dialogs:
|
case let _1 as Api.messages.Dialogs:
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
|
case let _1 as Api.stats.MessageStats:
|
||||||
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.EmojiKeyword:
|
case let _1 as Api.EmojiKeyword:
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.upload.CdnFile:
|
case let _1 as Api.upload.CdnFile:
|
||||||
|
@ -6037,6 +6037,7 @@ public extension Api {
|
|||||||
case updateDialogFilterOrder(order: [Int32])
|
case updateDialogFilterOrder(order: [Int32])
|
||||||
case updateDialogFilters
|
case updateDialogFilters
|
||||||
case updatePhoneCallSignalingData(phoneCallId: Int64, data: Buffer)
|
case updatePhoneCallSignalingData(phoneCallId: Int64, data: Buffer)
|
||||||
|
case updateChannelMessageForwards(channelId: Int32, id: Int32, forwards: Int32)
|
||||||
|
|
||||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
switch self {
|
switch self {
|
||||||
@ -6717,6 +6718,14 @@ public extension Api {
|
|||||||
serializeInt64(phoneCallId, buffer: buffer, boxed: false)
|
serializeInt64(phoneCallId, buffer: buffer, boxed: false)
|
||||||
serializeBytes(data, buffer: buffer, boxed: false)
|
serializeBytes(data, buffer: buffer, boxed: false)
|
||||||
break
|
break
|
||||||
|
case .updateChannelMessageForwards(let channelId, let id, let forwards):
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(1854571743)
|
||||||
|
}
|
||||||
|
serializeInt32(channelId, buffer: buffer, boxed: false)
|
||||||
|
serializeInt32(id, buffer: buffer, boxed: false)
|
||||||
|
serializeInt32(forwards, buffer: buffer, boxed: false)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6884,6 +6893,8 @@ public extension Api {
|
|||||||
return ("updateDialogFilters", [])
|
return ("updateDialogFilters", [])
|
||||||
case .updatePhoneCallSignalingData(let phoneCallId, let data):
|
case .updatePhoneCallSignalingData(let phoneCallId, let data):
|
||||||
return ("updatePhoneCallSignalingData", [("phoneCallId", phoneCallId), ("data", data)])
|
return ("updatePhoneCallSignalingData", [("phoneCallId", phoneCallId), ("data", data)])
|
||||||
|
case .updateChannelMessageForwards(let channelId, let id, let forwards):
|
||||||
|
return ("updateChannelMessageForwards", [("channelId", channelId), ("id", id), ("forwards", forwards)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -8229,6 +8240,23 @@ public extension Api {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public static func parse_updateChannelMessageForwards(_ reader: BufferReader) -> Update? {
|
||||||
|
var _1: Int32?
|
||||||
|
_1 = reader.readInt32()
|
||||||
|
var _2: Int32?
|
||||||
|
_2 = reader.readInt32()
|
||||||
|
var _3: Int32?
|
||||||
|
_3 = reader.readInt32()
|
||||||
|
let _c1 = _1 != nil
|
||||||
|
let _c2 = _2 != nil
|
||||||
|
let _c3 = _3 != nil
|
||||||
|
if _c1 && _c2 && _c3 {
|
||||||
|
return Api.Update.updateChannelMessageForwards(channelId: _1!, id: _2!, forwards: _3!)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
public enum PopularContact: TypeConstructorDescription {
|
public enum PopularContact: TypeConstructorDescription {
|
||||||
@ -11626,6 +11654,44 @@ public extension Api {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
public enum MessageViews: TypeConstructorDescription {
|
||||||
|
case messageViews(views: Int32, forwards: Int32)
|
||||||
|
|
||||||
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
|
switch self {
|
||||||
|
case .messageViews(let views, let forwards):
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(1831138451)
|
||||||
|
}
|
||||||
|
serializeInt32(views, buffer: buffer, boxed: false)
|
||||||
|
serializeInt32(forwards, buffer: buffer, boxed: false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||||
|
switch self {
|
||||||
|
case .messageViews(let views, let forwards):
|
||||||
|
return ("messageViews", [("views", views), ("forwards", forwards)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func parse_messageViews(_ reader: BufferReader) -> MessageViews? {
|
||||||
|
var _1: Int32?
|
||||||
|
_1 = reader.readInt32()
|
||||||
|
var _2: Int32?
|
||||||
|
_2 = reader.readInt32()
|
||||||
|
let _c1 = _1 != nil
|
||||||
|
let _c2 = _2 != nil
|
||||||
|
if _c1 && _c2 {
|
||||||
|
return Api.MessageViews.messageViews(views: _1!, forwards: _2!)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
public enum InputPrivacyRule: TypeConstructorDescription {
|
public enum InputPrivacyRule: TypeConstructorDescription {
|
||||||
case inputPrivacyValueAllowContacts
|
case inputPrivacyValueAllowContacts
|
||||||
@ -17000,7 +17066,7 @@ public extension Api {
|
|||||||
public enum Message: TypeConstructorDescription {
|
public enum Message: TypeConstructorDescription {
|
||||||
case messageEmpty(id: Int32)
|
case messageEmpty(id: Int32)
|
||||||
case messageService(flags: Int32, id: Int32, fromId: Int32?, toId: Api.Peer, replyToMsgId: Int32?, date: Int32, action: Api.MessageAction)
|
case messageService(flags: Int32, id: Int32, fromId: Int32?, toId: Api.Peer, replyToMsgId: Int32?, date: Int32, action: Api.MessageAction)
|
||||||
case message(flags: Int32, id: Int32, fromId: Int32?, toId: Api.Peer, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int32?, replyToMsgId: Int32?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, restrictionReason: [Api.RestrictionReason]?)
|
case message(flags: Int32, id: Int32, fromId: Int32?, toId: Api.Peer, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int32?, replyToMsgId: Int32?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, restrictionReason: [Api.RestrictionReason]?)
|
||||||
|
|
||||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
switch self {
|
switch self {
|
||||||
@ -17022,9 +17088,9 @@ public extension Api {
|
|||||||
serializeInt32(date, buffer: buffer, boxed: false)
|
serializeInt32(date, buffer: buffer, boxed: false)
|
||||||
action.serialize(buffer, true)
|
action.serialize(buffer, true)
|
||||||
break
|
break
|
||||||
case .message(let flags, let id, let fromId, let toId, let fwdFrom, let viaBotId, let replyToMsgId, let date, let message, let media, let replyMarkup, let entities, let views, let editDate, let postAuthor, let groupedId, let restrictionReason):
|
case .message(let flags, let id, let fromId, let toId, let fwdFrom, let viaBotId, let replyToMsgId, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let editDate, let postAuthor, let groupedId, let restrictionReason):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(1160515173)
|
buffer.appendInt32(-181507201)
|
||||||
}
|
}
|
||||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||||
serializeInt32(id, buffer: buffer, boxed: false)
|
serializeInt32(id, buffer: buffer, boxed: false)
|
||||||
@ -17043,6 +17109,7 @@ public extension Api {
|
|||||||
item.serialize(buffer, true)
|
item.serialize(buffer, true)
|
||||||
}}
|
}}
|
||||||
if Int(flags) & Int(1 << 10) != 0 {serializeInt32(views!, buffer: buffer, boxed: false)}
|
if Int(flags) & Int(1 << 10) != 0 {serializeInt32(views!, buffer: buffer, boxed: false)}
|
||||||
|
if Int(flags) & Int(1 << 10) != 0 {serializeInt32(forwards!, buffer: buffer, boxed: false)}
|
||||||
if Int(flags) & Int(1 << 15) != 0 {serializeInt32(editDate!, buffer: buffer, boxed: false)}
|
if Int(flags) & Int(1 << 15) != 0 {serializeInt32(editDate!, buffer: buffer, boxed: false)}
|
||||||
if Int(flags) & Int(1 << 16) != 0 {serializeString(postAuthor!, buffer: buffer, boxed: false)}
|
if Int(flags) & Int(1 << 16) != 0 {serializeString(postAuthor!, buffer: buffer, boxed: false)}
|
||||||
if Int(flags) & Int(1 << 17) != 0 {serializeInt64(groupedId!, buffer: buffer, boxed: false)}
|
if Int(flags) & Int(1 << 17) != 0 {serializeInt64(groupedId!, buffer: buffer, boxed: false)}
|
||||||
@ -17061,8 +17128,8 @@ public extension Api {
|
|||||||
return ("messageEmpty", [("id", id)])
|
return ("messageEmpty", [("id", id)])
|
||||||
case .messageService(let flags, let id, let fromId, let toId, let replyToMsgId, let date, let action):
|
case .messageService(let flags, let id, let fromId, let toId, let replyToMsgId, let date, let action):
|
||||||
return ("messageService", [("flags", flags), ("id", id), ("fromId", fromId), ("toId", toId), ("replyToMsgId", replyToMsgId), ("date", date), ("action", action)])
|
return ("messageService", [("flags", flags), ("id", id), ("fromId", fromId), ("toId", toId), ("replyToMsgId", replyToMsgId), ("date", date), ("action", action)])
|
||||||
case .message(let flags, let id, let fromId, let toId, let fwdFrom, let viaBotId, let replyToMsgId, let date, let message, let media, let replyMarkup, let entities, let views, let editDate, let postAuthor, let groupedId, let restrictionReason):
|
case .message(let flags, let id, let fromId, let toId, let fwdFrom, let viaBotId, let replyToMsgId, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let editDate, let postAuthor, let groupedId, let restrictionReason):
|
||||||
return ("message", [("flags", flags), ("id", id), ("fromId", fromId), ("toId", toId), ("fwdFrom", fwdFrom), ("viaBotId", viaBotId), ("replyToMsgId", replyToMsgId), ("date", date), ("message", message), ("media", media), ("replyMarkup", replyMarkup), ("entities", entities), ("views", views), ("editDate", editDate), ("postAuthor", postAuthor), ("groupedId", groupedId), ("restrictionReason", restrictionReason)])
|
return ("message", [("flags", flags), ("id", id), ("fromId", fromId), ("toId", toId), ("fwdFrom", fwdFrom), ("viaBotId", viaBotId), ("replyToMsgId", replyToMsgId), ("date", date), ("message", message), ("media", media), ("replyMarkup", replyMarkup), ("entities", entities), ("views", views), ("forwards", forwards), ("editDate", editDate), ("postAuthor", postAuthor), ("groupedId", groupedId), ("restrictionReason", restrictionReason)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17148,14 +17215,16 @@ public extension Api {
|
|||||||
var _13: Int32?
|
var _13: Int32?
|
||||||
if Int(_1!) & Int(1 << 10) != 0 {_13 = reader.readInt32() }
|
if Int(_1!) & Int(1 << 10) != 0 {_13 = reader.readInt32() }
|
||||||
var _14: Int32?
|
var _14: Int32?
|
||||||
if Int(_1!) & Int(1 << 15) != 0 {_14 = reader.readInt32() }
|
if Int(_1!) & Int(1 << 10) != 0 {_14 = reader.readInt32() }
|
||||||
var _15: String?
|
var _15: Int32?
|
||||||
if Int(_1!) & Int(1 << 16) != 0 {_15 = parseString(reader) }
|
if Int(_1!) & Int(1 << 15) != 0 {_15 = reader.readInt32() }
|
||||||
var _16: Int64?
|
var _16: String?
|
||||||
if Int(_1!) & Int(1 << 17) != 0 {_16 = reader.readInt64() }
|
if Int(_1!) & Int(1 << 16) != 0 {_16 = parseString(reader) }
|
||||||
var _17: [Api.RestrictionReason]?
|
var _17: Int64?
|
||||||
|
if Int(_1!) & Int(1 << 17) != 0 {_17 = reader.readInt64() }
|
||||||
|
var _18: [Api.RestrictionReason]?
|
||||||
if Int(_1!) & Int(1 << 22) != 0 {if let _ = reader.readInt32() {
|
if Int(_1!) & Int(1 << 22) != 0 {if let _ = reader.readInt32() {
|
||||||
_17 = Api.parseVector(reader, elementSignature: 0, elementType: Api.RestrictionReason.self)
|
_18 = Api.parseVector(reader, elementSignature: 0, elementType: Api.RestrictionReason.self)
|
||||||
} }
|
} }
|
||||||
let _c1 = _1 != nil
|
let _c1 = _1 != nil
|
||||||
let _c2 = _2 != nil
|
let _c2 = _2 != nil
|
||||||
@ -17170,12 +17239,13 @@ public extension Api {
|
|||||||
let _c11 = (Int(_1!) & Int(1 << 6) == 0) || _11 != nil
|
let _c11 = (Int(_1!) & Int(1 << 6) == 0) || _11 != nil
|
||||||
let _c12 = (Int(_1!) & Int(1 << 7) == 0) || _12 != nil
|
let _c12 = (Int(_1!) & Int(1 << 7) == 0) || _12 != nil
|
||||||
let _c13 = (Int(_1!) & Int(1 << 10) == 0) || _13 != nil
|
let _c13 = (Int(_1!) & Int(1 << 10) == 0) || _13 != nil
|
||||||
let _c14 = (Int(_1!) & Int(1 << 15) == 0) || _14 != nil
|
let _c14 = (Int(_1!) & Int(1 << 10) == 0) || _14 != nil
|
||||||
let _c15 = (Int(_1!) & Int(1 << 16) == 0) || _15 != nil
|
let _c15 = (Int(_1!) & Int(1 << 15) == 0) || _15 != nil
|
||||||
let _c16 = (Int(_1!) & Int(1 << 17) == 0) || _16 != nil
|
let _c16 = (Int(_1!) & Int(1 << 16) == 0) || _16 != nil
|
||||||
let _c17 = (Int(_1!) & Int(1 << 22) == 0) || _17 != nil
|
let _c17 = (Int(_1!) & Int(1 << 17) == 0) || _17 != nil
|
||||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 {
|
let _c18 = (Int(_1!) & Int(1 << 22) == 0) || _18 != nil
|
||||||
return Api.Message.message(flags: _1!, id: _2!, fromId: _3, toId: _4!, fwdFrom: _5, viaBotId: _6, replyToMsgId: _7, date: _8!, message: _9!, media: _10, replyMarkup: _11, entities: _12, views: _13, editDate: _14, postAuthor: _15, groupedId: _16, restrictionReason: _17)
|
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 {
|
||||||
|
return Api.Message.message(flags: _1!, id: _2!, fromId: _3, toId: _4!, fwdFrom: _5, viaBotId: _6, replyToMsgId: _7, date: _8!, message: _9!, media: _10, replyMarkup: _11, entities: _12, views: _13, forwards: _14, editDate: _15, postAuthor: _16, groupedId: _17, restrictionReason: _18)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -810,6 +810,42 @@ public struct stats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
public enum MessageStats: TypeConstructorDescription {
|
||||||
|
case messageStats(viewsGraph: Api.StatsGraph)
|
||||||
|
|
||||||
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
|
switch self {
|
||||||
|
case .messageStats(let viewsGraph):
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(-1986399595)
|
||||||
|
}
|
||||||
|
viewsGraph.serialize(buffer, true)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||||
|
switch self {
|
||||||
|
case .messageStats(let viewsGraph):
|
||||||
|
return ("messageStats", [("viewsGraph", viewsGraph)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func parse_messageStats(_ reader: BufferReader) -> MessageStats? {
|
||||||
|
var _1: Api.StatsGraph?
|
||||||
|
if let signature = reader.readInt32() {
|
||||||
|
_1 = Api.parse(reader, signature: signature) as? Api.StatsGraph
|
||||||
|
}
|
||||||
|
let _c1 = _1 != nil
|
||||||
|
if _c1 {
|
||||||
|
return Api.stats.MessageStats.messageStats(viewsGraph: _1!)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public extension Api {
|
public extension Api {
|
||||||
|
@ -2243,26 +2243,6 @@ public extension Api {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getMessagesViews(peer: Api.InputPeer, id: [Int32], increment: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Int32]>) {
|
|
||||||
let buffer = Buffer()
|
|
||||||
buffer.appendInt32(-993483427)
|
|
||||||
peer.serialize(buffer, true)
|
|
||||||
buffer.appendInt32(481674261)
|
|
||||||
buffer.appendInt32(Int32(id.count))
|
|
||||||
for item in id {
|
|
||||||
serializeInt32(item, buffer: buffer, boxed: false)
|
|
||||||
}
|
|
||||||
increment.serialize(buffer, true)
|
|
||||||
return (FunctionDescription(name: "messages.getMessagesViews", parameters: [("peer", peer), ("id", id), ("increment", increment)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Int32]? in
|
|
||||||
let reader = BufferReader(buffer)
|
|
||||||
var result: [Int32]?
|
|
||||||
if let _ = reader.readInt32() {
|
|
||||||
result = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func editChatAdmin(chatId: Int32, userId: Api.InputUser, isAdmin: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
public static func editChatAdmin(chatId: Int32, userId: Api.InputUser, isAdmin: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||||
let buffer = Buffer()
|
let buffer = Buffer()
|
||||||
buffer.appendInt32(-1444503762)
|
buffer.appendInt32(-1444503762)
|
||||||
@ -3718,6 +3698,26 @@ public extension Api {
|
|||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func getMessagesViews(peer: Api.InputPeer, id: [Int32], increment: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.MessageViews]>) {
|
||||||
|
let buffer = Buffer()
|
||||||
|
buffer.appendInt32(-39035462)
|
||||||
|
peer.serialize(buffer, true)
|
||||||
|
buffer.appendInt32(481674261)
|
||||||
|
buffer.appendInt32(Int32(id.count))
|
||||||
|
for item in id {
|
||||||
|
serializeInt32(item, buffer: buffer, boxed: false)
|
||||||
|
}
|
||||||
|
increment.serialize(buffer, true)
|
||||||
|
return (FunctionDescription(name: "messages.getMessagesViews", parameters: [("peer", peer), ("id", id), ("increment", increment)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.MessageViews]? in
|
||||||
|
let reader = BufferReader(buffer)
|
||||||
|
var result: [Api.MessageViews]?
|
||||||
|
if let _ = reader.readInt32() {
|
||||||
|
result = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageViews.self)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public struct channels {
|
public struct channels {
|
||||||
public static func readHistory(channel: Api.InputChannel, maxId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
public static func readHistory(channel: Api.InputChannel, maxId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||||
@ -4450,6 +4450,41 @@ public extension Api {
|
|||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func getMessagePublicForwards(channel: Api.InputChannel, msgId: Int32, offsetRate: Int32, offsetPeer: Api.InputPeer, offsetId: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.Messages>) {
|
||||||
|
let buffer = Buffer()
|
||||||
|
buffer.appendInt32(1445996571)
|
||||||
|
channel.serialize(buffer, true)
|
||||||
|
serializeInt32(msgId, buffer: buffer, boxed: false)
|
||||||
|
serializeInt32(offsetRate, buffer: buffer, boxed: false)
|
||||||
|
offsetPeer.serialize(buffer, true)
|
||||||
|
serializeInt32(offsetId, buffer: buffer, boxed: false)
|
||||||
|
serializeInt32(limit, buffer: buffer, boxed: false)
|
||||||
|
return (FunctionDescription(name: "stats.getMessagePublicForwards", parameters: [("channel", channel), ("msgId", msgId), ("offsetRate", offsetRate), ("offsetPeer", offsetPeer), ("offsetId", offsetId), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Messages? in
|
||||||
|
let reader = BufferReader(buffer)
|
||||||
|
var result: Api.messages.Messages?
|
||||||
|
if let signature = reader.readInt32() {
|
||||||
|
result = Api.parse(reader, signature: signature) as? Api.messages.Messages
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func getMessageStats(flags: Int32, channel: Api.InputChannel, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.stats.MessageStats>) {
|
||||||
|
let buffer = Buffer()
|
||||||
|
buffer.appendInt32(-1226791947)
|
||||||
|
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||||
|
channel.serialize(buffer, true)
|
||||||
|
serializeInt32(msgId, buffer: buffer, boxed: false)
|
||||||
|
return (FunctionDescription(name: "stats.getMessageStats", parameters: [("flags", flags), ("channel", channel), ("msgId", msgId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.MessageStats? in
|
||||||
|
let reader = BufferReader(buffer)
|
||||||
|
var result: Api.stats.MessageStats?
|
||||||
|
if let signature = reader.readInt32() {
|
||||||
|
result = Api.parse(reader, signature: signature) as? Api.stats.MessageStats
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public struct auth {
|
public struct auth {
|
||||||
public static func checkPhone(phoneNumber: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.auth.CheckedPhone>) {
|
public static func checkPhone(phoneNumber: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.auth.CheckedPhone>) {
|
||||||
|
@ -94,6 +94,7 @@ enum AccountStateMutationOperation {
|
|||||||
case UpdatePinnedItemIds(PeerGroupId, AccountStateUpdatePinnedItemIdsOperation)
|
case UpdatePinnedItemIds(PeerGroupId, AccountStateUpdatePinnedItemIdsOperation)
|
||||||
case ReadMessageContents((PeerId?, [Int32]))
|
case ReadMessageContents((PeerId?, [Int32]))
|
||||||
case UpdateMessageImpressionCount(MessageId, Int32)
|
case UpdateMessageImpressionCount(MessageId, Int32)
|
||||||
|
case UpdateMessageForwardsCount(MessageId, Int32)
|
||||||
case UpdateInstalledStickerPacks(AccountStateUpdateStickerPacksOperation)
|
case UpdateInstalledStickerPacks(AccountStateUpdateStickerPacksOperation)
|
||||||
case UpdateRecentGifs
|
case UpdateRecentGifs
|
||||||
case UpdateChatInputState(PeerId, SynchronizeableChatInputState?)
|
case UpdateChatInputState(PeerId, SynchronizeableChatInputState?)
|
||||||
@ -435,6 +436,10 @@ struct AccountMutableState {
|
|||||||
self.addOperation(.UpdateMessageImpressionCount(id, count))
|
self.addOperation(.UpdateMessageImpressionCount(id, count))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutating func addUpdateMessageForwardsCount(id: MessageId, count: Int32) {
|
||||||
|
self.addOperation(.UpdateMessageForwardsCount(id, count))
|
||||||
|
}
|
||||||
|
|
||||||
mutating func addUpdateInstalledStickerPacks(_ operation: AccountStateUpdateStickerPacksOperation) {
|
mutating func addUpdateInstalledStickerPacks(_ operation: AccountStateUpdateStickerPacksOperation) {
|
||||||
self.addOperation(.UpdateInstalledStickerPacks(operation))
|
self.addOperation(.UpdateInstalledStickerPacks(operation))
|
||||||
}
|
}
|
||||||
@ -469,7 +474,7 @@ struct AccountMutableState {
|
|||||||
|
|
||||||
mutating func addOperation(_ operation: AccountStateMutationOperation) {
|
mutating func addOperation(_ operation: AccountStateMutationOperation) {
|
||||||
switch operation {
|
switch operation {
|
||||||
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll/*, .UpdateMessageReactions*/, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter:
|
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll/*, .UpdateMessageReactions*/, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter:
|
||||||
break
|
break
|
||||||
case let .AddMessages(messages, location):
|
case let .AddMessages(messages, location):
|
||||||
for message in messages {
|
for message in messages {
|
||||||
|
@ -35,6 +35,7 @@ private var declaredEncodables: Void = {
|
|||||||
declareEncodable(CloudDocumentMediaResource.self, f: { CloudDocumentMediaResource(decoder: $0) })
|
declareEncodable(CloudDocumentMediaResource.self, f: { CloudDocumentMediaResource(decoder: $0) })
|
||||||
declareEncodable(TelegramMediaWebpage.self, f: { TelegramMediaWebpage(decoder: $0) })
|
declareEncodable(TelegramMediaWebpage.self, f: { TelegramMediaWebpage(decoder: $0) })
|
||||||
declareEncodable(ViewCountMessageAttribute.self, f: { ViewCountMessageAttribute(decoder: $0) })
|
declareEncodable(ViewCountMessageAttribute.self, f: { ViewCountMessageAttribute(decoder: $0) })
|
||||||
|
declareEncodable(ForwardCountMessageAttribute.self, f: { ForwardCountMessageAttribute(decoder: $0) })
|
||||||
declareEncodable(NotificationInfoMessageAttribute.self, f: { NotificationInfoMessageAttribute(decoder: $0) })
|
declareEncodable(NotificationInfoMessageAttribute.self, f: { NotificationInfoMessageAttribute(decoder: $0) })
|
||||||
declareEncodable(TelegramMediaAction.self, f: { TelegramMediaAction(decoder: $0) })
|
declareEncodable(TelegramMediaAction.self, f: { TelegramMediaAction(decoder: $0) })
|
||||||
declareEncodable(TelegramPeerNotificationSettings.self, f: { TelegramPeerNotificationSettings(decoder: $0) })
|
declareEncodable(TelegramPeerNotificationSettings.self, f: { TelegramPeerNotificationSettings(decoder: $0) })
|
||||||
|
@ -1253,6 +1253,8 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo
|
|||||||
updatedState.addReadMessagesContents((PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), messages))
|
updatedState.addReadMessagesContents((PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), messages))
|
||||||
case let .updateChannelMessageViews(channelId, id, views):
|
case let .updateChannelMessageViews(channelId, id, views):
|
||||||
updatedState.addUpdateMessageImpressionCount(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), namespace: Namespaces.Message.Cloud, id: id), count: views)
|
updatedState.addUpdateMessageImpressionCount(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), namespace: Namespaces.Message.Cloud, id: id), count: views)
|
||||||
|
case let .updateChannelMessageForwards(channelId, id, forwards):
|
||||||
|
updatedState.addUpdateMessageForwardsCount(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), namespace: Namespaces.Message.Cloud, id: id), count: forwards)
|
||||||
case let .updateNewStickerSet(stickerset):
|
case let .updateNewStickerSet(stickerset):
|
||||||
updatedState.addUpdateInstalledStickerPacks(.add(stickerset))
|
updatedState.addUpdateInstalledStickerPacks(.add(stickerset))
|
||||||
case let .updateStickerSetsOrder(flags, order):
|
case let .updateStickerSetsOrder(flags, order):
|
||||||
@ -1896,6 +1898,8 @@ private func pollChannel(network: Network, peer: Peer, state: AccountMutableStat
|
|||||||
updatedState.addReadMessagesContents((peer.id, messages))
|
updatedState.addReadMessagesContents((peer.id, messages))
|
||||||
case let .updateChannelMessageViews(_, id, views):
|
case let .updateChannelMessageViews(_, id, views):
|
||||||
updatedState.addUpdateMessageImpressionCount(id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id), count: views)
|
updatedState.addUpdateMessageImpressionCount(id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id), count: views)
|
||||||
|
case let .updateChannelMessageForwards(_, id, views):
|
||||||
|
updatedState.addUpdateMessageForwardsCount(id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id), count: views)
|
||||||
case let .updateChannelWebPage(_, apiWebpage, _, _):
|
case let .updateChannelWebPage(_, apiWebpage, _, _):
|
||||||
switch apiWebpage {
|
switch apiWebpage {
|
||||||
case let .webPageEmpty(id):
|
case let .webPageEmpty(id):
|
||||||
@ -2090,7 +2094,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation])
|
|||||||
var currentAddScheduledMessages: OptimizeAddMessagesState?
|
var currentAddScheduledMessages: OptimizeAddMessagesState?
|
||||||
for operation in operations {
|
for operation in operations {
|
||||||
switch operation {
|
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, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder:
|
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, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder:
|
||||||
if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty {
|
if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty {
|
||||||
result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location))
|
result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location))
|
||||||
}
|
}
|
||||||
@ -2773,6 +2777,21 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP
|
|||||||
}
|
}
|
||||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||||
})
|
})
|
||||||
|
case let .UpdateMessageForwardsCount(id, count):
|
||||||
|
transaction.updateMessage(id, update: { currentMessage in
|
||||||
|
var storeForwardInfo: StoreMessageForwardInfo?
|
||||||
|
if let forwardInfo = currentMessage.forwardInfo {
|
||||||
|
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
|
||||||
|
}
|
||||||
|
var attributes = currentMessage.attributes
|
||||||
|
loop: for j in 0 ..< attributes.count {
|
||||||
|
if let attribute = attributes[j] as? ForwardCountMessageAttribute {
|
||||||
|
attributes[j] = ForwardCountMessageAttribute(count: max(attribute.count, Int(count)))
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||||
|
})
|
||||||
case let .UpdateInstalledStickerPacks(operation):
|
case let .UpdateInstalledStickerPacks(operation):
|
||||||
stickerPackOperations.append(operation)
|
stickerPackOperations.append(operation)
|
||||||
case .UpdateRecentGifs:
|
case .UpdateRecentGifs:
|
||||||
|
@ -591,7 +591,7 @@ public final class AccountViewTracker {
|
|||||||
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
|
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
|
||||||
return account.network.request(Api.functions.messages.getMessagesViews(peer: inputPeer, id: messageIds.map { $0.id }, increment: .boolTrue))
|
return account.network.request(Api.functions.messages.getMessagesViews(peer: inputPeer, id: messageIds.map { $0.id }, increment: .boolTrue))
|
||||||
|> map(Optional.init)
|
|> map(Optional.init)
|
||||||
|> `catch` { _ -> Signal<[Int32]?, NoError> in
|
|> `catch` { _ -> Signal<[Api.MessageViews]?, NoError> in
|
||||||
return .single(nil)
|
return .single(nil)
|
||||||
}
|
}
|
||||||
|> mapToSignal { viewCounts -> Signal<Void, NoError> in
|
|> mapToSignal { viewCounts -> Signal<Void, NoError> in
|
||||||
@ -599,20 +599,21 @@ public final class AccountViewTracker {
|
|||||||
return account.postbox.transaction { transaction -> Void in
|
return account.postbox.transaction { transaction -> Void in
|
||||||
for i in 0 ..< messageIds.count {
|
for i in 0 ..< messageIds.count {
|
||||||
if i < viewCounts.count {
|
if i < viewCounts.count {
|
||||||
transaction.updateMessage(messageIds[i], update: { currentMessage in
|
if case let .messageViews(views, forwards) = viewCounts[i] {
|
||||||
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
|
transaction.updateMessage(messageIds[i], update: { currentMessage in
|
||||||
var attributes = currentMessage.attributes
|
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
|
||||||
loop: for j in 0 ..< attributes.count {
|
var attributes = currentMessage.attributes
|
||||||
if let attribute = attributes[j] as? ViewCountMessageAttribute {
|
loop: for j in 0 ..< attributes.count {
|
||||||
if attribute.count >= Int(viewCounts[i]) {
|
if let attribute = attributes[j] as? ViewCountMessageAttribute {
|
||||||
return .skip
|
attributes[j] = ViewCountMessageAttribute(count: max(attribute.count, Int(views)))
|
||||||
|
}
|
||||||
|
if let _ = attributes[j] as? ForwardCountMessageAttribute {
|
||||||
|
attributes[j] = ForwardCountMessageAttribute(count: Int(forwards))
|
||||||
}
|
}
|
||||||
attributes[j] = ViewCountMessageAttribute(count: max(attribute.count, Int(viewCounts[i])))
|
|
||||||
break loop
|
|
||||||
}
|
}
|
||||||
}
|
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
})
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
194
submodules/TelegramCore/Sources/MessageStatistics.swift
Normal file
194
submodules/TelegramCore/Sources/MessageStatistics.swift
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftSignalKit
|
||||||
|
import Postbox
|
||||||
|
import TelegramApi
|
||||||
|
import MtProtoKit
|
||||||
|
import SyncCore
|
||||||
|
|
||||||
|
//stats.getMessagePublicForwards channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
|
||||||
|
|
||||||
|
public struct MessageStats: Equatable {
|
||||||
|
public let views: Int
|
||||||
|
public let publicForwards: Int
|
||||||
|
public let privateForwards: Int
|
||||||
|
public let interactionsGraph: StatsGraph
|
||||||
|
|
||||||
|
init(views: Int, publicForwards: Int, privateForwards: Int, interactionsGraph: StatsGraph) {
|
||||||
|
self.views = views
|
||||||
|
self.publicForwards = publicForwards
|
||||||
|
self.privateForwards = privateForwards
|
||||||
|
self.interactionsGraph = interactionsGraph
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func == (lhs: MessageStats, rhs: MessageStats) -> Bool {
|
||||||
|
if lhs.views != rhs.views {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.publicForwards != rhs.publicForwards {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.privateForwards != rhs.privateForwards {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.interactionsGraph != rhs.interactionsGraph {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public func withUpdatedInteractionsGraph(_ interactionsGraph: StatsGraph) -> MessageStats {
|
||||||
|
return MessageStats(views: self.views, publicForwards: self.publicForwards, privateForwards: self.privateForwards, interactionsGraph: self.interactionsGraph)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct MessageStatsContextState: Equatable {
|
||||||
|
public var stats: MessageStats?
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestMessageStats(postbox: Postbox, network: Network, datacenterId: Int32, messageId: MessageId, dark: Bool = false) -> Signal<MessageStats?, NoError> {
|
||||||
|
return postbox.transaction { transaction -> (Peer, Message)? in
|
||||||
|
if let peer = transaction.getPeer(messageId.peerId), let message = transaction.getMessage(messageId) {
|
||||||
|
return (peer, message)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} |> mapToSignal { peerAndMessage -> Signal<MessageStats?, NoError> in
|
||||||
|
guard let (peer, message) = peerAndMessage, let inputChannel = apiInputChannel(peer) else {
|
||||||
|
return .never()
|
||||||
|
}
|
||||||
|
|
||||||
|
var flags: Int32 = 0
|
||||||
|
if dark {
|
||||||
|
flags |= (1 << 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = Api.functions.stats.getMessageStats(flags: flags, channel: inputChannel, msgId: messageId.id)
|
||||||
|
let signal: Signal<Api.stats.MessageStats, MTRpcError>
|
||||||
|
if network.datacenterId != datacenterId {
|
||||||
|
signal = network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil)
|
||||||
|
|> castError(MTRpcError.self)
|
||||||
|
|> mapToSignal { worker in
|
||||||
|
return worker.request(request)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
signal = network.request(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
var views: Int = 0
|
||||||
|
var forwards: Int = 0
|
||||||
|
for attribute in message.attributes {
|
||||||
|
if let viewsAttribute = attribute as? ViewCountMessageAttribute {
|
||||||
|
views = viewsAttribute.count
|
||||||
|
} else if let forwardsAttribute = attribute as? ForwardCountMessageAttribute {
|
||||||
|
forwards = forwardsAttribute.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return signal
|
||||||
|
|> map { result -> MessageStats? in
|
||||||
|
if case let .messageStats(apiViewsGraph) = result {
|
||||||
|
return MessageStats(views: views, publicForwards: forwards, privateForwards: forwards, interactionsGraph: StatsGraph(apiStatsGraph: apiViewsGraph))
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> retryRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class MessageStatsContextImpl {
|
||||||
|
private let postbox: Postbox
|
||||||
|
private let network: Network
|
||||||
|
private let datacenterId: Int32
|
||||||
|
private let messageId: MessageId
|
||||||
|
|
||||||
|
private var _state: MessageStatsContextState {
|
||||||
|
didSet {
|
||||||
|
if self._state != oldValue {
|
||||||
|
self._statePromise.set(.single(self._state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private let _statePromise = Promise<MessageStatsContextState>()
|
||||||
|
var state: Signal<MessageStatsContextState, NoError> {
|
||||||
|
return self._statePromise.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private let disposable = MetaDisposable()
|
||||||
|
private let disposables = DisposableDict<String>()
|
||||||
|
|
||||||
|
init(postbox: Postbox, network: Network, datacenterId: Int32, messageId: MessageId) {
|
||||||
|
assert(Queue.mainQueue().isCurrent())
|
||||||
|
|
||||||
|
self.postbox = postbox
|
||||||
|
self.network = network
|
||||||
|
self.datacenterId = datacenterId
|
||||||
|
self.messageId = messageId
|
||||||
|
self._state = MessageStatsContextState(stats: nil)
|
||||||
|
self._statePromise.set(.single(self._state))
|
||||||
|
|
||||||
|
self.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
assert(Queue.mainQueue().isCurrent())
|
||||||
|
self.disposable.dispose()
|
||||||
|
self.disposables.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func load() {
|
||||||
|
assert(Queue.mainQueue().isCurrent())
|
||||||
|
|
||||||
|
self.disposable.set((requestMessageStats(postbox: self.postbox, network: self.network, datacenterId: self.datacenterId, messageId: self.messageId)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] stats in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf._state = MessageStatsContextState(stats: stats)
|
||||||
|
strongSelf._statePromise.set(.single(strongSelf._state))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal<StatsGraph?, NoError> {
|
||||||
|
if let token = graph.token {
|
||||||
|
return requestGraph(network: self.network, datacenterId: self.datacenterId, token: token, x: x)
|
||||||
|
} else {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class MessageStatsContext {
|
||||||
|
private let impl: QueueLocalObject<MessageStatsContextImpl>
|
||||||
|
|
||||||
|
public var state: Signal<MessageStatsContextState, NoError> {
|
||||||
|
return Signal { subscriber in
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
self.impl.with { impl in
|
||||||
|
disposable.set(impl.state.start(next: { value in
|
||||||
|
subscriber.putNext(value)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(postbox: Postbox, network: Network, datacenterId: Int32, messageId: MessageId) {
|
||||||
|
self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: {
|
||||||
|
return MessageStatsContextImpl(postbox: postbox, network: network, datacenterId: datacenterId, messageId: messageId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal<StatsGraph?, NoError> {
|
||||||
|
return Signal { subscriber in
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
self.impl.with { impl in
|
||||||
|
disposable.set(impl.loadDetailedGraph(graph, x: x).start(next: { value in
|
||||||
|
subscriber.putNext(value)
|
||||||
|
subscriber.putCompletion()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -193,15 +193,16 @@ private func requestChannelStats(postbox: Postbox, network: Network, datacenterI
|
|||||||
flags |= (1 << 1)
|
flags |= (1 << 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let request = Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel)
|
||||||
let signal: Signal<Api.stats.BroadcastStats, MTRpcError>
|
let signal: Signal<Api.stats.BroadcastStats, MTRpcError>
|
||||||
if network.datacenterId != datacenterId {
|
if network.datacenterId != datacenterId {
|
||||||
signal = network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil)
|
signal = network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil)
|
||||||
|> castError(MTRpcError.self)
|
|> castError(MTRpcError.self)
|
||||||
|> mapToSignal { worker in
|
|> mapToSignal { worker in
|
||||||
return worker.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel))
|
return worker.request(request)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
signal = network.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel))
|
signal = network.request(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
return signal
|
return signal
|
||||||
@ -212,7 +213,7 @@ private func requestChannelStats(postbox: Postbox, network: Network, datacenterI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func requestGraph(network: Network, datacenterId: Int32, token: String, x: Int64? = nil) -> Signal<StatsGraph?, NoError> {
|
func requestGraph(network: Network, datacenterId: Int32, token: String, x: Int64? = nil) -> Signal<StatsGraph?, NoError> {
|
||||||
var flags: Int32 = 0
|
var flags: Int32 = 0
|
||||||
if let _ = x {
|
if let _ = x {
|
||||||
flags |= (1 << 0)
|
flags |= (1 << 0)
|
||||||
|
@ -10,6 +10,7 @@ public enum SearchMessagesLocation: Equatable {
|
|||||||
case general
|
case general
|
||||||
case group(PeerGroupId)
|
case group(PeerGroupId)
|
||||||
case peer(peerId: PeerId, fromId: PeerId?, tags: MessageTags?)
|
case peer(peerId: PeerId, fromId: PeerId?, tags: MessageTags?)
|
||||||
|
case publicForwards(MessageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct SearchMessagesPeerState: Equatable {
|
private struct SearchMessagesPeerState: Equatable {
|
||||||
@ -282,7 +283,7 @@ public func searchMessages(account: Account, location: SearchMessagesLocation, q
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|> mapToSignal { (nextRate, lowerBound, inputPeer) in
|
|> mapToSignal { (nextRate, lowerBound, inputPeer) in
|
||||||
account.network.request(Api.functions.messages.searchGlobal(flags: 0, folderId: nil, q: query, offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit), automaticFloodWait: false)
|
return account.network.request(Api.functions.messages.searchGlobal(flags: 0, folderId: nil, q: query, offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit), automaticFloodWait: false)
|
||||||
|> map { result -> (Api.messages.Messages?, Api.messages.Messages?) in
|
|> map { result -> (Api.messages.Messages?, Api.messages.Messages?) in
|
||||||
return (result, nil)
|
return (result, nil)
|
||||||
}
|
}
|
||||||
@ -290,6 +291,34 @@ public func searchMessages(account: Account, location: SearchMessagesLocation, q
|
|||||||
return .single((nil, nil))
|
return .single((nil, nil))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case let .publicForwards(messageId):
|
||||||
|
remoteSearchResult = account.postbox.transaction { transaction -> (Api.InputChannel?, Int32, MessageIndex?, Api.InputPeer) in
|
||||||
|
let sourcePeer = transaction.getPeer(messageId.peerId)
|
||||||
|
let inputChannel = sourcePeer.flatMap { apiInputChannel($0) }
|
||||||
|
|
||||||
|
var lowerBound: MessageIndex?
|
||||||
|
if let state = state, let message = state.main.messages.last {
|
||||||
|
lowerBound = message.index
|
||||||
|
}
|
||||||
|
if let lowerBound = lowerBound, let peer = transaction.getPeer(lowerBound.id.peerId), let inputPeer = apiInputPeer(peer) {
|
||||||
|
return (inputChannel, state?.main.nextRate ?? 0, lowerBound, inputPeer)
|
||||||
|
} else {
|
||||||
|
return (inputChannel, 0, lowerBound, .inputPeerEmpty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> mapToSignal { (inputChannel, nextRate, lowerBound, inputPeer) in
|
||||||
|
guard let inputChannel = inputChannel else {
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
return account.network.request(Api.functions.stats.getMessagePublicForwards(channel: inputChannel, msgId: messageId.id, offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit), automaticFloodWait: false)
|
||||||
|
|> map { result -> (Api.messages.Messages?, Api.messages.Messages?) in
|
||||||
|
return (result, nil)
|
||||||
|
}
|
||||||
|
|> `catch` { _ -> Signal<(Api.messages.Messages?, Api.messages.Messages?), NoError> in
|
||||||
|
return .single((nil, nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return remoteSearchResult
|
return remoteSearchResult
|
||||||
@ -572,11 +601,11 @@ public func searchMessageIdByTimestamp(account: Account, peerId: PeerId, timesta
|
|||||||
} |> switchToLatest
|
} |> switchToLatest
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UpdatedRemotePeerError {
|
public enum UpdatedRemotePeerError {
|
||||||
case generic
|
case generic
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatedRemotePeer(postbox: Postbox, network: Network, peer: PeerReference) -> Signal<Peer, UpdatedRemotePeerError> {
|
public func updatedRemotePeer(postbox: Postbox, network: Network, peer: PeerReference) -> Signal<Peer, UpdatedRemotePeerError> {
|
||||||
if let inputUser = peer.inputUser {
|
if let inputUser = peer.inputUser {
|
||||||
return network.request(Api.functions.users.getUsers(id: [inputUser]))
|
return network.request(Api.functions.users.getUsers(id: [inputUser]))
|
||||||
|> mapError { _ -> UpdatedRemotePeerError in
|
|> mapError { _ -> UpdatedRemotePeerError in
|
||||||
|
@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
|
|||||||
|
|
||||||
public class Serialization: NSObject, MTSerialization {
|
public class Serialization: NSObject, MTSerialization {
|
||||||
public func currentLayer() -> UInt {
|
public func currentLayer() -> UInt {
|
||||||
return 116
|
return 117
|
||||||
}
|
}
|
||||||
|
|
||||||
public func parseMessage(_ data: Data!) -> Any! {
|
public func parseMessage(_ data: Data!) -> Any! {
|
||||||
|
@ -136,7 +136,7 @@ func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? {
|
|||||||
|
|
||||||
func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
|
func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
|
||||||
switch message {
|
switch message {
|
||||||
case let .message(flags, _, fromId, toId, fwdHeader, viaBotId, _, _, _, media, _, entities, _, _, _, _, _):
|
case let .message(flags, _, fromId, toId, fwdHeader, viaBotId, _, _, _, media, _, entities, _, _, _, _, _, _):
|
||||||
let peerId: PeerId
|
let peerId: PeerId
|
||||||
switch toId {
|
switch toId {
|
||||||
case let .peerUser(userId):
|
case let .peerUser(userId):
|
||||||
@ -240,7 +240,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
|
|||||||
|
|
||||||
func apiMessageAssociatedMessageIds(_ message: Api.Message) -> [MessageId]? {
|
func apiMessageAssociatedMessageIds(_ message: Api.Message) -> [MessageId]? {
|
||||||
switch message {
|
switch message {
|
||||||
case let .message(flags, _, fromId, toId, _, _, replyToMsgId, _, _, _, _, _, _, _, _, _, _):
|
case let .message(flags, _, fromId, toId, _, _, replyToMsgId, _, _, _, _, _, _, _, _, _, _, _):
|
||||||
if let replyToMsgId = replyToMsgId {
|
if let replyToMsgId = replyToMsgId {
|
||||||
let peerId: PeerId
|
let peerId: PeerId
|
||||||
switch toId {
|
switch toId {
|
||||||
@ -398,7 +398,7 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes
|
|||||||
extension StoreMessage {
|
extension StoreMessage {
|
||||||
convenience init?(apiMessage: Api.Message, namespace: MessageId.Namespace = Namespaces.Message.Cloud) {
|
convenience init?(apiMessage: Api.Message, namespace: MessageId.Namespace = Namespaces.Message.Cloud) {
|
||||||
switch apiMessage {
|
switch apiMessage {
|
||||||
case let .message(flags, id, fromId, toId, fwdFrom, viaBotId, replyToMsgId, date, message, media, replyMarkup, entities, views, editDate, postAuthor, groupingId, restrictionReason):
|
case let .message(flags, id, fromId, toId, fwdFrom, viaBotId, replyToMsgId, date, message, media, replyMarkup, entities, views, forwards, editDate, postAuthor, groupingId, restrictionReason):
|
||||||
let peerId: PeerId
|
let peerId: PeerId
|
||||||
var authorId: PeerId?
|
var authorId: PeerId?
|
||||||
switch toId {
|
switch toId {
|
||||||
@ -521,6 +521,10 @@ extension StoreMessage {
|
|||||||
attributes.append(ViewCountMessageAttribute(count: Int(views)))
|
attributes.append(ViewCountMessageAttribute(count: Int(views)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let forwards = forwards, namespace != Namespaces.Message.ScheduledCloud {
|
||||||
|
attributes.append(ForwardCountMessageAttribute(count: Int(forwards)))
|
||||||
|
}
|
||||||
|
|
||||||
if let editDate = editDate {
|
if let editDate = editDate {
|
||||||
attributes.append(EditedMessageAttribute(date: editDate, isHidden: (flags & (1 << 21)) != 0))
|
attributes.append(EditedMessageAttribute(date: editDate, isHidden: (flags & (1 << 21)) != 0))
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ class UpdateMessageService: NSObject, MTMessageService {
|
|||||||
self.putNext(groups)
|
self.putNext(groups)
|
||||||
}
|
}
|
||||||
case let .updateShortChatMessage(flags, id, fromId, chatId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyToMsgId, entities):
|
case let .updateShortChatMessage(flags, id, fromId, chatId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyToMsgId, entities):
|
||||||
let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: fromId, toId: Api.Peer.peerChat(chatId: chatId), fwdFrom: fwdFrom, viaBotId: viaBotId, replyToMsgId: replyToMsgId, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, editDate: nil, postAuthor: nil, groupedId: nil, restrictionReason: nil)
|
let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: fromId, toId: Api.Peer.peerChat(chatId: chatId), fwdFrom: fwdFrom, viaBotId: viaBotId, replyToMsgId: replyToMsgId, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, editDate: nil, postAuthor: nil, groupedId: nil, restrictionReason: nil)
|
||||||
let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount)
|
let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount)
|
||||||
let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
|
let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
|
||||||
if groups.count != 0 {
|
if groups.count != 0 {
|
||||||
@ -75,7 +75,7 @@ class UpdateMessageService: NSObject, MTMessageService {
|
|||||||
generatedToId = Api.Peer.peerUser(userId: self.peerId.id)
|
generatedToId = Api.Peer.peerUser(userId: self.peerId.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: generatedFromId, toId: generatedToId, fwdFrom: fwdFrom, viaBotId: viaBotId, replyToMsgId: replyToMsgId, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, editDate: nil, postAuthor: nil, groupedId: nil, restrictionReason: nil)
|
let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: generatedFromId, toId: generatedToId, fwdFrom: fwdFrom, viaBotId: viaBotId, replyToMsgId: replyToMsgId, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, editDate: nil, postAuthor: nil, groupedId: nil, restrictionReason: nil)
|
||||||
let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount)
|
let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount)
|
||||||
let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
|
let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
|
||||||
if groups.count != 0 {
|
if groups.count != 0 {
|
||||||
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -1164,8 +1164,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
return $0.updatedInputMode(f)
|
return $0.updatedInputMode(f)
|
||||||
})
|
})
|
||||||
}, openMessageShareMenu: { [weak self] id in
|
}, openMessageShareMenu: { [weak self] id in
|
||||||
if let strongSelf = self, let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id) {
|
if let strongSelf = self, let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let message = messages.first {
|
||||||
let shareController = ShareController(context: strongSelf.context, subject: .messages(messages))
|
var shares: Int = 0
|
||||||
|
for attribute in message.attributes {
|
||||||
|
if let forwardsAttribute = attribute as? ForwardCountMessageAttribute {
|
||||||
|
shares = forwardsAttribute.count
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let shareController = ShareController(context: strongSelf.context, subject: .messages(messages), openStats: { [weak self] in
|
||||||
|
if let strongSelf = self, let cachedChannelData = strongSelf.peerView?.cachedData as? CachedChannelData {
|
||||||
|
strongSelf.push(messageStatsController(context: strongSelf.context, messageId: id, cachedPeerData: cachedChannelData))
|
||||||
|
}
|
||||||
|
}, shares: shares)
|
||||||
shareController.dismissed = { shared in
|
shareController.dismissed = { shared in
|
||||||
if shared {
|
if shared {
|
||||||
self?.commitPurposefulAction()
|
self?.commitPurposefulAction()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user