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.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.";
"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
if let _ = rawEntry.videoRepresentations.last {
deleteTitle = self.presentationData.strings.Settings_RemoveVideo

View File

@ -290,6 +290,8 @@ public final class ShareController: ViewController {
private let presetText: String?
private let switchableAccounts: [AccountWithInfo]
private let immediatePeerId: PeerId?
private let openStats: (() -> Void)?
private let shares: Int?
private let peers = Promise<([(RenderedPeer, PeerPresence?)], Peer)>()
private let peersDisposable = MetaDisposable()
@ -300,11 +302,11 @@ public final class ShareController: ViewController {
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) {
self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, presetText: presetText, preferredAction: preferredAction, showInChat: showInChat, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId)
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, 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.currentContext = currentContext
self.currentAccount = currentContext.account
@ -314,6 +316,8 @@ public final class ShareController: ViewController {
self.immediateExternalShare = immediateExternalShare
self.switchableAccounts = switchableAccounts
self.immediatePeerId = immediatePeerId
self.openStats = openStats
self.shares = shares
self.presentationData = self.sharedContext.currentPresentationData.with { $0 }
@ -437,7 +441,7 @@ public final class ShareController: ViewController {
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))
}, 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?.presentingViewController?.dismiss(animated: false, completion: nil)
self?.dismissed?(shared)
@ -714,6 +718,11 @@ public final class ShareController: ViewController {
strongSelf.view.endEditing(true)
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.peersDisposable.set((self.peers.get()

View File

@ -32,6 +32,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
private let externalShare: Bool
private let immediateExternalShare: Bool
private var immediatePeerId: PeerId?
private let shares: Int?
private let defaultAction: ShareControllerAction?
private let requestLayout: (ContainedViewLayoutTransition) -> Void
@ -61,6 +62,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
var share: ((String, [PeerId]) -> Signal<ShareState, NoError>)?
var shareExternal: (() -> Signal<ShareExternalState, NoError>)?
var switchToAnotherAccount: (() -> Void)?
var openStats: (() -> Void)?
let ready = Promise<Bool>()
private var didSetReady = false
@ -78,12 +80,13 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
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.presentationData = sharedContext.currentPresentationData.with { $0 }
self.externalShare = externalShare
self.immediateExternalShare = immediateExternalShare
self.immediatePeerId = immediatePeerId
self.shares = shares
self.presentError = presentError
self.presetText = presetText
@ -670,7 +673,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
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
self?.switchToAnotherAccount?()
}, extendedInitialReveal: self.presetText != nil)
}, extendedInitialReveal: self.presetText != nil, statsCount: self.shares ?? 0)
self.peersContentNode = peersContentNode
peersContentNode.openSearch = { [weak self] in
let _ = (recentlySearchedPeers(postbox: context.account.postbox)
@ -736,6 +739,14 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
peersContentNode.openShare = {
openShare(false)
}
if let openStats = self.openStats {
peersContentNode.openStats = { [weak self] in
openStats()
self?.animateOut(shared: true, completion: {
self?.dismiss?(true)
})
}
}
if self.immediateExternalShare {
openShare(true)
} else {

View File

@ -81,6 +81,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
private let controllerInteraction: ShareControllerInteraction
private let switchToAnotherAccount: () -> Void
private let extendedInitialReveal: Bool
private let statsCount: Int?
let accountPeer: Peer
private let foundPeers = Promise<[RenderedPeer]>([])
@ -96,11 +97,13 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
private let contentSeparatorNode: ASDisplayNode
private let searchButtonNode: HighlightableButtonNode
private let shareButtonNode: HighlightableButtonNode
private let statsButtonNode: HighlightableButtonNode
private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
var openSearch: (() -> Void)?
var openShare: (() -> Void)?
var openStats: (() -> Void)?
private var ensurePeerVisibleOnLayout: PeerId?
private var validLayout: (CGSize, CGFloat)?
@ -108,7 +111,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
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.context = context
self.theme = theme
@ -118,6 +121,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
self.accountPeer = accountPeer
self.switchToAnotherAccount = switchToAnotherAccount
self.extendedInitialReveal = extendedInitialReveal
self.statsCount = statsCount
self.peersValue.set(.single(peers))
@ -174,6 +178,13 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
self.shareButtonNode = HighlightableButtonNode()
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.isLayerBacked = true
self.contentSeparatorNode.displaysAsynchronously = false
@ -192,6 +203,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
self.addSubnode(self.contentTitleAccountNode)
self.addSubnode(self.searchButtonNode)
self.addSubnode(self.shareButtonNode)
self.addSubnode(self.statsButtonNode)
self.addSubnode(self.contentSeparatorNode)
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.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(_:))))
}
@ -341,6 +354,9 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
self.contentSubtitleNode.frame = originalSubtitleFrame
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 searchButtonFrame = CGRect(origin: CGPoint(x: 12.0, y: titleOffset + 12.0), size: titleButtonSize)
transition.updateFrame(node: self.searchButtonNode, frame: searchButtonFrame)
@ -376,25 +392,34 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
}
func updateSelectedPeers() {
var subtitleText = self.strings.ShareMenu_SelectChats
if !self.controllerInteraction.selectedPeers.isEmpty {
subtitleText = self.controllerInteraction.selectedPeers.reduce("", { string, peer in
let text: String
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
}
})
if let _ = self.openStats, self.controllerInteraction.selectedPeers.isEmpty {
self.statsButtonNode.isHidden = false
self.contentTitleNode.isHidden = true
self.contentSubtitleNode.isHidden = true
} else {
self.statsButtonNode.isHidden = true
self.contentTitleNode.isHidden = false
self.contentSubtitleNode.isHidden = false
var subtitleText = self.strings.ShareMenu_SelectChats
if !self.controllerInteraction.selectedPeers.isEmpty {
subtitleText = self.controllerInteraction.selectedPeers.reduce("", { string, peer in
let text: String
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
if let itemNode = itemNode as? ShareControllerPeerGridItemNode {
itemNode.updateSelection(animated: true)
@ -410,6 +435,10 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
self.openShare?()
}
@objc func statsPressed() {
self.openStats?()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let nodes: [ASDisplayNode] = [self.searchButtonNode, self.shareButtonNode, self.contentTitleAccountNode]
for node in nodes {

View File

@ -5,7 +5,6 @@ import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
import MapKit
import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting

View File

@ -5,7 +5,6 @@ import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
import MapKit
import TelegramPresentationData
import TelegramUIPreferences
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 Postbox
public class ViewCountMessageAttribute: MessageAttribute {
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[889491791] = { return Api.Update.parse_updateDialogFilters($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[1558266229] = { return Api.PopularContact.parse_popularContact($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[-1663561404] = { return Api.messages.Chats.parse_chatsSlice($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[407582158] = { return Api.InputPrivacyRule.parse_inputPrivacyValueAllowAll($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[-2082087340] = { return Api.Message.parse_messageEmpty($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[186120336] = { return Api.messages.RecentStickers.parse_recentStickersNotModified($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[1910543603] = { return Api.messages.Dialogs.parse_dialogsSlice($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[594408994] = { return Api.EmojiKeyword.parse_emojiKeywordDeleted($0) }
dict[-290921362] = { return Api.upload.CdnFile.parse_cdnFileReuploadNeeded($0) }
@ -1103,6 +1106,8 @@ public struct Api {
_1.serialize(buffer, boxed)
case let _1 as Api.InputSingleMedia:
_1.serialize(buffer, boxed)
case let _1 as Api.MessageViews:
_1.serialize(buffer, boxed)
case let _1 as Api.InputPrivacyRule:
_1.serialize(buffer, boxed)
case let _1 as Api.messages.DhConfig:
@ -1363,6 +1368,8 @@ public struct Api {
_1.serialize(buffer, boxed)
case let _1 as Api.messages.Dialogs:
_1.serialize(buffer, boxed)
case let _1 as Api.stats.MessageStats:
_1.serialize(buffer, boxed)
case let _1 as Api.EmojiKeyword:
_1.serialize(buffer, boxed)
case let _1 as Api.upload.CdnFile:

View File

@ -6037,6 +6037,7 @@ public extension Api {
case updateDialogFilterOrder(order: [Int32])
case updateDialogFilters
case updatePhoneCallSignalingData(phoneCallId: Int64, data: Buffer)
case updateChannelMessageForwards(channelId: Int32, id: Int32, forwards: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
@ -6717,6 +6718,14 @@ public extension Api {
serializeInt64(phoneCallId, buffer: buffer, boxed: false)
serializeBytes(data, buffer: buffer, boxed: false)
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", [])
case .updatePhoneCallSignalingData(let phoneCallId, let 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
}
}
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 {
@ -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 {
case inputPrivacyValueAllowContacts
@ -17000,7 +17066,7 @@ public extension Api {
public enum Message: TypeConstructorDescription {
case messageEmpty(id: Int32)
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) {
switch self {
@ -17022,9 +17088,9 @@ public extension Api {
serializeInt32(date, buffer: buffer, boxed: false)
action.serialize(buffer, true)
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 {
buffer.appendInt32(1160515173)
buffer.appendInt32(-181507201)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(id, buffer: buffer, boxed: false)
@ -17043,6 +17109,7 @@ public extension Api {
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(forwards!, 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 << 17) != 0 {serializeInt64(groupedId!, buffer: buffer, boxed: false)}
@ -17061,8 +17128,8 @@ public extension Api {
return ("messageEmpty", [("id", id)])
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)])
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):
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)])
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), ("forwards", forwards), ("editDate", editDate), ("postAuthor", postAuthor), ("groupedId", groupedId), ("restrictionReason", restrictionReason)])
}
}
@ -17148,14 +17215,16 @@ public extension Api {
var _13: Int32?
if Int(_1!) & Int(1 << 10) != 0 {_13 = reader.readInt32() }
var _14: Int32?
if Int(_1!) & Int(1 << 15) != 0 {_14 = reader.readInt32() }
var _15: String?
if Int(_1!) & Int(1 << 16) != 0 {_15 = parseString(reader) }
var _16: Int64?
if Int(_1!) & Int(1 << 17) != 0 {_16 = reader.readInt64() }
var _17: [Api.RestrictionReason]?
if Int(_1!) & Int(1 << 10) != 0 {_14 = reader.readInt32() }
var _15: Int32?
if Int(_1!) & Int(1 << 15) != 0 {_15 = reader.readInt32() }
var _16: String?
if Int(_1!) & Int(1 << 16) != 0 {_16 = parseString(reader) }
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() {
_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 _c2 = _2 != nil
@ -17170,12 +17239,13 @@ public extension Api {
let _c11 = (Int(_1!) & Int(1 << 6) == 0) || _11 != nil
let _c12 = (Int(_1!) & Int(1 << 7) == 0) || _12 != nil
let _c13 = (Int(_1!) & Int(1 << 10) == 0) || _13 != nil
let _c14 = (Int(_1!) & Int(1 << 15) == 0) || _14 != nil
let _c15 = (Int(_1!) & Int(1 << 16) == 0) || _15 != nil
let _c16 = (Int(_1!) & Int(1 << 17) == 0) || _16 != nil
let _c17 = (Int(_1!) & Int(1 << 22) == 0) || _17 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 {
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)
let _c14 = (Int(_1!) & Int(1 << 10) == 0) || _14 != nil
let _c15 = (Int(_1!) & Int(1 << 15) == 0) || _15 != nil
let _c16 = (Int(_1!) & Int(1 << 16) == 0) || _16 != nil
let _c17 = (Int(_1!) & Int(1 << 17) == 0) || _17 != nil
let _c18 = (Int(_1!) & Int(1 << 22) == 0) || _18 != nil
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 {
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 {

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>) {
let buffer = Buffer()
buffer.appendInt32(-1444503762)
@ -3718,6 +3698,26 @@ public extension Api {
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 static func readHistory(channel: Api.InputChannel, maxId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
@ -4450,6 +4450,41 @@ public extension Api {
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 static func checkPhone(phoneNumber: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.auth.CheckedPhone>) {

View File

@ -94,6 +94,7 @@ enum AccountStateMutationOperation {
case UpdatePinnedItemIds(PeerGroupId, AccountStateUpdatePinnedItemIdsOperation)
case ReadMessageContents((PeerId?, [Int32]))
case UpdateMessageImpressionCount(MessageId, Int32)
case UpdateMessageForwardsCount(MessageId, Int32)
case UpdateInstalledStickerPacks(AccountStateUpdateStickerPacksOperation)
case UpdateRecentGifs
case UpdateChatInputState(PeerId, SynchronizeableChatInputState?)
@ -435,6 +436,10 @@ struct AccountMutableState {
self.addOperation(.UpdateMessageImpressionCount(id, count))
}
mutating func addUpdateMessageForwardsCount(id: MessageId, count: Int32) {
self.addOperation(.UpdateMessageForwardsCount(id, count))
}
mutating func addUpdateInstalledStickerPacks(_ operation: AccountStateUpdateStickerPacksOperation) {
self.addOperation(.UpdateInstalledStickerPacks(operation))
}
@ -469,7 +474,7 @@ struct AccountMutableState {
mutating func addOperation(_ operation: AccountStateMutationOperation) {
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
case let .AddMessages(messages, location):
for message in messages {

View File

@ -35,6 +35,7 @@ private var declaredEncodables: Void = {
declareEncodable(CloudDocumentMediaResource.self, f: { CloudDocumentMediaResource(decoder: $0) })
declareEncodable(TelegramMediaWebpage.self, f: { TelegramMediaWebpage(decoder: $0) })
declareEncodable(ViewCountMessageAttribute.self, f: { ViewCountMessageAttribute(decoder: $0) })
declareEncodable(ForwardCountMessageAttribute.self, f: { ForwardCountMessageAttribute(decoder: $0) })
declareEncodable(NotificationInfoMessageAttribute.self, f: { NotificationInfoMessageAttribute(decoder: $0) })
declareEncodable(TelegramMediaAction.self, f: { TelegramMediaAction(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))
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)
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):
updatedState.addUpdateInstalledStickerPacks(.add(stickerset))
case let .updateStickerSetsOrder(flags, order):
@ -1896,6 +1898,8 @@ private func pollChannel(network: Network, peer: Peer, state: AccountMutableStat
updatedState.addReadMessagesContents((peer.id, messages))
case let .updateChannelMessageViews(_, id, 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, _, _):
switch apiWebpage {
case let .webPageEmpty(id):
@ -2090,7 +2094,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation])
var currentAddScheduledMessages: OptimizeAddMessagesState?
for operation in operations {
switch operation {
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll/*, .UpdateMessageReactions*/, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .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 {
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))
})
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):
stickerPackOperations.append(operation)
case .UpdateRecentGifs:

View File

@ -591,7 +591,7 @@ public final class AccountViewTracker {
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))
|> map(Optional.init)
|> `catch` { _ -> Signal<[Int32]?, NoError> in
|> `catch` { _ -> Signal<[Api.MessageViews]?, NoError> in
return .single(nil)
}
|> mapToSignal { viewCounts -> Signal<Void, NoError> in
@ -599,20 +599,21 @@ public final class AccountViewTracker {
return account.postbox.transaction { transaction -> Void in
for i in 0 ..< messageIds.count {
if i < viewCounts.count {
transaction.updateMessage(messageIds[i], update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ViewCountMessageAttribute {
if attribute.count >= Int(viewCounts[i]) {
return .skip
if case let .messageViews(views, forwards) = viewCounts[i] {
transaction.updateMessage(messageIds[i], update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ViewCountMessageAttribute {
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)
}
let request = Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel)
let signal: Signal<Api.stats.BroadcastStats, MTRpcError>
if network.datacenterId != datacenterId {
signal = network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil)
|> castError(MTRpcError.self)
|> mapToSignal { worker in
return worker.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel))
return worker.request(request)
}
} else {
signal = network.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel))
signal = network.request(request)
}
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
if let _ = x {
flags |= (1 << 0)

View File

@ -10,6 +10,7 @@ public enum SearchMessagesLocation: Equatable {
case general
case group(PeerGroupId)
case peer(peerId: PeerId, fromId: PeerId?, tags: MessageTags?)
case publicForwards(MessageId)
}
private struct SearchMessagesPeerState: Equatable {
@ -282,7 +283,7 @@ public func searchMessages(account: Account, location: SearchMessagesLocation, q
}
}
|> 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
return (result, nil)
}
@ -290,6 +291,34 @@ public func searchMessages(account: Account, location: SearchMessagesLocation, q
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
@ -572,11 +601,11 @@ public func searchMessageIdByTimestamp(account: Account, peerId: PeerId, timesta
} |> switchToLatest
}
enum UpdatedRemotePeerError {
public enum UpdatedRemotePeerError {
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 {
return network.request(Api.functions.users.getUsers(id: [inputUser]))
|> mapError { _ -> UpdatedRemotePeerError in

View File

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt {
return 116
return 117
}
public func parseMessage(_ data: Data!) -> Any! {

View File

@ -136,7 +136,7 @@ func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? {
func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
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
switch toId {
case let .peerUser(userId):
@ -240,7 +240,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
func apiMessageAssociatedMessageIds(_ message: Api.Message) -> [MessageId]? {
switch message {
case let .message(flags, _, fromId, toId, _, _, replyToMsgId, _, _, _, _, _, _, _, _, _, _):
case let .message(flags, _, fromId, toId, _, _, replyToMsgId, _, _, _, _, _, _, _, _, _, _, _):
if let replyToMsgId = replyToMsgId {
let peerId: PeerId
switch toId {
@ -398,7 +398,7 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes
extension StoreMessage {
convenience init?(apiMessage: Api.Message, namespace: MessageId.Namespace = Namespaces.Message.Cloud) {
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
var authorId: PeerId?
switch toId {
@ -521,6 +521,10 @@ extension StoreMessage {
attributes.append(ViewCountMessageAttribute(count: Int(views)))
}
if let forwards = forwards, namespace != Namespaces.Message.ScheduledCloud {
attributes.append(ForwardCountMessageAttribute(count: Int(forwards)))
}
if let editDate = editDate {
attributes.append(EditedMessageAttribute(date: editDate, isHidden: (flags & (1 << 21)) != 0))
}

View File

@ -58,7 +58,7 @@ class UpdateMessageService: NSObject, MTMessageService {
self.putNext(groups)
}
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 groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
if groups.count != 0 {
@ -75,7 +75,7 @@ class UpdateMessageService: NSObject, MTMessageService {
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 groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
if groups.count != 0 {

View File

@ -1164,8 +1164,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return $0.updatedInputMode(f)
})
}, openMessageShareMenu: { [weak self] id in
if let strongSelf = self, let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id) {
let shareController = ShareController(context: strongSelf.context, subject: .messages(messages))
if let strongSelf = self, let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let message = messages.first {
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
if shared {
self?.commitPurposefulAction()