Statistics improvements

This commit is contained in:
Ilya Laktyushin 2023-11-17 07:22:25 +04:00
parent 1fced74098
commit 13baadc3e7
29 changed files with 1467 additions and 119 deletions

View File

@ -10498,7 +10498,13 @@ Sorry for the inconvenience.";
"Stats.MessageReactionsTitle" = "REACTIONS";
"MediaEditor.RemoveVideo" = "Remove Video";
"Conversation.LaunchApp" = "LAUNCH APP";
"Message.AdSponsoredLabel" = "Sponsored";
"Message.AdRecommendedLabel" = "Recommended";
"Stats.StoryTitle" = "Story Statistics";
"Channel.Info.Settings" = "Channel Settings";

View File

@ -213,7 +213,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
}
return ContactsPeerItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder),
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat),
sortOrder: nameSortOrder,
displayOrder: nameDisplayOrder,
context: context,

View File

@ -355,7 +355,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
}
}
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder),
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat),
context: context,
title: title,
image: image,
@ -569,7 +569,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
}
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder),
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat),
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
@ -608,7 +608,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
let status: ContactsPeerItemStatus = .none
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder),
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat),
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
@ -670,7 +670,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
let peerContent: ContactsPeerItemPeer = .peer(peer: contactEntry.peer, chatPeer: contactEntry.peer)
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder),
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat),
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
@ -889,7 +889,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
}
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder),
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat),
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
@ -928,7 +928,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
let status: ContactsPeerItemStatus = .none
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder),
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat),
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
@ -990,7 +990,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
let peerContent: ContactsPeerItemPeer = .peer(peer: contactEntry.peer, chatPeer: contactEntry.peer)
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder),
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat),
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
@ -1057,7 +1057,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
}
}
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder),
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat),
context: context,
title: title,
image: image,

View File

@ -161,12 +161,14 @@ public final class ItemListPresentationData: Equatable {
public let fontSize: PresentationFontSize
public let strings: PresentationStrings
public let nameDisplayOrder: PresentationPersonNameOrder
public let dateTimeFormat: PresentationDateTimeFormat
public init(theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) {
public init(theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat) {
self.theme = theme
self.fontSize = fontSize
self.strings = strings
self.nameDisplayOrder = nameDisplayOrder
self.dateTimeFormat = dateTimeFormat
}
public static func ==(lhs: ItemListPresentationData, rhs: ItemListPresentationData) -> Bool {
@ -182,6 +184,9 @@ public final class ItemListPresentationData: Equatable {
if lhs.nameDisplayOrder != rhs.nameDisplayOrder {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
return true
}
}
@ -232,6 +237,6 @@ public extension PresentationFontSize {
public extension ItemListPresentationData {
convenience init(_ presentationData: PresentationData) {
self.init(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder)
self.init(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat)
}
}

View File

@ -13,6 +13,7 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/ComponentFlow",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
@ -35,6 +36,7 @@ swift_library(
"//submodules/PremiumUI:PremiumUI",
"//submodules/InviteLinksUI:InviteLinksUI",
"//submodules/ShareController:ShareController",
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
],
visibility = [
"//visibility:public",

View File

@ -27,7 +27,7 @@ private let initialBoostersDisplayedLimit: Int32 = 5
private final class ChannelStatsControllerArguments {
let context: AccountContext
let loadDetailedGraph: (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>
let openMessageStats: (MessageId) -> Void
let openPostStats: (EnginePeer, StatsPostItem) -> Void
let contextAction: (MessageId, ASDisplayNode, ContextGesture?) -> Void
let copyBoostLink: (String) -> Void
let shareBoostLink: (String) -> Void
@ -37,10 +37,10 @@ private final class ChannelStatsControllerArguments {
let createPrepaidGiveaway: (PrepaidGiveaway) -> Void
let updateGiftsSelected: (Bool) -> Void
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openMessage: @escaping (MessageId) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void) {
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openPostStats: @escaping (EnginePeer, StatsPostItem) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void) {
self.context = context
self.loadDetailedGraph = loadDetailedGraph
self.openMessageStats = openMessage
self.openPostStats = openPostStats
self.contextAction = contextAction
self.copyBoostLink = copyBoostLink
self.shareBoostLink = shareBoostLink
@ -75,6 +75,45 @@ private enum StatsSection: Int32 {
case gifts
}
enum StatsPostItem: Equatable {
static func == (lhs: StatsPostItem, rhs: StatsPostItem) -> Bool {
switch lhs {
case let .message(lhsMessage):
if case let .message(rhsMessage) = rhs {
return lhsMessage.id == rhsMessage.id
} else {
return false
}
case let .story(lhsStory):
if case let .story(rhsStory) = rhs, lhsStory == rhsStory {
return true
} else {
return false
}
}
}
case message(Message)
case story(EngineStoryItem)
var isStory: Bool {
if case .story = self {
return true
} else {
return false
}
}
var timestamp: Int32 {
switch self {
case let .message(message):
return message.timestamp
case let .story(story):
return story.timestamp
}
}
}
private enum StatsEntry: ItemListNodeEntry {
case overviewTitle(PresentationTheme, String, String)
case overview(PresentationTheme, ChannelStats)
@ -116,7 +155,7 @@ private enum StatsEntry: ItemListNodeEntry {
case instantPageInteractionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType)
case postsTitle(PresentationTheme, String)
case post(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Message, ChannelStatsMessageInteractions)
case post(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, StatsPostItem, ChannelStatsMessageInteractions)
case boostLevel(PresentationTheme, Int32, Int32, CGFloat)
@ -242,7 +281,7 @@ private enum StatsEntry: ItemListNodeEntry {
return 25
case .postsTitle:
return 26
case let .post(index, _, _, _, _, _):
case let .post(index, _, _, _, _, _, _):
return 27 + index
case .boostLevel:
return 2000
@ -445,8 +484,8 @@ private enum StatsEntry: ItemListNodeEntry {
} else {
return false
}
case let .post(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsMessage, lhsInteractions):
if case let .post(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsMessage, rhsInteractions) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsMessage.id == rhsMessage.id, lhsInteractions == rhsInteractions {
case let .post(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsPost, lhsInteractions):
if case let .post(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsPost, rhsInteractions) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsPost == rhsPost, lhsInteractions == rhsInteractions {
return true
} else {
return false
@ -610,12 +649,14 @@ private enum StatsEntry: ItemListNodeEntry {
}
})
}, sectionId: self.section, style: .blocks)
case let .post(_, _, _, _, message, interactions):
return StatsMessageItem(context: arguments.context, presentationData: presentationData, message: message, views: interactions.views, forwards: interactions.forwards, sectionId: self.section, style: .blocks, action: {
arguments.openMessageStats(message.id)
}, contextAction: { node, gesture in
case let .post(_, _, _, _, peer, post, interactions):
return StatsMessageItem(context: arguments.context, presentationData: presentationData, peer: peer, item: post, views: interactions.views, reactions: interactions.reactions, forwards: interactions.forwards, sectionId: self.section, style: .blocks, action: {
arguments.openPostStats(EnginePeer(peer), post)
}, contextAction: !post.isStory ? { node, gesture in
if case let .message(message) = post {
arguments.contextAction(message.id, node, gesture)
})
}
} : nil)
case let .boosterTabs(_, boostText, giftText, giftSelected):
return BoostsTabsItem(theme: presentationData.theme, boostsText: boostText, giftsText: giftText, selectedTab: giftSelected ? .gifts : .boosts, sectionId: self.section, selectionUpdated: { tab in
arguments.updateGiftsSelected(tab == .gifts)
@ -775,7 +816,7 @@ private struct ChannelStatsControllerState: Equatable {
}
private func channelStatsControllerEntries(state: ChannelStatsControllerState, peer: EnginePeer?, data: ChannelStats?, messages: [Message]?, interactions: [MessageId: ChannelStatsMessageInteractions]?, boostData: ChannelBoostStatus?, boostersState: ChannelBoostersContext.State?, giftsState: ChannelBoostersContext.State?, presentationData: PresentationData, giveawayAvailable: Bool) -> [StatsEntry] {
private func channelStatsControllerEntries(state: ChannelStatsControllerState, peer: EnginePeer?, data: ChannelStats?, messages: [Message]?, stories: PeerStoryListContext.State?, interactions: [MessageId: ChannelStatsMessageInteractions]?, boostData: ChannelBoostStatus?, boostersState: ChannelBoostersContext.State?, giftsState: ChannelBoostersContext.State?, presentationData: PresentationData, giveawayAvailable: Bool) -> [StatsEntry] {
var entries: [StatsEntry] = []
switch state.section {
@ -847,14 +888,35 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
entries.append(.storyReactionsByEmotionGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.storyReactionsByEmotionGraph, .bars))
}
if let messages = messages, !messages.isEmpty, let interactions = interactions, !interactions.isEmpty {
var posts: [StatsPostItem] = []
if let messages, let interactions {
for message in messages {
if let _ = interactions[message.id] {
posts.append(.message(message))
}
}
}
if let stories {
for story in stories.items {
posts.append(.story(story))
}
}
posts.sort(by: { $0.timestamp > $1.timestamp })
if !posts.isEmpty, let interactions, let peer = peer?._asPeer() {
entries.append(.postsTitle(presentationData.theme, presentationData.strings.Stats_PostsTitle))
var index: Int32 = 0
for message in messages {
for post in posts {
switch post {
case let .message(message):
if let interactions = interactions[message.id] {
entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, message, interactions))
index += 1
entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, post, interactions))
}
case let .story(story):
entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, post, ChannelStatsMessageInteractions(messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), views: Int32(story.views?.seenCount ?? 0), forwards: Int32(story.views?.forwardCount ?? 0), reactions: Int32(story.views?.reactedCount ?? 0))))
}
index += 1
}
}
}
@ -979,13 +1041,15 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
var openMessageStatsImpl: ((MessageId) -> Void)?
var openPostStatsImpl: ((EnginePeer, StatsPostItem) -> Void)?
var contextActionImpl: ((MessageId, ASDisplayNode, ContextGesture?) -> Void)?
let actionsDisposable = DisposableSet()
let dataPromise = Promise<ChannelStats?>(nil)
let messagesPromise = Promise<MessageHistoryView?>(nil)
let storiesPromise = Promise<PeerStoryListContext.State?>()
let datacenterId: Int32 = statsDatacenterId ?? 0
let statsContext = ChannelStatsContext(postbox: context.account.postbox, network: context.account.network, datacenterId: datacenterId, peerId: peerId)
@ -1027,8 +1091,8 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
let arguments = ChannelStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal<StatsGraph?, NoError> in
return statsContext.loadDetailedGraph(graph, x: x)
}, openMessage: { messageId in
openMessageStatsImpl?(messageId)
}, openPostStats: { peer, item in
openPostStatsImpl?(peer, item)
}, contextAction: { messageId, node, gesture in
contextActionImpl?(messageId, node, gesture)
}, copyBoostLink: { link in
@ -1138,6 +1202,16 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
}
messagesPromise.set(.single(nil) |> then(messageView))
let storyList = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false)
storyList.loadMore()
storiesPromise.set(
.single(nil)
|> then(
storyList.state
|> map(Optional.init)
)
)
let longLoadingSignal: Signal<Bool, NoError> = .single(false) |> then(.single(true) |> delay(2.0, queue: Queue.mainQueue()))
let previousData = Atomic<ChannelStats?>(value: nil)
@ -1149,13 +1223,14 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)),
dataPromise.get(),
messagesPromise.get(),
storiesPromise.get(),
boostData,
boostsContext.state,
giftsContext.state,
longLoadingSignal
)
|> deliverOnMainQueue
|> map { presentationData, state, peer, data, messageView, boostData, boostersState, giftsState, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
|> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
let previous = previousData.swap(data)
var emptyStateItem: ItemListControllerEmptyStateItem?
switch state.section {
@ -1183,13 +1258,14 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .sectionControl([presentationData.strings.Stats_Statistics, presentationData.strings.Stats_Boosts], state.section == .boosts ? 1 : 0), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(state: state, peer: peer, data: data, messages: messages, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, presentationData: presentationData, giveawayAvailable: premiumConfiguration.giveawayGiftsPurchaseAvailable), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(state: state, peer: peer, data: data, messages: messages, stories: stories, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, presentationData: presentationData, giveawayAvailable: premiumConfiguration.giveawayGiftsPurchaseAvailable), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
let _ = statsContext.state
let _ = storyList.state
}
let controller = ItemListController(context: context, state: signal)
@ -1206,8 +1282,15 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
controller.didDisappear = { [weak controller] _ in
controller?.clearItemNodesHighlight(animated: true)
}
openMessageStatsImpl = { [weak controller] messageId in
controller?.push(messageStatsController(context: context, messageId: messageId, statsDatacenterId: statsDatacenterId))
openPostStatsImpl = { [weak controller] peer, post in
let subject: StatsSubject
switch post {
case let .message(message):
subject = .message(id: message.id)
case let .story(story):
subject = .story(peer: peer, storyItem: story)
}
controller?.push(messageStatsController(context: context, subject: subject, statsDatacenterId: statsDatacenterId))
}
contextActionImpl = { [weak controller] messageId, sourceNode, gesture in
guard let controller = controller, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else {

View File

@ -2,6 +2,7 @@ import Foundation
import UIKit
import Display
import SwiftSignalKit
import AsyncDisplayKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
@ -35,7 +36,7 @@ private enum StatsSection: Int32 {
private enum StatsEntry: ItemListNodeEntry {
case overviewTitle(PresentationTheme, String)
case overview(PresentationTheme, MessageStats, Int32?)
case overview(PresentationTheme, PostStats, Int32?)
case interactionsTitle(PresentationTheme, String)
case interactionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType)
@ -89,8 +90,14 @@ private enum StatsEntry: ItemListNodeEntry {
return false
}
case let .overview(lhsTheme, lhsStats, lhsPublicShares):
if case let .overview(rhsTheme, rhsStats, rhsPublicShares) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsPublicShares == rhsPublicShares {
return true
if case let .overview(rhsTheme, rhsStats, rhsPublicShares) = rhs, lhsTheme === rhsTheme, lhsPublicShares == rhsPublicShares {
if let lhsMessageStats = lhsStats as? MessageStats, let rhsMessageStats = rhsStats as? MessageStats {
return lhsMessageStats == rhsMessageStats
} else if let lhsStoryStats = lhsStats as? StoryStats, let rhsStoryStats = rhsStats as? StoryStats {
return lhsStoryStats == rhsStoryStats
} else {
return false
}
} else {
return false
}
@ -172,7 +179,7 @@ private enum StatsEntry: ItemListNodeEntry {
}
}
private func messageStatsControllerEntries(data: MessageStats?, messages: SearchMessagesResult?, presentationData: PresentationData) -> [StatsEntry] {
private func messageStatsControllerEntries(data: PostStats?, messages: SearchMessagesResult?, presentationData: PresentationData) -> [StatsEntry] {
var entries: [StatsEntry] = []
if let data = data {
@ -212,33 +219,76 @@ private func messageStatsControllerEntries(data: MessageStats?, messages: Search
return entries
}
public func messageStatsController(context: AccountContext, messageId: EngineMessage.Id, statsDatacenterId: Int32?) -> ViewController {
public enum StatsSubject {
case message(id: EngineMessage.Id)
case story(peer: EnginePeer, storyItem: EngineStoryItem)
}
protocol PostStats {
var views: Int { get }
var forwards: Int { get }
var interactionsGraph: StatsGraph { get }
var interactionsGraphDelta: Int64 { get }
var reactionsGraph: StatsGraph { get }
}
extension MessageStats: PostStats {
}
extension StoryStats: PostStats {
}
public func messageStatsController(context: AccountContext, subject: StatsSubject, statsDatacenterId: Int32?) -> ViewController {
var navigateToMessageImpl: ((EngineMessage.Id) -> Void)?
let actionsDisposable = DisposableSet()
let dataPromise = Promise<MessageStats?>(nil)
let dataPromise = Promise<PostStats?>(nil)
let messagesPromise = Promise<(SearchMessagesResult, SearchMessagesState)?>(nil)
let datacenterId: Int32 = statsDatacenterId ?? 0
let statsContext = MessageStatsContext(postbox: context.account.postbox, network: context.account.network, datacenterId: datacenterId, messageId: messageId)
let dataSignal: Signal<MessageStats?, NoError> = statsContext.state
let anyStatsContext: Any
let dataSignal: Signal<PostStats?, NoError>
var loadDetailedGraphImpl: ((StatsGraph, Int64) -> Signal<StatsGraph?, NoError>)?
switch subject {
case let .message(id):
let statsContext = MessageStatsContext(postbox: context.account.postbox, network: context.account.network, datacenterId: datacenterId, messageId: id)
loadDetailedGraphImpl = { [weak statsContext] graph, x in
return statsContext?.loadDetailedGraph(graph, x: x) ?? .single(nil)
}
dataSignal = statsContext.state
|> map { state in
return state.stats
}
dataPromise.set(.single(nil) |> then(dataSignal))
anyStatsContext = statsContext
case let .story(peer, storyItem):
let statsContext = StoryStatsContext(postbox: context.account.postbox, network: context.account.network, datacenterId: datacenterId, peerId: peer.id, storyId: storyItem.id)
loadDetailedGraphImpl = { [weak statsContext] graph, x in
return statsContext?.loadDetailedGraph(graph, x: x) ?? .single(nil)
}
dataSignal = statsContext.state
|> map { state in
return state.stats
}
dataPromise.set(.single(nil) |> then(dataSignal))
anyStatsContext = statsContext
}
let arguments = MessageStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal<StatsGraph?, NoError> in
return statsContext.loadDetailedGraph(graph, x: x)
return loadDetailedGraphImpl?(graph, x) ?? .single(nil)
}, 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 previousData = Atomic<PostStats?>(value: nil)
let searchSignal = context.engine.messages.searchMessages(location: .publicForwards(messageId: messageId, datacenterId: Int(datacenterId)), query: "", state: nil)
if case let .message(id) = subject {
let searchSignal = context.engine.messages.searchMessages(location: .publicForwards(messageId: id, datacenterId: Int(datacenterId)), query: "", state: nil)
|> map(Optional.init)
|> afterNext { result in
if let result = result {
@ -250,6 +300,17 @@ public func messageStatsController(context: AccountContext, messageId: EngineMes
}
}
messagesPromise.set(.single(nil) |> then(searchSignal))
} else {
messagesPromise.set(.single(nil))
}
let iconNode: ASDisplayNode?
if case let .story(peer, storyItem) = subject {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
iconNode = StoryIconNode(context: context, theme: presentationData.theme, peer: peer._asPeer(), storyItem: storyItem)
} else {
iconNode = nil
}
let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), messagesPromise.get(), longLoadingSignal)
|> deliverOnMainQueue
@ -264,14 +325,22 @@ public func messageStatsController(context: AccountContext, messageId: EngineMes
}
}
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 title: String
switch subject {
case .message:
title = presentationData.strings.Stats_MessageTitle
case .story:
title = presentationData.strings.Stats_StoryTitle
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: iconNode.flatMap { ItemListNavigationButton(content: .node($0), style: .regular, enabled: true, action: { }) }, 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 _ = anyStatsContext
}
let controller = ItemListController(context: context, state: signal)
@ -282,6 +351,12 @@ public func messageStatsController(context: AccountContext, messageId: EngineMes
}
})
}
// controller.visibleBottomContentOffsetChanged = { offset in
// let state = stateValue.with { $0 }
// if case let .known(value) = offset, value < 100.0, case .boosts = state.section, state.boostersExpanded {
// boostsContext.loadMore()
// }
// }
controller.didDisappear = { [weak controller] _ in
controller?.clearItemNodesHighlight(animated: true)
}

View File

@ -10,12 +10,12 @@ import PresentationDataUtils
class MessageStatsOverviewItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let stats: MessageStats
let stats: PostStats
let publicShares: Int32?
let sectionId: ItemListSectionId
let style: ItemListStyle
init(presentationData: ItemListPresentationData, stats: MessageStats, publicShares: Int32?, sectionId: ItemListSectionId, style: ItemListStyle) {
init(presentationData: ItemListPresentationData, stats: PostStats, publicShares: Int32?, sectionId: ItemListSectionId, style: ItemListStyle) {
self.presentationData = presentationData
self.stats = stats
self.publicShares = publicShares

View File

@ -2,6 +2,7 @@ import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
@ -11,23 +12,28 @@ import TelegramStringFormatting
import ItemListUI
import PresentationDataUtils
import PhotoResources
import AvatarStoryIndicatorComponent
public class StatsMessageItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let message: Message
let peer: Peer
let item: StatsPostItem
let views: Int32
let reactions: Int32
let forwards: Int32
public let sectionId: ItemListSectionId
let style: ItemListStyle
let action: (() -> Void)?
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
init(context: AccountContext, presentationData: ItemListPresentationData, message: Message, views: Int32, forwards: Int32, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?) {
init(context: AccountContext, presentationData: ItemListPresentationData, peer: Peer, item: StatsPostItem, views: Int32, reactions: Int32, forwards: Int32, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?) {
self.context = context
self.presentationData = presentationData
self.message = message
self.peer = peer
self.item = item
self.views = views
self.reactions = reactions
self.forwards = forwards
self.sectionId = sectionId
self.style = style
@ -79,7 +85,7 @@ public class StatsMessageItem: ListViewItem, ItemListItem {
private let badgeFont = Font.regular(15.0)
public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
final class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
@ -96,9 +102,14 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
private var nonExtractedRect: CGRect?
let contentImageNode: TransformImageNode
var storyIndicator: ComponentView<Empty>?
let titleNode: TextNode
let labelNode: TextNode
let viewsNode: TextNode
let reactionsIconNode: ASImageNode
let reactionsNode: TextNode
let forwardsIconNode: ASImageNode
let forwardsNode: TextNode
private let activateArea: AccessibilityAreaNode
@ -152,6 +163,15 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
self.forwardsNode = TextNode()
self.forwardsNode.isUserInteractionEnabled = false
self.forwardsIconNode = ASImageNode()
self.forwardsIconNode.displaysAsynchronously = false
self.reactionsNode = TextNode()
self.reactionsNode.isUserInteractionEnabled = false
self.reactionsIconNode = ASImageNode()
self.reactionsIconNode.displaysAsynchronously = false
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
@ -172,6 +192,9 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
self.offsetContainerNode.addSubnode(self.labelNode)
self.countersContainerNode.addSubnode(self.viewsNode)
self.countersContainerNode.addSubnode(self.forwardsNode)
self.countersContainerNode.addSubnode(self.forwardsIconNode)
self.countersContainerNode.addSubnode(self.reactionsNode)
self.countersContainerNode.addSubnode(self.reactionsIconNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.addSubnode(self.activateArea)
@ -200,8 +223,8 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
transition.updateAlpha(node: strongSelf.countersContainerNode, alpha: isExtracted ? 0.0 : 1.0)
transition.updateSublayerTransformOffset(layer: strongSelf.countersContainerNode.layer, offset: CGPoint(x: isExtracted ? -24.0 : 0.0, y: 0.0))
transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0))
transition.updateSublayerTransformOffset(layer: strongSelf.countersContainerNode.layer, offset: CGPoint(x: isExtracted ? -16.0 : 0.0, y: 0.0))
transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 16.0 : 0.0, y: 0.0))
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
if !isExtracted {
@ -215,6 +238,7 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeViewsLayout = TextNode.asyncLayout(self.viewsNode)
let makeReactionsLayout = TextNode.asyncLayout(self.reactionsNode)
let makeForwardsLayout = TextNode.asyncLayout(self.forwardsNode)
let currentItem = self.item
@ -236,17 +260,23 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
let leftInset = 16.0 + params.leftInset
let rightInset = 16.0 + params.rightInset
var totalLeftInset = leftInset
let additionalRightInset: CGFloat = 128.0
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let titleFont = Font.semibold(item.presentationData.fontSize.itemListBaseFontSize)
let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0))
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
let contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: EngineMessage(item.message), strings: item.presentationData.strings, nameDisplayOrder: .firstLast, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId)
var text = !item.message.text.isEmpty ? item.message.text : stringForMediaKind(contentKind, strings: item.presentationData.strings).0.string
text = foldLineBreaks(text)
var text: String
var contentImageMedia: Media?
for media in item.message.media {
let timestamp: Int32
switch item.item {
case let .message(message):
let contentKind: MessageContentKind
contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: .firstLast, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId)
text = !message.text.isEmpty ? message.text : stringForMediaKind(contentKind, strings: item.presentationData.strings).0.string
for media in message.media {
if let image = media as? TelegramMediaImage {
contentImageMedia = image
break
@ -267,36 +297,63 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
}
}
}
timestamp = message.timestamp
case let .story(story):
text = item.presentationData.strings.Message_Story
timestamp = story.timestamp
if let image = story.media._asMedia() as? TelegramMediaImage {
contentImageMedia = image
break
} else if let file = story.media._asMedia() as? TelegramMediaFile {
contentImageMedia = file
break
}
}
text = foldLineBreaks(text)
if let _ = contentImageMedia {
totalLeftInset += 48.0
totalLeftInset += 46.0
}
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
if let contentImageMedia = contentImageMedia {
if let currentContentImageMedia = currentContentImageMedia, contentImageMedia.isSemanticallyEqual(to: currentContentImageMedia) {
} else {
switch item.item {
case let .message(message):
if let image = contentImageMedia as? TelegramMediaImage {
updateImageSignal = mediaGridMessagePhoto(account: item.context.account, userLocation: .peer(item.message.id.peerId), photoReference: .message(message: MessageReference(item.message), media: image))
updateImageSignal = mediaGridMessagePhoto(account: item.context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image))
} else if let file = contentImageMedia as? TelegramMediaFile {
updateImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, userLocation: .peer(item.message.id.peerId), videoReference: .message(message: MessageReference(item.message), media: file), autoFetchFullSizeThumbnail: true)
updateImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), autoFetchFullSizeThumbnail: true)
}
case let .story(story):
if let peerReference = PeerReference(item.peer) {
if let image = contentImageMedia as? TelegramMediaImage {
updateImageSignal = mediaGridMessagePhoto(account: item.context.account, userLocation: .peer(item.peer.id), photoReference: .story(peer: peerReference, id: story.id, media: image))
} else if let file = contentImageMedia as? TelegramMediaFile {
updateImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, userLocation: .peer(item.peer.id), videoReference: .story(peer: peerReference, id: story.id, media: file), autoFetchFullSizeThumbnail: true)
}
}
}
}
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0))
let label = stringForMediumDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: label, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (viewsLayout, viewsApply) = makeViewsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_MessageViews(item.views), font: labelFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 128.0, height: CGFloat.greatestFiniteMagnitude), alignment: .right, cutout: nil, insets: UIEdgeInsets()))
let (forwardsLayout, forwardsApply) = makeForwardsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_MessageForwards(item.forwards), font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 128.0, height: CGFloat.greatestFiniteMagnitude), alignment: .right, cutout: nil, insets: UIEdgeInsets()))
let reactions = item.reactions > 0 ? compactNumericCountString(Int(item.reactions), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) : ""
let (reactionsLayout, reactionsApply) = makeReactionsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: reactions, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 128.0, height: CGFloat.greatestFiniteMagnitude), alignment: .right, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat = 11.0
let forwards = item.forwards > 0 ? compactNumericCountString(Int(item.forwards), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) : ""
let (forwardsLayout, forwardsApply) = makeForwardsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: forwards, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 128.0, height: CGFloat.greatestFiniteMagnitude), alignment: .right, cutout: nil, insets: UIEdgeInsets()))
let additionalRightInset = max(viewsLayout.size.width, reactionsLayout.size.width + forwardsLayout.size.width + 36.0) + 8.0
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let label = stringForMediumDate(timestamp: timestamp, strings: item.presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: label, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat = 10.0
let titleSpacing: CGFloat = 3.0
let height: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height
@ -318,8 +375,14 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
if let strongSelf = self {
let themeUpdated = strongSelf.item?.presentationData.theme !== item.presentationData.theme
strongSelf.item = item
if themeUpdated {
strongSelf.forwardsIconNode.image = PresentationResourcesItemList.statsForwardsIcon(item.presentationData.theme)
strongSelf.reactionsIconNode.image = PresentationResourcesItemList.statsReactionsIcon(item.presentationData.theme)
}
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width - 16.0, height: layout.contentSize.height))
let extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0)
strongSelf.extractedRect = extractedRect
@ -357,13 +420,21 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
dimensions = contentImageMedia.dimensions?.cgSize
}
let contentImageSize = CGSize(width: 40.0, height: 40.0)
var contentImageSize = CGSize(width: 40.0, height: 40.0)
var contentImageInset = leftInset - 6.0
if let dimensions = dimensions {
let makeImageLayout = strongSelf.contentImageNode.asyncLayout()
let imageSize = contentImageSize
let applyImageLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: dimensions.aspectFilled(imageSize), boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))
let cornerRadius: CGFloat
if case .story = item.item {
contentImageInset += 3.0
contentImageSize = CGSize(width: 34.0, height: 34.0)
cornerRadius = contentImageSize.width / 2.0
} else {
cornerRadius = 6.0
}
let applyImageLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(radius: cornerRadius), imageSize: dimensions.aspectFilled(contentImageSize), boundingSize: contentImageSize, intrinsicInsets: UIEdgeInsets()))
applyImageLayout()
if let updateImageSignal = updateImageSignal {
@ -384,6 +455,7 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
let _ = labelApply()
let _ = viewsApply()
let _ = forwardsApply()
let _ = reactionsApply()
switch item.style {
case .plain:
@ -427,7 +499,7 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
bottomStripeInset = totalLeftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
@ -443,22 +515,106 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
let contentImageFrame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((height - contentImageSize.height) / 2.0)), size: contentImageSize)
let contentImageFrame = CGRect(origin: CGPoint(x: contentImageInset, y: floorToScreenPixels((height - contentImageSize.height) / 2.0)), size: contentImageSize)
strongSelf.contentImageNode.frame = contentImageFrame
let titleFrame = CGRect(origin: CGPoint(x: totalLeftInset, y: 11.0), size: titleLayout.size)
let titleFrame = CGRect(origin: CGPoint(x: totalLeftInset, y: 9.0), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
let labelFrame = CGRect(origin: CGPoint(x: totalLeftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
let viewsFrame = CGRect(origin: CGPoint(x: params.width - rightInset - viewsLayout.size.width, y: 15.0), size: viewsLayout.size)
let viewsFrame = CGRect(origin: CGPoint(x: params.width - rightInset - viewsLayout.size.width, y: 13.0), size: viewsLayout.size)
strongSelf.viewsNode.frame = viewsFrame
let forwardsFrame = CGRect(origin: CGPoint(x: params.width - rightInset - forwardsLayout.size.width, y: titleFrame.maxY + titleSpacing), size: forwardsLayout.size)
let iconSpacing: CGFloat = 3.0 - UIScreenPixel
var rightContentInset: CGFloat = rightInset
if forwardsLayout.size.width > 0.0 {
strongSelf.forwardsIconNode.isHidden = false
strongSelf.forwardsNode.isHidden = false
let forwardsFrame = CGRect(origin: CGPoint(x: params.width - rightContentInset - forwardsLayout.size.width, y: titleFrame.maxY + titleSpacing), size: forwardsLayout.size)
strongSelf.forwardsNode.frame = forwardsFrame
if let icon = strongSelf.forwardsIconNode.image {
let forwardsIconFrame = CGRect(origin: CGPoint(x: params.width - rightContentInset - forwardsLayout.size.width - icon.size.width - iconSpacing, y: titleFrame.maxY + titleSpacing - 2.0 + UIScreenPixel), size: icon.size)
strongSelf.forwardsIconNode.frame = forwardsIconFrame
rightContentInset += forwardsIconFrame.width + forwardsFrame.width + iconSpacing
}
rightContentInset += 10.0
} else {
strongSelf.forwardsIconNode.isHidden = true
strongSelf.forwardsNode.isHidden = true
}
if reactionsLayout.size.width > 0.0 {
strongSelf.reactionsIconNode.isHidden = false
strongSelf.reactionsNode.isHidden = false
let reactionsFrame = CGRect(origin: CGPoint(x: params.width - rightContentInset - reactionsLayout.size.width, y: titleFrame.maxY + titleSpacing), size: reactionsLayout.size)
strongSelf.reactionsNode.frame = reactionsFrame
if let icon = strongSelf.reactionsIconNode.image {
let reactionsIconFrame = CGRect(origin: CGPoint(x: params.width - rightContentInset - reactionsLayout.size.width - icon.size.width - iconSpacing, y: titleFrame.maxY + titleSpacing - 2.0 + UIScreenPixel), size: icon.size)
strongSelf.reactionsIconNode.frame = reactionsIconFrame
rightContentInset += reactionsIconFrame.width + reactionsFrame.width + iconSpacing
}
} else {
strongSelf.reactionsIconNode.isHidden = true
strongSelf.reactionsNode.isHidden = true
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: height + UIScreenPixel))
if case .story = item.item {
let lineWidth: CGFloat = 1.5
let imageSize = CGSize(width: contentImageFrame.width + 6.0, height: contentImageFrame.height + 6.0)
let indicatorSize = CGSize(width: imageSize.width - lineWidth * 4.0, height: imageSize.height - lineWidth * 4.0)
let storyIndicator: ComponentView<Empty>
let indicatorTransition: Transition = .immediate
if let current = strongSelf.storyIndicator {
storyIndicator = current
} else {
storyIndicator = ComponentView()
strongSelf.storyIndicator = storyIndicator
}
let _ = storyIndicator.update(
transition: indicatorTransition,
component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: true,
hasUnseenCloseFriendsItems: false,
colors: AvatarStoryIndicatorComponent.Colors(
unseenColors: item.presentationData.theme.chatList.storyUnseenColors.array,
unseenCloseFriendsColors: item.presentationData.theme.chatList.storyUnseenPrivateColors.array,
seenColors: item.presentationData.theme.chatList.storySeenColors.array
),
activeLineWidth: lineWidth,
inactiveLineWidth: lineWidth,
counters: AvatarStoryIndicatorComponent.Counters(
totalCount: 1,
unseenCount: 1
),
progress: nil
)),
environment: {},
containerSize: indicatorSize
)
if let storyIndicatorView = storyIndicator.view {
if storyIndicatorView.superview == nil {
strongSelf.offsetContainerNode.view.addSubview(storyIndicatorView)
}
indicatorTransition.setFrame(view: storyIndicatorView, frame: CGRect(origin: CGPoint(x: contentImageFrame.midX - indicatorSize.width / 2.0, y: contentImageFrame.midY - indicatorSize.height / 2.0), size: indicatorSize))
}
} else if let storyIndicator = strongSelf.storyIndicator {
if let storyIndicatorView = storyIndicator.view {
storyIndicatorView.removeFromSuperview()
}
strongSelf.storyIndicator = nil
}
}
})
}

View File

@ -0,0 +1,106 @@
import Foundation
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import PhotoResources
import AvatarStoryIndicatorComponent
import AccountContext
import TelegramPresentationData
final class StoryIconNode: ASDisplayNode {
private let imageNode = TransformImageNode()
private let storyIndicator = ComponentView<Empty>()
init(context: AccountContext, theme: PresentationTheme, peer: Peer, storyItem: EngineStoryItem) {
self.imageNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.imageNode)
let imageSize = CGSize(width: 30.0, height: 30.0)
let size = CGSize(width: 36.0, height: 36.0)
let bounds = CGRect(origin: .zero, size: size)
self.imageNode.frame = bounds.insetBy(dx: 3.0, dy: 3.0)
self.frame = bounds
let media: Media?
switch storyItem.media {
case let .image(image):
media = image
case let .file(file):
media = file
default:
media = nil
}
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var dimensions: CGSize?
if let peerReference = PeerReference(peer), let media {
if let image = media as? TelegramMediaImage {
updateImageSignal = mediaGridMessagePhoto(account: context.account, userLocation: .peer(peer.id), photoReference: .story(peer: peerReference, id: storyItem.id, media: image))
dimensions = largestRepresentationForPhoto(image)?.dimensions.cgSize
} else if let file = media as? TelegramMediaFile {
updateImageSignal = mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(peer.id), videoReference: .story(peer: peerReference, id: storyItem.id, media: file), autoFetchFullSizeThumbnail: true)
dimensions = file.dimensions?.cgSize
}
}
if let updateImageSignal {
self.imageNode.setSignal(updateImageSignal)
}
if let dimensions {
let cornerRadius = imageSize.width / 2.0
let makeImageLayout = self.imageNode.asyncLayout()
let applyImageLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(radius: cornerRadius), imageSize: dimensions.aspectFilled(imageSize), boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))
applyImageLayout()
}
let lineWidth: CGFloat = 1.5
let indicatorSize = CGSize(width: size.width - lineWidth * 4.0, height: size.height - lineWidth * 4.0)
let _ = self.storyIndicator.update(
transition: .immediate,
component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: true,
hasUnseenCloseFriendsItems: false,
colors: AvatarStoryIndicatorComponent.Colors(
unseenColors: theme.chatList.storyUnseenColors.array,
unseenCloseFriendsColors: theme.chatList.storyUnseenPrivateColors.array,
seenColors: theme.chatList.storySeenColors.array
),
activeLineWidth: lineWidth,
inactiveLineWidth: lineWidth,
counters: AvatarStoryIndicatorComponent.Counters(
totalCount: 1,
unseenCount: 1
),
progress: nil
)),
environment: {},
containerSize: indicatorSize
)
if let storyIndicatorView = self.storyIndicator.view {
storyIndicatorView.frame = CGRect(origin: CGPoint(x: bounds.midX - indicatorSize.width / 2.0, y: bounds.midY - indicatorSize.height / 2.0), size: indicatorSize)
}
}
override func didLoad() {
super.didLoad()
if let storyIndicatorView = self.storyIndicator.view {
if storyIndicatorView.superview == nil {
self.view.addSubview(storyIndicatorView)
}
}
}
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: 36.0, height: 36.0)
}
}

View File

@ -52,6 +52,14 @@ public struct ChannelStatsMessageInteractions: Equatable {
public let messageId: MessageId
public let views: Int32
public let forwards: Int32
public let reactions: Int32
public init(messageId: MessageId, views: Int32, forwards: Int32, reactions: Int32) {
self.messageId = messageId
self.views = views
self.forwards = forwards
self.reactions = reactions
}
}
public final class ChannelStats: Equatable {
@ -1143,7 +1151,7 @@ extension ChannelStatsMessageInteractions {
init(apiMessageInteractionCounters: Api.MessageInteractionCounters, peerId: PeerId) {
switch apiMessageInteractionCounters {
case let .messageInteractionCounters(msgId, views, forwards):
self = ChannelStatsMessageInteractions(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: msgId), views: views, forwards: forwards)
self = ChannelStatsMessageInteractions(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: msgId), views: views, forwards: forwards, reactions: 0)
}
}
}

View File

@ -17,6 +17,10 @@ public final class PresentationThemeGradientColors {
return (self.topColor, self.bottomColor)
}
public var array: [UIColor] {
return [self.topColor, self.bottomColor]
}
public func withUpdated(topColor: UIColor? = nil, bottomColor: UIColor? = nil) -> PresentationThemeGradientColors {
return PresentationThemeGradientColors(topColor: topColor ?? self.topColor, bottomColor: bottomColor ?? self.bottomColor)
}

View File

@ -72,6 +72,9 @@ public enum PresentationResourceKey: Int32 {
case itemListTopicArrowIcon
case itemListAddBoostsIcon
case statsReactionsIcon
case statsForwardsIcon
case itemListVoiceCallIcon
case itemListVideoCallIcon

View File

@ -329,4 +329,16 @@ public struct PresentationResourcesItemList {
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/TopicArrowIcon"), color: theme.list.itemSecondaryTextColor)
})
}
public static func statsReactionsIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.statsReactionsIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chart/Reactions"), color: theme.list.itemSecondaryTextColor)
})
}
public static func statsForwardsIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.statsForwardsIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chart/Forwards"), color: theme.list.itemSecondaryTextColor)
})
}
}

View File

@ -1123,7 +1123,7 @@ private final class CameraScreenComponent: CombinedComponent {
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: durationString, font: Font.with(size: 21.0, design: .camera), textColor: controlsTintColor)),
horizontalAlignment: .center,
textShadowColor: UIColor(rgb: 0x000000, alpha: 0.2)
textShadowColor: controlsTintColor == .black ? .clear : UIColor(rgb: 0x000000, alpha: 0.2)
),
availableSize: context.availableSize,
transition: context.transition

View File

@ -31,6 +31,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/ChatMessageBackground",
],
visibility = [

View File

@ -561,6 +561,7 @@ private final class PeerInfoInteraction {
let displayTopicsLimited: (TopicsLimitedReason) -> Void
let openPeerMention: (String, ChatControllerInteractionNavigateToPeer) -> Void
let openBotApp: (AttachMenuBot) -> Void
let openEditing: () -> Void
init(
openUsername: @escaping (String) -> Void,
@ -614,7 +615,8 @@ private final class PeerInfoInteraction {
toggleForumTopics: @escaping (Bool) -> Void,
displayTopicsLimited: @escaping (TopicsLimitedReason) -> Void,
openPeerMention: @escaping (String, ChatControllerInteractionNavigateToPeer) -> Void,
openBotApp: @escaping (AttachMenuBot) -> Void
openBotApp: @escaping (AttachMenuBot) -> Void,
openEditing: @escaping () -> Void
) {
self.openUsername = openUsername
self.openPhone = openPhone
@ -668,6 +670,7 @@ private final class PeerInfoInteraction {
self.displayTopicsLimited = displayTopicsLimited
self.openPeerMention = openPeerMention
self.openBotApp = openBotApp
self.openEditing = openEditing
}
}
@ -1208,6 +1211,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
let ItemAdmins = 6
let ItemMembers = 7
let ItemMemberRequests = 8
let ItemEdit = 9
if let _ = data.threadData {
let mainUsername: String
@ -1358,6 +1362,10 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
interaction.openParticipantsSection(.memberRequests)
}))
}
items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: ItemEdit, label: .none, text: presentationData.strings.Channel_Info_Settings, icon: UIImage(bundleImageName: "Chat/Info/SettingsIcon"), action: {
interaction.openEditing()
}))
}
}
}
@ -2425,6 +2433,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
},
openBotApp: { [weak self] bot in
self?.openBotApp(bot)
},
openEditing: { [weak self] in
self?.headerNode.navigationButtonContainer.performAction?(.edit, nil, nil)
}
)

View File

@ -1,7 +1,7 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "arrowshape_18.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,148 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 1.809143 2.532226 cm
0.000000 0.000000 0.000000 scn
1.381874 3.080939 m
1.812524 5.409000 3.194212 8.717773 7.569556 8.717773 c
7.569556 9.917774 l
7.569556 10.446452 l
7.569556 10.480809 l
7.569556 10.707834 l
7.569556 11.144928 7.569556 11.363475 7.657061 11.467960 c
7.733040 11.558682 7.847383 11.608219 7.965533 11.601600 c
8.101606 11.593976 8.261045 11.444503 8.579921 11.145556 c
8.745543 10.990285 l
8.770609 10.966786 l
13.102652 6.905496 l
13.265036 6.753260 13.346226 6.677144 13.376384 6.588246 c
13.402888 6.510118 13.402888 6.425430 13.376384 6.347302 c
13.346226 6.258403 13.265034 6.182286 13.102651 6.030052 c
8.770609 1.968762 l
8.745543 1.945263 l
8.579920 1.789990 l
8.261045 1.491045 8.101606 1.341572 7.965533 1.333949 c
7.847383 1.327330 7.733040 1.376866 7.657061 1.467588 c
7.569556 1.572074 7.569556 1.790621 7.569556 2.227714 c
7.569556 2.454739 l
7.569556 2.489097 l
7.569556 3.017775 l
7.569556 4.217774 l
7.144288 4.217774 6.744702 4.192091 6.369556 4.145205 c
4.554104 3.918311 3.310977 3.194876 2.499113 2.482739 c
2.362022 2.362489 2.237234 2.242566 2.124057 2.125404 c
2.050126 2.048869 1.981147 1.973513 1.916941 1.900021 c
1.888999 1.868037 1.861959 1.836406 1.835807 1.805183 c
1.802983 1.765995 1.771556 1.727451 1.741497 1.689663 c
1.527425 1.420543 1.420391 1.285985 1.360707 1.277870 c
1.306377 1.270482 1.259393 1.288921 1.224590 1.331290 c
1.186357 1.377833 1.198391 1.535853 1.222459 1.851893 c
1.228173 1.926921 1.234867 2.005210 1.242695 2.086436 c
1.243798 2.097883 1.244923 2.109387 1.246072 2.120950 c
1.257281 2.233779 1.270675 2.352077 1.286657 2.475003 c
1.311591 2.666784 1.342823 2.869831 1.381874 3.080939 c
h
0.592504 2.179043 m
0.592526 2.179102 0.593651 2.180346 0.595821 2.182594 c
0.593566 2.180108 0.592482 2.178984 0.592504 2.179043 c
h
6.369556 2.934145 m
6.369556 2.227714 l
6.369556 2.216033 6.369552 2.204231 6.369547 2.192325 c
6.369482 2.005756 6.369406 1.793388 6.385262 1.616055 c
6.399108 1.461200 6.439729 1.052163 6.737077 0.697114 c
7.056188 0.316081 7.536427 0.108026 8.032658 0.135827 c
8.495048 0.161733 8.821238 0.411856 8.943680 0.507667 c
9.083900 0.617389 9.238787 0.762698 9.374854 0.890351 c
9.383532 0.898493 9.392133 0.906562 9.400649 0.914546 c
13.923381 5.154607 l
13.928342 5.159257 13.933517 5.164101 13.938882 5.169123 c
14.005202 5.231203 14.100604 5.320504 14.180048 5.408552 c
14.274767 5.513531 14.422819 5.696626 14.512774 5.961792 c
14.624091 6.289929 14.624091 6.645618 14.512774 6.973756 c
14.422819 7.238921 14.274767 7.422017 14.180048 7.526996 c
14.100601 7.615047 14.005195 7.704352 13.938873 7.766432 c
13.933511 7.771451 13.928339 7.776292 13.923381 7.780941 c
9.400650 12.021002 l
9.392125 12.028994 9.383514 12.037071 9.374827 12.045221 c
9.238766 12.172870 9.083892 12.318167 8.943680 12.427882 c
8.821238 12.523692 8.495049 12.773815 8.032658 12.799721 c
7.536427 12.827522 7.056188 12.619467 6.737077 12.238434 c
6.439729 11.883385 6.399108 11.474348 6.385262 11.319493 c
6.369406 11.142159 6.369482 10.929791 6.369547 10.743222 c
6.369552 10.731316 6.369556 10.719515 6.369556 10.707834 c
6.369556 9.839663 l
3.993418 9.518946 2.436784 8.247826 1.476669 6.720201 c
0.412234 5.026595 0.112609 3.081284 0.025924 1.943016 c
0.025268 1.934407 0.024604 1.925713 0.023934 1.916945 c
0.013668 1.782581 0.002083 1.630936 0.000289 1.508040 c
-0.000665 1.442641 -0.000246 1.333831 0.017544 1.213016 c
0.030832 1.122771 0.076845 0.837997 0.297327 0.569592 c
0.596399 0.205513 1.055517 0.025331 1.522386 0.088812 c
1.882774 0.137815 2.117930 0.331811 2.178427 0.381719 c
2.178911 0.382119 l
2.270915 0.458017 2.344114 0.537148 2.388324 0.586662 c
2.472725 0.681190 2.571892 0.805902 2.664181 0.921966 c
2.680620 0.942637 l
3.203021 1.599372 4.310469 2.634761 6.369556 2.934145 c
h
f*
n
Q
endstream
endobj
3 0 obj
3854
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 18.000000 18.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000003944 00000 n
0000003967 00000 n
0000004140 00000 n
0000004214 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4273
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "heart_18.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,95 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 2.024963 2.215471 cm
0.000000 0.000000 0.000000 scn
0.000000 8.477227 m
0.000000 11.090064 1.837903 13.009529 4.216708 13.009529 c
5.378695 13.009529 6.314748 12.504827 6.976930 11.731193 c
7.642573 12.506644 8.575254 13.009529 9.739605 13.009529 c
12.119733 13.009529 13.950001 11.088655 13.950001 8.477227 c
13.950001 5.445473 11.442898 2.625515 7.886870 0.335408 c
7.883046 0.332945 l
7.883036 0.332960 l
7.773217 0.263408 7.639669 0.186707 7.499969 0.125453 c
7.373253 0.069893 7.182288 0.000000 6.975000 0.000000 c
6.768435 0.000000 6.577350 0.071704 6.454765 0.125453 c
6.317346 0.185706 6.184213 0.261022 6.074458 0.328291 c
6.063049 0.335283 l
6.063130 0.335408 l
2.507103 2.625515 0.000000 5.445473 0.000000 8.477227 c
h
4.216708 11.809529 m
2.568532 11.809529 1.200000 10.497311 1.200000 8.477227 c
1.200000 6.107325 3.209978 3.601517 6.707050 1.348038 c
6.790931 1.296850 6.870368 1.253510 6.936634 1.224454 c
6.952608 1.217450 6.966116 1.212033 6.977225 1.207909 c
6.988396 1.211988 7.002014 1.217401 7.018100 1.224454 c
7.083025 1.252921 7.160221 1.295694 7.239110 1.345563 c
10.738551 3.599682 12.750001 6.106457 12.750001 8.477227 c
12.750001 10.498720 11.386456 11.809529 9.739605 11.809529 c
8.743175 11.809529 7.985357 11.273088 7.501738 10.386451 c
7.396244 10.193047 7.193245 10.073009 6.972942 10.073765 c
6.752639 10.074521 6.550468 10.195948 6.446304 10.390072 c
5.975964 11.266614 5.213501 11.809529 4.216708 11.809529 c
h
f*
n
Q
endstream
endobj
3 0 obj
1521
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 18.000000 18.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001611 00000 n
0000001634 00000 n
0000001807 00000 n
0000001881 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1940
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "settings_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,290 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 30.000000 30.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
1.000000 0.584314 0.000000 scn
0.000000 18.799999 m
0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c
1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c
5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c
18.799999 30.000000 l
22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c
27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c
30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c
30.000000 11.200001 l
30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c
28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c
24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c
11.200000 0.000000 l
7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c
2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c
0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c
0.000000 18.799999 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 6.000000 9.000000 cm
1.000000 1.000000 1.000000 scn
17.000078 10.000000 m
17.552362 10.000000 18.000078 10.447716 18.000078 11.000000 c
18.000078 11.552285 17.552362 12.000000 17.000078 12.000000 c
1.000078 12.000000 l
0.447794 12.000000 0.000078 11.552285 0.000078 11.000000 c
0.000078 10.447716 0.447794 10.000000 1.000078 10.000000 c
17.000078 10.000000 l
h
17.000000 0.000078 m
17.552284 0.000078 18.000000 0.447793 18.000000 1.000078 c
18.000000 1.552363 17.552284 2.000078 17.000000 2.000078 c
1.000000 2.000078 l
0.447716 2.000078 0.000000 1.552363 0.000000 1.000078 c
0.000000 0.447793 0.447716 0.000078 1.000000 0.000078 c
17.000000 0.000078 l
h
f*
n
Q
q
q
1.000000 0.000000 -0.000000 1.000000 6.000000 16.000000 cm
1.000000 0.584314 0.000000 scn
8.000000 4.000000 m
8.000000 1.790861 6.209139 0.000000 4.000000 0.000000 c
1.790861 0.000000 0.000000 1.790861 0.000000 4.000000 c
0.000000 6.209139 1.790861 8.000000 4.000000 8.000000 c
6.209139 8.000000 8.000000 6.209139 8.000000 4.000000 c
h
f
n
Q
14.000000 20.000000 m
14.000000 17.790861 12.209139 16.000000 10.000000 16.000000 c
7.790861 16.000000 6.000000 17.790861 6.000000 20.000000 c
6.000000 22.209139 7.790861 24.000000 10.000000 24.000000 c
12.209139 24.000000 14.000000 22.209139 14.000000 20.000000 c
h
W*
n
q
1.000000 0.000000 -0.000000 1.000000 6.000000 16.000000 cm
1.000000 1.000000 1.000000 scn
6.000000 4.000000 m
6.000000 2.895431 5.104569 2.000000 4.000000 2.000000 c
4.000000 -2.000000 l
7.313708 -2.000000 10.000000 0.686292 10.000000 4.000000 c
6.000000 4.000000 l
h
4.000000 2.000000 m
2.895431 2.000000 2.000000 2.895431 2.000000 4.000000 c
-2.000000 4.000000 l
-2.000000 0.686292 0.686292 -2.000000 4.000000 -2.000000 c
4.000000 2.000000 l
h
2.000000 4.000000 m
2.000000 5.104569 2.895431 6.000000 4.000000 6.000000 c
4.000000 10.000000 l
0.686292 10.000000 -2.000000 7.313708 -2.000000 4.000000 c
2.000000 4.000000 l
h
4.000000 6.000000 m
5.104569 6.000000 6.000000 5.104569 6.000000 4.000000 c
10.000000 4.000000 l
10.000000 7.313708 7.313708 10.000000 4.000000 10.000000 c
4.000000 6.000000 l
h
f
n
Q
Q
q
q
1.000000 0.000000 -0.000000 1.000000 16.000000 6.000000 cm
1.000000 0.584314 0.000000 scn
8.000000 4.000000 m
8.000000 1.790861 6.209139 0.000000 4.000000 0.000000 c
1.790861 0.000000 0.000000 1.790861 0.000000 4.000000 c
0.000000 6.209139 1.790861 8.000000 4.000000 8.000000 c
6.209139 8.000000 8.000000 6.209139 8.000000 4.000000 c
h
f
n
Q
24.000000 10.000000 m
24.000000 7.790861 22.209139 6.000000 20.000000 6.000000 c
17.790861 6.000000 16.000000 7.790861 16.000000 10.000000 c
16.000000 12.209139 17.790861 14.000000 20.000000 14.000000 c
22.209139 14.000000 24.000000 12.209139 24.000000 10.000000 c
h
W*
n
q
1.000000 0.000000 -0.000000 1.000000 16.000000 6.000000 cm
1.000000 1.000000 1.000000 scn
6.000000 4.000000 m
6.000000 2.895431 5.104569 2.000000 4.000000 2.000000 c
4.000000 -2.000000 l
7.313708 -2.000000 10.000000 0.686292 10.000000 4.000000 c
6.000000 4.000000 l
h
4.000000 2.000000 m
2.895431 2.000000 2.000000 2.895431 2.000000 4.000000 c
-2.000000 4.000000 l
-2.000000 0.686292 0.686292 -2.000000 4.000000 -2.000000 c
4.000000 2.000000 l
h
2.000000 4.000000 m
2.000000 5.104569 2.895431 6.000000 4.000000 6.000000 c
4.000000 10.000000 l
0.686292 10.000000 -2.000000 7.313708 -2.000000 4.000000 c
2.000000 4.000000 l
h
4.000000 6.000000 m
5.104569 6.000000 6.000000 5.104569 6.000000 4.000000 c
10.000000 4.000000 l
10.000000 7.313708 7.313708 10.000000 4.000000 10.000000 c
4.000000 6.000000 l
h
f
n
Q
Q
endstream
endobj
2 0 obj
4505
endobj
3 0 obj
<< /Type /XObject
/Length 4 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 30.000000 30.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
0.000000 18.799999 m
0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c
1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c
5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c
18.799999 30.000000 l
22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c
27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c
30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c
30.000000 11.200001 l
30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c
28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c
24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c
11.200000 0.000000 l
7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c
2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c
0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c
0.000000 18.799999 l
h
f
n
Q
endstream
endobj
4 0 obj
944
endobj
5 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E1 << /SMask << /Type /Mask
/G 3 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
>>
endobj
6 0 obj
<< /Length 7 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
/X1 Do
Q
endstream
endobj
7 0 obj
46
endobj
8 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 5 0 R
/Contents 6 0 R
/Parent 9 0 R
>>
endobj
9 0 obj
<< /Kids [ 8 0 R ]
/Count 1
/Type /Pages
>>
endobj
10 0 obj
<< /Pages 9 0 R
/Type /Catalog
>>
endobj
xref
0 11
0000000000 65535 f
0000000010 00000 n
0000004763 00000 n
0000004786 00000 n
0000005978 00000 n
0000006000 00000 n
0000006298 00000 n
0000006400 00000 n
0000006421 00000 n
0000006594 00000 n
0000006668 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 10 0 R
/Size 11
>>
startxref
6728
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "person_6.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,78 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.018066 0.500000 cm
0.000000 0.000000 0.000000 scn
2.980786 3.972965 m
3.816679 3.972965 4.494304 4.650590 4.494304 5.486483 c
4.494304 6.322375 3.816679 7.000000 2.980786 7.000000 c
2.144894 7.000000 1.467269 6.322375 1.467269 5.486483 c
1.467269 4.650590 2.144894 3.972965 2.980786 3.972965 c
h
5.734580 1.858224 m
5.255256 2.484169 4.423840 3.027039 2.981694 3.027039 c
1.539547 3.027039 0.708129 2.484169 0.228806 1.858224 c
-0.442749 0.981247 0.471706 0.000003 1.576275 0.000003 c
4.387107 0.000003 l
5.491676 0.000003 6.406134 0.981247 5.734580 1.858224 c
h
f*
n
Q
endstream
endobj
3 0 obj
639
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 6.000000 8.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000729 00000 n
0000000751 00000 n
0000000922 00000 n
0000000996 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1055
%%EOF

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "lockedaudiototext.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1,202 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 3.834961 10.247070 cm
0.000000 0.000000 0.000000 scn
14.784782 9.993996 m
14.685381 10.249602 14.439255 10.417969 14.165000 10.417969 c
13.890745 10.417969 13.644619 10.249602 13.545217 9.993996 c
10.045217 0.993996 l
9.912102 0.651699 10.081676 0.266301 10.423973 0.133185 c
10.766270 0.000071 11.151668 0.169645 11.284783 0.511942 c
12.286572 3.087969 l
15.680867 3.087969 l
15.758190 3.902153 16.114431 4.635052 16.652981 5.190056 c
16.534782 5.493996 l
14.784782 9.993996 l
h
15.526207 4.417969 m
12.803794 4.417969 l
14.165000 7.918214 l
15.295218 5.011942 l
15.526207 4.417969 l
h
3.694774 8.723195 m
3.954473 8.982893 4.375527 8.982893 4.635226 8.723195 c
8.135226 5.223195 l
8.394924 4.963496 8.394924 4.542441 8.135226 4.282743 c
4.635226 0.782743 l
4.375527 0.523045 3.954473 0.523045 3.694774 0.782743 c
3.435075 1.042441 3.435075 1.463496 3.694774 1.723194 c
6.059548 4.087969 l
0.665000 4.087969 l
0.297731 4.087969 0.000000 4.385699 0.000000 4.752969 c
0.000000 5.120238 0.297731 5.417969 0.665000 5.417969 c
6.059548 5.417969 l
3.694774 7.782743 l
3.435075 8.042441 3.435075 8.463496 3.694774 8.723195 c
h
f*
n
Q
q
1.000000 0.000000 -0.000000 1.000000 21.000000 7.669922 cm
0.898039 0.949020 1.000000 scn
3.335000 1.330078 m
3.335000 0.962809 3.632731 0.665078 4.000000 0.665078 c
4.367270 0.665078 4.665000 0.962809 4.665000 1.330078 c
3.335000 1.330078 l
h
-0.665000 1.330078 m
-0.665000 0.962809 -0.367269 0.665078 0.000000 0.665078 c
0.367269 0.665078 0.665000 0.962809 0.665000 1.330078 c
-0.665000 1.330078 l
h
3.335000 5.330078 m
3.335000 1.330078 l
4.665000 1.330078 l
4.665000 5.330078 l
3.335000 5.330078 l
h
0.665000 1.330078 m
0.665000 5.330078 l
-0.665000 5.330078 l
-0.665000 1.330078 l
0.665000 1.330078 l
h
2.000000 6.665078 m
2.737300 6.665078 3.335000 6.067378 3.335000 5.330078 c
4.665000 5.330078 l
4.665000 6.801917 3.471839 7.995078 2.000000 7.995078 c
2.000000 6.665078 l
h
2.000000 7.995078 m
0.528161 7.995078 -0.665000 6.801917 -0.665000 5.330078 c
0.665000 5.330078 l
0.665000 6.067378 1.262700 6.665078 2.000000 6.665078 c
2.000000 7.995078 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 21.000000 7.669922 cm
0.000000 0.000000 0.000000 scn
3.335000 1.330078 m
3.335000 0.962809 3.632731 0.665078 4.000000 0.665078 c
4.367270 0.665078 4.665000 0.962809 4.665000 1.330078 c
3.335000 1.330078 l
h
-0.665000 1.330078 m
-0.665000 0.962809 -0.367269 0.665078 0.000000 0.665078 c
0.367269 0.665078 0.665000 0.962809 0.665000 1.330078 c
-0.665000 1.330078 l
h
3.335000 5.330078 m
3.335000 1.330078 l
4.665000 1.330078 l
4.665000 5.330078 l
3.335000 5.330078 l
h
0.665000 1.330078 m
0.665000 5.330078 l
-0.665000 5.330078 l
-0.665000 1.330078 l
0.665000 1.330078 l
h
2.000000 6.665078 m
2.737300 6.665078 3.335000 6.067378 3.335000 5.330078 c
4.665000 5.330078 l
4.665000 6.801917 3.471839 7.995078 2.000000 7.995078 c
2.000000 6.665078 l
h
2.000000 7.995078 m
0.528161 7.995078 -0.665000 6.801917 -0.665000 5.330078 c
0.665000 5.330078 l
0.665000 6.067378 1.262700 6.665078 2.000000 6.665078 c
2.000000 7.995078 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 19.500000 6.000000 cm
0.000000 0.000000 0.000000 scn
0.000000 3.000000 m
0.000000 3.931883 0.000000 4.397825 0.152241 4.765367 c
0.355229 5.255423 0.744577 5.644771 1.234633 5.847759 c
1.602175 6.000000 2.068117 6.000000 3.000000 6.000000 c
4.000000 6.000000 l
4.931883 6.000000 5.397825 6.000000 5.765367 5.847759 c
6.255423 5.644771 6.644771 5.255423 6.847759 4.765367 c
7.000000 4.397825 7.000000 3.931883 7.000000 3.000000 c
7.000000 3.000000 l
7.000000 2.068117 7.000000 1.602175 6.847759 1.234633 c
6.644771 0.744577 6.255423 0.355228 5.765367 0.152241 c
5.397825 0.000000 4.931883 0.000000 4.000000 0.000000 c
3.000000 0.000000 l
2.068117 0.000000 1.602175 0.000000 1.234633 0.152241 c
0.744577 0.355228 0.355229 0.744577 0.152241 1.234633 c
0.000000 1.602175 0.000000 2.068117 0.000000 3.000000 c
0.000000 3.000000 l
h
f
n
Q
endstream
endobj
3 0 obj
3999
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000004089 00000 n
0000004112 00000 n
0000004285 00000 n
0000004359 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4418
%%EOF

View File

@ -3694,7 +3694,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self, let (id, statsDatacenterId) = messageIdAndStatsDatacenterId, let statsDatacenterId = statsDatacenterId else {
return
}
strongSelf.push(messageStatsController(context: context, messageId: id, statsDatacenterId: statsDatacenterId))
strongSelf.push(messageStatsController(context: context, subject: .message(id: id), statsDatacenterId: statsDatacenterId))
})
}, delay: true)
}, editMessageMedia: { [weak self] messageId, draw in