mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Message statistics
This commit is contained in:
parent
e28ec6b7ca
commit
c67feac3ce
@ -5710,3 +5710,8 @@ Any member of this group will be able to see messages in the channel.";
|
||||
"Cache.MaximumCacheSize" = "Maximum Cache Size";
|
||||
"Cache.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";
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -5,7 +5,6 @@ import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import MapKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import TelegramStringFormatting
|
||||
|
@ -5,7 +5,6 @@ import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import MapKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import TelegramStringFormatting
|
||||
|
265
submodules/StatisticsUI/Sources/MessageStatsController.swift
Normal file
265
submodules/StatisticsUI/Sources/MessageStatsController.swift
Normal file
@ -0,0 +1,265 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import TelegramStringFormatting
|
||||
import ItemListUI
|
||||
import ItemListPeerItem
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import PresentationDataUtils
|
||||
import AppBundle
|
||||
import GraphUI
|
||||
|
||||
private final class MessageStatsControllerArguments {
|
||||
let context: AccountContext
|
||||
let loadDetailedGraph: (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>
|
||||
let openMessage: (MessageId) -> Void
|
||||
|
||||
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openMessage: @escaping (MessageId) -> Void) {
|
||||
self.context = context
|
||||
self.loadDetailedGraph = loadDetailedGraph
|
||||
self.openMessage = openMessage
|
||||
}
|
||||
}
|
||||
|
||||
private enum StatsSection: Int32 {
|
||||
case overview
|
||||
case interactions
|
||||
case publicForwards
|
||||
}
|
||||
|
||||
private enum StatsEntry: ItemListNodeEntry {
|
||||
case overviewTitle(PresentationTheme, String)
|
||||
case overview(PresentationTheme, MessageStats, Int32?)
|
||||
|
||||
case interactionsTitle(PresentationTheme, String)
|
||||
case interactionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType)
|
||||
|
||||
case publicForwardsTitle(PresentationTheme, String)
|
||||
case publicForward(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Message)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .overviewTitle, .overview:
|
||||
return StatsSection.overview.rawValue
|
||||
case .interactionsTitle, .interactionsGraph:
|
||||
return StatsSection.interactions.rawValue
|
||||
case .publicForwardsTitle, .publicForward:
|
||||
return StatsSection.publicForwards.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: Int32 {
|
||||
switch self {
|
||||
case .overviewTitle:
|
||||
return 0
|
||||
case .overview:
|
||||
return 1
|
||||
case .interactionsTitle:
|
||||
return 2
|
||||
case .interactionsGraph:
|
||||
return 3
|
||||
case .publicForwardsTitle:
|
||||
return 4
|
||||
case let .publicForward(index, _, _, _, _):
|
||||
return 5 + index
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: StatsEntry, rhs: StatsEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .overviewTitle(lhsTheme, lhsText):
|
||||
if case let .overviewTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText{
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .overview(lhsTheme, lhsStats, lhsPublicShares):
|
||||
if case let .overview(rhsTheme, rhsStats, rhsPublicShares) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsPublicShares == rhsPublicShares {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .interactionsTitle(lhsTheme, lhsText):
|
||||
if case let .interactionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .interactionsGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType):
|
||||
if case let .interactionsGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .publicForwardsTitle(lhsTheme, lhsText):
|
||||
if case let .publicForwardsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .publicForward(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsMessage):
|
||||
if case let .publicForward(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsMessage) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsMessage.id == rhsMessage.id {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: StatsEntry, rhs: StatsEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! MessageStatsControllerArguments
|
||||
switch self {
|
||||
case let .overviewTitle(_, text),
|
||||
let .interactionsTitle(_, text),
|
||||
let .publicForwardsTitle(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .overview(_, stats, publicShares):
|
||||
return MessageStatsOverviewItem(presentationData: presentationData, stats: stats, publicShares: publicShares, sectionId: self.section, style: .blocks)
|
||||
case let .interactionsGraph(_, _, _, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
|
||||
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
|
||||
if let graph = graph, case let .Loaded(_, data) = graph {
|
||||
completion(data)
|
||||
}
|
||||
})
|
||||
}, sectionId: self.section, style: .blocks)
|
||||
case let .publicForward(_, _, _, _, message):
|
||||
var views: Int = 0
|
||||
for attribute in message.attributes {
|
||||
if let viewsAttribute = attribute as? ViewCountMessageAttribute {
|
||||
views = viewsAttribute.count
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var text: String = ""
|
||||
text += "\(views) views"
|
||||
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", decimalSeparator: ",", groupingSeparator: ""), nameDisplayOrder: .firstLast, context: arguments.context, peer: message.peers[message.id.peerId]!, height: .generic, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .text(text), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil), revealOptions: nil, switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: {
|
||||
arguments.openMessage(message.id)
|
||||
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, contextAction: nil)
|
||||
// return StatsMessageItem(context: arguments.context, presentationData: presentationData, message: message, views: 0, forwards: 0, sectionId: self.section, style: .blocks, action: {
|
||||
// arguments.openMessage(message.id)
|
||||
// })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func messageStatsControllerEntries(data: MessageStats?, messages: SearchMessagesResult?, presentationData: PresentationData) -> [StatsEntry] {
|
||||
var entries: [StatsEntry] = []
|
||||
|
||||
if let data = data {
|
||||
entries.append(.overviewTitle(presentationData.theme, presentationData.strings.Stats_MessageOverview.uppercased()))
|
||||
entries.append(.overview(presentationData.theme, data, messages?.totalCount))
|
||||
|
||||
if !data.interactionsGraph.isEmpty {
|
||||
entries.append(.interactionsTitle(presentationData.theme, presentationData.strings.Stats_MessageInteractionsTitle.uppercased()))
|
||||
entries.append(.interactionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, .twoAxisStep))
|
||||
}
|
||||
|
||||
if let messages = messages, !messages.messages.isEmpty {
|
||||
entries.append(.publicForwardsTitle(presentationData.theme, presentationData.strings.Stats_MessagePublicForwardsTitle.uppercased()))
|
||||
var index: Int32 = 0
|
||||
for message in messages.messages {
|
||||
entries.append(.publicForward(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, message))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
public func messageStatsController(context: AccountContext, messageId: MessageId, cachedPeerData: CachedPeerData) -> ViewController {
|
||||
var navigateToMessageImpl: ((MessageId) -> Void)?
|
||||
|
||||
let actionsDisposable = DisposableSet()
|
||||
let dataPromise = Promise<MessageStats?>(nil)
|
||||
let messagesPromise = Promise<(SearchMessagesResult, SearchMessagesState)?>(nil)
|
||||
|
||||
var datacenterId: Int32 = 0
|
||||
if let cachedData = cachedPeerData as? CachedChannelData {
|
||||
datacenterId = cachedData.statsDatacenterId
|
||||
}
|
||||
|
||||
let statsContext = MessageStatsContext(postbox: context.account.postbox, network: context.account.network, datacenterId: datacenterId, messageId: messageId)
|
||||
let dataSignal: Signal<MessageStats?, NoError> = statsContext.state
|
||||
|> map { state in
|
||||
return state.stats
|
||||
}
|
||||
dataPromise.set(.single(nil) |> then(dataSignal))
|
||||
|
||||
let arguments = MessageStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal<StatsGraph?, NoError> in
|
||||
return statsContext.loadDetailedGraph(graph, x: x)
|
||||
}, openMessage: { messageId in
|
||||
navigateToMessageImpl?(messageId)
|
||||
})
|
||||
|
||||
let longLoadingSignal: Signal<Bool, NoError> = .single(false) |> then(.single(true) |> delay(2.0, queue: Queue.mainQueue()))
|
||||
|
||||
let previousData = Atomic<MessageStats?>(value: nil)
|
||||
|
||||
let searchSignal = searchMessages(account: context.account, location: .publicForwards(messageId), query: "", state: nil)
|
||||
|> map(Optional.init)
|
||||
|> afterNext { result in
|
||||
if let result = result {
|
||||
for message in result.0.messages {
|
||||
if let peer = message.peers[message.id.peerId], let peerReference = PeerReference(peer) {
|
||||
let _ = updatedRemotePeer(postbox: context.account.postbox, network: context.account.network, peer: peerReference).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
messagesPromise.set(.single(nil) |> then(searchSignal))
|
||||
|
||||
let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), messagesPromise.get(), longLoadingSignal)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, data, search, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let previous = previousData.swap(data)
|
||||
var emptyStateItem: ItemListControllerEmptyStateItem?
|
||||
if data == nil {
|
||||
if longLoading {
|
||||
emptyStateItem = StatsEmptyStateItem(theme: presentationData.theme, strings: presentationData.strings)
|
||||
} else {
|
||||
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
|
||||
}
|
||||
}
|
||||
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Stats_MessageTitle), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: messageStatsControllerEntries(data: data, messages: search?.0, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
let _ = statsContext.state
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
controller.contentOffsetChanged = { [weak controller] _, _ in
|
||||
controller?.forEachItemNode({ itemNode in
|
||||
if let itemNode = itemNode as? StatsGraphItemNode {
|
||||
itemNode.resetInteraction()
|
||||
}
|
||||
})
|
||||
}
|
||||
controller.didDisappear = { [weak controller] _ in
|
||||
controller?.clearItemNodesHighlight(animated: true)
|
||||
}
|
||||
navigateToMessageImpl = { [weak controller] messageId in
|
||||
if let navigationController = controller?.navigationController as? NavigationController {
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(messageId.peerId), subject: .message(messageId), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil))
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
289
submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift
Normal file
289
submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift
Normal file
@ -0,0 +1,289 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
|
||||
class MessageStatsOverviewItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let stats: MessageStats
|
||||
let publicShares: Int32?
|
||||
let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
|
||||
init(presentationData: ItemListPresentationData, stats: MessageStats, publicShares: Int32?, sectionId: ItemListSectionId, style: ItemListStyle) {
|
||||
self.presentationData = presentationData
|
||||
self.stats = stats
|
||||
self.publicShares = publicShares
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = MessageStatsOverviewItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? MessageStatsOverviewItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectable: Bool = false
|
||||
}
|
||||
|
||||
class MessageStatsOverviewItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let leftValueLabel: ImmediateTextNode
|
||||
private let centerValueLabel: ImmediateTextNode
|
||||
private let rightValueLabel: ImmediateTextNode
|
||||
|
||||
private let leftTitleLabel: ImmediateTextNode
|
||||
private let centerTitleLabel: ImmediateTextNode
|
||||
private let rightTitleLabel: ImmediateTextNode
|
||||
|
||||
private var item: MessageStatsOverviewItem?
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.leftValueLabel = ImmediateTextNode()
|
||||
self.centerValueLabel = ImmediateTextNode()
|
||||
self.rightValueLabel = ImmediateTextNode()
|
||||
|
||||
self.leftTitleLabel = ImmediateTextNode()
|
||||
self.centerTitleLabel = ImmediateTextNode()
|
||||
self.rightTitleLabel = ImmediateTextNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.addSubnode(self.leftValueLabel)
|
||||
self.addSubnode(self.centerValueLabel)
|
||||
self.addSubnode(self.rightValueLabel)
|
||||
|
||||
self.addSubnode(self.leftTitleLabel)
|
||||
self.addSubnode(self.centerTitleLabel)
|
||||
self.addSubnode(self.rightTitleLabel)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: MessageStatsOverviewItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeLeftValueLabelLayout = TextNode.asyncLayout(self.leftValueLabel)
|
||||
let makeRightValueLabelLayout = TextNode.asyncLayout(self.rightValueLabel)
|
||||
let makeCenterValueLabelLayout = TextNode.asyncLayout(self.centerValueLabel)
|
||||
|
||||
let makeLeftTitleLabelLayout = TextNode.asyncLayout(self.leftTitleLabel)
|
||||
let makeRightTitleLabelLayout = TextNode.asyncLayout(self.rightTitleLabel)
|
||||
let makeCenterTitleLabelLayout = TextNode.asyncLayout(self.centerTitleLabel)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
let insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
let itemBackgroundColor: UIColor
|
||||
let itemSeparatorColor: UIColor
|
||||
|
||||
let horizontalSpacing: CGFloat = 62.0
|
||||
let topInset: CGFloat = 14.0
|
||||
let sideInset: CGFloat = 16.0
|
||||
|
||||
var height: CGFloat = topInset * 2.0
|
||||
|
||||
let leftInset = params.leftInset
|
||||
let rightInset: CGFloat = params.rightInset
|
||||
var updatedTheme: PresentationTheme?
|
||||
|
||||
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||||
updatedTheme = item.presentationData.theme
|
||||
}
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
||||
insets = itemListNeighborsPlainInsets(neighbors)
|
||||
case .blocks:
|
||||
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
||||
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||
insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
}
|
||||
|
||||
let valueFont = Font.semibold(item.presentationData.fontSize.itemListBaseFontSize)
|
||||
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
|
||||
|
||||
let leftValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let rightValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let centerValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
|
||||
let leftTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let rightTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let centerTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
|
||||
leftValueLabelLayoutAndApply = makeLeftValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(item.stats.views), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
centerValueLabelLayoutAndApply = makeCenterValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
rightValueLabelLayoutAndApply = makeRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { "≈\( compactNumericCountString(item.stats.privateForwards - Int($0)))" } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
leftTitleLabelLayoutAndApply = makeLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Views", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
centerTitleLabelLayoutAndApply = makeCenterTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Public Shares", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
rightTitleLabelLayoutAndApply = makeRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Private Shares", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
height += rightValueLabelLayoutAndApply!.0.size.height + rightTitleLabelLayoutAndApply!.0.size.height
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: height)
|
||||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
let _ = leftValueLabelLayoutAndApply?.1()
|
||||
let _ = centerValueLabelLayoutAndApply?.1()
|
||||
let _ = rightValueLabelLayoutAndApply?.1()
|
||||
let _ = leftTitleLabelLayoutAndApply?.1()
|
||||
let _ = centerTitleLabelLayoutAndApply?.1()
|
||||
let _ = rightTitleLabelLayoutAndApply?.1()
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||
}
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
if strongSelf.backgroundNode.supernode != nil {
|
||||
strongSelf.backgroundNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode != nil {
|
||||
strongSelf.topStripeNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
||||
}
|
||||
if strongSelf.maskNode.supernode != nil {
|
||||
strongSelf.maskNode.removeFromSupernode()
|
||||
}
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
|
||||
case .blocks:
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
||||
}
|
||||
|
||||
var x: CGFloat = sideInset + leftInset
|
||||
if let leftValueLabelLayout = leftValueLabelLayoutAndApply?.0, let leftTitleLabelLayout = leftTitleLabelLayoutAndApply?.0 {
|
||||
strongSelf.leftValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: leftValueLabelLayout.size)
|
||||
strongSelf.leftTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.leftValueLabel.frame.maxY), size: leftTitleLabelLayout.size)
|
||||
x += max(leftValueLabelLayout.size.width, leftTitleLabelLayout.size.width) + horizontalSpacing
|
||||
}
|
||||
|
||||
if let centerValueLabelLayout = centerValueLabelLayoutAndApply?.0, let centerTitleLabelLayout = centerTitleLabelLayoutAndApply?.0 {
|
||||
strongSelf.centerValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: centerValueLabelLayout.size)
|
||||
strongSelf.centerTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.centerValueLabel.frame.maxY), size: centerTitleLabelLayout.size)
|
||||
x += max(centerValueLabelLayout.size.width, centerTitleLabelLayout.size.width) + horizontalSpacing
|
||||
}
|
||||
|
||||
if let rightValueLabelLayout = rightValueLabelLayoutAndApply?.0, let rightTitleLabelLayout = rightTitleLabelLayoutAndApply?.0 {
|
||||
strongSelf.rightValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: rightValueLabelLayout.size)
|
||||
strongSelf.rightTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.rightValueLabel.frame.maxY), size: rightTitleLabelLayout.size)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
|
||||
public class ForwardCountMessageAttribute: MessageAttribute {
|
||||
public let count: Int
|
||||
|
||||
public var associatedMessageIds: [MessageId] = []
|
||||
|
||||
public init(count: Int) {
|
||||
self.count = count
|
||||
}
|
||||
|
||||
required public init(decoder: PostboxDecoder) {
|
||||
self.count = Int(decoder.decodeInt32ForKey("c", orElse: 0))
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(Int32(self.count), forKey: "c")
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
|
||||
|
||||
public class ViewCountMessageAttribute: MessageAttribute {
|
||||
public let count: Int
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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>) {
|
||||
|
@ -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 {
|
||||
|
@ -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) })
|
||||
|
@ -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:
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
194
submodules/TelegramCore/Sources/MessageStatistics.swift
Normal file
194
submodules/TelegramCore/Sources/MessageStatistics.swift
Normal file
@ -0,0 +1,194 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramApi
|
||||
import MtProtoKit
|
||||
import SyncCore
|
||||
|
||||
//stats.getMessagePublicForwards channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
|
||||
|
||||
public struct MessageStats: Equatable {
|
||||
public let views: Int
|
||||
public let publicForwards: Int
|
||||
public let privateForwards: Int
|
||||
public let interactionsGraph: StatsGraph
|
||||
|
||||
init(views: Int, publicForwards: Int, privateForwards: Int, interactionsGraph: StatsGraph) {
|
||||
self.views = views
|
||||
self.publicForwards = publicForwards
|
||||
self.privateForwards = privateForwards
|
||||
self.interactionsGraph = interactionsGraph
|
||||
}
|
||||
|
||||
public static func == (lhs: MessageStats, rhs: MessageStats) -> Bool {
|
||||
if lhs.views != rhs.views {
|
||||
return false
|
||||
}
|
||||
if lhs.publicForwards != rhs.publicForwards {
|
||||
return false
|
||||
}
|
||||
if lhs.privateForwards != rhs.privateForwards {
|
||||
return false
|
||||
}
|
||||
if lhs.interactionsGraph != rhs.interactionsGraph {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public func withUpdatedInteractionsGraph(_ interactionsGraph: StatsGraph) -> MessageStats {
|
||||
return MessageStats(views: self.views, publicForwards: self.publicForwards, privateForwards: self.privateForwards, interactionsGraph: self.interactionsGraph)
|
||||
}
|
||||
}
|
||||
|
||||
public struct MessageStatsContextState: Equatable {
|
||||
public var stats: MessageStats?
|
||||
}
|
||||
|
||||
private func requestMessageStats(postbox: Postbox, network: Network, datacenterId: Int32, messageId: MessageId, dark: Bool = false) -> Signal<MessageStats?, NoError> {
|
||||
return postbox.transaction { transaction -> (Peer, Message)? in
|
||||
if let peer = transaction.getPeer(messageId.peerId), let message = transaction.getMessage(messageId) {
|
||||
return (peer, message)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} |> mapToSignal { peerAndMessage -> Signal<MessageStats?, NoError> in
|
||||
guard let (peer, message) = peerAndMessage, let inputChannel = apiInputChannel(peer) else {
|
||||
return .never()
|
||||
}
|
||||
|
||||
var flags: Int32 = 0
|
||||
if dark {
|
||||
flags |= (1 << 1)
|
||||
}
|
||||
|
||||
let request = Api.functions.stats.getMessageStats(flags: flags, channel: inputChannel, msgId: messageId.id)
|
||||
let signal: Signal<Api.stats.MessageStats, MTRpcError>
|
||||
if network.datacenterId != datacenterId {
|
||||
signal = network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil)
|
||||
|> castError(MTRpcError.self)
|
||||
|> mapToSignal { worker in
|
||||
return worker.request(request)
|
||||
}
|
||||
} else {
|
||||
signal = network.request(request)
|
||||
}
|
||||
|
||||
var views: Int = 0
|
||||
var forwards: Int = 0
|
||||
for attribute in message.attributes {
|
||||
if let viewsAttribute = attribute as? ViewCountMessageAttribute {
|
||||
views = viewsAttribute.count
|
||||
} else if let forwardsAttribute = attribute as? ForwardCountMessageAttribute {
|
||||
forwards = forwardsAttribute.count
|
||||
}
|
||||
}
|
||||
|
||||
return signal
|
||||
|> map { result -> MessageStats? in
|
||||
if case let .messageStats(apiViewsGraph) = result {
|
||||
return MessageStats(views: views, publicForwards: forwards, privateForwards: forwards, interactionsGraph: StatsGraph(apiStatsGraph: apiViewsGraph))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|> retryRequest
|
||||
}
|
||||
}
|
||||
|
||||
private final class MessageStatsContextImpl {
|
||||
private let postbox: Postbox
|
||||
private let network: Network
|
||||
private let datacenterId: Int32
|
||||
private let messageId: MessageId
|
||||
|
||||
private var _state: MessageStatsContextState {
|
||||
didSet {
|
||||
if self._state != oldValue {
|
||||
self._statePromise.set(.single(self._state))
|
||||
}
|
||||
}
|
||||
}
|
||||
private let _statePromise = Promise<MessageStatsContextState>()
|
||||
var state: Signal<MessageStatsContextState, NoError> {
|
||||
return self._statePromise.get()
|
||||
}
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
private let disposables = DisposableDict<String>()
|
||||
|
||||
init(postbox: Postbox, network: Network, datacenterId: Int32, messageId: MessageId) {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
|
||||
self.postbox = postbox
|
||||
self.network = network
|
||||
self.datacenterId = datacenterId
|
||||
self.messageId = messageId
|
||||
self._state = MessageStatsContextState(stats: nil)
|
||||
self._statePromise.set(.single(self._state))
|
||||
|
||||
self.load()
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
self.disposable.dispose()
|
||||
self.disposables.dispose()
|
||||
}
|
||||
|
||||
private func load() {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
|
||||
self.disposable.set((requestMessageStats(postbox: self.postbox, network: self.network, datacenterId: self.datacenterId, messageId: self.messageId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] stats in
|
||||
if let strongSelf = self {
|
||||
strongSelf._state = MessageStatsContextState(stats: stats)
|
||||
strongSelf._statePromise.set(.single(strongSelf._state))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal<StatsGraph?, NoError> {
|
||||
if let token = graph.token {
|
||||
return requestGraph(network: self.network, datacenterId: self.datacenterId, token: token, x: x)
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class MessageStatsContext {
|
||||
private let impl: QueueLocalObject<MessageStatsContextImpl>
|
||||
|
||||
public var state: Signal<MessageStatsContextState, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.state.start(next: { value in
|
||||
subscriber.putNext(value)
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public init(postbox: Postbox, network: Network, datacenterId: Int32, messageId: MessageId) {
|
||||
self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: {
|
||||
return MessageStatsContextImpl(postbox: postbox, network: network, datacenterId: datacenterId, messageId: messageId)
|
||||
})
|
||||
}
|
||||
|
||||
public func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal<StatsGraph?, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.loadDetailedGraph(graph, x: x).start(next: { value in
|
||||
subscriber.putNext(value)
|
||||
subscriber.putCompletion()
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -193,15 +193,16 @@ private func requestChannelStats(postbox: Postbox, network: Network, datacenterI
|
||||
flags |= (1 << 1)
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -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
|
||||
|
@ -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! {
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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 {
|
||||
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -1164,8 +1164,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return $0.updatedInputMode(f)
|
||||
})
|
||||
}, 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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user