Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2020-10-17 22:09:57 +04:00
commit ce911f3f9b
38 changed files with 1884 additions and 73 deletions

View File

@ -1 +1 @@
8tRwQybvfoDddhSIfdMSOMv4FZd9LSHiWmObmx6d7rE= gCh0ST/jBZ+NM8mvcBcsd12A5FMFT4q6fETcWd5elO0=

View File

@ -22,15 +22,20 @@ public enum PeerMessagesMediaPlaylistId: Equatable, SharedMediaPlaylistId {
} }
public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation { public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation {
case messages(peerId: PeerId, tagMask: MessageTags, at: MessageId) case messages(chatLocation: ChatLocation, tagMask: MessageTags, at: MessageId)
case singleMessage(MessageId) case singleMessage(MessageId)
case recentActions(Message) case recentActions(Message)
case custom(messages: Signal<([Message], Int32, Bool), NoError>, at: MessageId, loadMore: (() -> Void)?) case custom(messages: Signal<([Message], Int32, Bool), NoError>, at: MessageId, loadMore: (() -> Void)?)
public var playlistId: PeerMessagesMediaPlaylistId { public var playlistId: PeerMessagesMediaPlaylistId {
switch self { switch self {
case let .messages(peerId, _, _): case let .messages(chatLocation, _, _):
return .peer(peerId) switch chatLocation {
case let .peer(peerId):
return .peer(peerId)
case let .replyThread(replyThreaMessage):
return .peer(replyThreaMessage.messageId.peerId)
}
case let .singleMessage(id): case let .singleMessage(id):
return .peer(id.peerId) return .peer(id.peerId)
case let .recentActions(message): case let .recentActions(message):
@ -59,8 +64,8 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation
public static func ==(lhs: PeerMessagesPlaylistLocation, rhs: PeerMessagesPlaylistLocation) -> Bool { public static func ==(lhs: PeerMessagesPlaylistLocation, rhs: PeerMessagesPlaylistLocation) -> Bool {
switch lhs { switch lhs {
case let .messages(peerId, tagMask, at): case let .messages(chatLocation, tagMask, at):
if case .messages(peerId, tagMask, at) = rhs { if case .messages(chatLocation, tagMask, at) = rhs {
return true return true
} else { } else {
return false return false

View File

@ -13,6 +13,7 @@ import ItemListUI
import PresentationDataUtils import PresentationDataUtils
import OverlayStatusController import OverlayStatusController
import AccountContext import AccountContext
import TelegramCallsUI
@objc private final class DebugControllerMailComposeDelegate: NSObject, MFMailComposeViewControllerDelegate { @objc private final class DebugControllerMailComposeDelegate: NSObject, MFMailComposeViewControllerDelegate {
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
@ -74,6 +75,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case alternativeFolderTabs(Bool) case alternativeFolderTabs(Bool)
case playerEmbedding(Bool) case playerEmbedding(Bool)
case playlistPlayback(Bool) case playlistPlayback(Bool)
case voiceConference
case preferredVideoCodec(Int, String, String?, Bool) case preferredVideoCodec(Int, String, String?, Bool)
case disableVideoAspectScaling(Bool) case disableVideoAspectScaling(Bool)
case enableVoipTcp(Bool) case enableVoipTcp(Bool)
@ -90,7 +92,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.logging.rawValue return DebugControllerSection.logging.rawValue
case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries: case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries:
return DebugControllerSection.experiments.rawValue return DebugControllerSection.experiments.rawValue
case .clearTips, .reimport, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .alternativeFolderTabs, .playerEmbedding, .playlistPlayback: case .clearTips, .reimport, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .alternativeFolderTabs, .playerEmbedding, .playlistPlayback, .voiceConference:
return DebugControllerSection.experiments.rawValue return DebugControllerSection.experiments.rawValue
case .preferredVideoCodec: case .preferredVideoCodec:
return DebugControllerSection.videoExperiments.rawValue return DebugControllerSection.videoExperiments.rawValue
@ -155,8 +157,10 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 24 return 24
case .playlistPlayback: case .playlistPlayback:
return 25 return 25
case .voiceConference:
return 26
case let .preferredVideoCodec(index, _, _, _): case let .preferredVideoCodec(index, _, _, _):
return 26 + index return 27 + index
case .disableVideoAspectScaling: case .disableVideoAspectScaling:
return 100 return 100
case .enableVoipTcp: case .enableVoipTcp:
@ -648,6 +652,15 @@ private enum DebugControllerEntry: ItemListNodeEntry {
}) })
}).start() }).start()
}) })
case .voiceConference:
return ItemListDisclosureItem(presentationData: presentationData, title: "Voice Conference (Test)", label: "", sectionId: self.section, style: .blocks, action: {
guard let context = arguments.context else {
return
}
let controller = GroupCallController(context: context)
controller.navigationPresentation = .modal
arguments.pushController(controller)
})
case let .preferredVideoCodec(_, title, value, isSelected): case let .preferredVideoCodec(_, title, value, isSelected):
return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .right, checked: isSelected, zeroSeparatorInsets: false, sectionId: self.section, action: { return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .right, checked: isSelected, zeroSeparatorInsets: false, sectionId: self.section, action: {
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
@ -725,6 +738,8 @@ private func debugControllerEntries(presentationData: PresentationData, loggingS
entries.append(.playerEmbedding(experimentalSettings.playerEmbedding)) entries.append(.playerEmbedding(experimentalSettings.playerEmbedding))
entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) entries.append(.playlistPlayback(experimentalSettings.playlistPlayback))
entries.append(.voiceConference)
let codecs: [(String, String?)] = [ let codecs: [(String, String?)] = [
("No Preference", nil), ("No Preference", nil),
("H265", "H265"), ("H265", "H265"),

View File

@ -0,0 +1,147 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import TelegramUIPreferences
import TelegramVoip
import TelegramAudio
import AccountContext
public final class GroupCallController: ViewController {
private final class Node: ViewControllerTracingNode {
private let context: AccountContext
private let presentationData: PresentationData
private var callContext: GroupCallContext?
private var callDisposable: Disposable?
private var memberCountDisposable: Disposable?
private var isMutedDisposable: Disposable?
private let audioSessionActive = Promise<Bool>(false)
private var memberCount: Int = 0
private let memberCountNode: ImmediateTextNode
private var isMuted: Bool = false
private let isMutedNode: ImmediateTextNode
private let muteButton: HighlightableButtonNode
private var validLayout: ContainerViewLayout?
init(context: AccountContext) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.memberCountNode = ImmediateTextNode()
self.isMutedNode = ImmediateTextNode()
self.muteButton = HighlightableButtonNode()
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.memberCountNode)
self.muteButton.addSubnode(self.isMutedNode)
self.addSubnode(self.muteButton)
let audioSessionActive = self.audioSessionActive
self.callDisposable = self.context.sharedContext.mediaManager.audioSession.push(audioSessionType: .voiceCall, manualActivate: { audioSessionControl in
audioSessionControl.activate({ _ in })
audioSessionActive.set(.single(true))
}, deactivate: {
return Signal { subscriber in
subscriber.putCompletion()
return EmptyDisposable
}
}, availableOutputsChanged: { _, _ in
})
let callContext = GroupCallContext(audioSessionActive: self.audioSessionActive.get())
self.callContext = callContext
self.memberCountDisposable = (callContext.memberCount
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.memberCount = value
if let layout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, transition: .immediate)
}
})
self.isMutedDisposable = (callContext.isMuted
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.isMuted = value
if let layout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, transition: .immediate)
}
})
self.muteButton.addTarget(self, action: #selector(self.muteButtonPressed), forControlEvents: .touchUpInside)
}
deinit {
self.callDisposable?.dispose()
self.memberCountDisposable?.dispose()
}
@objc private func muteButtonPressed() {
self.callContext?.toggleIsMuted()
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
self.memberCountNode.attributedText = NSAttributedString(string: "Members: \(self.memberCount)", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.isMutedNode.attributedText = NSAttributedString(string: self.isMuted ? "Unmute" : "Mute", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemAccentColor)
let textSize = self.memberCountNode.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: 100.0))
let isMutedSize = self.isMutedNode.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: 100.0))
let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: floor((layout.size.height - textSize.width) / 2.0)), size: textSize)
transition.updateFrameAdditiveToCenter(node: self.memberCountNode, frame: textFrame)
let isMutedFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - isMutedSize.width) / 2.0), y: textFrame.maxY + 12.0), size: isMutedSize)
transition.updateFrame(node: self.muteButton, frame: isMutedFrame)
self.isMutedNode.frame = CGRect(origin: CGPoint(), size: isMutedFrame.size)
}
}
private let context: AccountContext
private let presentationData: PresentationData
private var controllerNode: Node {
return self.displayNode as! Node
}
public init(context: AccountContext) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = Node(context: self.context)
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, transition: transition)
}
}

View File

@ -170,6 +170,10 @@ public final class PrincipalThemeEssentialGraphics {
public let outgoingDateAndStatusRepliesIcon: UIImage public let outgoingDateAndStatusRepliesIcon: UIImage
public let mediaRepliesIcon: UIImage public let mediaRepliesIcon: UIImage
public let freeRepliesIcon: UIImage public let freeRepliesIcon: UIImage
public let incomingDateAndStatusPinnedIcon: UIImage
public let outgoingDateAndStatusPinnedIcon: UIImage
public let mediaPinnedIcon: UIImage
public let freePinnedIcon: UIImage
public let dateStaticBackground: UIImage public let dateStaticBackground: UIImage
public let dateFloatingBackground: UIImage public let dateFloatingBackground: UIImage
@ -333,6 +337,12 @@ public final class PrincipalThemeEssentialGraphics {
self.mediaRepliesIcon = generateTintedImage(image: repliesImage, color: .white)! self.mediaRepliesIcon = generateTintedImage(image: repliesImage, color: .white)!
self.freeRepliesIcon = generateTintedImage(image: repliesImage, color: serviceColor.primaryText)! self.freeRepliesIcon = generateTintedImage(image: repliesImage, color: serviceColor.primaryText)!
let pinnedImage = UIImage(bundleImageName: "Chat/Message/Pinned")!
self.incomingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.incoming.secondaryTextColor)!
self.outgoingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.outgoing.secondaryTextColor)!
self.mediaPinnedIcon = generateTintedImage(image: pinnedImage, color: .white)!
self.freePinnedIcon = generateTintedImage(image: pinnedImage, color: serviceColor.primaryText)!
self.radialIndicatorFileIconIncoming = emptyImage self.radialIndicatorFileIconIncoming = emptyImage
self.radialIndicatorFileIconOutgoing = emptyImage self.radialIndicatorFileIconOutgoing = emptyImage
} else { } else {
@ -438,6 +448,12 @@ public final class PrincipalThemeEssentialGraphics {
self.mediaRepliesIcon = generateTintedImage(image: repliesImage, color: .white)! self.mediaRepliesIcon = generateTintedImage(image: repliesImage, color: .white)!
self.freeRepliesIcon = generateTintedImage(image: repliesImage, color: serviceColor.primaryText)! self.freeRepliesIcon = generateTintedImage(image: repliesImage, color: serviceColor.primaryText)!
let pinnedImage = UIImage(bundleImageName: "Chat/Message/Pinned")!
self.incomingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.incoming.secondaryTextColor)!
self.outgoingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.outgoing.secondaryTextColor)!
self.mediaPinnedIcon = generateTintedImage(image: pinnedImage, color: .white)!
self.freePinnedIcon = generateTintedImage(image: pinnedImage, color: serviceColor.primaryText)!
self.radialIndicatorFileIconIncoming = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)! self.radialIndicatorFileIconIncoming = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)!
self.radialIndicatorFileIconOutgoing = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)! self.radialIndicatorFileIconOutgoing = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)!
} }

View File

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

View File

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

View File

@ -358,6 +358,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
public var purposefulAction: (() -> Void)? public var purposefulAction: (() -> Void)?
private let scrolledToMessageId = ValuePromise<MessageId?>(nil, ignoreRepeated: true)
public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil) { public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil) {
let _ = ChatControllerCount.modify { value in let _ = ChatControllerCount.modify { value in
return value + 1 return value + 1
@ -3258,11 +3260,36 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}) })
|> restartIfError |> restartIfError
struct ReferenceMessage {
var id: MessageId
var isScrolled: Bool
}
let referenceMessage: Signal<ReferenceMessage?, NoError>
if latest {
referenceMessage = .single(nil)
} else {
referenceMessage = combineLatest(
queue: Queue.mainQueue(),
self.scrolledToMessageId.get(),
self.chatDisplayNode.historyNode.topVisibleMessageRange.get()
)
|> map { scrolledToMessageId, topVisibleMessageRange -> ReferenceMessage? in
if let scrolledToMessageId = scrolledToMessageId {
return ReferenceMessage(id: scrolledToMessageId, isScrolled: true)
} else if let topVisibleMessageRange = topVisibleMessageRange {
return ReferenceMessage(id: topVisibleMessageRange.upperBound, isScrolled: false)
} else {
return nil
}
}
}
topPinnedMessage = combineLatest( topPinnedMessage = combineLatest(
replyHistory, replyHistory,
latest ? .single(nil) : self.chatDisplayNode.historyNode.topVisibleMessageRange.get() referenceMessage
) )
|> map { update, topVisibleMessageRange -> ChatPinnedMessage? in |> map { update, referenceMessage -> ChatPinnedMessage? in
var message: ChatPinnedMessage? var message: ChatPinnedMessage?
switch update { switch update {
case .Loading: case .Loading:
@ -3278,9 +3305,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var matches = false var matches = false
if message == nil { if message == nil {
matches = true matches = true
} else if let topVisibleMessageRange = topVisibleMessageRange { } else if let referenceMessage = referenceMessage {
if entry.message.id <= topVisibleMessageRange.upperBound { if referenceMessage.isScrolled {
matches = true if entry.message.id < referenceMessage.id {
matches = true
}
} else {
if entry.message.id <= referenceMessage.id {
matches = true
}
} }
} else { } else {
matches = true matches = true
@ -3318,6 +3351,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
} }
self.chatDisplayNode.historyNode.beganDragging = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.scrolledToMessageId.set(nil)
}
self.chatDisplayNode.peerView = self.peerView self.chatDisplayNode.peerView = self.peerView
let initialData = self.chatDisplayNode.historyNode.initialData let initialData = self.chatDisplayNode.historyNode.initialData
@ -3645,6 +3686,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId) let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId)
controllerInteraction.highlightedState = highlightedState controllerInteraction.highlightedState = highlightedState
strongSelf.updateItemNodesHighlightedStates(animated: false) strongSelf.updateItemNodesHighlightedStates(animated: false)
strongSelf.scrolledToMessageId.set(index.id)
strongSelf.messageContextDisposable.set((Signal<Void, NoError>.complete() |> delay(0.7, queue: Queue.mainQueue())).start(completed: { strongSelf.messageContextDisposable.set((Signal<Void, NoError>.complete() |> delay(0.7, queue: Queue.mainQueue())).start(completed: {
if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction {
@ -4874,7 +4916,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
} }
} }
}, unpinMessage: { [weak self] id in }, unpinMessage: { [weak self] id, askForConfirmation in
if let strongSelf = self { if let strongSelf = self {
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
var canManagePin = false var canManagePin = false
@ -4896,7 +4938,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
if canManagePin { if canManagePin {
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_UnpinMessageAlert, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_Unpin, action: { let action: () -> Void = {
guard let strongSelf = self else {
return
}
if let strongSelf = self { if let strongSelf = self {
let disposable: MetaDisposable let disposable: MetaDisposable
if let current = strongSelf.unpinMessageDisposable { if let current = strongSelf.unpinMessageDisposable {
@ -4907,7 +4952,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
disposable.set(requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id)).start()) disposable.set(requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id)).start())
} }
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root)) }
if askForConfirmation {
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_UnpinMessageAlert, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_Unpin, action: {
action()
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root))
} else {
action()
}
} else { } else {
if let pinnedMessage = strongSelf.presentationInterfaceState.pinnedMessage { if let pinnedMessage = strongSelf.presentationInterfaceState.pinnedMessage {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {

View File

@ -526,6 +526,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
var maxVisibleMessageIndexUpdated: ((MessageIndex) -> Void)? var maxVisibleMessageIndexUpdated: ((MessageIndex) -> Void)?
var scrolledToIndex: ((MessageHistoryAnchorIndex) -> Void)? var scrolledToIndex: ((MessageHistoryAnchorIndex) -> Void)?
var beganDragging: (() -> Void)?
private let hasVisiblePlayableItemNodesPromise = ValuePromise<Bool>(false, ignoreRepeated: true) private let hasVisiblePlayableItemNodesPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
var hasVisiblePlayableItemNodes: Signal<Bool, NoError> { var hasVisiblePlayableItemNodes: Signal<Bool, NoError> {
@ -1121,6 +1122,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self.beganInteractiveDragging = { [weak self] in self.beganInteractiveDragging = { [weak self] in
self?.isInteractivelyScrollingValue = true self?.isInteractivelyScrollingValue = true
self?.isInteractivelyScrollingPromise.set(true) self?.isInteractivelyScrollingPromise.set(true)
self?.beganDragging?()
} }
self.didEndScrolling = { [weak self] in self.didEndScrolling = { [weak self] in

View File

@ -664,7 +664,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_Unpin, icon: { theme in actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_Unpin, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unpin"), color: theme.actionSheet.primaryTextColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unpin"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in }, action: { _, f in
interfaceInteraction.unpinMessage(pinnedSelectedMessageId) interfaceInteraction.unpinMessage(pinnedSelectedMessageId, false)
f(.default) f(.default)
}))) })))
} else { } else {

View File

@ -702,7 +702,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .minimal, reactionCount: dateReactionCount) let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .minimal, reactionCount: dateReactionCount)
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies) let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned))
var viaBotApply: (TextNodeLayout, () -> TextNode)? var viaBotApply: (TextNodeLayout, () -> TextNode)?
var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)?

View File

@ -572,7 +572,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
} }
} }
statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies) statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, message.tags.contains(.pinned))
} }
default: default:
break break

View File

@ -609,9 +609,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
if let strongSelf = self { if let strongSelf = self {
for contentNode in strongSelf.contentNodes { for contentNode in strongSelf.contentNodes {
var translatedPoint: CGPoint? var translatedPoint: CGPoint?
let convertedNodeFrame = contentNode.convert(contentNode.bounds, to: strongSelf) let convertedNodeFrame = contentNode.view.convert(contentNode.bounds, to: strongSelf.view)
if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) { if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) {
translatedPoint = strongSelf.convert(point, to: contentNode) translatedPoint = strongSelf.view.convert(point, to: contentNode.view)
} }
contentNode.updateTouchesAtPoint(translatedPoint) contentNode.updateTouchesAtPoint(translatedPoint)
} }
@ -905,7 +905,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode), forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode),
replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode), replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode),
actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (Bool) -> ChatMessageActionButtonsNode)), actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (Bool) -> ChatMessageActionButtonsNode)),
mosaicStatusLayout: (AccountContext, ChatPresentationData, Bool, Int?, String, ChatMessageDateAndStatusType, CGSize, [MessageReaction], Int) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode), mosaicStatusLayout: (AccountContext, ChatPresentationData, Bool, Int?, String, ChatMessageDateAndStatusType, CGSize, [MessageReaction], Int, Bool) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode),
currentShareButtonNode: HighlightableButtonNode?, currentShareButtonNode: HighlightableButtonNode?,
layoutConstants: ChatMessageItemLayoutConstants, layoutConstants: ChatMessageItemLayoutConstants,
currentItem: ChatMessageItem?, currentItem: ChatMessageItem?,
@ -1442,7 +1442,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} }
} }
mosaicStatusSizeAndApply = mosaicStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies) mosaicStatusSizeAndApply = mosaicStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, message.tags.contains(.pinned))
} }
} }
@ -2873,7 +2873,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} }
} }
loop: for contentNode in self.contentNodes { loop: for contentNode in self.contentNodes {
let convertedLocation = self.convert(location, to: contentNode) let convertedLocation = self.view.convert(location, to: contentNode.view)
let tapAction = contentNode.tapActionAtPoint(convertedLocation, gesture: gesture, isEstimating: false) let tapAction = contentNode.tapActionAtPoint(convertedLocation, gesture: gesture, isEstimating: false)
switch tapAction { switch tapAction {
@ -2982,9 +2982,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
var tapMessage: Message? = item.content.firstMessage var tapMessage: Message? = item.content.firstMessage
var selectAll = true var selectAll = true
loop: for contentNode in self.contentNodes { loop: for contentNode in self.contentNodes {
let convertedLocation = self.convert(location, to: contentNode) let convertedLocation = self.view.convert(location, to: contentNode.view)
let convertedNodeFrame = contentNode.convert(contentNode.bounds, to: self) let convertedNodeFrame = contentNode.view.convert(contentNode.bounds, to: self.view)
if !convertedNodeFrame.contains(location) { if !convertedNodeFrame.contains(location) {
continue loop continue loop
} else if contentNode is ChatMessageMediaBubbleContentNode { } else if contentNode is ChatMessageMediaBubbleContentNode {

View File

@ -196,7 +196,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
var statusApply: ((Bool) -> Void)? var statusApply: ((Bool) -> Void)?
if let statusType = statusType { if let statusType = statusType {
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies) let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned))
statusSize = size statusSize = size
statusApply = apply statusApply = apply
} }

View File

@ -180,7 +180,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
self.addSubnode(self.dateNode) self.addSubnode(self.dateNode)
} }
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction], _ replies: Int) -> (CGSize, (Bool) -> Void) { func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction], _ replies: Int, _ isPinned: Bool) -> (CGSize, (Bool) -> Void) {
let dateLayout = TextNode.asyncLayout(self.dateNode) let dateLayout = TextNode.asyncLayout(self.dateNode)
var checkReadNode = self.checkReadNode var checkReadNode = self.checkReadNode
@ -200,7 +200,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
let previousLayoutSize = self.layoutSize let previousLayoutSize = self.layoutSize
return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replyCount in return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replyCount, isPinned in
let dateColor: UIColor let dateColor: UIColor
var backgroundImage: UIImage? var backgroundImage: UIImage?
var outgoingStatus: ChatMessageDateAndStatusOutgoingType? var outgoingStatus: ChatMessageDateAndStatusOutgoingType?
@ -234,6 +234,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
} }
if replyCount != 0 { if replyCount != 0 {
repliesImage = graphics.incomingDateAndStatusRepliesIcon repliesImage = graphics.incomingDateAndStatusRepliesIcon
} else if isPinned {
repliesImage = graphics.incomingDateAndStatusPinnedIcon
} }
case let .BubbleOutgoing(status): case let .BubbleOutgoing(status):
dateColor = presentationData.theme.theme.chat.message.outgoing.secondaryTextColor dateColor = presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
@ -248,6 +250,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
} }
if replyCount != 0 { if replyCount != 0 {
repliesImage = graphics.outgoingDateAndStatusRepliesIcon repliesImage = graphics.outgoingDateAndStatusRepliesIcon
} else if isPinned {
repliesImage = graphics.outgoingDateAndStatusPinnedIcon
} }
case .ImageIncoming: case .ImageIncoming:
dateColor = presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor dateColor = presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor
@ -262,6 +266,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
} }
if replyCount != 0 { if replyCount != 0 {
repliesImage = graphics.mediaRepliesIcon repliesImage = graphics.mediaRepliesIcon
} else if isPinned {
repliesImage = graphics.mediaPinnedIcon
} }
case let .ImageOutgoing(status): case let .ImageOutgoing(status):
dateColor = presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor dateColor = presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor
@ -277,6 +283,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
} }
if replyCount != 0 { if replyCount != 0 {
repliesImage = graphics.mediaRepliesIcon repliesImage = graphics.mediaRepliesIcon
} else if isPinned {
repliesImage = graphics.mediaPinnedIcon
} }
case .FreeIncoming: case .FreeIncoming:
let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
@ -292,6 +300,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
} }
if replyCount != 0 { if replyCount != 0 {
repliesImage = graphics.freeRepliesIcon repliesImage = graphics.freeRepliesIcon
} else if isPinned {
repliesImage = graphics.freePinnedIcon
} }
case let .FreeOutgoing(status): case let .FreeOutgoing(status):
let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
@ -308,6 +318,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
} }
if replyCount != 0 { if replyCount != 0 {
repliesImage = graphics.freeRepliesIcon repliesImage = graphics.freeRepliesIcon
} else if isPinned {
repliesImage = graphics.freePinnedIcon
} }
} }
@ -511,6 +523,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
let layoutAndApply = makeReplyCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) let layoutAndApply = makeReplyCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0 reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0
replyCountLayoutAndApply = layoutAndApply replyCountLayoutAndApply = layoutAndApply
} else if isPinned {
reactionInset += 12.0
} }
leftInset += reactionInset leftInset += reactionInset
@ -817,17 +831,17 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
} }
} }
static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction], _ replies: Int) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode) { static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction], _ replies: Int, _ isPinned: Bool) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode) {
let currentLayout = node?.asyncLayout() let currentLayout = node?.asyncLayout()
return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies in return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies, isPinned in
let resultNode: ChatMessageDateAndStatusNode let resultNode: ChatMessageDateAndStatusNode
let resultSizeAndApply: (CGSize, (Bool) -> Void) let resultSizeAndApply: (CGSize, (Bool) -> Void)
if let node = node, let currentLayout = currentLayout { if let node = node, let currentLayout = currentLayout {
resultNode = node resultNode = node
resultSizeAndApply = currentLayout(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies) resultSizeAndApply = currentLayout(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies, isPinned)
} else { } else {
resultNode = ChatMessageDateAndStatusNode() resultNode = ChatMessageDateAndStatusNode()
resultSizeAndApply = resultNode.asyncLayout()(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies) resultSizeAndApply = resultNode.asyncLayout()(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies, isPinned)
} }
return (resultSizeAndApply.0, { animated in return (resultSizeAndApply.0, { animated in

View File

@ -327,7 +327,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, reactionCount: dateReactionCount) let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, reactionCount: dateReactionCount)
let (size, apply) = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, constrainedSize, dateReactions, dateReplies) let (size, apply) = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, constrainedSize, dateReactions, dateReplies, message.tags.contains(.pinned))
statusSize = size statusSize = size
statusApply = apply statusApply = apply
} }

View File

@ -285,7 +285,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
} else { } else {
maxDateAndStatusWidth = width - videoFrame.midX - 85.0 maxDateAndStatusWidth = width - videoFrame.midX - 85.0
} }
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies) let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned))
var contentSize = imageSize var contentSize = imageSize
var dateAndStatusOverflow = false var dateAndStatusOverflow = false

View File

@ -246,7 +246,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
var statusApply: ((Bool) -> Void)? var statusApply: ((Bool) -> Void)?
if let statusType = statusType { if let statusType = statusType {
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies) let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned))
statusSize = size statusSize = size
statusApply = apply statusApply = apply
} }

View File

@ -231,7 +231,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
var statusApply: ((Bool) -> Void)? var statusApply: ((Bool) -> Void)?
if let statusType = statusType { if let statusType = statusType {
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: imageSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies) let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: imageSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned))
statusSize = size statusSize = size
statusApply = apply statusApply = apply
} }

View File

@ -1074,7 +1074,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
var statusApply: ((Bool) -> Void)? var statusApply: ((Bool) -> Void)?
if let statusType = statusType { if let statusType = statusType {
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies) let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned))
statusSize = size statusSize = size
statusApply = apply statusApply = apply
} }

View File

@ -105,7 +105,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
var statusApply: ((Bool) -> Void)? var statusApply: ((Bool) -> Void)?
if let statusType = statusType { if let statusType = statusType {
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies) let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned))
statusSize = size statusSize = size
statusApply = apply statusApply = apply
} }

View File

@ -382,7 +382,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular, reactionCount: dateReactionCount) let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular, reactionCount: dateReactionCount)
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies) let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned))
var viaBotApply: (TextNodeLayout, () -> TextNode)? var viaBotApply: (TextNodeLayout, () -> TextNode)?
var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)?

View File

@ -169,7 +169,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
var statusApply: ((Bool) -> Void)? var statusApply: ((Bool) -> Void)?
if let statusType = statusType { if let statusType = statusType {
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies) let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned))
statusSize = size statusSize = size
statusApply = apply statusApply = apply
} }

View File

@ -95,7 +95,7 @@ final class ChatPanelInterfaceInteraction {
let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Bool let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Bool
let unblockPeer: () -> Void let unblockPeer: () -> Void
let pinMessage: (MessageId) -> Void let pinMessage: (MessageId) -> Void
let unpinMessage: (MessageId) -> Void let unpinMessage: (MessageId, Bool) -> Void
let shareAccountContact: () -> Void let shareAccountContact: () -> Void
let reportPeer: () -> Void let reportPeer: () -> Void
let presentPeerContact: () -> Void let presentPeerContact: () -> Void
@ -171,7 +171,7 @@ final class ChatPanelInterfaceInteraction {
sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool,
unblockPeer: @escaping () -> Void, unblockPeer: @escaping () -> Void,
pinMessage: @escaping (MessageId) -> Void, pinMessage: @escaping (MessageId) -> Void,
unpinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping (MessageId, Bool) -> Void,
shareAccountContact: @escaping () -> Void, shareAccountContact: @escaping () -> Void,
reportPeer: @escaping () -> Void, reportPeer: @escaping () -> Void,
presentPeerContact: @escaping () -> Void, presentPeerContact: @escaping () -> Void,

View File

@ -362,7 +362,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
@objc func closePressed() { @objc func closePressed() {
if let interfaceInteraction = self.interfaceInteraction, let message = self.currentMessage { if let interfaceInteraction = self.interfaceInteraction, let message = self.currentMessage {
interfaceInteraction.unpinMessage(message.message.id) interfaceInteraction.unpinMessage(message.message.id, true)
} }
} }
} }

View File

@ -100,7 +100,7 @@ final class ChatRecentActionsController: TelegramBaseController {
return false return false
}, unblockPeer: { }, unblockPeer: {
}, pinMessage: { _ in }, pinMessage: { _ in
}, unpinMessage: { _ in }, unpinMessage: { _, _ in
}, shareAccountContact: { }, shareAccountContact: {
}, reportPeer: { }, reportPeer: {
}, presentPeerContact: { }, presentPeerContact: {

View File

@ -130,7 +130,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
} else if params.standalone { } else if params.standalone {
location = .recentActions(params.message) location = .recentActions(params.message)
} else { } else {
location = .messages(peerId: params.message.id.peerId, tagMask: .voiceOrInstantVideo, at: params.message.id) location = .messages(chatLocation: params.chatLocation ?? .peer(params.message.id.peerId), tagMask: .voiceOrInstantVideo, at: params.message.id)
} }
playerType = .voice playerType = .voice
} else if file.isMusic && params.message.tags.contains(.music) { } else if file.isMusic && params.message.tags.contains(.music) {
@ -139,7 +139,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
} else if params.standalone { } else if params.standalone {
location = .recentActions(params.message) location = .recentActions(params.message)
} else { } else {
location = .messages(peerId: params.message.id.peerId, tagMask: .music, at: params.message.id) location = .messages(chatLocation: params.chatLocation ?? .peer(params.message.id.peerId), tagMask: .music, at: params.message.id)
} }
playerType = .music playerType = .music
} else { } else {
@ -150,7 +150,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
} }
playerType = (file.isVoice || file.isInstantVideo) ? .voice : .music playerType = (file.isVoice || file.isInstantVideo) ? .voice : .music
} }
params.context.sharedContext.mediaManager.setPlaylist((params.context.account, PeerMessagesMediaPlaylist(postbox: params.context.account.postbox, network: params.context.account.network, location: location)), type: playerType, control: control) params.context.sharedContext.mediaManager.setPlaylist((params.context.account, PeerMessagesMediaPlaylist(context: params.context, location: location, chatLocationContextHolder: params.chatLocationContextHolder)), type: playerType, control: control)
return true return true
case let .gallery(gallery): case let .gallery(gallery):
params.dismissInput() params.dismissInput()

View File

@ -52,6 +52,7 @@ import PeerInfoUI
import ListMessageItem import ListMessageItem
import GalleryData import GalleryData
import ChatInterfaceState import ChatInterfaceState
import TelegramVoip
protocol PeerInfoScreenItem: class { protocol PeerInfoScreenItem: class {
var id: AnyHashable { get } var id: AnyHashable { get }
@ -405,7 +406,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
return false return false
}, unblockPeer: { }, unblockPeer: {
}, pinMessage: { _ in }, pinMessage: { _ in
}, unpinMessage: { _ in }, unpinMessage: { _, _ in
}, shareAccountContact: { }, shareAccountContact: {
}, reportPeer: { }, reportPeer: {
}, presentPeerContact: { }, presentPeerContact: {
@ -3153,6 +3154,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
self.controller?.present(shareController, in: .window(.root)) self.controller?.present(shareController, in: .window(.root))
} }
private let groupCallDisposable = MetaDisposable()
private var groupCall: GroupCallContext?
private func requestCall(isVideo: Bool) { private func requestCall(isVideo: Bool) {
guard let peer = self.data?.peer as? TelegramUser, let cachedUserData = self.data?.cachedData as? CachedUserData else { guard let peer = self.data?.peer as? TelegramUser, let cachedUserData = self.data?.cachedData as? CachedUserData else {
return return

View File

@ -307,9 +307,9 @@ private struct PlaybackStack {
} }
final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { final class PeerMessagesMediaPlaylist: SharedMediaPlaylist {
private let postbox: Postbox private let context: AccountContext
private let network: Network
private let messagesLocation: PeerMessagesPlaylistLocation private let messagesLocation: PeerMessagesPlaylistLocation
private let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>?
var location: SharedMediaPlaylistLocation { var location: SharedMediaPlaylistLocation {
return self.messagesLocation return self.messagesLocation
@ -338,13 +338,13 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist {
return self.stateValue.get() return self.stateValue.get()
} }
init(postbox: Postbox, network: Network, location: PeerMessagesPlaylistLocation) { init(context: AccountContext, location: PeerMessagesPlaylistLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>?) {
assert(Queue.mainQueue().isCurrent()) assert(Queue.mainQueue().isCurrent())
self.id = location.playlistId self.id = location.playlistId
self.postbox = postbox self.context = context
self.network = network self.chatLocationContextHolder = chatLocationContextHolder
self.messagesLocation = location self.messagesLocation = location
switch self.messagesLocation { switch self.messagesLocation {
@ -446,7 +446,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist {
self.currentlyObservedMessageId = item?.message.id self.currentlyObservedMessageId = item?.message.id
if let id = item?.message.id { if let id = item?.message.id {
let key: PostboxViewKey = .messages(Set([id])) let key: PostboxViewKey = .messages(Set([id]))
self.currentlyObservedMessageDisposable.set((self.postbox.combinedView(keys: [key]) self.currentlyObservedMessageDisposable.set((self.context.account.postbox.combinedView(keys: [key])
|> filter { views in |> filter { views in
if let view = views.views[key] as? MessagesView { if let view = views.views[key] as? MessagesView {
if !view.messages.isEmpty { if !view.messages.isEmpty {
@ -479,15 +479,15 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist {
switch anchor { switch anchor {
case let .messageId(messageId): case let .messageId(messageId):
switch self.messagesLocation { switch self.messagesLocation {
case let .messages(peerId, tagMask, _): case let .messages(chatLocation, tagMask, _):
let historySignal = self.postbox.messageAtId(messageId) let historySignal = self.context.account.postbox.messageAtId(messageId)
|> take(1) |> take(1)
|> mapToSignal { message -> Signal<(Message, [Message])?, NoError> in |> mapToSignal { message -> Signal<(Message, [Message])?, NoError> in
guard let message = message else { guard let message = message else {
return .single(nil) return .single(nil)
} }
return self.postbox.aroundMessageHistoryViewForLocation(.peer(peerId), anchor: .index(message.index), count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: []) return self.context.account.postbox.aroundMessageHistoryViewForLocation(self.context.chatLocationInput(for: chatLocation, contextHolder: self.chatLocationContextHolder ?? Atomic<ChatLocationContextHolder?>(value: nil)), anchor: .index(message.index), count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: [])
|> mapToSignal { view -> Signal<(Message, [Message])?, NoError> in |> mapToSignal { view -> Signal<(Message, [Message])?, NoError> in
if let (message, aroundMessages, _) = navigatedMessageFromView(view.0, anchorIndex: message.index, position: .exact) { if let (message, aroundMessages, _) = navigatedMessageFromView(view.0, anchorIndex: message.index, position: .exact) {
return .single((message, aroundMessages)) return .single((message, aroundMessages))
@ -539,7 +539,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist {
} }
})) }))
default: default:
self.navigationDisposable.set((self.postbox.messageAtId(messageId) self.navigationDisposable.set((self.context.account.postbox.messageAtId(messageId)
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { [weak self] message in |> deliverOnMainQueue).start(next: { [weak self] message in
if let strongSelf = self { if let strongSelf = self {
@ -559,7 +559,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist {
} }
case let .index(index): case let .index(index):
switch self.messagesLocation { switch self.messagesLocation {
case let .messages(peerId, tagMask, _): case let .messages(chatLocation, tagMask, _):
let inputIndex: Signal<MessageIndex, NoError> let inputIndex: Signal<MessageIndex, NoError>
let looping = self.looping let looping = self.looping
switch self.order { switch self.order {
@ -567,7 +567,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist {
inputIndex = .single(index) inputIndex = .single(index)
case .random: case .random:
var playbackStack = self.playbackStack var playbackStack = self.playbackStack
inputIndex = self.postbox.transaction { transaction -> MessageIndex in inputIndex = self.context.account.postbox.transaction { transaction -> MessageIndex in
if case let .random(previous) = navigation, previous { if case let .random(previous) = navigation, previous {
let _ = playbackStack.pop() let _ = playbackStack.pop()
while true { while true {
@ -580,12 +580,12 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist {
} }
} }
} }
return transaction.findRandomMessage(peerId: peerId, namespace: Namespaces.Message.Cloud, tag: tagMask, ignoreIds: (playbackStack.ids, playbackStack.set)) ?? index return transaction.findRandomMessage(peerId: chatLocation.peerId, namespace: Namespaces.Message.Cloud, tag: tagMask, ignoreIds: (playbackStack.ids, playbackStack.set)) ?? index
} }
} }
let historySignal = inputIndex let historySignal = inputIndex
|> mapToSignal { inputIndex -> Signal<(Message, [Message])?, NoError> in |> mapToSignal { inputIndex -> Signal<(Message, [Message])?, NoError> in
return self.postbox.aroundMessageHistoryViewForLocation(.peer(peerId), anchor: .index(inputIndex), count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: []) return self.context.account.postbox.aroundMessageHistoryViewForLocation(self.context.chatLocationInput(for: chatLocation, contextHolder: self.chatLocationContextHolder ?? Atomic<ChatLocationContextHolder?>(value: nil)), anchor: .index(inputIndex), count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: [])
|> mapToSignal { view -> Signal<(Message, [Message])?, NoError> in |> mapToSignal { view -> Signal<(Message, [Message])?, NoError> in
let position: NavigatedMessageFromViewPosition let position: NavigatedMessageFromViewPosition
switch navigation { switch navigation {
@ -615,7 +615,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist {
} else { } else {
viewIndex = .lowerBound viewIndex = .lowerBound
} }
return self.postbox.aroundMessageHistoryViewForLocation(.peer(peerId), anchor: viewIndex, count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: []) return self.context.account.postbox.aroundMessageHistoryViewForLocation(self.context.chatLocationInput(for: chatLocation, contextHolder: self.chatLocationContextHolder ?? Atomic<ChatLocationContextHolder?>(value: nil)), anchor: viewIndex, count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: [])
|> mapToSignal { view -> Signal<(Message, [Message])?, NoError> in |> mapToSignal { view -> Signal<(Message, [Message])?, NoError> in
let position: NavigatedMessageFromViewPosition let position: NavigatedMessageFromViewPosition
switch navigation { switch navigation {
@ -657,7 +657,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist {
} }
})) }))
case .singleMessage: case .singleMessage:
self.navigationDisposable.set((self.postbox.messageAtId(index.id) self.navigationDisposable.set((self.context.account.postbox.messageAtId(index.id)
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { [weak self] message in |> deliverOnMainQueue).start(next: { [weak self] message in
if let strongSelf = self { if let strongSelf = self {
@ -815,7 +815,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist {
default: default:
break break
} }
let _ = markMessageContentAsConsumedInteractively(postbox: self.postbox, messageId: item.message.id).start() let _ = markMessageContentAsConsumedInteractively(postbox: self.context.account.postbox, messageId: item.message.id).start()
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
#ifndef GroupCallThreadLocalContext_h
#define GroupCallThreadLocalContext_h
#import <Foundation/Foundation.h>
#import <TgVoipWebrtc/OngoingCallThreadLocalContext.h>
@interface GroupCallThreadLocalContext : NSObject
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue relaySdpAnswer:(void (^ _Nonnull)(NSString * _Nonnull))relaySdpAnswer;
- (void)emitOffer;
- (void)setOfferSdp:(NSString * _Nonnull)offerSdp isPartial:(bool)isPartial;
- (void)setIsMuted:(bool)isMuted;
@end
#endif

View File

@ -0,0 +1,57 @@
#import <TgVoipWebrtc/GroupCallThreadLocalContext.h>
#import "group/GroupInstanceImpl.h"
@interface GroupCallThreadLocalContext () {
id<OngoingCallThreadLocalContextQueueWebrtc> _queue;
std::unique_ptr<tgcalls::GroupInstanceImpl> _instance;
}
@end
@implementation GroupCallThreadLocalContext
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue relaySdpAnswer:(void (^ _Nonnull)(NSString * _Nonnull))relaySdpAnswer {
self = [super init];
if (self != nil) {
_queue = queue;
tgcalls::GroupInstanceDescriptor descriptor;
__weak GroupCallThreadLocalContext *weakSelf = self;
descriptor.sdpAnswerEmitted = [weakSelf, queue, relaySdpAnswer](std::string const &sdpAnswer) {
NSString *string = [NSString stringWithUTF8String:sdpAnswer.c_str()];
[queue dispatch:^{
__strong GroupCallThreadLocalContext *strongSelf = weakSelf;
if (strongSelf == nil) {
return;
}
relaySdpAnswer(string);
}];
};
_instance.reset(new tgcalls::GroupInstanceImpl(std::move(descriptor)));
}
return self;
}
- (void)emitOffer {
if (_instance) {
_instance->emitOffer();
}
}
- (void)setOfferSdp:(NSString * _Nonnull)offerSdp isPartial:(bool)isPartial {
if (_instance) {
_instance->setOfferSdp([offerSdp UTF8String], isPartial);
}
}
- (void)setIsMuted:(bool)isMuted {
if (_instance) {
_instance->setIsMuted(isMuted);
}
}
@end

View File

@ -1,7 +1,7 @@
#ifndef WEBRTC_IOS #ifndef WEBRTC_IOS
#import "OngoingCallThreadLocalContext.h" #import "OngoingCallThreadLocalContext.h"
#else #else
#import <TgVoip/OngoingCallThreadLocalContext.h> #import <TgVoipWebrtc/OngoingCallThreadLocalContext.h>
#endif #endif

@ -1 +1 @@
Subproject commit 64f96a1b4fcfb8afdb0fb7749082cb42cdad7901 Subproject commit 4f7501d281b851e6302b2a2d7298c733eee82414

View File

@ -1,4 +1,4 @@
use_gn_build = True use_gn_build = False
webrtc_libs = [ webrtc_libs = [
"libwebrtc.a", "libwebrtc.a",

@ -1 +1 @@
Subproject commit 11255bcfff3180210a012f368e2d2bcd169b6877 Subproject commit 782743c7931d09c1d2e4a0cf6cd349ee45452f1d