Message statistics

This commit is contained in:
Ilya Laktyushin 2020-07-25 00:51:33 +03:00
parent e28ec6b7ca
commit c67feac3ce
28 changed files with 5430 additions and 4393 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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