Initial support for channel stats

UI fixes
This commit is contained in:
Peter 2019-02-22 18:22:55 +03:00
parent cd08fa1f3b
commit c1067d24cb
22 changed files with 3084 additions and 2661 deletions

View File

@ -1120,6 +1120,8 @@
D0F67FF21EE6B915000E5906 /* ChannelMembersSearchControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F67FF11EE6B915000E5906 /* ChannelMembersSearchControllerNode.swift */; };
D0F67FF41EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F67FF31EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift */; };
D0F6800A1EE750EE000E5906 /* ChannelBannedMemberController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F680091EE750EE000E5906 /* ChannelBannedMemberController.swift */; };
D0F760DB222034910074F7E5 /* ChannelStatsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F760DA222034910074F7E5 /* ChannelStatsController.swift */; };
D0F760DD222034980074F7E5 /* ChannelStatsControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F760DC222034980074F7E5 /* ChannelStatsControllerNode.swift */; };
D0F8C397201774A200236FC5 /* FeedGroupingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F8C396201774A200236FC5 /* FeedGroupingController.swift */; };
D0F8C399201774AF00236FC5 /* FeedGroupingControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F8C398201774AF00236FC5 /* FeedGroupingControllerNode.swift */; };
D0F9720F1FFE4BD5002595C8 /* notification.caf in Resources */ = {isa = PBXBuildFile; fileRef = D0C50E431E93FCD200F62E39 /* notification.caf */; };
@ -2332,6 +2334,8 @@
D0F69EA91D6B9BCB0046BCD6 /* libavformat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavformat.a; path = "third-party/FFmpeg-iOS/lib/libavformat.a"; sourceTree = "<group>"; };
D0F69EAA1D6B9BCB0046BCD6 /* libavutil.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavutil.a; path = "third-party/FFmpeg-iOS/lib/libavutil.a"; sourceTree = "<group>"; };
D0F69EAB1D6B9BCB0046BCD6 /* libswresample.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libswresample.a; path = "third-party/FFmpeg-iOS/lib/libswresample.a"; sourceTree = "<group>"; };
D0F760DA222034910074F7E5 /* ChannelStatsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelStatsController.swift; sourceTree = "<group>"; };
D0F760DC222034980074F7E5 /* ChannelStatsControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelStatsControllerNode.swift; sourceTree = "<group>"; };
D0F7AB341DCFADCD009AD9A1 /* ChatMessageBubbleImages.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageBubbleImages.swift; sourceTree = "<group>"; };
D0F7AB381DCFF87B009AD9A1 /* ChatMessageDateHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageDateHeader.swift; sourceTree = "<group>"; };
D0F8C396201774A200236FC5 /* FeedGroupingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedGroupingController.swift; sourceTree = "<group>"; };
@ -4206,6 +4210,8 @@
D0F4B019211073C500912B92 /* DeviceContactInfoController.swift */,
092F368F2157AB46001A9F49 /* ItemListCallListItem.swift */,
09DD88E821BAF65E000766BC /* ItemListAddressItem.swift */,
D0F760DA222034910074F7E5 /* ChannelStatsController.swift */,
D0F760DC222034980074F7E5 /* ChannelStatsControllerNode.swift */,
);
name = "Peer Info";
sourceTree = "<group>";
@ -5335,6 +5341,7 @@
D0EC6D161EB9F58800EBF1C3 /* MediaTrackFrameDecoder.swift in Sources */,
D056CD701FF147B000880D28 /* IconButtonNode.swift in Sources */,
D0EC6D171EB9F58800EBF1C3 /* FFMpegAudioFrameDecoder.swift in Sources */,
D0F760DB222034910074F7E5 /* ChannelStatsController.swift in Sources */,
D0EC6D181EB9F58800EBF1C3 /* FFMpegMediaFrameSource.swift in Sources */,
D0EC6D191EB9F58800EBF1C3 /* FFMpegMediaFrameSourceContext.swift in Sources */,
D02D60AE206BD47300FEFE1E /* SecureIdDocumentTypeSelectionController.swift in Sources */,
@ -5369,6 +5376,7 @@
D007019E2029EFDD006B9E34 /* ICloudResources.swift in Sources */,
D0EC6D221EB9F58800EBF1C3 /* PhotoResources.swift in Sources */,
D048EA871F4F296400188713 /* InstantPageSettingsFontSizeItemNode.swift in Sources */,
D0F760DD222034980074F7E5 /* ChannelStatsControllerNode.swift in Sources */,
D0EC6D231EB9F58800EBF1C3 /* StickerResources.swift in Sources */,
09C9EA3821A044B500E90146 /* StringForDuration.swift in Sources */,
D0EC6D241EB9F58800EBF1C3 /* CachedResourceRepresentations.swift in Sources */,

View File

@ -15,6 +15,7 @@ private final class ChannelInfoControllerArguments {
let openChannelTypeSetup: () -> Void
let changeNotificationMuteSettings: () -> Void
let openSharedMedia: () -> Void
let openStats: () -> Void
let openAdmins: () -> Void
let openMembers: () -> Void
let openBanned: () -> Void
@ -25,7 +26,7 @@ private final class ChannelInfoControllerArguments {
let displayContextMenu: (ChannelInfoEntryTag, String) -> Void
let aboutLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void
let toggleSignatures:(Bool) -> Void
init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, openChannelTypeSetup: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdmins: @escaping () -> Void, openMembers: @escaping () -> Void, openBanned: @escaping () -> Void, reportChannel: @escaping () -> Void, leaveChannel: @escaping () -> Void, deleteChannel: @escaping () -> Void, displayAddressNameContextMenu: @escaping (String) -> Void, displayContextMenu: @escaping (ChannelInfoEntryTag, String) -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, toggleSignatures: @escaping(Bool)->Void) {
init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, openChannelTypeSetup: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openStats: @escaping () -> Void, openAdmins: @escaping () -> Void, openMembers: @escaping () -> Void, openBanned: @escaping () -> Void, reportChannel: @escaping () -> Void, leaveChannel: @escaping () -> Void, deleteChannel: @escaping () -> Void, displayAddressNameContextMenu: @escaping (String) -> Void, displayContextMenu: @escaping (ChannelInfoEntryTag, String) -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, toggleSignatures: @escaping(Bool)->Void) {
self.account = account
self.avatarAndNameInfoContext = avatarAndNameInfoContext
self.tapAvatarAction = tapAvatarAction
@ -35,6 +36,7 @@ private final class ChannelInfoControllerArguments {
self.openChannelTypeSetup = openChannelTypeSetup
self.changeNotificationMuteSettings = changeNotificationMuteSettings
self.openSharedMedia = openSharedMedia
self.openStats = openStats
self.openAdmins = openAdmins
self.openMembers = openMembers
self.openBanned = openBanned
@ -73,6 +75,7 @@ private enum ChannelInfoEntry: ItemListNodeEntry {
case banned(theme: PresentationTheme, text: String, value: String)
case notifications(theme: PresentationTheme, text: String, value: String)
case sharedMedia(theme: PresentationTheme, text: String)
case stats(theme: PresentationTheme, text: String)
case signMessages(theme: PresentationTheme, text: String, value: Bool)
case signInfo(theme: PresentationTheme, text: String)
case report(theme: PresentationTheme, text: String)
@ -87,7 +90,7 @@ private enum ChannelInfoEntry: ItemListNodeEntry {
return ChannelInfoSection.discriptionAndType.rawValue
case .admins, .members, .banned:
return ChannelInfoSection.members.rawValue
case .sharedMedia, .notifications:
case .sharedMedia, .notifications, .stats:
return ChannelInfoSection.sharedMediaAndNotifications.rawValue
case .report, .leave, .deleteChannel:
return ChannelInfoSection.reportOrLeave.rawValue
@ -122,12 +125,14 @@ private enum ChannelInfoEntry: ItemListNodeEntry {
return 12
case .sharedMedia:
return 14
case .report:
case .stats:
return 15
case .leave:
case .report:
return 16
case .deleteChannel:
case .leave:
return 17
case .deleteChannel:
return 18
}
}
@ -235,6 +240,12 @@ private enum ChannelInfoEntry: ItemListNodeEntry {
} else {
return false
}
case let .stats(lhsTheme, lhsText):
if case let .stats(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .report(lhsTheme, lhsText):
if case let .report(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
@ -322,6 +333,10 @@ private enum ChannelInfoEntry: ItemListNodeEntry {
return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .plain, action: {
arguments.openSharedMedia()
})
case let .stats(theme, text):
return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .plain, action: {
arguments.openStats()
})
case let .notifications(theme, text, value):
return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: {
arguments.changeNotificationMuteSettings()
@ -482,6 +497,9 @@ private func channelInfoEntries(account: Account, presentationData: Presentation
}
if state.editingState == nil {
entries.append(ChannelInfoEntry.sharedMedia(theme: presentationData.theme, text: presentationData.strings.GroupInfo_SharedMedia))
if let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.flags.contains(.canViewStats) {
entries.append(ChannelInfoEntry.stats(theme: presentationData.theme, text: presentationData.strings.ChannelInfo_Stats))
}
}
if peer.flags.contains(.isCreator) {
@ -557,6 +575,9 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi
let navigateDisposable = MetaDisposable()
actionsDisposable.add(navigateDisposable)
let statsUrlDisposable = MetaDisposable()
actionsDisposable.add(statsUrlDisposable)
var avatarGalleryTransitionArguments: ((AvatarGalleryEntry) -> GalleryTransitionArguments?)?
let avatarAndNameInfoContext = ItemListAvatarAndNameInfoItemContext()
var updateHiddenAvatarImpl: (() -> Void)?
@ -722,6 +743,43 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi
if let controller = peerSharedMediaController(context: context, peerId: peerId) {
pushControllerImpl?(controller)
}
}, openStats: {
var urlSignal = channelStatsUrl(postbox: context.account.postbox, network: context.account.network, peerId: peerId, params: "")
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { subscriber in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .loading(cancelled: {
cancelImpl?()
}))
presentControllerImpl?(controller, nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
urlSignal = urlSignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
statsUrlDisposable.set(nil)
}
statsUrlDisposable.set((urlSignal
|> deliverOnMainQueue).start(next: { url in
pushControllerImpl?(ChannelStatsController(context: context, url: url, peerId: peerId))
}, error: { _ in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
}))
}, openAdmins: {
pushControllerImpl?(channelAdminsController(context: context, peerId: peerId))
}, openMembers: {

View File

@ -0,0 +1,67 @@
import Foundation
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import Postbox
final class ChannelStatsController: ViewController {
private var controllerNode: ChannelStatsControllerNode {
return self.displayNode as! ChannelStatsControllerNode
}
private let context: AccountContext
private let url: String
private let peerId: PeerId
private var presentationData: PresentationData
init(context: AccountContext, url: String, peerId: PeerId) {
self.context = context
self.url = url
self.peerId = peerId
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style
self.navigationItem.title = self.presentationData.strings.ChannelInfo_Stats
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func closePressed() {
self.dismiss()
}
override func loadDisplayNode() {
self.displayNode = ChannelStatsControllerNode(context: self.context, presentationData: self.presentationData, peerId: self.peerId, url: self.url, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
})
}
override func dismiss(completion: (() -> Void)? = nil) {
self.controllerNode.animateOut(completion: { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
completion?()
})
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
}
override var presentationController: UIPresentationController? {
get {
return nil
} set(value) {
}
}
}

View File

@ -0,0 +1,144 @@
import Foundation
import Display
import AsyncDisplayKit
import WebKit
import TelegramCore
import Postbox
import SwiftSignalKit
private class WeakChannelStatsScriptMessageHandler: NSObject, WKScriptMessageHandler {
private let f: (WKScriptMessage) -> ()
init(_ f: @escaping (WKScriptMessage) -> ()) {
self.f = f
super.init()
}
func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) {
self.f(scriptMessage)
}
}
final class ChannelStatsControllerNode: ViewControllerTracingNode, WKNavigationDelegate {
private var webView: WKWebView?
private let context: AccountContext
private let peerId: PeerId
var presentationData: PresentationData
private let present: (ViewController, Any?) -> Void
private let refreshDisposable = MetaDisposable()
init(context: AccountContext, presentationData: PresentationData, peerId: PeerId, url: String, present: @escaping (ViewController, Any?) -> Void) {
self.context = context
self.presentationData = presentationData
self.peerId = peerId
self.present = present
super.init()
self.backgroundColor = .white
let js = "var TelegramWebviewProxyProto = function() {}; " +
"TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " +
"window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " +
"}; " +
"var TelegramWebviewProxy = new TelegramWebviewProxyProto();"
let configuration = WKWebViewConfiguration()
let userController = WKUserContentController()
let userScript = WKUserScript(source: js, injectionTime: .atDocumentStart, forMainFrameOnly: false)
userController.addUserScript(userScript)
userController.add(WeakChannelStatsScriptMessageHandler { [weak self] message in
if let strongSelf = self {
strongSelf.handleScriptMessage(message)
}
}, name: "performAction")
configuration.userContentController = userController
let webView = WKWebView(frame: CGRect(), configuration: configuration)
if #available(iOSApplicationExtension 9.0, *) {
webView.allowsLinkPreview = false
}
if #available(iOSApplicationExtension 11.0, *) {
webView.scrollView.contentInsetAdjustmentBehavior = .never
}
webView.navigationDelegate = self
webView.interactiveTransitionGestureRecognizerTest = { point -> Bool in
return point.x > 30.0
}
self.view.addSubview(webView)
self.webView = webView
if let parsedUrl = URL(string: url) {
webView.load(URLRequest(url: parsedUrl))
}
}
deinit {
self.refreshDisposable.dispose()
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
if let webView = self.webView {
webView.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - navigationBarHeight)))
}
}
func animateIn() {
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
func animateOut(completion: (() -> Void)? = nil) {
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { _ in
completion?()
})
}
private func handleScriptMessage(_ message: WKScriptMessage) {
guard let body = message.body as? [String: Any] else {
return
}
guard let eventName = body["eventName"] as? String else {
return
}
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Swift.Void) {
if let url = navigationAction.request.url, url.scheme == "tg" {
if url.path == "statsrefresh" {
var params = ""
if let query = url.query, let components = URLComponents(string: "/" + query) {
if let queryItems = components.queryItems {
for queryItem in queryItems {
if let value = queryItem.value {
if queryItem.name == "params" {
params = value
}
}
}
}
}
self.refreshDisposable.set((channelStatsUrl(postbox: self.context.account.postbox, network: self.context.account.network, peerId: self.peerId, params: params)
|> deliverOnMainQueue).start(next: { [weak self] url in
guard let strongSelf = self else {
return
}
if let parsedUrl = URL(string: url) {
strongSelf.webView?.load(URLRequest(url: parsedUrl))
}
}, error: { _ in
}))
}
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
}
}
}

View File

@ -2117,6 +2117,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
if let strongSelf = self, strongSelf.isNodeLoaded, canSendMessagesToChat(strongSelf.presentationInterfaceState) {
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageId(message.id) }).updatedSearch(nil) })
strongSelf.updateItemNodesSearchTextHighlightStates()
strongSelf.chatDisplayNode.ensureInputViewFocused()
}
}
@ -2353,6 +2354,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
}
}.updatedSearch(current.search == nil ? ChatSearchData(domain: domain).withUpdatedQuery(query) : current.search?.withUpdatedDomain(domain).withUpdatedQuery(query))
})
strongSelf.updateItemNodesSearchTextHighlightStates()
}
}, dismissMessageSearch: { [weak self] in
if let strongSelf = self {
@ -2369,6 +2371,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
return current
}
})
strongSelf.updateItemNodesSearchTextHighlightStates()
}
}, navigateMessageSearch: { [weak self] action in
if let strongSelf = self {
@ -2395,6 +2398,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
}
return current
})
strongSelf.updateItemNodesSearchTextHighlightStates()
if let navigateIndex = navigateIndex {
switch strongSelf.chatLocation {
case .peer:
@ -2441,6 +2445,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
return state
}
})
strongSelf.updateItemNodesSearchTextHighlightStates()
}
}, navigateToMessage: { [weak self] messageId in
self?.navigateToMessage(from: nil, to: .id(messageId))
@ -3692,6 +3697,21 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
}
}
private func updateItemNodesSearchTextHighlightStates() {
var searchString: String?
if let search = self.presentationInterfaceState.search, let resultsState = search.resultsState, !resultsState.messageIndices.isEmpty {
searchString = search.query
}
if searchString != self.controllerInteraction?.searchTextHighightState {
self.controllerInteraction?.searchTextHighightState = searchString
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
itemNode.updateSearchTextHighlightState()
}
}
}
}
private func updateItemNodesHighlightedStates(animated: Bool) {
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
@ -4721,6 +4741,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex))
}
}
strongSelf.updateItemNodesSearchTextHighlightStates()
}, completed: { [weak self] in
if let strongSelf = self {
strongSelf.searching.set(false)
@ -4771,6 +4792,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
}
}
}
self.updateItemNodesSearchTextHighlightStates()
return nil
}
@ -5303,7 +5325,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
}
self.commitPurposefulAction()
self.chatDisplayNode.historyNode.disconnect()
let _ = removePeerChat(postbox: self.context.account.postbox, peerId: peerId, reportChatSpam: reportChatSpam).start()
let _ = removePeerChat(account: self.context.account, peerId: peerId, reportChatSpam: reportChatSpam).start()
(self.navigationController as? NavigationController)?.popToRoot(animated: true)
let _ = requestUpdatePeerIsBlocked(account: self.context.account, peerId: peerId, isBlocked: true).start()

View File

@ -96,6 +96,7 @@ public final class ChatControllerInteraction {
var contextHighlightedState: ChatInterfaceHighlightedState?
var automaticMediaDownloadSettings: MediaAutoDownloadSettings
var pollActionState: ChatInterfacePollActionState
var searchTextHighightState: String?
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool) -> Void, sendGif: @escaping (FileMediaReference) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState) {
self.openMessage = openMessage

View File

@ -445,7 +445,7 @@ public class ChatListController: TelegramController, KeyShortcutResponder, UIVie
if let strongSelf = self {
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerId)
let _ = removePeerChat(postbox: strongSelf.context.account.postbox, peerId: peerId, reportChatSpam: false).start(completed: {
let _ = removePeerChat(account: strongSelf.context.account, peerId: peerId, reportChatSpam: false).start(completed: {
self?.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerId)
})
let _ = requestUpdatePeerIsBlocked(account: strongSelf.context.account, peerId: peer.peerId, isBlocked: true).start()
@ -1087,7 +1087,7 @@ public class ChatListController: TelegramController, KeyShortcutResponder, UIVie
let signal: Signal<Void, NoError> = strongSelf.context.account.postbox.transaction { transaction -> Void in
for peerId in peerIds {
removePeerChat(transaction: transaction, mediaBox: context.account.postbox.mediaBox, peerId: peerId, reportChatSpam: false, deleteGloballyIfPossible: false)
removePeerChat(account: context.account, transaction: transaction, mediaBox: context.account.postbox.mediaBox, peerId: peerId, reportChatSpam: false, deleteGloballyIfPossible: false)
}
}
|> afterDisposed {
@ -1157,7 +1157,7 @@ public class ChatListController: TelegramController, KeyShortcutResponder, UIVie
}
if shouldCommit {
strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerId)
let _ = removePeerChat(postbox: strongSelf.context.account.postbox, peerId: peerId, reportChatSpam: false, deleteGloballyIfPossible: deleteGloballyIfPossible).start(completed: {
let _ = removePeerChat(account: strongSelf.context.account, peerId: peerId, reportChatSpam: false, deleteGloballyIfPossible: deleteGloballyIfPossible).start(completed: {
guard let strongSelf = self else {
return
}

View File

@ -141,6 +141,9 @@ class ChatMessageBubbleContentNode: ASDisplayNode {
return false
}
func updateSearchTextHighlightState(text: String?) {
}
func updateAutomaticMediaDownloadSettings(_ settings: MediaAutoDownloadSettings) {
}

View File

@ -1449,6 +1449,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
actionButtonsNode.removeFromSupernode()
strongSelf.actionButtonsNode = nil
}
strongSelf.updateSearchTextHighlightState()
}
})
}
@ -1958,6 +1960,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
}
}
override func updateSearchTextHighlightState() {
for contentNode in self.contentNodes {
contentNode.updateSearchTextHighlightState(text: self.item?.controllerInteraction.searchTextHighightState)
}
}
override func updateHighlightedState(animated: Bool) {
super.updateHighlightedState(animated: animated)

View File

@ -209,6 +209,9 @@ public class ChatMessageItemView: ListViewItemNode {
func updateSelectionState(animated: Bool) {
}
func updateSearchTextHighlightState() {
}
func updateHighlightedState(animated: Bool) {
var isHighlightedInOverlay = false
if let item = self.item, let contextHighlightedState = item.controllerInteraction.contextHighlightedState {

View File

@ -35,6 +35,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
private let statusNode: ChatMessageDateAndStatusNode
private var linkHighlightingNode: LinkHighlightingNode?
private var textHighlightingNodes: [LinkHighlightingNode] = []
private var cachedChatMessageText: CachedChatMessageText?
required init() {
@ -384,4 +386,33 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
return nil
}
override func updateSearchTextHighlightState(text: String?) {
guard let item = self.item else {
return
}
let rectsSet: [[CGRect]]
if let text = text, !text.isEmpty {
rectsSet = self.textNode.textRangesRects(text: text)
} else {
rectsSet = []
}
for i in 0 ..< rectsSet.count {
let rects = rectsSet[i]
let textHighlightNode: LinkHighlightingNode
if self.textHighlightingNodes.count < i {
textHighlightNode = self.textHighlightingNodes[i]
} else {
textHighlightNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.bubble.incomingTextHighlightColor : item.presentationData.theme.theme.chat.bubble.outgoingTextHighlightColor)
self.textHighlightingNodes.append(textHighlightNode)
self.insertSubnode(textHighlightNode, belowSubnode: self.textNode)
}
textHighlightNode.frame = self.textNode.frame
textHighlightNode.updateRects(rects)
}
for i in (rectsSet.count ..< self.textHighlightingNodes.count).reversed() {
self.textHighlightingNodes[i].removeFromSupernode()
self.textHighlightingNodes.remove(at: i)
}
}
}

View File

@ -158,6 +158,8 @@ private let bubble = PresentationThemeChatBubble(
outgoingLinkHighlightColor: accentColor.withAlphaComponent(0.5),
infoPrimaryTextColor: UIColor(rgb: 0xffffff),
infoLinkTextColor: accentColor,
incomingTextHighlightColor: UIColor(rgb: 0xffe438),
outgoingTextHighlightColor: UIColor(rgb: 0xffe438),
incomingAccentTextColor: UIColor(rgb: 0xffffff),
outgoingAccentTextColor: UIColor(rgb: 0xffffff),
incomingAccentControlColor: UIColor(rgb: 0xffffff),

View File

@ -158,6 +158,8 @@ private let bubble = PresentationThemeChatBubble(
outgoingLinkHighlightColor: accentColor.withAlphaComponent(0.5),
infoPrimaryTextColor: UIColor(rgb: 0xffffff),
infoLinkTextColor: accentColor,
incomingTextHighlightColor: UIColor(rgb: 0xffe438),
outgoingTextHighlightColor: UIColor(rgb: 0xffe438),
incomingAccentTextColor: UIColor(rgb: 0xffffff),
outgoingAccentTextColor: UIColor(rgb: 0xffffff),
incomingAccentControlColor: UIColor(rgb: 0xffffff),

View File

@ -189,6 +189,8 @@ private func makeDefaultPresentationTheme(accentColor: UIColor, serviceBackgroun
outgoingLinkHighlightColor: accentColor.withAlphaComponent(0.3),
infoPrimaryTextColor: UIColor(rgb: 0x000000),
infoLinkTextColor: UIColor(rgb: 0x004bad),
incomingTextHighlightColor: UIColor(rgb: 0xffe438),
outgoingTextHighlightColor: UIColor(rgb: 0xffe438),
incomingAccentTextColor: UIColor(rgb: 0x007ee5),
outgoingAccentTextColor: UIColor(rgb: 0x00a700),
incomingAccentControlColor: UIColor(rgb: 0x007ee5),
@ -245,6 +247,8 @@ private func makeDefaultPresentationTheme(accentColor: UIColor, serviceBackgroun
outgoingLinkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.3),
infoPrimaryTextColor: UIColor(rgb: 0x000000),
infoLinkTextColor: UIColor(rgb: 0x004bad),
incomingTextHighlightColor: UIColor(rgb: 0xffe438),
outgoingTextHighlightColor: UIColor(rgb: 0xffe438),
incomingAccentTextColor: accentColor,
outgoingAccentTextColor: UIColor(rgb: 0xffffff),
incomingAccentControlColor: accentColor,

View File

@ -970,6 +970,25 @@ public func deviceContactInfoController(context: AccountContext, subject: Device
}
if let contactDataManager = context.sharedContext.contactDataManager {
let _ = (contactDataManager.createContactWithData(composedContactData)
|> mapToSignal { contactIdAndData -> Signal<(DeviceContactStableId, DeviceContactExtendedData, Peer?)?, NoError> in
guard let (id, data) = contactIdAndData else {
return .single(nil)
}
if filteredPhoneNumbers.count == 1 {
return importContact(account: context.account, firstName: composedContactData.basicData.firstName, lastName: composedContactData.basicData.lastName, phoneNumber: filteredPhoneNumbers[0].value)
|> mapToSignal { peerId -> Signal<(DeviceContactStableId, DeviceContactExtendedData, Peer?)?, NoError> in
if let peerId = peerId {
return context.account.postbox.transaction { transaction -> (DeviceContactStableId, DeviceContactExtendedData, Peer?)? in
return (id, data, transaction.getPeer(peerId))
}
} else {
return .single((id, data, nil))
}
}
} else {
return .single((id, data, nil))
}
}
|> deliverOnMainQueue).start(next: { contactIdAndData in
updateState { state in
var state = state
@ -977,7 +996,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device
return state
}
if let contactIdAndData = contactIdAndData {
//completion(nil, contactIdAndData.0, contactIdAndData.1)
completion(contactIdAndData.2, contactIdAndData.0, contactIdAndData.1)
}
completed?()
dismissImpl?(true)

View File

@ -206,7 +206,18 @@ final class FFMpegMediaFrameSource: NSObject, MediaFrameSource {
let preferSoftwareDecoding = self.preferSoftwareDecoding
let fetchAutomatically = self.fetchAutomatically
let currentSemaphore = Atomic<Atomic<DispatchSemaphore?>?>(value: nil)
disposable.set(ActionDisposable {
self.performWithContext({ context in
context.close()
})
currentSemaphore.with({ $0 })?.with({ $0 })?.signal()
})
self.performWithContext { [weak self] context in
let _ = currentSemaphore.swap(context.currentSemaphore)
context.initializeState(postbox: postbox, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically)
context.seek(timestamp: timestamp, completed: { streamDescriptions, timestamp in

View File

@ -85,14 +85,21 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa
fetchedData = fileData
} else {
let semaphore = DispatchSemaphore(value: 0)
let _ = context.currentSemaphore.swap(semaphore)
var completedRequest = false
let disposable = data.start(next: { data in
if data.count == readCount {
fetchedData = data
completedRequest = true
semaphore.signal()
}
})
semaphore.wait()
let _ = context.currentSemaphore.swap(nil)
disposable.dispose()
if !completedRequest {
return -1
}
}
}
} else {
@ -115,7 +122,9 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa
} else {
let data = postbox.mediaBox.resourceData(resourceReference.resource, pathExtension: nil, option: .complete(waitUntilFetchStatus: false))
let semaphore = DispatchSemaphore(value: 0)
let _ = context.currentSemaphore.swap(semaphore)
let readingOffset = context.readingOffset
var completedRequest = false
let disposable = data.start(next: { next in
if next.complete {
let readCount = max(0, min(next.size - readingOffset, Int(bufferSize)))
@ -132,11 +141,16 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa
fetchedData = data
close(fd)
}
completedRequest = true
semaphore.signal()
}
})
semaphore.wait()
let _ = context.currentSemaphore.swap(nil)
disposable.dispose()
if !completedRequest {
return -1
}
}
}
if let fetchedData = fetchedData {
@ -147,6 +161,9 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa
context.readingOffset += Int(fetchedCount)
}
if context.closed {
return -1
}
return fetchedCount
}
@ -169,14 +186,21 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe
var resultSize: Int = Int(Int32.max - 1)
let data = postbox.mediaBox.resourceData(resourceReference.resource, pathExtension: nil, option: .complete(waitUntilFetchStatus: false))
let semaphore = DispatchSemaphore(value: 0)
let _ = context.currentSemaphore.swap(semaphore)
var completedRequest = false
let disposable = data.start(next: { next in
if next.complete {
resultSize = Int(next.size)
completedRequest = true
semaphore.signal()
}
})
semaphore.wait()
let _ = context.currentSemaphore.swap(nil)
disposable.dispose()
if !completedRequest {
return -1
}
resourceSize = resultSize
}
} else {
@ -210,6 +234,10 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe
}
}
if context.closed {
return -1
}
return result
}
@ -240,6 +268,8 @@ final class FFMpegMediaFrameSourceContext: NSObject {
private var preferSoftwareDecoding: Bool = false
fileprivate var fetchAutomatically: Bool = true
let currentSemaphore = Atomic<DispatchSemaphore?>(value: nil)
init(thread: Thread) {
self.thread = thread
}
@ -584,6 +614,10 @@ final class FFMpegMediaFrameSourceContext: NSObject {
completed(FFMpegMediaFrameSourceDescriptionSet(audio: audioDescription, video: videoDescription, extraVideoFrames: extraVideoFrames), actualPts)
}
}
func close() {
self.closed = true
}
}
private func videoFrameFromPacket(_ packet: FFMpegPacket, videoStream: StreamContext) -> MediaTrackDecodableFrame {

View File

@ -21,7 +21,7 @@ func fetchPhotoLibraryResource(localIdentifier: String) -> Signal<MediaResourceD
let madeProgress = Atomic<Bool>(value: false)
option.progressHandler = { progress, error, _, _ in
if !madeProgress.swap(true) {
subscriber.putNext(.reset)
//subscriber.putNext(.reset)
}
}
let size = CGSize(width: 1280.0, height: 1280.0)
@ -37,7 +37,7 @@ func fetchPhotoLibraryResource(localIdentifier: String) -> Signal<MediaResourceD
if let image = image {
if let info = info, let degraded = info[PHImageResultIsDegradedKey], (degraded as AnyObject).boolValue!{
if !madeProgress.swap(true) {
subscriber.putNext(.reset)
//subscriber.putNext(.reset)
}
} else {
_ = madeProgress.swap(true)
@ -71,7 +71,7 @@ func fetchPhotoLibraryResource(localIdentifier: String) -> Signal<MediaResourceD
}
} else {
if !madeProgress.swap(true) {
subscriber.putNext(.reset)
//subscriber.putNext(.reset)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -502,6 +502,8 @@ public final class PresentationThemeChatBubble {
public let outgoingLinkHighlightColor: UIColor
public let infoPrimaryTextColor: UIColor
public let infoLinkTextColor: UIColor
public let incomingTextHighlightColor: UIColor
public let outgoingTextHighlightColor: UIColor
public let incomingAccentTextColor: UIColor
public let outgoingAccentTextColor: UIColor
@ -557,7 +559,7 @@ public final class PresentationThemeChatBubble {
public let incomingPolls: PresentationThemeChatBubblePolls
public let outgoingPolls: PresentationThemeChatBubblePolls
public init(incoming: PresentationThemeBubbleColor, outgoing: PresentationThemeBubbleColor, freeform: PresentationThemeBubbleColor, incomingPrimaryTextColor: UIColor, incomingSecondaryTextColor: UIColor, incomingLinkTextColor: UIColor, incomingLinkHighlightColor: UIColor, outgoingPrimaryTextColor: UIColor, outgoingSecondaryTextColor: UIColor, outgoingLinkTextColor: UIColor, outgoingLinkHighlightColor: UIColor, infoPrimaryTextColor: UIColor, infoLinkTextColor: UIColor, incomingAccentTextColor: UIColor, outgoingAccentTextColor: UIColor, incomingAccentControlColor: UIColor, outgoingAccentControlColor: UIColor, incomingMediaActiveControlColor: UIColor, outgoingMediaActiveControlColor: UIColor, incomingMediaInactiveControlColor: UIColor, outgoingMediaInactiveControlColor: UIColor, outgoingCheckColor: UIColor, incomingPendingActivityColor: UIColor, outgoingPendingActivityColor: UIColor, mediaDateAndStatusFillColor: UIColor, mediaDateAndStatusTextColor: UIColor, incomingFileTitleColor: UIColor, outgoingFileTitleColor: UIColor, incomingFileDescriptionColor: UIColor, outgoingFileDescriptionColor: UIColor, incomingFileDurationColor: UIColor, outgoingFileDurationColor: UIColor, shareButtonFillColor: PresentationThemeVariableColor, shareButtonStrokeColor: PresentationThemeVariableColor, shareButtonForegroundColor: PresentationThemeVariableColor, mediaOverlayControlBackgroundColor: UIColor, mediaOverlayControlForegroundColor: UIColor, actionButtonsIncomingFillColor: PresentationThemeVariableColor, actionButtonsIncomingStrokeColor: PresentationThemeVariableColor, actionButtonsIncomingTextColor: PresentationThemeVariableColor, actionButtonsOutgoingFillColor: PresentationThemeVariableColor, actionButtonsOutgoingStrokeColor: PresentationThemeVariableColor, actionButtonsOutgoingTextColor: PresentationThemeVariableColor, selectionControlBorderColor: UIColor, selectionControlFillColor: UIColor, selectionControlForegroundColor: UIColor, mediaHighlightOverlayColor: UIColor, deliveryFailedFillColor: UIColor, deliveryFailedForegroundColor: UIColor, incomingMediaPlaceholderColor: UIColor, outgoingMediaPlaceholderColor: UIColor, incomingPolls: PresentationThemeChatBubblePolls, outgoingPolls: PresentationThemeChatBubblePolls) {
public init(incoming: PresentationThemeBubbleColor, outgoing: PresentationThemeBubbleColor, freeform: PresentationThemeBubbleColor, incomingPrimaryTextColor: UIColor, incomingSecondaryTextColor: UIColor, incomingLinkTextColor: UIColor, incomingLinkHighlightColor: UIColor, outgoingPrimaryTextColor: UIColor, outgoingSecondaryTextColor: UIColor, outgoingLinkTextColor: UIColor, outgoingLinkHighlightColor: UIColor, infoPrimaryTextColor: UIColor, infoLinkTextColor: UIColor, incomingTextHighlightColor: UIColor, outgoingTextHighlightColor: UIColor, incomingAccentTextColor: UIColor, outgoingAccentTextColor: UIColor, incomingAccentControlColor: UIColor, outgoingAccentControlColor: UIColor, incomingMediaActiveControlColor: UIColor, outgoingMediaActiveControlColor: UIColor, incomingMediaInactiveControlColor: UIColor, outgoingMediaInactiveControlColor: UIColor, outgoingCheckColor: UIColor, incomingPendingActivityColor: UIColor, outgoingPendingActivityColor: UIColor, mediaDateAndStatusFillColor: UIColor, mediaDateAndStatusTextColor: UIColor, incomingFileTitleColor: UIColor, outgoingFileTitleColor: UIColor, incomingFileDescriptionColor: UIColor, outgoingFileDescriptionColor: UIColor, incomingFileDurationColor: UIColor, outgoingFileDurationColor: UIColor, shareButtonFillColor: PresentationThemeVariableColor, shareButtonStrokeColor: PresentationThemeVariableColor, shareButtonForegroundColor: PresentationThemeVariableColor, mediaOverlayControlBackgroundColor: UIColor, mediaOverlayControlForegroundColor: UIColor, actionButtonsIncomingFillColor: PresentationThemeVariableColor, actionButtonsIncomingStrokeColor: PresentationThemeVariableColor, actionButtonsIncomingTextColor: PresentationThemeVariableColor, actionButtonsOutgoingFillColor: PresentationThemeVariableColor, actionButtonsOutgoingStrokeColor: PresentationThemeVariableColor, actionButtonsOutgoingTextColor: PresentationThemeVariableColor, selectionControlBorderColor: UIColor, selectionControlFillColor: UIColor, selectionControlForegroundColor: UIColor, mediaHighlightOverlayColor: UIColor, deliveryFailedFillColor: UIColor, deliveryFailedForegroundColor: UIColor, incomingMediaPlaceholderColor: UIColor, outgoingMediaPlaceholderColor: UIColor, incomingPolls: PresentationThemeChatBubblePolls, outgoingPolls: PresentationThemeChatBubblePolls) {
self.incoming = incoming
self.outgoing = outgoing
self.freeform = freeform
@ -572,6 +574,8 @@ public final class PresentationThemeChatBubble {
self.outgoingLinkHighlightColor = outgoingLinkHighlightColor
self.infoPrimaryTextColor = infoPrimaryTextColor
self.infoLinkTextColor = infoLinkTextColor
self.incomingTextHighlightColor = incomingTextHighlightColor
self.outgoingTextHighlightColor = outgoingTextHighlightColor
self.incomingAccentTextColor = incomingAccentTextColor
self.outgoingAccentTextColor = outgoingAccentTextColor

View File

@ -140,7 +140,8 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me
let result: Signal<MediaResourceData, NoError>
if opportunistic {
result = signal |> take(1)
result = signal
|> take(1)
} else {
result = signal
}