diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 1f8fd53658..501e54bf15 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -568,6 +568,7 @@ public enum PeerInfoControllerMode { case forumTopic(thread: ChatReplyThreadMessage) case recommendedChannels case myProfile + case myProfileGifts } public enum ContactListActionItemInlineIconPosition { @@ -975,6 +976,8 @@ public protocol SharedAccountContext: AnyObject { func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController + + func makeStarsGiftController(context: AccountContext, birthdays: [EnginePeer.Id: TelegramBirthday]?, completion: @escaping (([EnginePeer.Id]) -> Void)) -> ViewController func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index f0a4d76653..ca84dca958 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -102,7 +102,7 @@ final class CameraDeviceContext { return 30.0 } switch DeviceModel.current { - case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax, .iPhone16ProMax: + case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax: return 60.0 default: return 30.0 diff --git a/submodules/Camera/Sources/CameraMetrics.swift b/submodules/Camera/Sources/CameraMetrics.swift index 01d68996e1..4d5c684da8 100644 --- a/submodules/Camera/Sources/CameraMetrics.swift +++ b/submodules/Camera/Sources/CameraMetrics.swift @@ -34,10 +34,6 @@ public extension Camera { self = .iPhone15Pro case .iPhone15ProMax: self = .iPhone15ProMax - case .iPhone16Pro: - self = .iPhone15Pro - case .iPhone16ProMax: - self = .iPhone15ProMax case .unknown: self = .unknown default: diff --git a/submodules/Display/Source/DeviceMetrics.swift b/submodules/Display/Source/DeviceMetrics.swift index 302af10890..b01c2a487d 100644 --- a/submodules/Display/Source/DeviceMetrics.swift +++ b/submodules/Display/Source/DeviceMetrics.swift @@ -36,8 +36,6 @@ public enum DeviceMetrics: CaseIterable, Equatable { case iPhone14ProZoomed case iPhone14ProMax case iPhone14ProMaxZoomed - case iPhone16Pro - case iPhone16ProMax case iPad case iPadMini case iPad102Inch @@ -70,8 +68,6 @@ public enum DeviceMetrics: CaseIterable, Equatable { .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, - .iPhone16Pro, - .iPhone16ProMax, .iPad, .iPadMini, .iPad102Inch, @@ -175,10 +171,6 @@ public enum DeviceMetrics: CaseIterable, Equatable { return CGSize(width: 430.0, height: 932.0) case .iPhone14ProMaxZoomed: return CGSize(width: 375.0, height: 812.0) - case .iPhone16Pro: - return CGSize(width: 402.0, height: 874.0) - case .iPhone16ProMax: - return CGSize(width: 440.0, height: 956.0) case .iPad: return CGSize(width: 768.0, height: 1024.0) case .iPadMini: @@ -212,8 +204,6 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 53.0 + UIScreenPixel case .iPhone14Pro, .iPhone14ProMax: return 55.0 - case .iPhone16Pro, .iPhone16ProMax: - return 55.0 case let .unknown(_, _, _, screenCornerRadius): return screenCornerRadius default: @@ -223,7 +213,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { func safeInsets(inLandscape: Bool) -> UIEdgeInsets { switch self { - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: return inLandscape ? UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0) default: return UIEdgeInsets.zero @@ -232,7 +222,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { public func onScreenNavigationHeight(inLandscape: Bool, systemOnScreenNavigationHeight: CGFloat?) -> CGFloat? { switch self { - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax, .iPhone16Pro, .iPhone16ProMax: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax: return inLandscape ? 21.0 : 34.0 case .iPhone14ProZoomed: return inLandscape ? 21.0 : 28.0 @@ -272,8 +262,6 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 54.0 case .iPhone14ProMaxZoomed: return 47.0 - case .iPhone16Pro, .iPhone16ProMax: - return 54.0 case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return 44.0 case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: @@ -292,7 +280,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 162.0 case .iPhone6, .iPhone6Plus: return 163.0 - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: return 172.0 case .iPad, .iPad102Inch, .iPadPro10Inch: return 348.0 @@ -311,9 +299,9 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 216.0 case .iPhone6Plus: return 226.0 - case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed: return 292.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax: + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax: return 302.0 case .iPad, .iPad102Inch, .iPadPro10Inch: return 263.0 @@ -332,7 +320,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { func predictiveInputHeight(inLandscape: Bool) -> CGFloat { if inLandscape { switch self { - case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: + case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: return 37.0 case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: return 50.0 @@ -343,7 +331,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { switch self { case .iPhone4, .iPhone5: return 37.0 - case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: + case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: return 44.0 case .iPhone6Plus: return 45.0 @@ -370,7 +358,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { public var hasDynamicIsland: Bool { switch self { - case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: + case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: return true default: return false diff --git a/submodules/MtProtoKit/Sources/MTApiEnvironment.m b/submodules/MtProtoKit/Sources/MTApiEnvironment.m index 1b9491b939..399acc5097 100644 --- a/submodules/MtProtoKit/Sources/MTApiEnvironment.m +++ b/submodules/MtProtoKit/Sources/MTApiEnvironment.m @@ -534,6 +534,14 @@ NSString *suffix = @""; return @"iPhone 15 Pro"; if ([platform isEqualToString:@"iPhone16,2"]) return @"iPhone 15 Pro Max"; + if ([platform isEqualToString:@"iPhone17,3"]) + return @"iPhone 16"; + if ([platform isEqualToString:@"iPhone17,4"]) + return @"iPhone 16 Plus"; + if ([platform isEqualToString:@"iPhone17,1"]) + return @"iPhone 16 Pro"; + if ([platform isEqualToString:@"iPhone17,2"]) + return @"iPhone 16 Pro Max"; if ([platform hasPrefix:@"iPod1"]) return @"iPod touch 1G"; diff --git a/submodules/PasscodeUI/Sources/PasscodeLayout.swift b/submodules/PasscodeUI/Sources/PasscodeLayout.swift index c5032937c6..5819862754 100644 --- a/submodules/PasscodeUI/Sources/PasscodeLayout.swift +++ b/submodules/PasscodeUI/Sources/PasscodeLayout.swift @@ -67,7 +67,7 @@ struct PasscodeKeyboardLayout { self.topOffset = 226.0 self.biometricsOffset = 30.0 self.deleteOffset = 20.0 - case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed: self.buttonSize = 75.0 self.horizontalSecond = 103.0 self.horizontalThird = 206.0 @@ -78,7 +78,7 @@ struct PasscodeKeyboardLayout { self.topOffset = 294.0 self.biometricsOffset = 30.0 self.deleteOffset = 20.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax: + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax: self.buttonSize = 85.0 self.horizontalSecond = 115.0 self.horizontalThird = 230.0 @@ -151,11 +151,11 @@ public struct PasscodeLayout { self.titleOffset = 112.0 self.subtitleOffset = -6.0 self.inputFieldOffset = 156.0 - case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed: self.titleOffset = 162.0 self.subtitleOffset = 0.0 self.inputFieldOffset = 206.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax: + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax: self.titleOffset = 180.0 self.subtitleOffset = 0.0 self.inputFieldOffset = 226.0 diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 4bcace59a3..d7f1cfd516 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -2110,7 +2110,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { var topParticipants: [GroupCallParticipantsContext.Participant] = [] var reportSpeakingParticipants: [PeerId: UInt32] = [:] - let timestamp = CACurrentMediaTime() + let timestamp = CFAbsoluteTimeGetCurrent() for (peerId, ssrc) in speakingParticipants { let shouldReport: Bool if let previousTimestamp = strongSelf.speakingParticipantsReportTimestamp[peerId] { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index ff0435974f..d013e312fb 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -109,6 +109,7 @@ final class VideoChatScreenComponent: Component { var applicationStateDisposable: Disposable? var expandedParticipantsVideoState: VideoChatParticipantsComponent.ExpandedVideoState? + var focusedSpeakerAutoSwitchDeadline: Double = 0.0 var isTwoColumnSidebarHidden: Bool = false let inviteDisposable = MetaDisposable() @@ -481,15 +482,6 @@ final class VideoChatScreenComponent: Component { guard let component = self.component, let environment = self.environment else { return } - guard let callState = self.callState else { - return - } - if case .connecting = callState.networkState { - return - } - if let muteState = callState.muteState, !muteState.canUnmute { - return - } HapticFeedback().impact(.light) if component.call.hasVideo { @@ -761,7 +753,7 @@ final class VideoChatScreenComponent: Component { if self.members != members { var members = members - #if DEBUG && true + #if DEBUG && false if let membersValue = members { var participants = membersValue.participants for i in 1 ... 20 { @@ -840,12 +832,13 @@ final class VideoChatScreenComponent: Component { if videoCount == 1, let participantsView = self.participants.view as? VideoChatParticipantsComponent.View, let participantsComponent = participantsView.component { if participantsComponent.layout.videoColumn != nil { self.expandedParticipantsVideoState = nil + self.focusedSpeakerAutoSwitchDeadline = 0.0 } } } if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, let members { - if !expandedParticipantsVideoState.isMainParticipantPinned, let participant = members.participants.first(where: { participant in + if CFAbsoluteTimeGetCurrent() > self.focusedSpeakerAutoSwitchDeadline, !expandedParticipantsVideoState.isMainParticipantPinned, let participant = members.participants.first(where: { participant in if let callState = self.callState, participant.peer.id == callState.myPeerId { return false } @@ -862,6 +855,7 @@ final class VideoChatScreenComponent: Component { } else { self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) } + self.focusedSpeakerAutoSwitchDeadline = CFAbsoluteTimeGetCurrent() + 1.0 } } @@ -894,11 +888,14 @@ final class VideoChatScreenComponent: Component { } else { self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) } + self.focusedSpeakerAutoSwitchDeadline = CFAbsoluteTimeGetCurrent() + 1.0 } else { self.expandedParticipantsVideoState = nil + self.focusedSpeakerAutoSwitchDeadline = 0.0 } } else { self.expandedParticipantsVideoState = nil + self.focusedSpeakerAutoSwitchDeadline = 0.0 } if !self.isUpdating { @@ -1468,6 +1465,7 @@ final class VideoChatScreenComponent: Component { } self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: key, isMainParticipantPinned: false, isUIHidden: isUIHidden) + self.focusedSpeakerAutoSwitchDeadline = CFAbsoluteTimeGetCurrent() + 3.0 self.state?.updated(transition: .spring(duration: 0.4)) } else if self.expandedParticipantsVideoState != nil { self.expandedParticipantsVideoState = nil diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index 9a0baa37cf..fd9398aa56 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -524,7 +524,7 @@ private func sendUploadedMessageContent( |> switchToLatest } -public func standaloneSendMessage(account: Account, peerId: PeerId, text: String, attributes: [MessageAttribute], media: StandaloneMedia?, replyToMessageId: MessageId?) -> Signal { +public func standaloneSendMessage(account: Account, peerId: PeerId, text: String, attributes: [MessageAttribute], media: StandaloneMedia?, replyToMessageId: MessageId?, threadId: Int32? = nil) -> Signal { let content: Signal if let media = media { switch media { @@ -561,14 +561,14 @@ public func standaloneSendMessage(account: Account, peerId: PeerId, text: String case let .progress(progress): return .single(progress) case let .result(result): - let sendContent = sendMessageContent(account: account, peerId: peerId, attributes: attributes, content: result) |> map({ _ -> Float in return 1.0 }) + let sendContent = sendMessageContent(account: account, peerId: peerId, attributes: attributes, content: result, threadId: threadId) |> map({ _ -> Float in return 1.0 }) return .single(1.0) |> then(sendContent |> mapError { _ -> StandaloneSendMessageError in }) } } } -private func sendMessageContent(account: Account, peerId: PeerId, attributes: [MessageAttribute], content: StandaloneMessageContent) -> Signal { +private func sendMessageContent(account: Account, peerId: PeerId, attributes: [MessageAttribute], content: StandaloneMessageContent, threadId: Int32?) -> Signal { return account.postbox.transaction { transaction -> Signal in if peerId.namespace == Namespaces.Peer.SecretChat { return .complete() @@ -631,9 +631,12 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M flags |= 1 << 0 replyTo = .inputReplyToStory(peer: inputPeer, storyId: replyToStoryId.id) } + } else if let threadId { + flags |= 1 << 0 + replyTo = .inputReplyToMessage(flags: flags, replyToMsgId: threadId, topMsgId: threadId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil) } - sendMessageRequest = account.network.request(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil)) + sendMessageRequest = account.network.request(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil)) |> `catch` { _ -> Signal in return .complete() } @@ -649,6 +652,9 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M flags |= 1 << 0 replyTo = .inputReplyToStory(peer: inputPeer, storyId: replyToStoryId.id) } + } else if let threadId { + flags |= 1 << 0 + replyTo = .inputReplyToMessage(flags: flags, replyToMsgId: threadId, topMsgId: threadId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil) } sendMessageRequest = account.network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil)) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index fdac6337f0..d36eeccb24 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -730,6 +730,34 @@ public extension TelegramEngine.EngineData.Item { } } } + + public struct StarGiftsCount: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Int32? + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedUserData { + return cachedData.starGiftsCount + } else { + return nil + } + } + } public struct LinkedDiscussionPeerId: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = EnginePeerCachedInfoItem diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index b297f7ba13..da2930b8c2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -191,7 +191,7 @@ func _internal_keepCachedStarGiftsUpdated(postbox: Postbox, network: Network) -> func managedStarGiftsUpdates(postbox: Postbox, network: Network) -> Signal { let poll = _internal_keepCachedStarGiftsUpdated(postbox: postbox, network: network) - return (poll |> then(.complete() |> suspendAwareDelay(2.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart + return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart } func _internal_convertStarGift(account: Account, messageId: EngineMessage.Id) -> Signal { diff --git a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift index b3574cd921..34da6c35b2 100644 --- a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift +++ b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift @@ -228,7 +228,8 @@ private final class SheetPageContent: CombinedComponent { component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.ReportAd_Help_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) } )), - items: items + items: items, + isModal: true ), environment: {}, availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/BUILD index ba1ade2779..512d7f7c6d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/BUILD @@ -30,6 +30,8 @@ swift_library( "//submodules/Markdown", "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/InvisibleInkDustNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 6f86b3e5d8..b168d1d48b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -20,6 +20,8 @@ import ShimmerEffect import Markdown import ChatMessageBubbleContentNode import ChatMessageItemCommon +import TextNodeWithEntities +import InvisibleInkDustNode private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? { return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false) @@ -34,7 +36,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private let mediaBackgroundMaskNode: ASImageNode private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? private let titleNode: TextNode - private let subtitleNode: TextNode + private let subtitleNode: TextNodeWithEntities + private var dustNode: InvisibleInkDustNode? private let placeholderNode: StickerShimmerEffectNode private let animationNode: AnimatedStickerNode @@ -60,6 +63,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if wasVisible != isVisible { self.visibilityStatus = isVisible + + switch self.visibility { + case .none: + self.subtitleNode.visibilityRect = nil + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + self.subtitleNode.visibilityRect = subRect + } } } } @@ -88,9 +101,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false - self.subtitleNode = TextNode() - self.subtitleNode.isUserInteractionEnabled = false - self.subtitleNode.displaysAsynchronously = false + self.subtitleNode = TextNodeWithEntities() + self.subtitleNode.textNode.isUserInteractionEnabled = false + self.subtitleNode.textNode.displaysAsynchronously = false self.buttonNode = HighlightTrackingButtonNode() self.buttonNode.clipsToBounds = true @@ -120,8 +133,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.labelNode) self.addSubnode(self.titleNode) - self.addSubnode(self.subtitleNode) - self.addSubnode(self.subtitleNode) + self.addSubnode(self.subtitleNode.textNode) self.addSubnode(self.placeholderNode) self.addSubnode(self.animationNode) @@ -236,7 +248,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) + let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode) let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode) @@ -259,6 +271,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { var animationFile: TelegramMediaFile? var title = item.presentationData.strings.Notification_PremiumGift_Title var text = "" + var entities: [MessageTextEntity] = [] var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View var ribbonTitle = "" var hasServiceMessage = true @@ -329,14 +342,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View hasServiceMessage = false } - case let .starGift(gift, convertStars, giftText, entities, nameHidden, savedToProfile, converted): - let _ = nameHidden + case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted): //TODO:localize + if !incoming { + buttonTitle = "" + } let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" title = "Gift from \(authorName)" if let giftText, !giftText.isEmpty { text = giftText - let _ = entities + entities = giftEntities ?? [] } else { if incoming { if converted { @@ -383,22 +398,30 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( - body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor), - bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor), - link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor), - linkAttribute: { url in - return ("URL", url) - } - ), textAlignment: .center) - + let attributedText: NSAttributedString + if let _ = animationFile { + attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: Font.regular(13.0), linkFont: Font.regular(13.0), boldFont: Font.semibold(13.0), italicFont: Font.italic(13.0), boldItalicFont: Font.semiboldItalic(13.0), fixedFont: Font.monospace(13.0), blockQuoteFont: Font.regular(13.0), message: nil) + } else { + attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center) + } + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: buttonTitle, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (ribbonTextLayout, ribbonTextApply) = makeRibbonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: ribbonTitle, font: Font.semibold(11.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - - giftSize.height = titleLayout.size.height + textSpacing + subtitleLayout.size.height + 212.0 + + giftSize.height = titleLayout.size.height + textSpacing + subtitleLayout.size.height + 164.0 + if !buttonTitle.isEmpty { + giftSize.height += 48.0 + } var labelRects = labelLayout.linesRects() if labelRects.count > 1 { @@ -458,6 +481,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let animationFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY - 16.0 + iconOffset), size: iconSize) strongSelf.animationNode.frame = animationFrame + strongSelf.buttonNode.isHidden = buttonTitle.isEmpty + strongSelf.buttonTitleNode.isHidden = buttonTitle.isEmpty + if strongSelf.item == nil { strongSelf.animationNode.started = { [weak self] in if let strongSelf = self { @@ -502,7 +528,13 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let _ = labelApply() let _ = titleApply() - let _ = subtitleApply() + let _ = subtitleApply(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.controllerInteraction.presentationContext.animationCache, + renderer: item.controllerInteraction.presentationContext.animationRenderer, + placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground, + attemptSynchronous: synchronousLoads + )) let _ = buttonTitleApply() let _ = ribbonTextApply() @@ -513,7 +545,26 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.titleNode.frame = titleFrame let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: subtitleLayout.size) - strongSelf.subtitleNode.frame = subtitleFrame + strongSelf.subtitleNode.textNode.frame = subtitleFrame + + if !subtitleLayout.spoilers.isEmpty { + let dustColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText + + let dustNode: InvisibleInkDustNode + if let current = strongSelf.dustNode { + dustNode = current + } else { + dustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency) + dustNode.isUserInteractionEnabled = false + strongSelf.dustNode = dustNode + strongSelf.insertSubnode(dustNode, aboveSubnode: strongSelf.subtitleNode.textNode) + } + dustNode.frame = subtitleFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 1.0) + dustNode.update(size: dustNode.frame.size, color: dustColor, textColor: dustColor, rects: subtitleLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: subtitleLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + } else if let dustNode = strongSelf.dustNode { + dustNode.removeFromSupernode() + strongSelf.dustNode = nil + } let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size) strongSelf.buttonTitleNode.frame = buttonTitleFrame @@ -607,6 +658,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if let (rect, size) = strongSelf.absoluteRect { strongSelf.updateAbsoluteRect(rect, within: size) } + + switch strongSelf.visibility { + case .none: + strongSelf.subtitleNode.visibilityRect = nil + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + strongSelf.subtitleNode.visibilityRect = subRect + } } }) }) @@ -733,6 +794,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.updateVisibility() } + private var internalPlayedOnce = false private func updateVisibility() { guard let item = self.item else { return @@ -763,9 +825,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } - if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) { + if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) && !self.internalPlayedOnce { item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id) self.animationNode.playOnce() + self.internalPlayedOnce = true Queue.mainQueue().after(0.05) { if let itemNode = self.itemNode, let supernode = itemNode.supernode { diff --git a/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift b/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift index a248532726..383aecb2c4 100644 --- a/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift +++ b/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift @@ -162,7 +162,7 @@ private final class SheetPageContent: CombinedComponent { transition: .immediate ) context.add(back - .position(CGPoint(x: sideInset + back.size.width / 2.0 - (component.title != nil ? 8.0 : 0.0), y: contentSize.height + back.size.height / 2.0)) + .position(CGPoint(x: sideInset + back.size.width / 2.0 - (!component.isFirst ? 8.0 : 0.0), y: contentSize.height + back.size.height / 2.0)) ) let constrainedTitleWidth = context.availableSize.width - (back.size.width + 16.0) * 2.0 @@ -280,7 +280,8 @@ private final class SheetPageContent: CombinedComponent { maximumNumberOfLines: 0 )), footer: footer, - items: items + items: items, + isModal: true ), environment: {}, availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), @@ -696,12 +697,10 @@ public final class ContentReportScreen: ViewControllerComponentContainer { switch result { case .reported: - Queue.mainQueue().after(0.1) { - completed() - } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } Queue.mainQueue().after(0.4, { + completed() + (navigationController?.viewControllers.last as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .emoji(name: "PoliceCar", text: presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return true }), in: .current) }) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index eb54cd350d..31acafe4dd 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -1011,14 +1011,15 @@ final class GiftOptionsScreenComponent: Component { } } -public final class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScreenProtocol { +open class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScreenProtocol { private let context: AccountContext public init( context: AccountContext, starsContext: StarsContext, peerId: EnginePeer.Id, - premiumOptions: [CachedPremiumGiftOption] + premiumOptions: [CachedPremiumGiftOption], + completion: @escaping () -> Void = {} ) { self.context = context diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD index 1c0a9254dc..a64e80c41d 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD @@ -34,10 +34,13 @@ swift_library( "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/AppBundle", "//submodules/WallpaperBackgroundNode", + "//submodules/TextFormat", "//submodules/ChatPresentationInterfaceState", "//submodules/TelegramUI/Components/TextFieldComponent", "//submodules/TelegramUI/Components/ListItemComponentAdaptor", "//submodules/BotPaymentsUI", + "//submodules/TelegramUI/Components/EmojiSuggestionsComponent", + "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift index 35f48321e7..cd02508e46 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -28,6 +28,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd let accountPeer: EnginePeer? let gift: StarGift let text: String + let entities: [MessageTextEntity] init( context: AccountContext, @@ -42,7 +43,8 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd nameDisplayOrder: PresentationPersonNameOrder, accountPeer: EnginePeer?, gift: StarGift, - text: String + text: String, + entities: [MessageTextEntity] ) { self.context = context self.theme = theme @@ -57,6 +59,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd self.accountPeer = accountPeer self.gift = gift self.text = text + self.entities = entities } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -130,6 +133,9 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd if lhs.text != rhs.text { return false } + if lhs.entities != rhs.entities { + return false + } return true } } @@ -201,7 +207,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { peers[authorPeerId] = item.accountPeer?._asPeer() let media: [Media] = [ - TelegramMediaAction(action: .starGift(gift: item.gift, convertStars: item.gift.convertStars, text: item.text, entities: [], nameHidden: false, savedToProfile: false, converted: false)) + TelegramMediaAction(action: .starGift(gift: item.gift, convertStars: item.gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false)) ] let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: "", attributes: [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)) @@ -221,21 +227,21 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { itemNode.insets = layout.insets itemNode.frame = nodeFrame itemNode.isUserInteractionEnabled = false + itemNode.visibility = .visible(1.0, .infinite) - Queue.mainQueue().after(0.01) { - apply(ListViewItemApply(isOnScreen: true)) - } + apply(ListViewItemApply(isOnScreen: true)) }) } } else { var messageNodes: [ListViewItemNode] = [] for i in 0 ..< items.count { var itemNode: ListViewItemNode? - items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in + items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: true, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in itemNode = node apply().1(ListViewItemApply(isOnScreen: true)) }) itemNode!.isUserInteractionEnabled = false + itemNode?.visibility = .visible(1.0, .infinite) messageNodes.append(itemNode!) self.initialBubbleHeight = itemNode?.frame.height diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 805b8aaf6c..d358dd40ea 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -22,6 +22,11 @@ import LottieComponent import TextFieldComponent import ButtonComponent import BotPaymentsUI +import ChatEntityKeyboardInputNode +import EmojiSuggestionsComponent +import ChatPresentationInterfaceState +import AudioToolbox +import TextFormat final class GiftSetupScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -81,9 +86,23 @@ final class GiftSetupScreenComponent: Component { private let textInputTag = NSObject() private var resetText: String? + private var currentInputMode: ListMultilineTextFieldItemComponent.InputMode = .keyboard + + private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? + private var inputMediaNodeDataDisposable: Disposable? + private var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext() + private var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction? + private var inputMediaNode: ChatEntityKeyboardInputNode? + private var inputMediaNodeBackground = SimpleLayer() + private var inputMediaNodeTargetTag: AnyObject? + private let inputMediaNodeDataPromise = Promise() + + private var currentEmojiSuggestionView: ComponentHostView? + private var hideName = false private var previousHadInputHeight: Bool = false + private var previousInputHeight: CGFloat? private var recenterOnTag: NSObject? private var peerMap: [EnginePeer.Id: EnginePeer] = [:] @@ -175,7 +194,8 @@ final class GiftSetupScreenComponent: Component { guard let self else { return } - let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: component.gift.id, text: self.textInputState.text.string, entities: []) + let entities = generateChatInputTextEntities(self.textInputState.text) + let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: component.gift.id, text: self.textInputState.text.string, entities: entities) let inputData = BotCheckoutController.InputData.fetch(context: component.context, source: source) |> map(Optional.init) |> `catch` { _ -> Signal in @@ -264,6 +284,108 @@ final class GiftSetupScreenComponent: Component { self.state?.updated() }) + + self.inputMediaNodeDataPromise.set( + ChatEntityKeyboardInputNode.inputData( + context: component.context, + chatPeerId: nil, + areCustomEmojiEnabled: true, + hasTrending: false, + hasSearch: true, + hasStickers: false, + hasGifs: false, + hideBackground: true, + sendGif: nil + ) + ) + self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + self.inputMediaNodeData = value + }) + + self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction( + sendSticker: { _, _, _, _, _, _, _, _, _ in + return false + }, + sendEmoji: { _, _, _ in + let _ = self + }, + sendGif: { _, _, _, _, _ in + return false + }, + sendBotContextResultAsGif: { _, _ , _, _, _, _ in + return false + }, + updateChoosingSticker: { _ in + }, + switchToTextInput: { [weak self] in + guard let self else { + return + } + self.currentInputMode = .keyboard + self.state?.updated(transition: .spring(duration: 0.4)) + }, + dismissTextInput: { + }, + insertText: { [weak self] text in + guard let self else { + return + } + if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { + if self.textInputState.isEditing { + textInputView.insertText(text: text) + } + } + }, + backwardsDeleteText: { [weak self] in + guard let self else { + return + } + if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { + if self.textInputState.isEditing { + textInputView.backwardsDeleteText() + } + } + }, + openStickerEditor: { + }, + presentController: { [weak self] c, a in + guard let self else { + return + } + self.environment?.controller()?.present(c, in: .window(.root), with: a) + }, + presentGlobalOverlayController: { [weak self] c, a in + guard let self else { + return + } + self.environment?.controller()?.presentInGlobalOverlay(c, with: a) + }, + getNavigationController: { [weak self] () -> NavigationController? in + guard let self else { + return nil + } + guard let controller = self.environment?.controller() as? GiftSetupScreen else { + return nil + } + + if let navigationController = controller.navigationController as? NavigationController { + return navigationController + } + return nil + }, + requestLayout: { [weak self] transition in + guard let self else { + return + } + if !self.isUpdating { + self.state?.updated(transition: ComponentTransition(transition)) + } + } + ) } let environment = environment[EnvironmentType.self].value @@ -316,16 +438,7 @@ final class GiftSetupScreenComponent: Component { contentHeight += environment.navigationHeight contentHeight += 26.0 - - self.recenterOnTag = nil - if let hint = transition.userData(TextFieldComponent.AnimationHint.self), let targetView = hint.view { - if let textView = self.introSection.findTaggedView(tag: self.textInputTag) { - if targetView.isDescendant(of: textView) { - self.recenterOnTag = self.textInputTag - } - } - } - + var introSectionItems: [AnyComponentWithIdentity] = [] introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(Rectangle(color: .clear, height: 346.0, tag: self.introPlaceholderTag)))) introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListMultilineTextFieldItemComponent( @@ -337,13 +450,14 @@ final class GiftSetupScreenComponent: Component { resetText: self.resetText.flatMap { return ListMultilineTextFieldItemComponent.ResetText(value: $0) }, - placeholder: environment.strings.Business_Intro_IntroTextPlaceholder, + placeholder: "Enter Message", autocapitalizationType: .none, autocorrectionType: .no, returnKeyType: .done, - characterLimit: 70, + characterLimit: 255, displayCharacterLimit: true, emptyLineHandling: .notAllowed, + formatMenuAvailability: .available([.bold, .italic, .underline, .strikethrough, .spoiler]), updated: { _ in }, returnKeyAction: { [weak self] in @@ -355,10 +469,173 @@ final class GiftSetupScreenComponent: Component { } }, textUpdateTransition: .spring(duration: 0.4), + inputMode: self.currentInputMode, + toggleInputMode: { [weak self] in + guard let self else { + return + } + switch self.currentInputMode { + case .keyboard: + self.currentInputMode = .emoji + case .emoji: + self.currentInputMode = .keyboard + } + self.state?.updated(transition: .spring(duration: 0.4)) + }, tag: self.textInputTag )))) self.resetText = nil + + var inputHeight: CGFloat = 0.0 + inputHeight += self.updateInputMediaNode( + component: component, + availableSize: availableSize, + bottomInset: environment.safeInsets.bottom, + inputHeight: 0.0, + effectiveInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false), + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + transition: transition + ) + if self.inputMediaNode == nil { + if environment.inputHeight.isZero && self.textInputState.isEditing, let previousInputHeight = self.previousInputHeight { + inputHeight = previousInputHeight + } else { + inputHeight = environment.inputHeight + } + } + + if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil { + emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value) + |> deliverOnMainQueue).start(next: { [weak self, weak emojiSuggestion] result in + guard let self, self.textInputState.currentEmojiSuggestion === emojiSuggestion else { + return + } + + emojiSuggestion?.value = result + self.state?.updated() + }) + } + + var hasTrackingView = self.textInputState.hasTrackingView + if let currentEmojiSuggestion = self.textInputState.currentEmojiSuggestion, let value = currentEmojiSuggestion.value as? [TelegramMediaFile], value.isEmpty { + hasTrackingView = false + } + if !self.textInputState.isEditing { + hasTrackingView = false + } + + if !hasTrackingView { + if let currentEmojiSuggestion = self.textInputState.currentEmojiSuggestion { + self.textInputState.currentEmojiSuggestion = nil + currentEmojiSuggestion.disposable?.dispose() + } + + if let currentEmojiSuggestionView = self.currentEmojiSuggestionView { + self.currentEmojiSuggestionView = nil + + currentEmojiSuggestionView.alpha = 0.0 + currentEmojiSuggestionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak currentEmojiSuggestionView] _ in + currentEmojiSuggestionView?.removeFromSuperview() + }) + } + } + + if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, let value = emojiSuggestion.value as? [TelegramMediaFile] { + let currentEmojiSuggestionView: ComponentHostView + if let current = self.currentEmojiSuggestionView { + currentEmojiSuggestionView = current + } else { + currentEmojiSuggestionView = ComponentHostView() + self.currentEmojiSuggestionView = currentEmojiSuggestionView + self.addSubview(currentEmojiSuggestionView) + + currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + let globalPosition: CGPoint + if let textView = (self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View)?.textFieldView { + globalPosition = textView.convert(emojiSuggestion.localPosition, to: self) + } else { + globalPosition = .zero + } + + let sideInset: CGFloat = 7.0 + + let viewSize = currentEmojiSuggestionView.update( + transition: .immediate, + component: AnyComponent(EmojiSuggestionsComponent( + context: component.context, + userLocation: .other, + theme: EmojiSuggestionsComponent.Theme(theme: environment.theme), + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + files: value, + action: { [weak self] file in + guard let self, let textView = (self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View)?.textFieldView, let currentEmojiSuggestion = self.textInputState.currentEmojiSuggestion else { + return + } + + AudioServicesPlaySystemSound(0x450) + + let inputState = textView.getInputState() + let inputText = NSMutableAttributedString(attributedString: inputState.inputText) + + var text: String? + var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? + loop: for attribute in file.attributes { + switch attribute { + case let .CustomEmoji(_, _, displayText, _): + text = displayText + emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) + break loop + default: + break + } + } + + if let emojiAttribute = emojiAttribute, let text = text { + let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]) + + let range = currentEmojiSuggestion.position.range + let previousText = inputText.attributedSubstring(from: range) + inputText.replaceCharacters(in: range, with: replacementText) + + var replacedUpperBound = range.lowerBound + while true { + if inputText.attributedSubstring(from: NSRange(location: 0, length: replacedUpperBound)).string.hasSuffix(previousText.string) { + let replaceRange = NSRange(location: replacedUpperBound - previousText.length, length: previousText.length) + if replaceRange.location < 0 { + break + } + let adjacentString = inputText.attributedSubstring(from: replaceRange) + if adjacentString.string != previousText.string || adjacentString.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) != nil { + break + } + inputText.replaceCharacters(in: replaceRange, with: NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: emojiAttribute.interactivelySelectedFromPackId, fileId: emojiAttribute.fileId, file: emojiAttribute.file)])) + replacedUpperBound = replaceRange.lowerBound + } else { + break + } + } + + let selectionPosition = range.lowerBound + (replacementText.string as NSString).length + textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition) + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + + let viewFrame = CGRect(origin: CGPoint(x: min(availableSize.width - sideInset - viewSize.width, max(sideInset, floor(globalPosition.x - viewSize.width / 2.0))), y: globalPosition.y - 4.0 - viewSize.height), size: viewSize) + currentEmojiSuggestionView.frame = viewFrame + if let componentView = currentEmojiSuggestionView.componentView as? EmojiSuggestionsComponent.View { + componentView.adjustBackground(relativePositionX: floor(globalPosition.x + 10.0)) + } + } + let introSectionSize = self.introSection.update( transition: transition, component: AnyComponent(ListSectionComponent( @@ -387,64 +664,44 @@ final class GiftSetupScreenComponent: Component { } contentHeight += introSectionSize.height contentHeight += sectionSpacing - -// let titleText: String -// if self.titleInputState.text.string.isEmpty { -// titleText = environment.strings.Conversation_EmptyPlaceholder -// } else { -// let rawTitle = self.titleInputState.text.string -// titleText = rawTitle.count <= maxTitleLength ? rawTitle : String(rawTitle[rawTitle.startIndex ..< rawTitle.index(rawTitle.startIndex, offsetBy: maxTitleLength)]) -// } - -// let textText: String -// if self.textInputState.text.string.isEmpty { -// textText = environment.strings.Conversation_GreetingText -// } else { -// let rawText = self.textInputState.text.string -// textText = rawText.count <= maxTextLength ? rawText : String(rawText[rawText.startIndex ..< rawText.index(rawText.startIndex, offsetBy: maxTextLength)]) -// } - + let listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) - let introContentSize = self.introContent.update( - transition: transition, - component: AnyComponent( - ListItemComponentAdaptor( - itemGenerator: ChatGiftPreviewItem( - context: component.context, - theme: environment.theme, - componentTheme: environment.theme, - strings: environment.strings, - sectionId: 0, - fontSize: presentationData.chatFontSize, - chatBubbleCorners: presentationData.chatBubbleCorners, - wallpaper: presentationData.chatWallpaper, - dateTimeFormat: environment.dateTimeFormat, - nameDisplayOrder: presentationData.nameDisplayOrder, - accountPeer: self.peerMap[component.context.account.peerId], - gift: component.gift, - text: self.textInputState.text.string - ), - params: listItemParams - ) - ), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) - ) - if let introContentView = self.introContent.view { - if introContentView.superview == nil { - if let placeholderView = self.introSection.findTaggedView(tag: self.introPlaceholderTag) { - placeholderView.addSubview(introContentView) + if let accountPeer = self.peerMap[component.context.account.peerId] { + let introContentSize = self.introContent.update( + transition: transition, + component: AnyComponent( + ListItemComponentAdaptor( + itemGenerator: ChatGiftPreviewItem( + context: component.context, + theme: environment.theme, + componentTheme: environment.theme, + strings: environment.strings, + sectionId: 0, + fontSize: presentationData.chatFontSize, + chatBubbleCorners: presentationData.chatBubbleCorners, + wallpaper: presentationData.chatWallpaper, + dateTimeFormat: environment.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + accountPeer: accountPeer, + gift: component.gift, + text: self.textInputState.text.string, + entities: generateChatInputTextEntities(self.textInputState.text) + ), + params: listItemParams + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + if let introContentView = self.introContent.view { + if introContentView.superview == nil { + if let placeholderView = self.introSection.findTaggedView(tag: self.introPlaceholderTag) { + placeholderView.addSubview(introContentView) + } } - } - transition.setFrame(view: introContentView, frame: CGRect(origin: CGPoint(), size: introContentSize)) - } - - if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0) { - if self.textInputState.isEditing { - self.recenterOnTag = self.textInputTag + transition.setFrame(view: introContentView, frame: CGRect(origin: CGPoint(), size: introContentSize)) } } - self.previousHadInputHeight = environment.inputHeight > 0.0 let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? "" let hideSectionSize = self.hideSection.update( @@ -498,11 +755,9 @@ final class GiftSetupScreenComponent: Component { contentHeight += bottomContentInset - let inputHeight: CGFloat = environment.inputHeight let combinedBottomInset = max(inputHeight, environment.safeInsets.bottom) contentHeight += combinedBottomInset - if self.starImage == nil || self.starImage?.1 !== environment.theme { self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme) } @@ -545,6 +800,22 @@ final class GiftSetupScreenComponent: Component { let previousBounds = self.scrollView.bounds + self.recenterOnTag = nil + if let hint = transition.userData(TextFieldComponent.AnimationHint.self), let targetView = hint.view { + if let textView = self.introSection.findTaggedView(tag: self.textInputTag) { + if targetView.isDescendant(of: textView) { + self.recenterOnTag = self.textInputTag + } + } + } + if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0) { + if self.textInputState.isEditing { + self.recenterOnTag = self.textInputTag + } + } + self.previousHadInputHeight = inputHeight > 0.0 + self.previousInputHeight = inputHeight + self.ignoreScrolling = true let contentSize = CGSize(width: availableSize.width, height: contentHeight) if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { @@ -592,6 +863,152 @@ final class GiftSetupScreenComponent: Component { return availableSize } + + private func updateInputMediaNode( + component: GiftSetupScreenComponent, + availableSize: CGSize, + bottomInset: CGFloat, + inputHeight: CGFloat, + effectiveInputHeight: CGFloat, + metrics: LayoutMetrics, + deviceMetrics: DeviceMetrics, + transition: ComponentTransition + ) -> CGFloat { + let bottomInset: CGFloat = bottomInset + 8.0 + let bottomContainerInset: CGFloat = 0.0 + let needsInputActivation: Bool = !"".isEmpty + + var height: CGFloat = 0.0 + if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData { + let inputMediaNode: ChatEntityKeyboardInputNode + var inputMediaNodeTransition = transition + var animateIn = false + if let current = self.inputMediaNode { + inputMediaNode = current + } else { + animateIn = true + inputMediaNodeTransition = inputMediaNodeTransition.withAnimation(.none) + inputMediaNode = ChatEntityKeyboardInputNode( + context: component.context, + currentInputData: inputData, + updatedInputData: self.inputMediaNodeDataPromise.get(), + defaultToEmojiTab: true, + opaqueTopPanelBackground: false, + useOpaqueTheme: true, + interaction: self.inputMediaInteraction, + chatPeerId: nil, + stateContext: self.inputMediaNodeStateContext + ) + inputMediaNode.clipsToBounds = true + + inputMediaNode.externalTopPanelContainerImpl = nil + inputMediaNode.useExternalSearchContainer = true + if inputMediaNode.view.superview == nil { + self.inputMediaNodeBackground.removeAllAnimations() + self.layer.addSublayer(self.inputMediaNodeBackground) + self.addSubview(inputMediaNode.view) + } + self.inputMediaNode = inputMediaNode + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let presentationInterfaceState = ChatPresentationInterfaceState( + chatWallpaper: .builtin(WallpaperSettings()), + theme: presentationData.theme, + strings: presentationData.strings, + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + limitsConfiguration: component.context.currentLimitsConfiguration.with { $0 }, + fontSize: presentationData.chatFontSize, + bubbleCorners: presentationData.chatBubbleCorners, + accountPeerId: component.context.account.peerId, + mode: .standard(.default), + chatLocation: .peer(id: component.context.account.peerId), + subject: nil, + peerNearbyData: nil, + greetingData: nil, + pendingUnpinnedAllMessages: false, + activeGroupCallInfo: nil, + hasActiveGroupCall: false, + importState: nil, + threadData: nil, + isGeneralThreadClosed: nil, + replyMessage: nil, + accountPeerColor: nil, + businessIntro: nil + ) + + self.inputMediaNodeBackground.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor + + let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight < 100.0 ? inputHeight - bottomContainerInset : inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false) + let inputNodeHeight = heightAndOverflow.0 + let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight)) + + let inputNodeBackgroundFrame = CGRect(origin: CGPoint(x: inputNodeFrame.minX, y: inputNodeFrame.minY - 6.0), size: CGSize(width: inputNodeFrame.width, height: inputNodeFrame.height + 6.0)) + + if needsInputActivation { + let inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight) + ComponentTransition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + ComponentTransition.immediate.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) + } + + if animateIn { + var targetFrame = inputNodeFrame + targetFrame.origin.y = availableSize.height + inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: targetFrame) + + let inputNodeBackgroundTargetFrame = CGRect(origin: CGPoint(x: targetFrame.minX, y: targetFrame.minY - 6.0), size: CGSize(width: targetFrame.width, height: targetFrame.height + 6.0)) + + inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundTargetFrame) + + transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) + } else { + inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) + } + + height = heightAndOverflow.0 + } else { + self.inputMediaNodeTargetTag = nil + + if let inputMediaNode = self.inputMediaNode { + self.inputMediaNode = nil + var targetFrame = inputMediaNode.frame + targetFrame.origin.y = availableSize.height + transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in + if let inputMediaNode { + Queue.mainQueue().after(0.3) { + inputMediaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in + inputMediaNode?.view.removeFromSuperview() + }) + } + } + }) + transition.setFrame(layer: self.inputMediaNodeBackground, frame: targetFrame, completion: { [weak self] _ in + Queue.mainQueue().after(0.3) { + guard let self else { + return + } + if self.currentInputMode == .keyboard { + self.inputMediaNodeBackground.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak self] finished in + guard let self else { + return + } + + if finished { + self.inputMediaNodeBackground.removeFromSuperlayer() + } + self.inputMediaNodeBackground.removeAllAnimations() + }) + } + } + }) + } + } + + return height + } } func makeView() -> View { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index d81772d75b..d7c14b4bc6 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -82,7 +82,10 @@ private final class GiftViewSheetContent: CombinedComponent { super.init() if let arguments = subject.arguments { - let peerIds: [EnginePeer.Id] = [arguments.peerId, context.account.peerId] + var peerIds: [EnginePeer.Id] = [arguments.peerId, context.account.peerId] + if let fromPeerId = arguments.fromPeerId { + peerIds.append(fromPeerId) + } self.disposable = (context.engine.data.get( EngineDataMap( peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in @@ -204,7 +207,11 @@ private final class GiftViewSheetContent: CombinedComponent { descriptionText = "You converted this gift to \(convertStars) Stars. [More About Stars >]()" } } else if let peerId = component.subject.arguments?.peerId, let peer = state.peerMap[peerId] { - descriptionText = "\(peer.compactDisplayTitle) can keep this gift in their Profile or convert it to \(convertStars) Stars. [More About Stars >]()" + if case .message = component.subject { + descriptionText = "\(peer.compactDisplayTitle) can keep this gift in their Profile or convert it to \(convertStars) Stars. [More About Stars >]()" + } else { + descriptionText = "" + } } else { descriptionText = "" } @@ -273,10 +280,10 @@ private final class GiftViewSheetContent: CombinedComponent { let tableLinkColor = theme.list.itemAccentColor var tableItems: [TableComponent.Item] = [] - if let peerId = component.subject.arguments?.peerId, let peer = state.peerMap[peerId] { + if let peerId = component.subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] { tableItems.append(.init( - id: "to", - title: incoming ? strings.Stars_Transaction_From : strings.Stars_Transaction_To, + id: "from", + title: strings.Stars_Transaction_From, component: AnyComponent( Button( content: AnyComponent( @@ -287,24 +294,24 @@ private final class GiftViewSheetContent: CombinedComponent { ) ), action: { - if "".isEmpty { - component.openPeer(peer) - Queue.mainQueue().after(1.0, { - component.cancel(false) - }) - } else { +// if "".isEmpty { +// component.openPeer(peer) +// Queue.mainQueue().after(1.0, { +// component.cancel(false) +// }) +// } else { if let controller = controller() as? GiftViewScreen, let navigationController = controller.navigationController, let chatController = navigationController.viewControllers.first(where: { $0 is ChatController }) as? ChatController { chatController.playShakeAnimation() } component.cancel(true) - } +// } } ) ) )) } else { tableItems.append(.init( - id: "from", + id: "from_anon", title: strings.Stars_Transaction_From, component: AnyComponent( PeerCellComponent( @@ -430,6 +437,8 @@ private final class GiftViewSheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: descriptionOrigin + description.size.height / 2.0)) ) originY += description.size.height + 10.0 + } else { + originY += 11.0 } let amountSpacing: CGFloat = 1.0 @@ -439,7 +448,11 @@ private final class GiftViewSheetContent: CombinedComponent { var amountOrigin = originY if "".isEmpty { amountOrigin -= descriptionSize.height + 10.0 - originY += amount.size.height + 26.0 + if descriptionSize.height > 0 { + originY += amount.size.height + 26.0 + } else { + originY += amount.size.height + 2.0 + } } else { originY += amount.size.height + 20.0 } @@ -696,14 +709,14 @@ public class GiftViewScreen: ViewControllerComponentContainer { case message(EngineMessage) case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) - var arguments: (peerId: EnginePeer.Id, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? { + var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? { switch self { case let .message(message): if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted) = action.action { - return (message.id.peerId, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted) + return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted) } case let .profileGift(peerId, gift): - return (peerId, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false) + return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false) } return nil } @@ -792,9 +805,25 @@ public class GiftViewScreen: ViewControllerComponentContainer { presentationData: presentationData, content: .sticker(context: context, file: arguments.gift.file, loop: false, title: added ? "Gift Saved to Profile" : "Gift Removed from Profile", text: added ? "The gift is now displayed in [your profile]()." : "The gift is no longer displayed in [your profile]().", undoText: nil, customAction: nil), elevatedLayout: lastController is ChatController, - action: { action in - if case .info = action { - + action: { [weak navigationController] action in + if case .info = action, let navigationController { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak navigationController] peer in + guard let peer, let navigationController else { + return + } + if let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .myProfileGifts, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + navigationController.pushViewController(controller, animated: true) + } + }) } return true } @@ -823,8 +852,12 @@ public class GiftViewScreen: ViewControllerComponentContainer { |> deliverOnMainQueue).startStandalone() } self?.dismissAnimated() - + if let navigationController { + if let starsContext = context.starsContext { + navigationController.pushViewController(context.sharedContext.makeStarsTransactionsScreen(context: context, starsContext: starsContext), animated: true) + } + Queue.mainQueue().after(0.5) { if let lastController = navigationController.viewControllers.last as? ViewController { let resultController = UndoOverlayController( diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/BUILD b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/BUILD index cab3565215..ac6a567525 100644 --- a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/BUILD +++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/BUILD @@ -10,12 +10,15 @@ swift_library( "-warnings-as-errors", ], deps = [ + "//submodules/SSignalKit/SwiftSignalKit", "//submodules/Display", "//submodules/ComponentFlow", "//submodules/TelegramPresentationData", "//submodules/Components/MultilineTextComponent", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/TextFieldComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/AccountContext", ], visibility = [ diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift index 0cfdd563cf..fda244d1c2 100644 --- a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift +++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift @@ -2,10 +2,13 @@ import Foundation import UIKit import Display import ComponentFlow +import SwiftSignalKit import TelegramPresentationData import MultilineTextComponent import ListSectionComponent import TextFieldComponent +import LottieComponent +import PlainButtonComponent import AccountContext public final class ListMultilineTextFieldItemComponent: Component { @@ -14,6 +17,11 @@ public final class ListMultilineTextFieldItemComponent: Component { public fileprivate(set) var text: NSAttributedString = NSAttributedString() public fileprivate(set) var isEditing: Bool = false + public var hasTrackingView = false + + public var currentEmojiSuggestion: TextFieldComponent.EmojiSuggestion? + public var dismissedEmojiSuggestionPosition: TextFieldComponent.EmojiSuggestion.Position? + public init() { } } @@ -30,6 +38,11 @@ public final class ListMultilineTextFieldItemComponent: Component { } } + public enum InputMode { + case keyboard + case emoji + } + public enum EmptyLineHandling { case allowed case oneConsecutive @@ -49,10 +62,13 @@ public final class ListMultilineTextFieldItemComponent: Component { public let characterLimit: Int? public let displayCharacterLimit: Bool public let emptyLineHandling: EmptyLineHandling + public let formatMenuAvailability: TextFieldComponent.FormatMenuAvailability public let updated: ((String) -> Void)? public let returnKeyAction: (() -> Void)? public let backspaceKeyAction: (() -> Void)? public let textUpdateTransition: ComponentTransition + public let inputMode: InputMode? + public let toggleInputMode: (() -> Void)? public let tag: AnyObject? public init( @@ -69,10 +85,13 @@ public final class ListMultilineTextFieldItemComponent: Component { characterLimit: Int? = nil, displayCharacterLimit: Bool = false, emptyLineHandling: EmptyLineHandling = .allowed, + formatMenuAvailability: TextFieldComponent.FormatMenuAvailability = .none, updated: ((String) -> Void)? = nil, returnKeyAction: (() -> Void)? = nil, backspaceKeyAction: (() -> Void)? = nil, textUpdateTransition: ComponentTransition = .immediate, + inputMode: InputMode? = nil, + toggleInputMode: (() -> Void)? = nil, tag: AnyObject? = nil ) { self.externalState = externalState @@ -88,10 +107,13 @@ public final class ListMultilineTextFieldItemComponent: Component { self.characterLimit = characterLimit self.displayCharacterLimit = displayCharacterLimit self.emptyLineHandling = emptyLineHandling + self.formatMenuAvailability = formatMenuAvailability self.updated = updated self.returnKeyAction = returnKeyAction self.backspaceKeyAction = backspaceKeyAction self.textUpdateTransition = textUpdateTransition + self.inputMode = inputMode + self.toggleInputMode = toggleInputMode self.tag = tag } @@ -135,9 +157,15 @@ public final class ListMultilineTextFieldItemComponent: Component { if lhs.emptyLineHandling != rhs.emptyLineHandling { return false } + if lhs.formatMenuAvailability != rhs.formatMenuAvailability { + return false + } if (lhs.updated == nil) != (rhs.updated == nil) { return false } + if lhs.inputMode != rhs.inputMode { + return false + } return true } @@ -145,6 +173,8 @@ public final class ListMultilineTextFieldItemComponent: Component { private let textField = ComponentView() private let textFieldExternalState = TextFieldComponent.ExternalState() + private var modeSelector: ComponentView? + private let placeholder = ComponentView() private var customPlaceholder: ComponentView? @@ -203,17 +233,40 @@ public final class ListMultilineTextFieldItemComponent: Component { } } + public func insertText(text: NSAttributedString) { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + textFieldView.insertText(text) + } + } + + public func backwardsDeleteText() { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + textFieldView.deleteBackward() + } + } + + public var textFieldView: TextFieldComponent.View? { + return self.textField.view as? TextFieldComponent.View + } + func update(component: ListMultilineTextFieldItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } + let previousComponent = self.component self.component = component self.state = state let verticalInset: CGFloat = 12.0 - let sideInset: CGFloat = 16.0 + let leftInset: CGFloat = 16.0 + var rightInset: CGFloat = 16.0 + let modeSelectorSize = CGSize(width: 32.0, height: 32.0) + + if component.inputMode != nil { + rightInset += 34.0 + } let textLimitFont = Font.regular(15.0) var measureTextLimitInset: CGFloat = 0.0 @@ -258,8 +311,8 @@ public final class ListMultilineTextFieldItemComponent: Component { fontSize: 17.0, textColor: component.theme.list.itemPrimaryTextColor, accentColor: component.theme.list.itemPrimaryTextColor, - insets: UIEdgeInsets(top: verticalInset, left: sideInset - 8.0, bottom: verticalInset, right: sideInset - 8.0 + measureTextLimitInset), - hideKeyboard: false, + insets: UIEdgeInsets(top: verticalInset, left: leftInset - 8.0, bottom: verticalInset, right: rightInset - 8.0 + measureTextLimitInset), + hideKeyboard: component.inputMode == .emoji, customInputView: nil, resetText: component.resetText.flatMap { resetText in return NSAttributedString(string: resetText.value, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor) @@ -267,7 +320,7 @@ public final class ListMultilineTextFieldItemComponent: Component { isOneLineWhenUnfocused: false, characterLimit: component.characterLimit, emptyLineHandling: mappedEmptyLineHandling, - formatMenuAvailability: .none, + formatMenuAvailability: component.formatMenuAvailability, returnKeyType: component.returnKeyType, lockedFormatAction: { }, @@ -309,9 +362,9 @@ public final class ListMultilineTextFieldItemComponent: Component { text: .plain(NSAttributedString(string: component.placeholder.isEmpty ? " " : component.placeholder, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor)) )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) ) - let placeholderFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: placeholderSize) + let placeholderFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: placeholderSize) if let placeholderView = self.placeholder.view { if placeholderView.superview == nil { placeholderView.layer.anchorPoint = CGPoint() @@ -329,6 +382,9 @@ public final class ListMultilineTextFieldItemComponent: Component { component.externalState?.hasText = self.textFieldExternalState.hasText component.externalState?.text = self.textFieldExternalState.text component.externalState?.isEditing = self.textFieldExternalState.isEditing + component.externalState?.currentEmojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion + component.externalState?.dismissedEmojiSuggestionPosition = self.textFieldExternalState.dismissedEmojiSuggestionPosition + component.externalState?.hasTrackingView = self.textFieldExternalState.hasTrackingView var displayRemainingLimit: Int? if let characterLimit = component.characterLimit, component.displayCharacterLimit { @@ -357,7 +413,7 @@ public final class ListMultilineTextFieldItemComponent: Component { environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) ) - let textLimitLabelFrame = CGRect(origin: CGPoint(x: availableSize.width - textLimitLabelSize.width - sideInset, y: verticalInset + 2.0), size: textLimitLabelSize) + let textLimitLabelFrame = CGRect(origin: CGPoint(x: availableSize.width - textLimitLabelSize.width - rightInset, y: verticalInset + 2.0), size: textLimitLabelSize) if let textLimitLabelView = textLimitLabel.view { if textLimitLabelView.superview == nil { textLimitLabelView.isUserInteractionEnabled = false @@ -374,6 +430,91 @@ public final class ListMultilineTextFieldItemComponent: Component { } } + if let inputMode = component.inputMode { + var modeSelectorTransition = transition + let modeSelector: ComponentView + if let current = self.modeSelector { + modeSelector = current + } else { + modeSelectorTransition = modeSelectorTransition.withAnimation(.none) + modeSelector = ComponentView() + self.modeSelector = modeSelector + } + let animationName: String + var playAnimation = false + if let previousComponent, let previousInputMode = previousComponent.inputMode { + if previousInputMode != inputMode { + playAnimation = true + } + } + switch inputMode { + case .keyboard: + animationName = "input_anim_keyToSmile" + case .emoji: + animationName = "input_anim_smileToKey" + } + + let _ = modeSelector.update( + transition: modeSelectorTransition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent( + name: animationName + ), + color: component.theme.chat.inputPanel.inputControlColor.blitOver(component.theme.list.itemBlocksBackgroundColor, alpha: 1.0), + size: modeSelectorSize + )), + effectAlignment: .center, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.toggleInputMode?() + }, + animateScale: false + )), + environment: {}, + containerSize: modeSelectorSize + ) + let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - 4.0 - modeSelectorSize.width, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize) + if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View { + let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) + + if modeSelectorView.superview == nil { + self.addSubview(modeSelectorView) + ComponentTransition.immediate.setAlpha(view: modeSelectorView, alpha: 0.0) + ComponentTransition.immediate.setScale(view: modeSelectorView, scale: 0.001) + } + + if playAnimation, let animationView = modeSelectorView.contentView as? LottieComponent.View { + animationView.playOnce() + } + + modeSelectorTransition.setPosition(view: modeSelectorView, position: modeSelectorFrame.center) + modeSelectorTransition.setBounds(view: modeSelectorView, bounds: CGRect(origin: CGPoint(), size: modeSelectorFrame.size)) + + if let externalState = component.externalState { + let displaySelector = externalState.isEditing + + alphaTransition.setAlpha(view: modeSelectorView, alpha: displaySelector ? 1.0 : 0.0) + alphaTransition.setScale(view: modeSelectorView, scale: displaySelector ? 1.0 : 0.001) + } + } + } else if let modeSelector = self.modeSelector { + self.modeSelector = nil + if let modeSelectorView = modeSelector.view { + if !transition.animation.isImmediate { + let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) + alphaTransition.setAlpha(view: modeSelectorView, alpha: 0.0, completion: { [weak modeSelectorView] _ in + modeSelectorView?.removeFromSuperview() + }) + alphaTransition.setScale(view: modeSelectorView, scale: 0.001) + } else { + modeSelectorView.removeFromSuperview() + } + } + } + return size } diff --git a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift index b8092ada4f..8f3c68384c 100644 --- a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift +++ b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift @@ -41,17 +41,20 @@ public final class ListSectionContentView: UIView { public final class Configuration { public let theme: PresentationTheme + public let isModal: Bool public let displaySeparators: Bool public let extendsItemHighlightToSection: Bool public let background: ListSectionComponent.Background public init( theme: PresentationTheme, + isModal: Bool = false, displaySeparators: Bool, extendsItemHighlightToSection: Bool, background: ListSectionComponent.Background ) { self.theme = theme + self.isModal = isModal self.displaySeparators = displaySeparators self.extendsItemHighlightToSection = extendsItemHighlightToSection self.background = background @@ -116,7 +119,7 @@ public final class ListSectionContentView: UIView { backgroundColor = configuration.theme.list.itemHighlightedBackgroundColor } else { transition = .easeInOut(duration: 0.2) - backgroundColor = configuration.theme.list.itemBlocksBackgroundColor + backgroundColor = configuration.isModal ? configuration.theme.list.itemModalBlocksBackgroundColor : configuration.theme.list.itemBlocksBackgroundColor } self.externalContentBackgroundView.updateColor(color: backgroundColor, transition: transition) @@ -144,7 +147,7 @@ public final class ListSectionContentView: UIView { if self.highlightedItemId != nil && configuration.extendsItemHighlightToSection { backgroundColor = configuration.theme.list.itemHighlightedBackgroundColor } else { - backgroundColor = configuration.theme.list.itemBlocksBackgroundColor + backgroundColor = configuration.isModal ? configuration.theme.list.itemModalBlocksBackgroundColor : configuration.theme.list.itemBlocksBackgroundColor } self.externalContentBackgroundView.updateColor(color: backgroundColor, transition: transition) @@ -305,6 +308,7 @@ public final class ListSectionComponent: Component { public let header: AnyComponent? public let footer: AnyComponent? public let items: [AnyComponentWithIdentity] + public let isModal: Bool public let displaySeparators: Bool public let extendsItemHighlightToSection: Bool @@ -314,6 +318,7 @@ public final class ListSectionComponent: Component { header: AnyComponent?, footer: AnyComponent?, items: [AnyComponentWithIdentity], + isModal: Bool = false, displaySeparators: Bool = true, extendsItemHighlightToSection: Bool = false ) { @@ -322,6 +327,7 @@ public final class ListSectionComponent: Component { self.header = header self.footer = footer self.items = items + self.isModal = isModal self.displaySeparators = displaySeparators self.extendsItemHighlightToSection = extendsItemHighlightToSection } @@ -342,6 +348,9 @@ public final class ListSectionComponent: Component { if lhs.items != rhs.items { return false } + if lhs.isModal != rhs.isModal { + return false + } if lhs.displaySeparators != rhs.displaySeparators { return false } @@ -448,6 +457,7 @@ public final class ListSectionComponent: Component { let contentResult = self.contentView.update( configuration: ListSectionContentView.Configuration( theme: component.theme, + isModal: component.isModal, displaySeparators: component.displaySeparators, extendsItemHighlightToSection: component.extendsItemHighlightToSection, background: component.background @@ -522,17 +532,20 @@ public final class ListSubSectionComponent: Component { public let theme: PresentationTheme public let leftInset: CGFloat public let items: [AnyComponentWithIdentity] + public let isModal: Bool public let displaySeparators: Bool public init( theme: PresentationTheme, leftInset: CGFloat, items: [AnyComponentWithIdentity], + isModal: Bool = false, displaySeparators: Bool = true ) { self.theme = theme self.leftInset = leftInset self.items = items + self.isModal = isModal self.displaySeparators = displaySeparators } @@ -546,6 +559,9 @@ public final class ListSubSectionComponent: Component { if lhs.items != rhs.items { return false } + if lhs.isModal != rhs.isModal { + return false + } if lhs.displaySeparators != rhs.displaySeparators { return false } @@ -615,6 +631,7 @@ public final class ListSubSectionComponent: Component { let contentResult = self.contentView.update( configuration: ListSectionContentView.Configuration( theme: component.theme, + isModal: component.isModal, displaySeparators: component.displaySeparators, extendsItemHighlightToSection: false, background: .none(clipped: false) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index e9a5f9f5c9..8d4948be80 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -8250,9 +8250,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } private func openReport(type: PeerInfoReportType, contextController: ContextControllerProtocol?, backAction: ((ContextControllerProtocol) -> Void)?) { - guard let controller = self.controller else { - return - } self.view.endEditing(true) switch type { @@ -8291,20 +8288,28 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro ]) self.controller?.present(actionSheet, in: .window(.root)) default: - let options: [PeerReportOption] - if case .user = type { - options = [.spam, .fake, .violence, .pornography, .childAbuse] - } else { - options = [.spam, .fake, .violence, .pornography, .childAbuse, .copyright, .other] - } + contextController?.dismiss() - presentPeerReportOptions(context: self.context, parent: controller, contextController: contextController, backAction: backAction, subject: .peer(self.peerId), options: options, passthrough: true, completion: { [weak self] reason, _ in - if let reason = reason { - DispatchQueue.main.async { - self?.openChatForReporting(reason) - } - } + self.context.sharedContext.makeContentReportScreen(context: self.context, subject: .peer(self.peerId), forceDark: false, present: { [weak self] controller in + self?.controller?.push(controller) + }, completion: { + }) + +// let options: [PeerReportOption] +// if case .user = type { +// options = [.spam, .fake, .violence, .pornography, .childAbuse] +// } else { +// options = [.spam, .fake, .violence, .pornography, .childAbuse, .copyright, .other] +// } +// +// presentPeerReportOptions(context: self.context, parent: controller, contextController: contextController, backAction: backAction, subject: .peer(self.peerId), options: options, passthrough: true, completion: { [weak self] reason, _ in +// if let reason = reason { +// DispatchQueue.main.async { +// self?.openChatForReporting(reason) +// } +// } +// }) } } @@ -11645,13 +11650,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } strongSelf.view.endEditing(true) - strongSelf.controller?.present(peerReportOptionsController(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), passthrough: false, present: { c, a in - self?.controller?.present(c, in: .window(.root), with: a) - }, push: { c in - self?.controller?.push(c) - }, completion: { _, _ in }), in: .window(.root)) - - + strongSelf.context.sharedContext.makeContentReportScreen(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), forceDark: false, present: { [weak self] controller in + self?.controller?.push(controller) + }, completion: {}) }, displayCopyProtectionTip: { [weak self] node, save in if let strongSelf = self, let peer = strongSelf.data?.peer, let messageIds = strongSelf.state.selectedMessageIds, !messageIds.isEmpty { let _ = (strongSelf.context.engine.data.get(EngineDataMap( @@ -12284,6 +12285,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc private weak var requestsContext: PeerInvitationImportersContext? fileprivate let starsContext: StarsContext? private let switchToRecommendedChannels: Bool + private let switchToGifts: Bool private let chatLocation: ChatLocation private let chatLocationContextHolder = Atomic(value: nil) @@ -12340,7 +12342,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool = false, isMyProfile: Bool = false, hintGroupInCommon: PeerId? = nil, requestsContext: PeerInvitationImportersContext? = nil, forumTopicThread: ChatReplyThreadMessage? = nil, switchToRecommendedChannels: Bool = false) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool = false, isMyProfile: Bool = false, hintGroupInCommon: PeerId? = nil, requestsContext: PeerInvitationImportersContext? = nil, forumTopicThread: ChatReplyThreadMessage? = nil, switchToRecommendedChannels: Bool = false, switchToGifts: Bool = false) { self.context = context self.updatedPresentationData = updatedPresentationData self.peerId = peerId @@ -12354,6 +12356,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.hintGroupInCommon = hintGroupInCommon self.requestsContext = requestsContext self.switchToRecommendedChannels = switchToRecommendedChannels + self.switchToGifts = switchToGifts if let forumTopicThread = forumTopicThread { self.chatLocation = .replyThread(message: forumTopicThread) @@ -12694,7 +12697,13 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } override public func loadDisplayNode() { - self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, starsContext: self.starsContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, initialPaneKey: self.switchToRecommendedChannels ? .recommended : nil) + var initialPaneKey: PeerInfoPaneKey? + if self.switchToRecommendedChannels { + initialPaneKey = .recommended + } else if self.switchToGifts { + initialPaneKey = .gifts + } + self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, starsContext: self.starsContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, initialPaneKey: initialPaneKey) self.controllerNode.accountsAndPeers.set(self.accountsAndPeers.get() |> map { $0.1 }) self.controllerNode.activeSessionsContextAndCount.set(self.activeSessionsContextAndCount.get()) self.cachedDataPromise.set(self.controllerNode.cachedDataPromise.get()) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index f99b353467..ff9d282d3e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -87,6 +87,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr guard let self else { return } + let isFirstTime = starsProducts == nil let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.statusPromise.set(.single(PeerInfoStatusData(text: presentationData.strings.SharedMedia_GiftCount(state.count ?? 0), isActivity: true, key: .gifts))) self.starsProducts = state.gifts @@ -96,7 +97,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.ready.set(.single(true)) } - self.updateScrolling() + self.updateScrolling(transition: isFirstTime ? .immediate : .easeInOut(duration: 0.25)) }) } @@ -119,10 +120,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } public func scrollViewDidScroll(_ scrollView: UIScrollView) { - self.updateScrolling() + self.updateScrolling(transition: .immediate) } - func updateScrolling() { + func updateScrolling(transition: ComponentTransition) { if let starsProducts = self.starsProducts, let params = self.currentParams { let optionSpacing: CGFloat = 10.0 let sideInset = params.sideInset + 16.0 @@ -140,13 +141,14 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr let itemId = AnyHashable(product.date) validIds.append(itemId) - let itemTransition = ComponentTransition.immediate + var itemTransition = transition let visibleItem: ComponentView if let current = self.starsItems[itemId] { visibleItem = current } else { visibleItem = ComponentView() self.starsItems[itemId] = visibleItem + itemTransition = .immediate } var isVisible = false @@ -221,6 +223,26 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } } + var removeIds: [AnyHashable] = [] + for (id, item) in self.starsItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemView = item.view { + if !transition.animation.isImmediate { + itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } + } + } + } + for id in removeIds { + self.starsItems.removeValue(forKey: id) + } + var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * starsOptionSize.height + 60.0 + 16.0 if self.peerId == self.context.account.peerId { @@ -354,7 +376,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } self.scrollNode.view.isScrollEnabled = !isScrollingLockedAtTop - self.updateScrolling() + self.updateScrolling(transition: ComponentTransition(transition)) } public func findLoadedMessage(id: MessageId) -> Message? { diff --git a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/BUILD b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/BUILD index 154b99fe70..69caa55031 100644 --- a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/BUILD +++ b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/BUILD @@ -17,7 +17,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AccountContext", "//submodules/AttachmentUI", - "//submodules/PremiumUI", + "//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift index 520c5d79da..bada8afbba 100644 --- a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift +++ b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift @@ -5,15 +5,17 @@ import AsyncDisplayKit import ComponentFlow import SwiftSignalKit import AccountContext -import PremiumUI import AttachmentUI +import GiftOptionsScreen -public class PremiumGiftAttachmentScreen: PremiumGiftScreen, AttachmentContainable { +public class PremiumGiftAttachmentScreen: GiftOptionsScreen, AttachmentContainable { public var requestAttachmentMenuExpansion: () -> Void = {} public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } public var parentController: () -> ViewController? = { return nil } + public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } + public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in } public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } @@ -25,17 +27,16 @@ public class PremiumGiftAttachmentScreen: PremiumGiftScreen, AttachmentContainab } private final class PremiumGiftContext: AttachmentMediaPickerContext { - private weak var controller: PremiumGiftScreen? + private weak var controller: GiftOptionsScreen? public var mainButtonState: Signal { - return self.controller?.mainButtonStatePromise.get() ?? .single(nil) + return .single(nil) } - init(controller: PremiumGiftScreen) { + init(controller: GiftOptionsScreen) { self.controller = controller } func mainButtonAction() { - self.controller?.mainButtonPressed() } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 7248d88a8e..52d550b52c 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -1037,7 +1037,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { guard let self else { return } - let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .stars(birthdays), completion: { [weak self] peerIds in + let controller = self.context.sharedContext.makeStarsGiftController(context: self.context, birthdays: birthdays, completion: { [weak self] peerIds in guard let self, let peerId = peerIds.first else { return } diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 12a9161dfd..35c5a4e6c3 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -741,6 +741,9 @@ public final class TextFieldComponent: Component { } self.insertText(NSAttributedString(string: insertString)) + } else if (range.length == 0 && text == "\n"), let returnKeyAction = component.returnKeyAction { + returnKeyAction() + return false } return false } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index e3ba2c90f7..37d82d35fd 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -588,19 +588,17 @@ extension ChatControllerImpl { strongSelf.controllerNavigationDisposable.set(nil) } case .gift: - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer, let starsContext = context.starsContext { let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions if !premiumGiftOptions.isEmpty { - let controller = PremiumGiftAttachmentScreen(context: context, peerIds: [peer.id], options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in - if let strongSelf = self { - strongSelf.push(c) - } - }, completion: { [weak self] in - if let strongSelf = self { - strongSelf.hintPlayNextOutgoingGift() - strongSelf.attachmentController?.dismiss(animated: true) + let controller = PremiumGiftAttachmentScreen(context: context, starsContext: starsContext, peerId: peer.id, premiumOptions: premiumGiftOptions, completion: { [weak self] in + guard let self else { + return } + self.hintPlayNextOutgoingGift() + self.attachmentController?.dismiss(animated: true) }) + completion(controller, controller.mediaPickerContext) strongSelf.controllerNavigationDisposable.set(nil) diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index cabf9baa3e..fdd0d2397d 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -14,6 +14,7 @@ import AttachmentUI import SearchBarNode import ChatSendAudioMessageContextPreview import ChatSendMessageActionUI +import ContextUI class ContactSelectionControllerImpl: ViewController, ContactSelectionController, PresentableController, AttachmentContainable { private let context: AccountContext @@ -42,6 +43,9 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController private let multipleSelection: Bool private let requirePhoneNumbers: Bool + private let openProfile: ((EnginePeer) -> Void)? + private let sendMessage: ((EnginePeer) -> Void)? + private var _ready = Promise() override var ready: Promise { return self._ready @@ -105,6 +109,9 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController self.multipleSelection = params.multipleSelection self.requirePhoneNumbers = params.requirePhoneNumbers + self.openProfile = params.openProfile + self.sendMessage = params.sendMessage + self.presentationData = params.updatedPresentationData?.initial ?? params.context.sharedContext.currentPresentationData.with { $0 } super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) @@ -219,15 +226,15 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController } self.contactsNode.requestOpenPeerFromSearch = { [weak self] peer in - self?.openPeer(peer: peer, action: .generic) + self?.openPeer(peer: peer, action: .generic, node: nil, gesture: nil) } self.contactsNode.contactListNode.activateSearch = { [weak self] in self?.activateSearch() } - self.contactsNode.contactListNode.openPeer = { [weak self] peer, action, _, _ in - self?.openPeer(peer: peer, action: action) + self.contactsNode.contactListNode.openPeer = { [weak self] peer, action, node, gesture in + self?.openPeer(peer: peer, action: action, node: node, gesture: gesture) } self.contactsNode.contactListNode.suppressPermissionWarning = { [weak self] in @@ -357,7 +364,40 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController } } - private func openPeer(peer: ContactListPeer, action: ContactListAction) { + private func openPeer(peer: ContactListPeer, action: ContactListAction, node: ASDisplayNode?, gesture: ContextGesture?) { + if case .more = action { + guard case let .peer(peer, _, _) = peer, let node = node as? ContextReferenceContentNode else { + return + } + + let presentationData = self.presentationData + + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Premium_Gift_ContactSelection_SendMessage, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MessageBubble"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { [weak self] _, a in + a(.default) + + if let self { + self.sendMessage?(EnginePeer(peer)) + } + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Premium_Gift_ContactSelection_OpenProfile, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { [weak self] _, a in + a(.default) + + if let self { + self.openProfile?(EnginePeer(peer)) + } + }))) + + let contextController = ContextController(presentationData: presentationData, source: .reference(ContactContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + self.present(contextController, in: .window(.root)) + return + } + self.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) self.confirmationDisposable.set((self.confirmation(peer) |> deliverOnMainQueue).startStrict(next: { [weak self] value in if let strongSelf = self { @@ -477,3 +517,17 @@ final class ContactsPickerContext: AttachmentMediaPickerContext { func mainButtonAction() { } } + +private final class ContactContextReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceNode: ContextReferenceContentNode + + init(controller: ViewController, sourceNode: ContextReferenceContentNode) { + self.controller = controller + self.sourceNode = sourceNode + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift index 685113732e..ba488f43c1 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift @@ -41,6 +41,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { var requestMultipleAction: ((_ silent: Bool, _ scheduleTime: Int32?, _ parameters: ChatSendMessageActionSheetController.SendParameters?) -> Void)? var dismiss: (() -> Void)? var cancelSearch: (() -> Void)? + var openPeerMore: ((ContactListPeer, ASDisplayNode?, ContextGesture?) -> Void)? var presentationData: PresentationData { didSet { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 5d212212f5..bbdf81e5d1 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2204,21 +2204,90 @@ public final class SharedAccountContextImpl: SharedAccountContext { return PremiumLimitScreen(context: context, subject: mappedSubject, count: count, forceDark: forceDark, cancel: cancel, action: action) } + public func makeStarsGiftController(context: AccountContext, birthdays: [EnginePeer.Id: TelegramBirthday]?, completion: @escaping (([EnginePeer.Id]) -> Void)) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var presentBirthdayPickerImpl: (() -> Void)? + let starsMode: ContactSelectionControllerMode = .starsGifting(birthdays: birthdays, hasActions: false) + + let contactOptions: Signal<[ContactListAdditionalOption], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId)) + |> map { birthday in + if birthday == nil { + return [ContactListAdditionalOption( + title: presentationData.strings.Premium_Gift_ContactSelection_AddBirthday, + icon: .generic(UIImage(bundleImageName: "Contact List/AddBirthdayIcon")!), + action: { + presentBirthdayPickerImpl?() + }, + clearHighlightAutomatically: true + )] + } else { + return [] + } + } + |> deliverOnMainQueue + + let options = Promise<[StarsGiftOption]>() + options.set(context.engine.payments.starsGiftOptions(peerId: nil)) + let controller = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( + context: context, + mode: starsMode, + autoDismiss: false, + title: { strings in return strings.Stars_Purchase_GiftStars }, + options: contactOptions + )) + let _ = (controller.result + |> deliverOnMainQueue).start(next: { result in + if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer { + completion([peer.id]) + } + }) + + presentBirthdayPickerImpl = { [weak controller] in + guard let controller else { + return + } + let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .setupBirthday).startStandalone() + + let settingsPromise: Promise + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface, let current = rootController.getPrivacySettings() { + settingsPromise = current + } else { + settingsPromise = Promise() + settingsPromise.set(.single(nil) |> then(context.engine.privacy.requestAccountPrivacySettings() |> map(Optional.init))) + } + let birthdayController = BirthdayPickerScreen(context: context, settings: settingsPromise.get(), openSettings: { + context.sharedContext.makeBirthdayPrivacyController(context: context, settings: settingsPromise, openedFromBirthdayScreen: true, present: { [weak controller] c in + controller?.push(c) + }) + }, completion: { [weak controller] value in + let _ = context.engine.accountData.updateBirthday(birthday: value).startStandalone() + + controller?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: presentationData.strings.Birthday_Added, cancel: nil, destructive: false), elevatedLayout: false, action: { _ in + return true + }), in: .current) + }) + controller.push(birthdayController) + } + + return controller + } + public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var presentBirthdayPickerImpl: (() -> Void)? - var starsMode: ContactSelectionControllerMode = .generic + var mode: ContactSelectionControllerMode = .generic var currentBirthdays: [EnginePeer.Id: TelegramBirthday]? if case let .chatList(birthdays) = source, let birthdays, !birthdays.isEmpty { - starsMode = .starsGifting(birthdays: birthdays, hasActions: true) + mode = .starsGifting(birthdays: birthdays, hasActions: true) currentBirthdays = birthdays } else if case let .settings(birthdays) = source, let birthdays, !birthdays.isEmpty { - starsMode = .starsGifting(birthdays: birthdays, hasActions: true) + mode = .starsGifting(birthdays: birthdays, hasActions: true) currentBirthdays = birthdays } else { - starsMode = .starsGifting(birthdays: nil, hasActions: true) + mode = .starsGifting(birthdays: nil, hasActions: true) } let contactOptions: Signal<[ContactListAdditionalOption], NoError> @@ -2247,122 +2316,35 @@ public final class SharedAccountContextImpl: SharedAccountContext { var sendMessageImpl: ((EnginePeer) -> Void)? //TODO:localize - let controller: ViewController -// if case .stars = source { -// let options = Promise<[StarsGiftOption]>() -// options.set(context.engine.payments.starsGiftOptions(peerId: nil)) - let options = Promise<[PremiumGiftCodeOption]>() - options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil)) - let contactsController = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( - context: context, - mode: starsMode, - autoDismiss: false, - title: { strings in return "Gift Premium or Stars" }, - options: contactOptions, - openProfile: { peer in - openProfileImpl?(peer) - }, - sendMessage: { peer in - sendMessageImpl?(peer) + let options = Promise<[PremiumGiftCodeOption]>() + options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil)) + let controller = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( + context: context, + mode: mode, + autoDismiss: false, + title: { strings in return "Gift Premium or Stars" }, + options: contactOptions, + openProfile: { peer in + openProfileImpl?(peer) + }, + sendMessage: { peer in + sendMessageImpl?(peer) + } + )) + let _ = combineLatest(queue: Queue.mainQueue(), controller.result, options.get()) + .startStandalone(next: { [weak controller] result, options in + if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer, let starsContext = context.starsContext { + let premiumOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } + let giftController = GiftOptionsScreen(context: context, starsContext: starsContext, peerId: peer.id, premiumOptions: premiumOptions) + giftController.navigationPresentation = .modal + controller?.push(giftController) + + if case .chatList = source, let _ = currentBirthdays { + let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .todayBirthdays).startStandalone() } - )) - let _ = combineLatest(queue: Queue.mainQueue(), contactsController.result, options.get()) - .startStandalone(next: { [weak contactsController] result, options in - if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer, let starsContext = context.starsContext { - let premiumOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } - let giftController = GiftOptionsScreen(context: context, starsContext: starsContext, peerId: peer.id, premiumOptions: premiumOptions) - giftController.navigationPresentation = .modal - contactsController?.push(giftController) - -// completion?([peer.id]) - - if case .chatList = source, let _ = currentBirthdays { - let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .todayBirthdays).startStandalone() - } - } - }) - controller = contactsController -// } else { -// let options = Promise<[PremiumGiftCodeOption]>() -// options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil)) -// let contactsController = context.sharedContext.makeContactMultiselectionController( -// ContactMultiselectionControllerParams( -// context: context, -// mode: mode, -// options: contactOptions, -// isPeerEnabled: { peer in -// if case let .user(user) = peer, user.botInfo == nil && !peer.isService && !user.flags.contains(.isSupport) { -// return true -// } else { -// return false -// } -// }, -// limit: limit, -// reachedLimit: { limit in -// reachedLimitImpl?(limit) -// }, -// openProfile: { peer in -// openProfileImpl?(peer) -// }, -// sendMessage: { peer in -// sendMessageImpl?(peer) -// } -// ) -// ) -// let _ = combineLatest(queue: Queue.mainQueue(), contactsController.result, options.get()) -// .startStandalone(next: { [weak contactsController] result, options in -// guard let controller = contactsController else { -// return -// } -// var peerIds: [PeerId] = [] -// if case let .result(peerIdsValue, _) = result { -// peerIds = peerIdsValue.compactMap({ peerId in -// if case let .peer(peerId) = peerId { -// return peerId -// } else { -// return nil -// } -// }) -// } -// guard !peerIds.isEmpty else { -// return -// } -// -// let mappedOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } -// var pushImpl: ((ViewController) -> Void)? -// var filterImpl: (() -> Void)? -// let giftController = PremiumGiftScreen(context: context, peerIds: peerIds, options: mappedOptions, source: source, pushController: { c in -// pushImpl?(c) -// }, completion: { -// filterImpl?() -// -// if case .chatList = source, let _ = currentBirthdays { -// let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .todayBirthdays).startStandalone() -// } -// }) -// pushImpl = { [weak giftController] c in -// giftController?.push(c) -// } -// filterImpl = { [weak giftController] in -// if let navigationController = giftController?.navigationController as? NavigationController { -// var controllers = navigationController.viewControllers -// controllers = controllers.filter { !($0 is ContactMultiselectionController) && !($0 is PremiumGiftScreen) } -// navigationController.setViewControllers(controllers, animated: true) -// } -// } -// controller.push(giftController) -// }) -// controller = contactsController -// } + } + }) -// reachedLimitImpl = { [weak controller] limit in -// guard let controller else { -// return -// } -// HapticFeedback().error() -// controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Premium_Gift_ContactSelection_MaximumReached("\(limit)").string, timeout: nil, customUndoText: nil), elevatedLayout: true, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current) -// } - sendMessageImpl = { [weak self, weak controller] peer in guard let self, let controller, let navigationController = controller.navigationController as? NavigationController else { return @@ -2864,6 +2846,7 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation var hintGroupInCommon: PeerId? var forumTopicThread: ChatReplyThreadMessage? var isMyProfile = false + var switchToGifts = false switch mode { case let .nearbyPeer(distance): @@ -2880,10 +2863,13 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation forumTopicThread = thread case .myProfile: isMyProfile = true + case .myProfileGifts: + isMyProfile = true + switchToGifts = true default: break } - return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, reactionSourceMessageId: reactionSourceMessageId, callMessages: callMessages, isMyProfile: isMyProfile, hintGroupInCommon: hintGroupInCommon, forumTopicThread: forumTopicThread) + return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, reactionSourceMessageId: reactionSourceMessageId, callMessages: callMessages, isMyProfile: isMyProfile, hintGroupInCommon: hintGroupInCommon, forumTopicThread: forumTopicThread, switchToGifts: switchToGifts) } else if peer is TelegramSecretChat { return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: []) } diff --git a/submodules/Utils/DeviceModel/Sources/DeviceModel.swift b/submodules/Utils/DeviceModel/Sources/DeviceModel.swift index 78cdfc5018..e74039e14e 100644 --- a/submodules/Utils/DeviceModel/Sources/DeviceModel.swift +++ b/submodules/Utils/DeviceModel/Sources/DeviceModel.swift @@ -49,6 +49,8 @@ public enum DeviceModel: CaseIterable, Equatable { .iPhone15Plus, .iPhone15Pro, .iPhone15ProMax, + .iPhone16, + .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax ] @@ -118,6 +120,8 @@ public enum DeviceModel: CaseIterable, Equatable { case iPhone15Pro case iPhone15ProMax + case iPhone16 + case iPhone16Plus case iPhone16Pro case iPhone16ProMax @@ -223,6 +227,10 @@ public enum DeviceModel: CaseIterable, Equatable { return ["iPhone16,1"] case .iPhone15ProMax: return ["iPhone16,2"] + case .iPhone16: + return ["iPhone17,3"] + case .iPhone16Plus: + return ["iPhone17,4"] case .iPhone16Pro: return ["iPhone17,1"] case .iPhone16ProMax: @@ -332,6 +340,10 @@ public enum DeviceModel: CaseIterable, Equatable { return "iPhone 15 Pro" case .iPhone15ProMax: return "iPhone 15 Pro Max" + case .iPhone16: + return "iPhone 16" + case .iPhone16Plus: + return "iPhone 16 Plus" case .iPhone16Pro: return "iPhone 16 Pro" case .iPhone16ProMax: diff --git a/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh b/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh index be58ac5e4f..0bd88402a1 100755 --- a/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh +++ b/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh @@ -48,7 +48,7 @@ CONFIGURE_FLAGS="--enable-cross-compile --disable-programs \ --enable-libvpx \ --enable-audiotoolbox \ --enable-bsf=aac_adtstoasc,vp9_superframe,h264_mp4toannexb \ - --enable-decoder=h264,libvpx_vp9,hevc,libopus,mp3,aac,flac,alac_at,pcm_s16le,pcm_s24le,gsm_ms_at \ + --enable-decoder=h264,libvpx_vp9,hevc,libopus,mp3,aac,flac,alac_at,pcm_s16le,pcm_s24le,pcm_f32le,gsm_ms_at \ --enable-encoder=libvpx_vp9,aac_at \ --enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska,mpegts \ --enable-parser=aac,h264,mp3,libopus \