mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Merge remote-tracking branch 'refs/remotes/origin/master'
# Conflicts: # Telegram/Telegram-iOS/en.lproj/Localizable.strings
This commit is contained in:
commit
c2b21c7e73
@ -7425,3 +7425,5 @@ Sorry for the inconvenience.";
|
||||
|
||||
"Notifications.UploadSound" = "Upload Sound";
|
||||
"Notifications.MessageSoundInfo" = "Long tap on any short voice note or mp3 file in chat\nand select \"Save for Notifications\". It will appear here.";
|
||||
|
||||
"Notification.WebAppSentData" = "You have successfully transferred data from the \"%@\" button to the bot.";
|
||||
|
@ -10,6 +10,21 @@ import MapKit
|
||||
|
||||
private let overflowInset: CGFloat = 0.0
|
||||
|
||||
public func attachmentDefaultTopInset(layout: ContainerViewLayout?) -> CGFloat {
|
||||
guard let layout = layout else {
|
||||
return 210.0
|
||||
}
|
||||
if case .compact = layout.metrics.widthClass {
|
||||
var factor: CGFloat = 0.2488
|
||||
if layout.size.width <= 320.0 {
|
||||
factor = 0.15
|
||||
}
|
||||
return floor(max(layout.size.width, layout.size.height) * factor)
|
||||
} else {
|
||||
return 210.0
|
||||
}
|
||||
}
|
||||
|
||||
final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
let wrappingNode: ASDisplayNode
|
||||
let clipNode: ASDisplayNode
|
||||
@ -130,29 +145,14 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)?
|
||||
|
||||
private var defaultTopInset: CGFloat {
|
||||
guard let (layout, _, _) = self.validLayout else{
|
||||
return 210.0
|
||||
}
|
||||
if case .compact = layout.metrics.widthClass {
|
||||
var factor: CGFloat = 0.2488
|
||||
if layout.size.width <= 320.0 {
|
||||
factor = 0.15
|
||||
}
|
||||
return floor(max(layout.size.width, layout.size.height) * factor)
|
||||
} else {
|
||||
return 210.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
guard let (layout, controllers, coveredByModalTransition) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let isLandscape = layout.orientation == .landscape
|
||||
let edgeTopInset = isLandscape ? 0.0 : defaultTopInset
|
||||
let edgeTopInset = isLandscape ? 0.0 : attachmentDefaultTopInset(layout: layout)
|
||||
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
@ -349,6 +349,7 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
|
||||
self.panGestureRecognizer?.isEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0)
|
||||
|
||||
let defaultTopInset = attachmentDefaultTopInset(layout: layout)
|
||||
let isLandscape = layout.orientation == .landscape
|
||||
let edgeTopInset = isLandscape ? 0.0 : defaultTopInset
|
||||
|
||||
|
@ -97,7 +97,8 @@ public class AttachmentController: ViewController {
|
||||
private let context: AccountContext
|
||||
private let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
||||
private let chatLocation: ChatLocation
|
||||
private var buttons: [AttachmentButtonType]
|
||||
private let buttons: [AttachmentButtonType]
|
||||
private let initialButton: AttachmentButtonType
|
||||
|
||||
public var mediaPickerContext: AttachmentMediaPickerContext? {
|
||||
get {
|
||||
@ -268,7 +269,20 @@ public class AttachmentController: ViewController {
|
||||
|
||||
self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
||||
|
||||
let _ = self.switchToController(.gallery)
|
||||
if let controller = self.controller {
|
||||
let _ = self.switchToController(controller.initialButton)
|
||||
if case let .app(botId, _, _) = controller.initialButton {
|
||||
if let index = controller.buttons.firstIndex(where: {
|
||||
if case let .app(otherBotId, _, _) = $0, otherBotId == botId {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
self.panel.updateSelectedIndex(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSelectionCount(_ count: Int) {
|
||||
@ -293,25 +307,6 @@ public class AttachmentController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func switchTo(_ type: AttachmentButtonType) {
|
||||
guard let buttons = self.controller?.buttons else {
|
||||
return
|
||||
}
|
||||
if case let .app(botId, _, _) = type {
|
||||
let index = buttons.firstIndex(where: {
|
||||
if case let .app(otherBotId, _, _) = $0, otherBotId == botId {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
if let index = index {
|
||||
self.panel.updateSelectedIndex(index)
|
||||
let _ = self.switchToController(buttons[index], animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func switchToController(_ type: AttachmentButtonType, animated: Bool = true) -> Bool {
|
||||
guard self.currentType != type else {
|
||||
if self.animating {
|
||||
@ -583,13 +578,12 @@ public class AttachmentController: ViewController {
|
||||
completion(nil, nil)
|
||||
}
|
||||
|
||||
private var buttonsDisposable: Disposable?
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatLocation: ChatLocation, buttons: Signal<[AttachmentButtonType], NoError>) {
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatLocation: ChatLocation, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery) {
|
||||
self.context = context
|
||||
self.updatedPresentationData = updatedPresentationData
|
||||
self.chatLocation = chatLocation
|
||||
self.buttons = []
|
||||
self.buttons = buttons
|
||||
self.initialButton = initialButton
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
@ -602,21 +596,6 @@ public class AttachmentController: ViewController {
|
||||
strongSelf.node.scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
self.buttonsDisposable = (buttons
|
||||
|> deliverOnMainQueue).start(next: { [weak self] buttons in
|
||||
if let strongSelf = self {
|
||||
let previousButtons = strongSelf.buttons
|
||||
strongSelf.buttons = buttons
|
||||
if let layout = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, transition: !previousButtons.isEmpty ? .animated(duration: 0.2, curve: .easeInOut) : .immediate)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.buttonsDisposable?.dispose()
|
||||
}
|
||||
|
||||
public required init(coder aDecoder: NSCoder) {
|
||||
@ -632,10 +611,6 @@ public class AttachmentController: ViewController {
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
public func switchTo(_ type: AttachmentButtonType) {
|
||||
(self.displayNode as! Node).switchTo(type)
|
||||
}
|
||||
|
||||
public func _dismiss() {
|
||||
super.dismiss(animated: false, completion: {})
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ private final class IconComponent: Component {
|
||||
self.image = nil
|
||||
}
|
||||
|
||||
_ = freeMediaFileInteractiveFetched(account: component.account, fileReference: .standalone(media: file)).start()
|
||||
let _ = freeMediaFileInteractiveFetched(account: component.account, fileReference: .standalone(media: file)).start()
|
||||
self.disposable = (svgIconImageFile(account: component.account, fileReference: .standalone(media: file), fetched: true)
|
||||
|> runOn(Queue.concurrentDefaultQueue())
|
||||
|> deliverOnMainQueue).start(next: { [weak self] transform in
|
||||
|
@ -81,7 +81,7 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
|
||||
case info(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, TelegramUserPresence?)
|
||||
case rankTitle(PresentationTheme, String, Int32?, Int32)
|
||||
case rank(PresentationTheme, PresentationStrings, String, String, Bool)
|
||||
case rankInfo(PresentationTheme, String)
|
||||
case rankInfo(PresentationTheme, String, Bool)
|
||||
case adminRights(PresentationTheme, String, Bool)
|
||||
case rightsTitle(PresentationTheme, String)
|
||||
case rightItem(PresentationTheme, Int, String, TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags, Bool, Bool)
|
||||
@ -167,8 +167,8 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .rankInfo(lhsTheme, lhsText):
|
||||
if case let .rankInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
case let .rankInfo(lhsTheme, lhsText, lhsTrimBottomInset):
|
||||
if case let .rankInfo(rhsTheme, rhsText, rhsTrimBottomInset) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsTrimBottomInset == rhsTrimBottomInset {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -332,8 +332,8 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
|
||||
}, action: {
|
||||
arguments.dismissInput()
|
||||
})
|
||||
case let .rankInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section, trimBottomInset: true)
|
||||
case let .rankInfo(_, text, trimBottomInset):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section, trimBottomInset: trimBottomInset)
|
||||
case let .adminRights(_, text, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .regular, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.updateAdminRights(value)
|
||||
@ -705,7 +705,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
|
||||
let rankEnabled = !state.updating && canEdit
|
||||
entries.append(.rankTitle(presentationData.theme, presentationData.strings.Group_EditAdmin_RankTitle.uppercased(), rankEnabled && state.focusedOnRank ? Int32(currentRank?.count ?? 0) : nil, rankMaxLength))
|
||||
entries.append(.rank(presentationData.theme, presentationData.strings, isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder, currentRank ?? "", rankEnabled))
|
||||
entries.append(.rankInfo(presentationData.theme, presentationData.strings.Group_EditAdmin_RankInfo(placeholder).string))
|
||||
entries.append(.rankInfo(presentationData.theme, presentationData.strings.Group_EditAdmin_RankInfo(placeholder).string, invite))
|
||||
}
|
||||
|
||||
if canDismiss {
|
||||
@ -788,7 +788,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
|
||||
let placeholder = isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder
|
||||
entries.append(.rankTitle(presentationData.theme, presentationData.strings.Group_EditAdmin_RankTitle.uppercased(), rankEnabled && state.focusedOnRank ? Int32(currentRank?.count ?? 0) : nil, rankMaxLength))
|
||||
entries.append(.rank(presentationData.theme, presentationData.strings, placeholder, currentRank ?? "", rankEnabled))
|
||||
entries.append(.rankInfo(presentationData.theme, presentationData.strings.Group_EditAdmin_RankInfo(placeholder).string))
|
||||
entries.append(.rankInfo(presentationData.theme, presentationData.strings.Group_EditAdmin_RankInfo(placeholder).string, invite))
|
||||
}
|
||||
|
||||
if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _) = initialParticipant, admin.id != accountPeerId, adminInfo != nil {
|
||||
|
@ -161,6 +161,7 @@ public final class ShimmerEffectNode: ASDisplayNode {
|
||||
case roundedRectLine(startPoint: CGPoint, width: CGFloat, diameter: CGFloat)
|
||||
case roundedRect(rect: CGRect, cornerRadius: CGFloat)
|
||||
case rect(rect: CGRect)
|
||||
case image(image: UIImage, rect: CGRect)
|
||||
}
|
||||
|
||||
private let backgroundNode: ASDisplayNode
|
||||
@ -231,6 +232,14 @@ public final class ShimmerEffectNode: ASDisplayNode {
|
||||
UIGraphicsPopContext()
|
||||
case let .rect(rect):
|
||||
context.fill(rect)
|
||||
case let .image(image, rect):
|
||||
if let image = image.cgImage {
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
context.clip(to: rect, mask: image)
|
||||
context.fill(rect)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -149,6 +149,7 @@ struct AccountMutableState {
|
||||
|
||||
var storedMessagesByPeerIdAndTimestamp: [PeerId: Set<MessageIndex>]
|
||||
var displayAlerts: [(text: String, isDropAuth: Bool)] = []
|
||||
var dismissBotWebViews: [Int64] = []
|
||||
|
||||
var insertedPeers: [PeerId: Peer] = [:]
|
||||
|
||||
@ -176,7 +177,7 @@ struct AccountMutableState {
|
||||
self.updatedOutgoingUniqueMessageIds = [:]
|
||||
}
|
||||
|
||||
init(initialState: AccountInitialState, operations: [AccountStateMutationOperation], state: AuthorizedAccountState.State, peers: [PeerId: Peer], channelStates: [PeerId: AccountStateChannelState], peerChatInfos: [PeerId: PeerChatInfo], referencedMessageIds: Set<MessageId>, storedMessages: Set<MessageId>, readInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set<MessageIndex>], namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]], updatedOutgoingUniqueMessageIds: [Int64: Int32], displayAlerts: [(text: String, isDropAuth: Bool)], branchOperationIndex: Int) {
|
||||
init(initialState: AccountInitialState, operations: [AccountStateMutationOperation], state: AuthorizedAccountState.State, peers: [PeerId: Peer], channelStates: [PeerId: AccountStateChannelState], peerChatInfos: [PeerId: PeerChatInfo], referencedMessageIds: Set<MessageId>, storedMessages: Set<MessageId>, readInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set<MessageIndex>], namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]], updatedOutgoingUniqueMessageIds: [Int64: Int32], displayAlerts: [(text: String, isDropAuth: Bool)], dismissBotWebViews: [Int64], branchOperationIndex: Int) {
|
||||
self.initialState = initialState
|
||||
self.operations = operations
|
||||
self.state = state
|
||||
@ -190,11 +191,12 @@ struct AccountMutableState {
|
||||
self.namespacesWithHolesFromPreviousState = namespacesWithHolesFromPreviousState
|
||||
self.updatedOutgoingUniqueMessageIds = updatedOutgoingUniqueMessageIds
|
||||
self.displayAlerts = displayAlerts
|
||||
self.dismissBotWebViews = dismissBotWebViews
|
||||
self.branchOperationIndex = branchOperationIndex
|
||||
}
|
||||
|
||||
func branch() -> AccountMutableState {
|
||||
return AccountMutableState(initialState: self.initialState, operations: self.operations, state: self.state, peers: self.peers, channelStates: self.channelStates, peerChatInfos: self.peerChatInfos, referencedMessageIds: self.referencedMessageIds, storedMessages: self.storedMessages, readInboxMaxIds: self.readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: self.storedMessagesByPeerIdAndTimestamp, namespacesWithHolesFromPreviousState: self.namespacesWithHolesFromPreviousState, updatedOutgoingUniqueMessageIds: self.updatedOutgoingUniqueMessageIds, displayAlerts: self.displayAlerts, branchOperationIndex: self.operations.count)
|
||||
return AccountMutableState(initialState: self.initialState, operations: self.operations, state: self.state, peers: self.peers, channelStates: self.channelStates, peerChatInfos: self.peerChatInfos, referencedMessageIds: self.referencedMessageIds, storedMessages: self.storedMessages, readInboxMaxIds: self.readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: self.storedMessagesByPeerIdAndTimestamp, namespacesWithHolesFromPreviousState: self.namespacesWithHolesFromPreviousState, updatedOutgoingUniqueMessageIds: self.updatedOutgoingUniqueMessageIds, displayAlerts: self.displayAlerts, dismissBotWebViews: self.dismissBotWebViews, branchOperationIndex: self.operations.count)
|
||||
}
|
||||
|
||||
mutating func merge(_ other: AccountMutableState) {
|
||||
@ -221,6 +223,7 @@ struct AccountMutableState {
|
||||
}
|
||||
self.updatedOutgoingUniqueMessageIds.merge(other.updatedOutgoingUniqueMessageIds, uniquingKeysWith: { lhs, _ in lhs })
|
||||
self.displayAlerts.append(contentsOf: other.displayAlerts)
|
||||
self.dismissBotWebViews.append(contentsOf: other.dismissBotWebViews)
|
||||
}
|
||||
|
||||
mutating func addPreCachedResource(_ resource: MediaResource, data: Data) {
|
||||
@ -242,7 +245,11 @@ struct AccountMutableState {
|
||||
mutating func addDisplayAlert(_ text: String, isDropAuth: Bool) {
|
||||
self.displayAlerts.append((text: text, isDropAuth: isDropAuth))
|
||||
}
|
||||
|
||||
|
||||
mutating func addDismissWebView(_ queryId: Int64) {
|
||||
self.dismissBotWebViews.append(queryId)
|
||||
}
|
||||
|
||||
mutating func deleteMessagesWithGlobalIds(_ globalIds: [Int32]) {
|
||||
self.addOperation(.DeleteMessagesWithGlobalIds(globalIds))
|
||||
}
|
||||
@ -501,6 +508,10 @@ struct AccountMutableState {
|
||||
self.addOperation(.UpdateAttachMenuBots)
|
||||
}
|
||||
|
||||
mutating func addDismissedWebView(queryId: Int64) {
|
||||
self.addOperation(.UpdateAttachMenuBots)
|
||||
}
|
||||
|
||||
mutating func addOperation(_ operation: AccountStateMutationOperation) {
|
||||
switch operation {
|
||||
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots:
|
||||
@ -644,6 +655,7 @@ struct AccountFinalStateEvents {
|
||||
let updatedPeersNearby: [PeerNearby]?
|
||||
let isContactUpdates: [(PeerId, Bool)]
|
||||
let displayAlerts: [(text: String, isDropAuth: Bool)]
|
||||
let dismissBotWebViews: [Int64]
|
||||
let delayNotificatonsUntil: Int32?
|
||||
let updatedMaxMessageId: Int32?
|
||||
let updatedQts: Int32?
|
||||
@ -653,10 +665,10 @@ struct AccountFinalStateEvents {
|
||||
let updatedOutgoingThreadReadStates: [MessageId: MessageId.Id]
|
||||
|
||||
var isEmpty: Bool {
|
||||
return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty
|
||||
return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty
|
||||
}
|
||||
|
||||
init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: String, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set<PeerId> = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:]) {
|
||||
init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: String, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set<PeerId> = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:]) {
|
||||
self.addedIncomingMessageIds = addedIncomingMessageIds
|
||||
self.addedReactionEvents = addedReactionEvents
|
||||
self.wasScheduledMessageIds = wasScheduledMessageIds
|
||||
@ -669,6 +681,7 @@ struct AccountFinalStateEvents {
|
||||
self.updatedPeersNearby = updatedPeersNearby
|
||||
self.isContactUpdates = isContactUpdates
|
||||
self.displayAlerts = displayAlerts
|
||||
self.dismissBotWebViews = dismissBotWebViews
|
||||
self.delayNotificatonsUntil = delayNotificatonsUntil
|
||||
self.updatedMaxMessageId = updatedMaxMessageId
|
||||
self.updatedQts = updatedQts
|
||||
@ -691,6 +704,7 @@ struct AccountFinalStateEvents {
|
||||
self.updatedPeersNearby = state.updatedPeersNearby
|
||||
self.isContactUpdates = state.isContactUpdates
|
||||
self.displayAlerts = state.state.state.displayAlerts
|
||||
self.dismissBotWebViews = state.state.state.dismissBotWebViews
|
||||
self.delayNotificatonsUntil = state.delayNotificatonsUntil
|
||||
self.updatedMaxMessageId = state.state.state.updatedMaxMessageId
|
||||
self.updatedQts = state.state.state.updatedQts
|
||||
@ -723,6 +737,6 @@ struct AccountFinalStateEvents {
|
||||
let externallyUpdatedPeerId = self.externallyUpdatedPeerId.union(other.externallyUpdatedPeerId)
|
||||
let authorizationListUpdated = self.authorizationListUpdated || other.authorizationListUpdated
|
||||
|
||||
return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }))
|
||||
return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }))
|
||||
}
|
||||
}
|
||||
|
@ -1514,6 +1514,8 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo
|
||||
updatedState.updateMessageReactions(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: msgId), reactions: reactions, eventTimestamp: updatesDate)
|
||||
case .updateAttachMenuBots:
|
||||
updatedState.addUpdateAttachMenuBots()
|
||||
case let .updateWebViewResultSent(_, _, queryId):
|
||||
updatedState.addDismissWebView(queryId)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -115,6 +115,11 @@ public final class AccountStateManager {
|
||||
return self.displayAlertsPipe.signal()
|
||||
}
|
||||
|
||||
private let dismissBotWebViewsPipe = ValuePipe<[Int64]>()
|
||||
public var dismissBotWebViews: Signal<[Int64], NoError> {
|
||||
return self.dismissBotWebViewsPipe.signal()
|
||||
}
|
||||
|
||||
private let externallyUpdatedPeerIdsPipe = ValuePipe<[PeerId]>()
|
||||
var externallyUpdatedPeerIds: Signal<[PeerId], NoError> {
|
||||
return self.externallyUpdatedPeerIdsPipe.signal()
|
||||
@ -760,6 +765,10 @@ public final class AccountStateManager {
|
||||
self.displayAlertsPipe.putNext(events.displayAlerts)
|
||||
}
|
||||
|
||||
if !events.dismissBotWebViews.isEmpty {
|
||||
self.dismissBotWebViewsPipe.putNext(events.dismissBotWebViews)
|
||||
}
|
||||
|
||||
if !events.externallyUpdatedPeerId.isEmpty {
|
||||
self.externallyUpdatedPeerIdsPipe.putNext(Array(events.externallyUpdatedPeerId))
|
||||
}
|
||||
|
@ -50,30 +50,49 @@ public enum RequestWebViewError {
|
||||
case generic
|
||||
}
|
||||
|
||||
private func keepWebView(network: Network, flags: Int32, peer: Api.InputPeer, bot: Api.InputUser, queryId: Int64, replyToMessageId: MessageId?) -> Signal<Never, KeepWebViewError> {
|
||||
let poll = Signal<Never, KeepWebViewError> { subscriber in
|
||||
let signal: Signal<Never, KeepWebViewError> = network.request(Api.functions.messages.prolongWebView(flags: flags, peer: peer, bot: bot, queryId: queryId, replyToMsgId: replyToMessageId?.id))
|
||||
|> mapError { _ -> KeepWebViewError in
|
||||
return .generic
|
||||
private func keepWebViewSignal(network: Network, stateManager: AccountStateManager, flags: Int32, peer: Api.InputPeer, bot: Api.InputUser, queryId: Int64, replyToMessageId: MessageId?) -> Signal<Never, KeepWebViewError> {
|
||||
let signal = Signal<Never, KeepWebViewError> { subscriber in
|
||||
let poll = Signal<Never, KeepWebViewError> { subscriber in
|
||||
let signal: Signal<Never, KeepWebViewError> = network.request(Api.functions.messages.prolongWebView(flags: flags, peer: peer, bot: bot, queryId: queryId, replyToMsgId: replyToMessageId?.id))
|
||||
|> mapError { _ -> KeepWebViewError in
|
||||
return .generic
|
||||
}
|
||||
|> ignoreValues
|
||||
|
||||
return signal.start(error: { error in
|
||||
subscriber.putError(error)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
}
|
||||
|> ignoreValues
|
||||
let keepAliveSignal = (
|
||||
.complete()
|
||||
|> suspendAwareDelay(60.0, queue: Queue.concurrentDefaultQueue())
|
||||
|> then (poll)
|
||||
)
|
||||
|> restart
|
||||
|
||||
return signal.start(error: { error in
|
||||
let pollDisposable = keepAliveSignal.start(error: { error in
|
||||
subscriber.putError(error)
|
||||
}, completed: {
|
||||
})
|
||||
|
||||
let dismissDisposable = (stateManager.dismissBotWebViews
|
||||
|> filter {
|
||||
$0.contains(queryId)
|
||||
}
|
||||
|> take(1)).start(completed: {
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
|
||||
let disposableSet = DisposableSet()
|
||||
disposableSet.add(pollDisposable)
|
||||
disposableSet.add(dismissDisposable)
|
||||
return disposableSet
|
||||
}
|
||||
|
||||
return (
|
||||
.complete()
|
||||
|> suspendAwareDelay(60.0, queue: Queue.concurrentDefaultQueue())
|
||||
|> then (poll)
|
||||
)
|
||||
|> restart
|
||||
return signal
|
||||
}
|
||||
|
||||
func _internal_requestWebView(postbox: Postbox, network: Network, peerId: PeerId, botId: PeerId, url: String?, themeParams: [String: Any]?, replyToMessageId: MessageId?) -> Signal<RequestWebViewResult, RequestWebViewError> {
|
||||
func _internal_requestWebView(postbox: Postbox, network: Network, stateManager: AccountStateManager, peerId: PeerId, botId: PeerId, url: String?, themeParams: [String: Any]?, replyToMessageId: MessageId?) -> Signal<RequestWebViewResult, RequestWebViewError> {
|
||||
var serializedThemeParams: Api.DataJSON?
|
||||
if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) {
|
||||
serializedThemeParams = .dataJSON(data: dataString)
|
||||
@ -122,7 +141,7 @@ func _internal_requestWebView(postbox: Postbox, network: Network, peerId: PeerId
|
||||
|> castError(RequestWebViewError.self)
|
||||
|> switchToLatest
|
||||
case let .webViewResultUrl(queryId, url):
|
||||
return .single(.webViewResult(queryId: queryId, url: url, keepAliveSignal: keepWebView(network: network, flags: flags, peer: inputPeer, bot: inputBot, queryId: queryId, replyToMessageId: replyToMessageId)))
|
||||
return .single(.webViewResult(queryId: queryId, url: url, keepAliveSignal: keepWebViewSignal(network: network, stateManager: stateManager, flags: flags, peer: inputPeer, bot: inputBot, queryId: queryId, replyToMessageId: replyToMessageId)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -323,7 +323,7 @@ public extension TelegramEngine {
|
||||
}
|
||||
|
||||
public func requestWebView(peerId: PeerId, botId: PeerId, url: String?, themeParams: [String: Any]?, replyToMessageId: MessageId?) -> Signal<RequestWebViewResult, RequestWebViewError> {
|
||||
return _internal_requestWebView(postbox: self.account.postbox, network: self.account.network, peerId: peerId, botId: botId, url: url, themeParams: themeParams, replyToMessageId: replyToMessageId)
|
||||
return _internal_requestWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, botId: botId, url: url, themeParams: themeParams, replyToMessageId: replyToMessageId)
|
||||
}
|
||||
|
||||
public func requestSimpleWebView(botId: PeerId, url: String, themeParams: [String: Any]?) -> Signal<String, RequestSimpleWebViewError> {
|
||||
|
@ -632,7 +632,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
}
|
||||
}
|
||||
case let .webViewData(text):
|
||||
attributedString = NSAttributedString(string: "Data for \(text) sent to bot", font: titleFont, textColor: primaryTextColor)
|
||||
attributedString = NSAttributedString(string: strings.Notification_WebAppSentData(text).string, font: titleFont, textColor: primaryTextColor)
|
||||
case .unknown:
|
||||
attributedString = nil
|
||||
}
|
||||
|
@ -3359,7 +3359,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, buttonText: buttonText, keepAliveSignal: nil)
|
||||
let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, buttonText: buttonText, keepAliveSignal: nil, replyToMessageId: nil, iconFile: nil)
|
||||
controller.getNavigationController = { [weak self] in
|
||||
return self?.effectiveNavigationController
|
||||
}
|
||||
controller.navigationPresentation = .modal
|
||||
strongSelf.push(controller)
|
||||
}, error: { [weak self] error in
|
||||
@ -3379,7 +3382,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
switch result {
|
||||
case let .webViewResult(queryId, url, keepAliveSignal):
|
||||
let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peerId, botId: peerId, botName: botName, url: url, queryId: queryId, buttonText: buttonText, keepAliveSignal: keepAliveSignal)
|
||||
let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peerId, botId: peerId, botName: botName, url: url, queryId: queryId, buttonText: buttonText, keepAliveSignal: keepAliveSignal, replyToMessageId: nil, iconFile: nil)
|
||||
controller.getNavigationController = { [weak self] in
|
||||
return self?.effectiveNavigationController
|
||||
}
|
||||
controller.navigationPresentation = .modal
|
||||
strongSelf.push(controller)
|
||||
case .requestConfirmation:
|
||||
@ -10493,6 +10499,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return
|
||||
}
|
||||
|
||||
let context = self.context
|
||||
|
||||
let inputIsActive = self.presentationInterfaceState.inputMode == .text
|
||||
|
||||
self.chatDisplayNode.dismissInput()
|
||||
@ -10531,254 +10539,220 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
isScheduledMessages = true
|
||||
}
|
||||
|
||||
var switchToBotImpl: ((AttachmentButtonType) -> Void)?
|
||||
var switchToBotId = botId
|
||||
let buttons: Signal<[AttachmentButtonType], NoError>
|
||||
let buttons: Signal<([AttachmentButtonType], AttachmentButtonType?), NoError>
|
||||
if let _ = peer as? TelegramUser, !isScheduledMessages {
|
||||
buttons = .single(availableButtons)
|
||||
|> then(
|
||||
self.context.engine.messages.attachMenuBots()
|
||||
|> map { attachMenuBots in
|
||||
var buttons = availableButtons
|
||||
for bot in attachMenuBots.reversed() {
|
||||
let peerTitle = EnginePeer(bot.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
buttons.insert(.app(bot.peer.id, peerTitle, bot.icon), at: 1)
|
||||
}
|
||||
return buttons
|
||||
buttons = self.context.engine.messages.attachMenuBots()
|
||||
|> map { attachMenuBots in
|
||||
var buttons = availableButtons
|
||||
var initialButton: AttachmentButtonType?
|
||||
if botId == nil {
|
||||
initialButton = .gallery
|
||||
}
|
||||
) |> afterNext { buttons in
|
||||
if let botId = switchToBotId, let button = buttons.first(where: {
|
||||
if case let .app(otherBotId, _,_) = $0, botId == otherBotId {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
for bot in attachMenuBots.reversed() {
|
||||
let peerTitle = EnginePeer(bot.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
let button: AttachmentButtonType = .app(bot.peer.id, peerTitle, bot.icon)
|
||||
buttons.insert(button, at: 1)
|
||||
|
||||
if initialButton == nil && bot.peer.id == botId {
|
||||
initialButton = button
|
||||
}
|
||||
}) {
|
||||
Queue.mainQueue().justDispatch {
|
||||
switchToBotImpl?(button)
|
||||
}
|
||||
switchToBotId = nil
|
||||
}
|
||||
return (buttons, initialButton)
|
||||
}
|
||||
} else {
|
||||
buttons = .single(availableButtons)
|
||||
buttons = .single((availableButtons, .gallery))
|
||||
}
|
||||
|
||||
let inputText = self.presentationInterfaceState.interfaceState.effectiveInputState.inputText
|
||||
|
||||
let currentMediaController = Atomic<MediaPickerScreen?>(value: nil)
|
||||
let currentFilesController = Atomic<AttachmentContainable?>(value: nil)
|
||||
let currentLocationController = Atomic<AttachmentContainable?>(value: nil)
|
||||
|
||||
let attachmentController = AttachmentController(context: self.context, updatedPresentationData: self.updatedPresentationData, chatLocation: self.chatLocation, buttons: buttons)
|
||||
switchToBotImpl = { [weak attachmentController] button in
|
||||
attachmentController?.switchTo(button)
|
||||
}
|
||||
attachmentController.requestController = { [weak self, weak attachmentController] type, completion in
|
||||
let _ = (buttons
|
||||
|> deliverOnMainQueue).start(next: { [weak self] buttons, initialButton in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
switch type {
|
||||
case .gallery:
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
let existingController = currentMediaController.with { $0 }
|
||||
if let controller = existingController {
|
||||
completion(controller, controller.mediaPickerContext)
|
||||
controller.prepareForReuse()
|
||||
return
|
||||
}
|
||||
strongSelf.presentMediaPicker(bannedSendMedia: bannedSendMedia, present: { controller, mediaPickerContext in
|
||||
let _ = currentMediaController.swap(controller)
|
||||
if !inputText.string.isEmpty {
|
||||
mediaPickerContext?.setCaption(inputText)
|
||||
}
|
||||
completion(controller, mediaPickerContext)
|
||||
}, updateMediaPickerContext: { [weak attachmentController] mediaPickerContext in
|
||||
attachmentController?.mediaPickerContext = mediaPickerContext
|
||||
}, completion: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in
|
||||
if !inputText.string.isEmpty {
|
||||
self?.clearInputText()
|
||||
}
|
||||
self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion)
|
||||
})
|
||||
case .file:
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
let existingController = currentFilesController.with { $0 }
|
||||
if let controller = existingController {
|
||||
completion(controller, nil)
|
||||
controller.prepareForReuse()
|
||||
return
|
||||
}
|
||||
let controller = attachmentFileController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bannedSendMedia: bannedSendMedia, presentGallery: { [weak self, weak attachmentController] in
|
||||
attachmentController?.dismiss(animated: true)
|
||||
self?.presentFileGallery()
|
||||
}, presentFiles: { [weak self, weak attachmentController] in
|
||||
attachmentController?.dismiss(animated: true)
|
||||
self?.presentICloudFileGallery()
|
||||
}, send: { [weak self] mediaReference in
|
||||
guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else {
|
||||
return
|
||||
}
|
||||
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: mediaReference, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)
|
||||
let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: strongSelf.transformEnqueueMessages([message]))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages {
|
||||
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||
}
|
||||
})
|
||||
})
|
||||
let _ = currentFilesController.swap(controller)
|
||||
completion(controller, nil)
|
||||
case .location:
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
let existingController = currentLocationController.with { $0 }
|
||||
if let controller = existingController {
|
||||
completion(controller, nil)
|
||||
controller.prepareForReuse()
|
||||
return
|
||||
}
|
||||
let selfPeerId: PeerId
|
||||
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
|
||||
selfPeerId = peer.id
|
||||
} else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) {
|
||||
selfPeerId = peer.id
|
||||
} else {
|
||||
selfPeerId = strongSelf.context.account.peerId
|
||||
}
|
||||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(selfPeerId)
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] selfPeer in
|
||||
guard let strongSelf = self, let selfPeer = selfPeer else {
|
||||
return
|
||||
}
|
||||
let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages
|
||||
let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: peer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _ in
|
||||
guard let strongSelf = self else {
|
||||
|
||||
guard let initialButton = initialButton else {
|
||||
if let botId = botId {
|
||||
let _ = (strongSelf.context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: botId)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let strongSelf = self, let peer = peer else {
|
||||
return
|
||||
}
|
||||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||||
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)
|
||||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
||||
})
|
||||
let _ = (strongSelf.context.engine.messages.requestWebView(peerId: peer.id, botId: botId, url: nil, themeParams: nil, replyToMessageId: nil)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
if let strongSelf = self, case let .requestConfirmation(botIcon) = result {
|
||||
if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canBeAddedToAttachMenu) {
|
||||
let controller = addWebAppToAttachmentController(context: context, peerName: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), peerIcon: botIcon, completion: {
|
||||
let _ = context.engine.messages.addBotToAttachMenu(peerId: botId).start()
|
||||
|
||||
Queue.mainQueue().after(1.0, {
|
||||
strongSelf.presentAttachmentBot(botId: botId)
|
||||
})
|
||||
})
|
||||
strongSelf.present(controller, in: .window(.root))
|
||||
} else {
|
||||
strongSelf.present(textAlertController(context: context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}
|
||||
}, nil)
|
||||
strongSelf.sendMessages([message])
|
||||
})
|
||||
})
|
||||
completion(controller, nil)
|
||||
|
||||
let _ = currentLocationController.swap(controller)
|
||||
})
|
||||
case .contact:
|
||||
let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true))
|
||||
contactsController.presentScheduleTimePicker = { [weak self] completion in
|
||||
if let strongSelf = self {
|
||||
strongSelf.presentScheduleTimePicker(completion: completion)
|
||||
}
|
||||
}
|
||||
contactsController.navigationPresentation = .modal
|
||||
completion(contactsController, contactsController.mediaPickerContext)
|
||||
strongSelf.controllerNavigationDisposable.set((contactsController.result
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peers in
|
||||
if let strongSelf = self, let (peers, _, silent, scheduleTime, text) = peers {
|
||||
var textEnqueueMessage: EnqueueMessage?
|
||||
if let text = text, text.length > 0 {
|
||||
var attributes: [MessageAttribute] = []
|
||||
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
|
||||
if !entities.isEmpty {
|
||||
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
||||
}
|
||||
textEnqueueMessage = .message(text: text.string, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)
|
||||
return
|
||||
}
|
||||
|
||||
let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText
|
||||
|
||||
let currentMediaController = Atomic<MediaPickerScreen?>(value: nil)
|
||||
let currentFilesController = Atomic<AttachmentContainable?>(value: nil)
|
||||
let currentLocationController = Atomic<AttachmentContainable?>(value: nil)
|
||||
|
||||
let attachmentController = AttachmentController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: strongSelf.chatLocation, buttons: buttons, initialButton: initialButton)
|
||||
attachmentController.requestController = { [weak self, weak attachmentController] type, completion in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
switch type {
|
||||
case .gallery:
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
let existingController = currentMediaController.with { $0 }
|
||||
if let controller = existingController {
|
||||
completion(controller, controller.mediaPickerContext)
|
||||
controller.prepareForReuse()
|
||||
return
|
||||
}
|
||||
strongSelf.presentMediaPicker(bannedSendMedia: bannedSendMedia, present: { controller, mediaPickerContext in
|
||||
let _ = currentMediaController.swap(controller)
|
||||
if !inputText.string.isEmpty {
|
||||
mediaPickerContext?.setCaption(inputText)
|
||||
}
|
||||
if peers.count > 1 {
|
||||
var enqueueMessages: [EnqueueMessage] = []
|
||||
if let textEnqueueMessage = textEnqueueMessage {
|
||||
enqueueMessages.append(textEnqueueMessage)
|
||||
completion(controller, mediaPickerContext)
|
||||
}, updateMediaPickerContext: { [weak attachmentController] mediaPickerContext in
|
||||
attachmentController?.mediaPickerContext = mediaPickerContext
|
||||
}, completion: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in
|
||||
if !inputText.string.isEmpty {
|
||||
self?.clearInputText()
|
||||
}
|
||||
self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion)
|
||||
})
|
||||
case .file:
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
let existingController = currentFilesController.with { $0 }
|
||||
if let controller = existingController {
|
||||
completion(controller, nil)
|
||||
controller.prepareForReuse()
|
||||
return
|
||||
}
|
||||
let controller = attachmentFileController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bannedSendMedia: bannedSendMedia, presentGallery: { [weak self, weak attachmentController] in
|
||||
attachmentController?.dismiss(animated: true)
|
||||
self?.presentFileGallery()
|
||||
}, presentFiles: { [weak self, weak attachmentController] in
|
||||
attachmentController?.dismiss(animated: true)
|
||||
self?.presentICloudFileGallery()
|
||||
}, send: { [weak self] mediaReference in
|
||||
guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else {
|
||||
return
|
||||
}
|
||||
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: mediaReference, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)
|
||||
let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: strongSelf.transformEnqueueMessages([message]))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages {
|
||||
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||
}
|
||||
for peer in peers {
|
||||
var media: TelegramMediaContact?
|
||||
switch peer {
|
||||
case let .peer(contact, _, _):
|
||||
guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else {
|
||||
continue
|
||||
}
|
||||
let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!<Mobile>!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
|
||||
|
||||
let phone = contactData.basicData.phoneNumbers[0].value
|
||||
media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil)
|
||||
case let .deviceContact(_, basicData):
|
||||
guard !basicData.phoneNumbers.isEmpty else {
|
||||
continue
|
||||
}
|
||||
let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
|
||||
|
||||
let phone = contactData.basicData.phoneNumbers[0].value
|
||||
media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil)
|
||||
})
|
||||
})
|
||||
let _ = currentFilesController.swap(controller)
|
||||
completion(controller, nil)
|
||||
case .location:
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
let existingController = currentLocationController.with { $0 }
|
||||
if let controller = existingController {
|
||||
completion(controller, nil)
|
||||
controller.prepareForReuse()
|
||||
return
|
||||
}
|
||||
let selfPeerId: PeerId
|
||||
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
|
||||
selfPeerId = peer.id
|
||||
} else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) {
|
||||
selfPeerId = peer.id
|
||||
} else {
|
||||
selfPeerId = strongSelf.context.account.peerId
|
||||
}
|
||||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(selfPeerId)
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] selfPeer in
|
||||
guard let strongSelf = self, let selfPeer = selfPeer else {
|
||||
return
|
||||
}
|
||||
let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages
|
||||
let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: peer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||||
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)
|
||||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
||||
})
|
||||
}
|
||||
}, nil)
|
||||
strongSelf.sendMessages([message])
|
||||
})
|
||||
completion(controller, nil)
|
||||
|
||||
let _ = currentLocationController.swap(controller)
|
||||
})
|
||||
case .contact:
|
||||
let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true))
|
||||
contactsController.presentScheduleTimePicker = { [weak self] completion in
|
||||
if let strongSelf = self {
|
||||
strongSelf.presentScheduleTimePicker(completion: completion)
|
||||
}
|
||||
}
|
||||
contactsController.navigationPresentation = .modal
|
||||
completion(contactsController, contactsController.mediaPickerContext)
|
||||
strongSelf.controllerNavigationDisposable.set((contactsController.result
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peers in
|
||||
if let strongSelf = self, let (peers, _, silent, scheduleTime, text) = peers {
|
||||
var textEnqueueMessage: EnqueueMessage?
|
||||
if let text = text, text.length > 0 {
|
||||
var attributes: [MessageAttribute] = []
|
||||
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
|
||||
if !entities.isEmpty {
|
||||
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
||||
}
|
||||
textEnqueueMessage = .message(text: text.string, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)
|
||||
}
|
||||
if peers.count > 1 {
|
||||
var enqueueMessages: [EnqueueMessage] = []
|
||||
if let textEnqueueMessage = textEnqueueMessage {
|
||||
enqueueMessages.append(textEnqueueMessage)
|
||||
}
|
||||
for peer in peers {
|
||||
var media: TelegramMediaContact?
|
||||
switch peer {
|
||||
case let .peer(contact, _, _):
|
||||
guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else {
|
||||
continue
|
||||
}
|
||||
let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!<Mobile>!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
|
||||
|
||||
let phone = contactData.basicData.phoneNumbers[0].value
|
||||
media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil)
|
||||
case let .deviceContact(_, basicData):
|
||||
guard !basicData.phoneNumbers.isEmpty else {
|
||||
continue
|
||||
}
|
||||
let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
|
||||
|
||||
let phone = contactData.basicData.phoneNumbers[0].value
|
||||
media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil)
|
||||
}
|
||||
|
||||
if let media = media {
|
||||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
||||
})
|
||||
}
|
||||
}, nil)
|
||||
let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)
|
||||
enqueueMessages.append(message)
|
||||
}
|
||||
}
|
||||
strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime))
|
||||
} else if let peer = peers.first {
|
||||
let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError>
|
||||
switch peer {
|
||||
case let .peer(contact, _, _):
|
||||
guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else {
|
||||
return
|
||||
}
|
||||
let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!<Mobile>!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
|
||||
let context = strongSelf.context
|
||||
dataSignal = (strongSelf.context.sharedContext.contactDataManager?.basicData() ?? .single([:]))
|
||||
|> take(1)
|
||||
|> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in
|
||||
var stableId: String?
|
||||
let queryPhoneNumber = formatPhoneNumber(phoneNumber)
|
||||
outer: for (id, data) in basicData {
|
||||
for phoneNumber in data.phoneNumbers {
|
||||
if formatPhoneNumber(phoneNumber.value) == queryPhoneNumber {
|
||||
stableId = id
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let stableId = stableId {
|
||||
return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil))
|
||||
|> take(1)
|
||||
|> map { extendedData -> (Peer?, DeviceContactExtendedData?) in
|
||||
return (contact, extendedData)
|
||||
}
|
||||
} else {
|
||||
return .single((contact, contactData))
|
||||
}
|
||||
}
|
||||
case let .deviceContact(id, _):
|
||||
dataSignal = (strongSelf.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil))
|
||||
|> take(1)
|
||||
|> map { extendedData -> (Peer?, DeviceContactExtendedData?) in
|
||||
return (nil, extendedData)
|
||||
}
|
||||
}
|
||||
strongSelf.controllerNavigationDisposable.set((dataSignal
|
||||
|> deliverOnMainQueue).start(next: { peerAndContactData in
|
||||
if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 {
|
||||
if contactData.isPrimitive {
|
||||
let phone = contactData.basicData.phoneNumbers[0].value
|
||||
let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil)
|
||||
if let media = media {
|
||||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||||
if let strongSelf = self {
|
||||
@ -10787,67 +10761,131 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
})
|
||||
}
|
||||
}, nil)
|
||||
|
||||
var enqueueMessages: [EnqueueMessage] = []
|
||||
if let textEnqueueMessage = textEnqueueMessage {
|
||||
enqueueMessages.append(textEnqueueMessage)
|
||||
}
|
||||
enqueueMessages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil))
|
||||
strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime))
|
||||
} else {
|
||||
let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in
|
||||
guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else {
|
||||
return
|
||||
}
|
||||
let phone = contactData.basicData.phoneNumbers[0].value
|
||||
if let vCardData = contactData.serializedVCard() {
|
||||
let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData)
|
||||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
||||
})
|
||||
}
|
||||
}, nil)
|
||||
|
||||
var enqueueMessages: [EnqueueMessage] = []
|
||||
if let textEnqueueMessage = textEnqueueMessage {
|
||||
enqueueMessages.append(textEnqueueMessage)
|
||||
}
|
||||
enqueueMessages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil))
|
||||
strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime))
|
||||
}
|
||||
}), completed: nil, cancelled: nil)
|
||||
strongSelf.effectiveNavigationController?.pushViewController(contactController)
|
||||
let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)
|
||||
enqueueMessages.append(message)
|
||||
}
|
||||
}
|
||||
}))
|
||||
strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime))
|
||||
} else if let peer = peers.first {
|
||||
let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError>
|
||||
switch peer {
|
||||
case let .peer(contact, _, _):
|
||||
guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else {
|
||||
return
|
||||
}
|
||||
let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!<Mobile>!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
|
||||
let context = strongSelf.context
|
||||
dataSignal = (strongSelf.context.sharedContext.contactDataManager?.basicData() ?? .single([:]))
|
||||
|> take(1)
|
||||
|> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in
|
||||
var stableId: String?
|
||||
let queryPhoneNumber = formatPhoneNumber(phoneNumber)
|
||||
outer: for (id, data) in basicData {
|
||||
for phoneNumber in data.phoneNumbers {
|
||||
if formatPhoneNumber(phoneNumber.value) == queryPhoneNumber {
|
||||
stableId = id
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let stableId = stableId {
|
||||
return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil))
|
||||
|> take(1)
|
||||
|> map { extendedData -> (Peer?, DeviceContactExtendedData?) in
|
||||
return (contact, extendedData)
|
||||
}
|
||||
} else {
|
||||
return .single((contact, contactData))
|
||||
}
|
||||
}
|
||||
case let .deviceContact(id, _):
|
||||
dataSignal = (strongSelf.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil))
|
||||
|> take(1)
|
||||
|> map { extendedData -> (Peer?, DeviceContactExtendedData?) in
|
||||
return (nil, extendedData)
|
||||
}
|
||||
}
|
||||
strongSelf.controllerNavigationDisposable.set((dataSignal
|
||||
|> deliverOnMainQueue).start(next: { peerAndContactData in
|
||||
if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 {
|
||||
if contactData.isPrimitive {
|
||||
let phone = contactData.basicData.phoneNumbers[0].value
|
||||
let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil)
|
||||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
||||
})
|
||||
}
|
||||
}, nil)
|
||||
|
||||
var enqueueMessages: [EnqueueMessage] = []
|
||||
if let textEnqueueMessage = textEnqueueMessage {
|
||||
enqueueMessages.append(textEnqueueMessage)
|
||||
}
|
||||
enqueueMessages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil))
|
||||
strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime))
|
||||
} else {
|
||||
let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in
|
||||
guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else {
|
||||
return
|
||||
}
|
||||
let phone = contactData.basicData.phoneNumbers[0].value
|
||||
if let vCardData = contactData.serializedVCard() {
|
||||
let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData)
|
||||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
||||
})
|
||||
}
|
||||
}, nil)
|
||||
|
||||
var enqueueMessages: [EnqueueMessage] = []
|
||||
if let textEnqueueMessage = textEnqueueMessage {
|
||||
enqueueMessages.append(textEnqueueMessage)
|
||||
}
|
||||
enqueueMessages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil))
|
||||
strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime))
|
||||
}
|
||||
}), completed: nil, cancelled: nil)
|
||||
strongSelf.effectiveNavigationController?.pushViewController(contactController)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}))
|
||||
case .poll:
|
||||
let controller = strongSelf.configurePollCreation()
|
||||
completion(controller, nil)
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
case let .app(botId, botName, botIcon):
|
||||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||||
let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peer.id, botId: botId, botName: botName, url: nil, queryId: nil, buttonText: nil, keepAliveSignal: nil, replyToMessageId: replyMessageId, iconFile: botIcon)
|
||||
controller.getNavigationController = { [weak self] in
|
||||
return self?.effectiveNavigationController
|
||||
}
|
||||
}))
|
||||
case .poll:
|
||||
let controller = strongSelf.configurePollCreation()
|
||||
completion(controller, nil)
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
case let .app(botId, botName, _):
|
||||
let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peer.id, botId: botId, botName: botName, url: nil, queryId: nil, buttonText: nil, keepAliveSignal: nil)
|
||||
completion(controller, nil)
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
completion(controller, nil)
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
let present = {
|
||||
self.present(attachmentController, in: .window(.root))
|
||||
self.attachmentController = attachmentController
|
||||
}
|
||||
|
||||
if inputIsActive {
|
||||
Queue.mainQueue().after(0.15, {
|
||||
let present = {
|
||||
strongSelf.present(attachmentController, in: .window(.root))
|
||||
strongSelf.attachmentController = attachmentController
|
||||
}
|
||||
|
||||
if inputIsActive {
|
||||
Queue.mainQueue().after(0.15, {
|
||||
present()
|
||||
})
|
||||
} else {
|
||||
present()
|
||||
})
|
||||
} else {
|
||||
present()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func oldPresentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) {
|
||||
|
@ -3160,16 +3160,24 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
botAddressName = attribute.title
|
||||
}
|
||||
|
||||
return .optionalAction({
|
||||
if let botAddressName = botAddressName {
|
||||
item.controllerInteraction.updateInputState { textInputState in
|
||||
return ChatTextInputState(inputText: NSAttributedString(string: "@" + botAddressName + " "))
|
||||
}
|
||||
item.controllerInteraction.updateInputMode { _ in
|
||||
return .text
|
||||
}
|
||||
if let peerId = attribute.peerId {
|
||||
if let botPeer = item.message.peers[peerId] as? TelegramUser, let inlinePlaceholder = botPeer.botInfo?.inlinePlaceholder, !inlinePlaceholder.isEmpty {
|
||||
return .optionalAction({
|
||||
if let botAddressName = botAddressName {
|
||||
item.controllerInteraction.updateInputState { textInputState in
|
||||
return ChatTextInputState(inputText: NSAttributedString(string: "@" + botAddressName + " "))
|
||||
}
|
||||
item.controllerInteraction.updateInputMode { _ in
|
||||
return .text
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return .optionalAction({
|
||||
item.controllerInteraction.openPeer(peerId, .chat(textInputState: nil, subject: nil, peekData: nil), nil, item.message.peers[peerId])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1269,7 +1269,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
}
|
||||
badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: string)
|
||||
}
|
||||
var animated: Bool = animated
|
||||
var animated = animated
|
||||
if let updatingMedia = attributes.updatingMedia, case .update = updatingMedia.media {
|
||||
state = .progress(color: messageTheme.mediaOverlayControlColors.foregroundColor, lineWidth: nil, value: CGFloat(updatingMedia.progress), cancelEnabled: true, animateRotation: true)
|
||||
} else if var fetchStatus = self.fetchStatus {
|
||||
@ -1497,6 +1497,14 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
self.statusNode = nil
|
||||
removeStatusNode = true
|
||||
}
|
||||
|
||||
var animated = animated
|
||||
if case .download = statusNode.state, case .progress = state {
|
||||
animated = true
|
||||
} else if case .progress = statusNode.state, case .download = state {
|
||||
animated = true
|
||||
}
|
||||
|
||||
statusNode.transitionToState(state, animated: animated, completion: { [weak statusNode] in
|
||||
if removeStatusNode {
|
||||
statusNode?.removeFromSupernode()
|
||||
|
@ -554,7 +554,11 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
||||
let _ = (context.engine.messages.attachMenuBots()
|
||||
|> deliverOnMainQueue).start(next: { attachMenuBots in
|
||||
if let _ = attachMenuBots.firstIndex(where: { $0.peer.id == peerId }) {
|
||||
presentError(presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError)
|
||||
if let navigationController = navigationController, case let .chat(chatPeerId, _) = urlContext {
|
||||
let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(id: chatPeerId), attachBotId: peerId, useExisting: true))
|
||||
} else {
|
||||
presentError(presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError)
|
||||
}
|
||||
} else {
|
||||
let _ = (context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
|
||||
@ -581,6 +585,8 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
||||
presentError(presentationData.strings.Login_UnknownError)
|
||||
}
|
||||
}
|
||||
}, error: { _ in
|
||||
presentError(presentationData.strings.Login_UnknownError)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -710,7 +710,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
||||
if let path = parsedUrl.pathComponents.last {
|
||||
var section: ResolvedUrlSettingsSection?
|
||||
switch path {
|
||||
case "theme":
|
||||
case "themes":
|
||||
section = .theme
|
||||
case "devices":
|
||||
section = .devices
|
||||
|
@ -21,6 +21,7 @@ swift_library(
|
||||
"//submodules/CounterContollerTitleView:CounterContollerTitleView",
|
||||
"//submodules/HexColor:HexColor",
|
||||
"//submodules/PhotoResources:PhotoResources",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -83,6 +83,7 @@ private final class WebAppAlertContentNode: AlertContentNode {
|
||||
|
||||
self.updateTheme(theme)
|
||||
|
||||
let _ = freeMediaFileInteractiveFetched(account: account, fileReference: .standalone(media: peerIcon)).start()
|
||||
self.iconDisposable = (svgIconImageFile(account: account, fileReference: .standalone(media: peerIcon))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] transform in
|
||||
if let strongSelf = self {
|
||||
|
@ -13,6 +13,8 @@ import CounterContollerTitleView
|
||||
import ContextUI
|
||||
import PresentationDataUtils
|
||||
import HexColor
|
||||
import ShimmerEffect
|
||||
import PhotoResources
|
||||
|
||||
private class WeakGameScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
||||
private let f: (WKScriptMessage) -> ()
|
||||
@ -43,6 +45,68 @@ public func generateWebAppThemeParams(_ presentationTheme: PresentationTheme) ->
|
||||
]
|
||||
}
|
||||
|
||||
private final class LoadingProgressNode: ASDisplayNode {
|
||||
var color: UIColor {
|
||||
didSet {
|
||||
self.foregroundNode.backgroundColor = self.color
|
||||
}
|
||||
}
|
||||
|
||||
private let foregroundNode: ASDisplayNode
|
||||
|
||||
init(color: UIColor) {
|
||||
self.color = color
|
||||
|
||||
self.foregroundNode = ASDisplayNode()
|
||||
self.foregroundNode.backgroundColor = color
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.foregroundNode)
|
||||
}
|
||||
|
||||
private var _progress: CGFloat = 0.0
|
||||
func updateProgress(_ progress: CGFloat, animated: Bool = false) {
|
||||
if self._progress == progress && animated {
|
||||
return
|
||||
}
|
||||
|
||||
var animated = animated
|
||||
if (progress < self._progress && animated) {
|
||||
animated = false
|
||||
}
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
self._progress = progress
|
||||
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated && progress > 0.0 {
|
||||
transition = .animated(duration: 0.7, curve: .spring)
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
let alpaTransition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
alpaTransition = .animated(duration: 0.3, curve: .easeInOut)
|
||||
} else {
|
||||
alpaTransition = .immediate
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.foregroundNode, frame: CGRect(x: -2.0, y: 0.0, width: (size.width + 4.0) * progress, height: size.height))
|
||||
|
||||
let alpha: CGFloat = progress < 0.001 || progress > 0.999 ? 0.0 : 1.0
|
||||
alpaTransition.updateAlpha(node: self.foregroundNode, alpha: alpha)
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
self.foregroundNode.cornerRadius = self.frame.height / 2.0
|
||||
}
|
||||
}
|
||||
|
||||
public final class WebAppController: ViewController, AttachmentContainable {
|
||||
public var requestAttachmentMenuExpansion: () -> Void = { }
|
||||
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in }
|
||||
@ -52,13 +116,19 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
private class Node: ViewControllerTracingNode, WKNavigationDelegate, UIScrollViewDelegate {
|
||||
private weak var controller: WebAppController?
|
||||
|
||||
private var webView: WKWebView?
|
||||
fileprivate var webView: WKWebView?
|
||||
|
||||
private var placeholderIcon: UIImage?
|
||||
private var placeholderNode: ShimmerEffectNode?
|
||||
|
||||
private let loadingProgressNode: LoadingProgressNode
|
||||
|
||||
private let context: AccountContext
|
||||
var presentationData: PresentationData
|
||||
private let present: (ViewController, Any?) -> Void
|
||||
private var queryId: Int64?
|
||||
|
||||
private var iconDisposable: Disposable?
|
||||
private var keepAliveDisposable: Disposable?
|
||||
|
||||
init(context: AccountContext, controller: WebAppController, present: @escaping (ViewController, Any?) -> Void) {
|
||||
@ -67,6 +137,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.presentationData = controller.presentationData
|
||||
self.present = present
|
||||
|
||||
self.loadingProgressNode = LoadingProgressNode(color: presentationData.theme.rootController.tabBar.selectedIconColor)
|
||||
|
||||
super.init()
|
||||
|
||||
if self.presentationData.theme.list.plainBackgroundColor.rgb == 0x000000 {
|
||||
@ -127,22 +199,54 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
webView.allowsBackForwardNavigationGestures = false
|
||||
webView.scrollView.delegate = self
|
||||
webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: [], context: nil)
|
||||
self.webView = webView
|
||||
|
||||
if let url = controller.url, let queryId = controller.queryId, let keepAliveSignal = controller.keepAliveSignal {
|
||||
self.queryId = queryId
|
||||
let placeholderNode = ShimmerEffectNode()
|
||||
self.addSubnode(placeholderNode)
|
||||
self.placeholderNode = placeholderNode
|
||||
|
||||
if controller.buttonText == nil {
|
||||
self.addSubnode(self.loadingProgressNode)
|
||||
}
|
||||
|
||||
if let iconFile = controller.iconFile {
|
||||
let _ = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: iconFile)).start()
|
||||
self.iconDisposable = (svgIconImageFile(account: self.context.account, fileReference: .standalone(media: iconFile))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] transform in
|
||||
if let strongSelf = self {
|
||||
let imageSize = CGSize(width: 75.0, height: 75.0)
|
||||
let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())
|
||||
let drawingContext = transform(arguments)
|
||||
if let image = drawingContext?.generateImage()?.withRenderingMode(.alwaysTemplate) {
|
||||
strongSelf.placeholderIcon = image
|
||||
|
||||
strongSelf.updatePlaceholder()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if let url = controller.url {
|
||||
self.queryId = controller.queryId
|
||||
if let parsedUrl = URL(string: url) {
|
||||
self.webView?.load(URLRequest(url: parsedUrl))
|
||||
}
|
||||
|
||||
self.keepAliveDisposable = (keepAliveSignal
|
||||
|> deliverOnMainQueue).start(error: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controller?.dismiss()
|
||||
}
|
||||
})
|
||||
if let keepAliveSignal = controller.keepAliveSignal {
|
||||
self.keepAliveDisposable = (keepAliveSignal
|
||||
|> deliverOnMainQueue).start(error: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controller?.dismiss()
|
||||
}
|
||||
}, completed: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controller?.dismiss()
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let _ = (context.engine.messages.requestWebView(peerId: controller.peerId, botId: controller.botId, url: controller.url, themeParams: generateWebAppThemeParams(presentationData.theme), replyToMessageId: nil)
|
||||
let _ = (context.engine.messages.requestWebView(peerId: controller.peerId, botId: controller.botId, url: controller.url, themeParams: generateWebAppThemeParams(presentationData.theme), replyToMessageId: controller.replyToMessageId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -158,6 +262,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
if let strongSelf = self {
|
||||
strongSelf.controller?.dismiss()
|
||||
}
|
||||
}, completed: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controller?.dismiss()
|
||||
}
|
||||
})
|
||||
}
|
||||
case .requestConfirmation:
|
||||
@ -168,7 +276,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.iconDisposable?.dispose()
|
||||
self.keepAliveDisposable?.dispose()
|
||||
|
||||
self.webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
@ -191,6 +302,14 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
}
|
||||
|
||||
private func updatePlaceholder() {
|
||||
guard let image = self.placeholderIcon else {
|
||||
return
|
||||
}
|
||||
let theme = self.presentationData.theme
|
||||
self.placeholderNode?.update(backgroundColor: self.backgroundColor ?? .clear, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: [.image(image: image, rect: CGRect(origin: CGPoint(), size: image.size))], horizontal: true, size: image.size)
|
||||
}
|
||||
|
||||
private var loadCount = 0
|
||||
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
||||
self.loadCount += 1
|
||||
@ -201,7 +320,14 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
|
||||
Queue.mainQueue().after(0.1, {
|
||||
if self.loadCount == 0, let webView = self.webView {
|
||||
ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear).updateAlpha(layer: webView.layer, alpha: 1.0)
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear)
|
||||
transition.updateAlpha(layer: webView.layer, alpha: 1.0)
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
self.placeholderNode = nil
|
||||
transition.updateAlpha(node: placeholderNode, alpha: 0.0, completion: { [weak placeholderNode] _ in
|
||||
placeholderNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -215,6 +341,30 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
if let webView = self.webView {
|
||||
webView.frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - layout.intrinsicInsets.bottom)))
|
||||
}
|
||||
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
let iconSize = CGSize(width: 75.0, height: 75.0)
|
||||
|
||||
let height: CGFloat
|
||||
if case .compact = layout.metrics.widthClass {
|
||||
height = layout.size.height - attachmentDefaultTopInset(layout: layout) - layout.intrinsicInsets.bottom - 14.0
|
||||
} else {
|
||||
height = layout.size.height - layout.intrinsicInsets.bottom
|
||||
}
|
||||
|
||||
let placeholderFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - iconSize.width) / 2.0), y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize)
|
||||
transition.updateFrame(node: placeholderNode, frame: placeholderFrame)
|
||||
placeholderNode.updateAbsoluteRect(placeholderFrame, within: layout.size)
|
||||
|
||||
let loadingProgressHeight: CGFloat = 2.0
|
||||
transition.updateFrame(node: self.loadingProgressNode, frame: CGRect(origin: CGPoint(x: 0.0, y: height - loadingProgressHeight), size: CGSize(width: layout.size.width, height: loadingProgressHeight)))
|
||||
}
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
if keyPath == "estimatedProgress", let webView = self.webView {
|
||||
self.loadingProgressNode.updateProgress(webView.estimatedProgress, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
@ -263,6 +413,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
resultString = convertedString
|
||||
}
|
||||
if let resultString = resultString {
|
||||
self.dismissed = true
|
||||
let _ = (self.context.engine.messages.sendWebViewData(botId: controller.botId, buttonText: buttonText, data: resultString)).start()
|
||||
}
|
||||
}
|
||||
@ -315,12 +466,16 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
private let queryId: Int64?
|
||||
private let buttonText: String?
|
||||
private let keepAliveSignal: Signal<Never, KeepWebViewError>?
|
||||
private let replyToMessageId: MessageId?
|
||||
private let iconFile: TelegramMediaFile?
|
||||
|
||||
private var presentationData: PresentationData
|
||||
fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, botId: PeerId, botName: String, url: String?, queryId: Int64?, buttonText: String?, keepAliveSignal: Signal<Never, KeepWebViewError>?) {
|
||||
public var getNavigationController: () -> NavigationController? = { return nil }
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, botId: PeerId, botName: String, url: String?, queryId: Int64?, buttonText: String?, keepAliveSignal: Signal<Never, KeepWebViewError>?, replyToMessageId: MessageId?, iconFile: TelegramMediaFile?) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.botId = botId
|
||||
@ -328,6 +483,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.queryId = queryId
|
||||
self.buttonText = buttonText
|
||||
self.keepAliveSignal = keepAliveSignal
|
||||
self.replyToMessageId = replyToMessageId
|
||||
self.iconFile = iconFile
|
||||
|
||||
self.updatedPresentationData = updatedPresentationData
|
||||
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
@ -408,26 +565,27 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
let botId = self.botId
|
||||
|
||||
let items = context.engine.messages.attachMenuBots()
|
||||
|> map { attachMenuBots -> ContextController.Items in
|
||||
|> map { [weak self] attachMenuBots -> ContextController.Items in
|
||||
var items: [ContextMenuItem] = []
|
||||
if peerId != botId {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_OpenBot, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Bots"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
}, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
|
||||
// if let strongSelf = self {
|
||||
// strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: strongSelf., context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId)))
|
||||
// }
|
||||
if let strongSelf = self, let navigationController = strongSelf.getNavigationController() {
|
||||
strongSelf.dismiss()
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id: strongSelf.botId)))
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebApp_ReloadPage, icon: { theme in
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_ReloadPage, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
}, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
|
||||
|
||||
self?.controllerNode.webView?.reload()
|
||||
})))
|
||||
|
||||
if let _ = attachMenuBots.firstIndex(where: { $0.peer.id == botId}) {
|
||||
|
@ -1,276 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
import PhotoResources
|
||||
|
||||
private final class WebAppPreviewContentNode: AlertContentNode {
|
||||
private let context: AccountContext
|
||||
private let presentationData: PresentationData
|
||||
private let result: ChatContextResult
|
||||
private let outgoingMessage: EnqueueMessage?
|
||||
private var previewItem: ListViewItem?
|
||||
private var previewNode: ListViewItemNode?
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private var iconDisposable: Disposable?
|
||||
|
||||
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
init(context: AccountContext, theme: AlertControllerTheme, presentationData: PresentationData, to peerId: PeerId, botId: PeerId, result: ChatContextResult, actions: [TextAlertAction]) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
self.result = result
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 0
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
self.outgoingMessage = self.context.engine.messages.outgoingMessageWithChatContextResult(to: peerId, botId: botId, result: result, replyToMessageId: nil, hideVia: true, silentPosting: false, scheduleTime: nil, correlationId: nil)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
|
||||
if let outgoingMessage = self.outgoingMessage, case let .message(text, attributes, mediaReference, _, _, _) = outgoingMessage {
|
||||
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1))
|
||||
var peers = SimpleDictionary<PeerId, Peer>()
|
||||
peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [])
|
||||
|
||||
var media: [Media] = []
|
||||
if let mediaReference = mediaReference {
|
||||
media.append(mediaReference.media)
|
||||
}
|
||||
|
||||
let previewMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: scheduleWhenOnlineTimestamp, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: text, attributes: attributes, media: media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [])
|
||||
|
||||
let previewItem = context.sharedContext.makeChatMessagePreviewItem(context: context, messages: [previewMessage], theme: presentationData.theme.withUpdated(preview: true), strings: presentationData.strings, wallpaper: .color(0xffffff), fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil, availableReactions: nil, isCentered: true)
|
||||
self.previewItem = previewItem
|
||||
}
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.WebApp_MessagePreview, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width , 290.0)
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
|
||||
let textSize = self.titleNode.measure(size)
|
||||
var textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)
|
||||
origin.y += textSize.height + 12.0
|
||||
|
||||
var iconSize = CGSize()
|
||||
var iconFrame = CGRect()
|
||||
|
||||
let sideInset: CGFloat = 0.0
|
||||
let params = ListViewItemLayoutParams(width: size.width, leftInset: sideInset, rightInset: sideInset, availableHeight: size.height)
|
||||
if let previewItem = self.previewItem {
|
||||
if let previewNode = self.previewNode {
|
||||
previewItem.updateNode(async: { $0() }, node: {
|
||||
return previewNode
|
||||
}, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in
|
||||
let nodeFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: layout.size.height))
|
||||
|
||||
previewNode.contentSize = layout.contentSize
|
||||
previewNode.insets = layout.insets
|
||||
previewNode.frame = nodeFrame
|
||||
previewNode.isUserInteractionEnabled = false
|
||||
|
||||
apply(ListViewItemApply(isOnScreen: true))
|
||||
})
|
||||
} else {
|
||||
var itemNode: ListViewItemNode?
|
||||
previewItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in
|
||||
itemNode = node
|
||||
apply().1(ListViewItemApply(isOnScreen: true))
|
||||
})
|
||||
itemNode!.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
|
||||
itemNode!.isUserInteractionEnabled = false
|
||||
self.addSubnode(itemNode!)
|
||||
self.previewNode = itemNode
|
||||
}
|
||||
iconSize = CGSize(width: 0.0, height: self.previewNode?.frame.height ?? 0.0)
|
||||
|
||||
self.previewNode?.frame = CGRect(x: 4.0, y: origin.y, width: size.width, height: iconSize.height)
|
||||
origin.y += iconSize.height
|
||||
}
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(textSize.width, minActionsWidth)
|
||||
contentWidth = max(contentWidth, 260.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
let resultSize = CGSize(width: resultWidth, height: iconSize.height + textSize.height + actionsHeight + 17.0 + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
iconFrame.origin.x = floorToScreenPixels((resultSize.width - iconFrame.width) / 2.0) + 19.0
|
||||
|
||||
|
||||
textFrame.origin.x = floorToScreenPixels((resultSize.width - textFrame.width) / 2.0)
|
||||
transition.updateFrame(node: self.titleNode, frame: textFrame)
|
||||
|
||||
return resultSize
|
||||
}
|
||||
}
|
||||
|
||||
public func webAppPreviewResultController(context: AccountContext, to peerId: PeerId, botId: PeerId, result: ChatContextResult, completion: @escaping () -> Void) -> AlertController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var contentNode: WebAppPreviewContentNode?
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.WebApp_Send, action: {
|
||||
dismissImpl?(true)
|
||||
|
||||
completion()
|
||||
})]
|
||||
|
||||
contentNode = WebAppPreviewContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), presentationData: presentationData, to: peerId, botId: botId, result: result, actions: actions)
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!)
|
||||
dismissImpl = { [weak controller] animated in
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user