Merge commit 'a30ab38ce41f3389fee9054eb99875382365a6b7' into beta

This commit is contained in:
Ilya Laktyushin 2024-09-26 02:53:29 +04:00
commit 4175c9f3b3
35 changed files with 1153 additions and 354 deletions

View File

@ -568,6 +568,7 @@ public enum PeerInfoControllerMode {
case forumTopic(thread: ChatReplyThreadMessage) case forumTopic(thread: ChatReplyThreadMessage)
case recommendedChannels case recommendedChannels
case myProfile case myProfile
case myProfileGifts
} }
public enum ContactListActionItemInlineIconPosition { public enum ContactListActionItemInlineIconPosition {
@ -975,6 +976,8 @@ public protocol SharedAccountContext: AnyObject {
func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController 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 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 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 makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController
func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> 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 func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController

View File

@ -102,7 +102,7 @@ final class CameraDeviceContext {
return 30.0 return 30.0
} }
switch DeviceModel.current { switch DeviceModel.current {
case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax, .iPhone16ProMax: case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax:
return 60.0 return 60.0
default: default:
return 30.0 return 30.0

View File

@ -34,10 +34,6 @@ public extension Camera {
self = .iPhone15Pro self = .iPhone15Pro
case .iPhone15ProMax: case .iPhone15ProMax:
self = .iPhone15ProMax self = .iPhone15ProMax
case .iPhone16Pro:
self = .iPhone15Pro
case .iPhone16ProMax:
self = .iPhone15ProMax
case .unknown: case .unknown:
self = .unknown self = .unknown
default: default:

View File

@ -36,8 +36,6 @@ public enum DeviceMetrics: CaseIterable, Equatable {
case iPhone14ProZoomed case iPhone14ProZoomed
case iPhone14ProMax case iPhone14ProMax
case iPhone14ProMaxZoomed case iPhone14ProMaxZoomed
case iPhone16Pro
case iPhone16ProMax
case iPad case iPad
case iPadMini case iPadMini
case iPad102Inch case iPad102Inch
@ -70,8 +68,6 @@ public enum DeviceMetrics: CaseIterable, Equatable {
.iPhone14ProZoomed, .iPhone14ProZoomed,
.iPhone14ProMax, .iPhone14ProMax,
.iPhone14ProMaxZoomed, .iPhone14ProMaxZoomed,
.iPhone16Pro,
.iPhone16ProMax,
.iPad, .iPad,
.iPadMini, .iPadMini,
.iPad102Inch, .iPad102Inch,
@ -175,10 +171,6 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return CGSize(width: 430.0, height: 932.0) return CGSize(width: 430.0, height: 932.0)
case .iPhone14ProMaxZoomed: case .iPhone14ProMaxZoomed:
return CGSize(width: 375.0, height: 812.0) 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: case .iPad:
return CGSize(width: 768.0, height: 1024.0) return CGSize(width: 768.0, height: 1024.0)
case .iPadMini: case .iPadMini:
@ -212,8 +204,6 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 53.0 + UIScreenPixel return 53.0 + UIScreenPixel
case .iPhone14Pro, .iPhone14ProMax: case .iPhone14Pro, .iPhone14ProMax:
return 55.0 return 55.0
case .iPhone16Pro, .iPhone16ProMax:
return 55.0
case let .unknown(_, _, _, screenCornerRadius): case let .unknown(_, _, _, screenCornerRadius):
return screenCornerRadius return screenCornerRadius
default: default:
@ -223,7 +213,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
func safeInsets(inLandscape: Bool) -> UIEdgeInsets { func safeInsets(inLandscape: Bool) -> UIEdgeInsets {
switch self { 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) 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: default:
return UIEdgeInsets.zero return UIEdgeInsets.zero
@ -232,7 +222,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
public func onScreenNavigationHeight(inLandscape: Bool, systemOnScreenNavigationHeight: CGFloat?) -> CGFloat? { public func onScreenNavigationHeight(inLandscape: Bool, systemOnScreenNavigationHeight: CGFloat?) -> CGFloat? {
switch self { 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 return inLandscape ? 21.0 : 34.0
case .iPhone14ProZoomed: case .iPhone14ProZoomed:
return inLandscape ? 21.0 : 28.0 return inLandscape ? 21.0 : 28.0
@ -272,8 +262,6 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 54.0 return 54.0
case .iPhone14ProMaxZoomed: case .iPhone14ProMaxZoomed:
return 47.0 return 47.0
case .iPhone16Pro, .iPhone16ProMax:
return 54.0
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax:
return 44.0 return 44.0
case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
@ -292,7 +280,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 162.0 return 162.0
case .iPhone6, .iPhone6Plus: case .iPhone6, .iPhone6Plus:
return 163.0 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 return 172.0
case .iPad, .iPad102Inch, .iPadPro10Inch: case .iPad, .iPad102Inch, .iPadPro10Inch:
return 348.0 return 348.0
@ -311,9 +299,9 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 216.0 return 216.0
case .iPhone6Plus: case .iPhone6Plus:
return 226.0 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 return 292.0
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax: case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax:
return 302.0 return 302.0
case .iPad, .iPad102Inch, .iPadPro10Inch: case .iPad, .iPad102Inch, .iPadPro10Inch:
return 263.0 return 263.0
@ -332,7 +320,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
func predictiveInputHeight(inLandscape: Bool) -> CGFloat { func predictiveInputHeight(inLandscape: Bool) -> CGFloat {
if inLandscape { if inLandscape {
switch self { 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 return 37.0
case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
return 50.0 return 50.0
@ -343,7 +331,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
switch self { switch self {
case .iPhone4, .iPhone5: case .iPhone4, .iPhone5:
return 37.0 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 return 44.0
case .iPhone6Plus: case .iPhone6Plus:
return 45.0 return 45.0
@ -370,7 +358,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
public var hasDynamicIsland: Bool { public var hasDynamicIsland: Bool {
switch self { switch self {
case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax: case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed:
return true return true
default: default:
return false return false

View File

@ -534,6 +534,14 @@ NSString *suffix = @"";
return @"iPhone 15 Pro"; return @"iPhone 15 Pro";
if ([platform isEqualToString:@"iPhone16,2"]) if ([platform isEqualToString:@"iPhone16,2"])
return @"iPhone 15 Pro Max"; 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"]) if ([platform hasPrefix:@"iPod1"])
return @"iPod touch 1G"; return @"iPod touch 1G";

View File

@ -67,7 +67,7 @@ struct PasscodeKeyboardLayout {
self.topOffset = 226.0 self.topOffset = 226.0
self.biometricsOffset = 30.0 self.biometricsOffset = 30.0
self.deleteOffset = 20.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.buttonSize = 75.0
self.horizontalSecond = 103.0 self.horizontalSecond = 103.0
self.horizontalThird = 206.0 self.horizontalThird = 206.0
@ -78,7 +78,7 @@ struct PasscodeKeyboardLayout {
self.topOffset = 294.0 self.topOffset = 294.0
self.biometricsOffset = 30.0 self.biometricsOffset = 30.0
self.deleteOffset = 20.0 self.deleteOffset = 20.0
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax: case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax:
self.buttonSize = 85.0 self.buttonSize = 85.0
self.horizontalSecond = 115.0 self.horizontalSecond = 115.0
self.horizontalThird = 230.0 self.horizontalThird = 230.0
@ -151,11 +151,11 @@ public struct PasscodeLayout {
self.titleOffset = 112.0 self.titleOffset = 112.0
self.subtitleOffset = -6.0 self.subtitleOffset = -6.0
self.inputFieldOffset = 156.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.titleOffset = 162.0
self.subtitleOffset = 0.0 self.subtitleOffset = 0.0
self.inputFieldOffset = 206.0 self.inputFieldOffset = 206.0
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax: case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax:
self.titleOffset = 180.0 self.titleOffset = 180.0
self.subtitleOffset = 0.0 self.subtitleOffset = 0.0
self.inputFieldOffset = 226.0 self.inputFieldOffset = 226.0

View File

@ -2110,7 +2110,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
var topParticipants: [GroupCallParticipantsContext.Participant] = [] var topParticipants: [GroupCallParticipantsContext.Participant] = []
var reportSpeakingParticipants: [PeerId: UInt32] = [:] var reportSpeakingParticipants: [PeerId: UInt32] = [:]
let timestamp = CACurrentMediaTime() let timestamp = CFAbsoluteTimeGetCurrent()
for (peerId, ssrc) in speakingParticipants { for (peerId, ssrc) in speakingParticipants {
let shouldReport: Bool let shouldReport: Bool
if let previousTimestamp = strongSelf.speakingParticipantsReportTimestamp[peerId] { if let previousTimestamp = strongSelf.speakingParticipantsReportTimestamp[peerId] {

View File

@ -109,6 +109,7 @@ final class VideoChatScreenComponent: Component {
var applicationStateDisposable: Disposable? var applicationStateDisposable: Disposable?
var expandedParticipantsVideoState: VideoChatParticipantsComponent.ExpandedVideoState? var expandedParticipantsVideoState: VideoChatParticipantsComponent.ExpandedVideoState?
var focusedSpeakerAutoSwitchDeadline: Double = 0.0
var isTwoColumnSidebarHidden: Bool = false var isTwoColumnSidebarHidden: Bool = false
let inviteDisposable = MetaDisposable() let inviteDisposable = MetaDisposable()
@ -481,15 +482,6 @@ final class VideoChatScreenComponent: Component {
guard let component = self.component, let environment = self.environment else { guard let component = self.component, let environment = self.environment else {
return 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) HapticFeedback().impact(.light)
if component.call.hasVideo { if component.call.hasVideo {
@ -761,7 +753,7 @@ final class VideoChatScreenComponent: Component {
if self.members != members { if self.members != members {
var members = members var members = members
#if DEBUG && true #if DEBUG && false
if let membersValue = members { if let membersValue = members {
var participants = membersValue.participants var participants = membersValue.participants
for i in 1 ... 20 { 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 videoCount == 1, let participantsView = self.participants.view as? VideoChatParticipantsComponent.View, let participantsComponent = participantsView.component {
if participantsComponent.layout.videoColumn != nil { if participantsComponent.layout.videoColumn != nil {
self.expandedParticipantsVideoState = nil self.expandedParticipantsVideoState = nil
self.focusedSpeakerAutoSwitchDeadline = 0.0
} }
} }
} }
if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, let members { 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 { if let callState = self.callState, participant.peer.id == callState.myPeerId {
return false return false
} }
@ -862,6 +855,7 @@ final class VideoChatScreenComponent: Component {
} else { } else {
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) 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 { } else {
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden)
} }
self.focusedSpeakerAutoSwitchDeadline = CFAbsoluteTimeGetCurrent() + 1.0
} else { } else {
self.expandedParticipantsVideoState = nil self.expandedParticipantsVideoState = nil
self.focusedSpeakerAutoSwitchDeadline = 0.0
} }
} else { } else {
self.expandedParticipantsVideoState = nil self.expandedParticipantsVideoState = nil
self.focusedSpeakerAutoSwitchDeadline = 0.0
} }
if !self.isUpdating { if !self.isUpdating {
@ -1468,6 +1465,7 @@ final class VideoChatScreenComponent: Component {
} }
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: key, isMainParticipantPinned: false, isUIHidden: isUIHidden) self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: key, isMainParticipantPinned: false, isUIHidden: isUIHidden)
self.focusedSpeakerAutoSwitchDeadline = CFAbsoluteTimeGetCurrent() + 3.0
self.state?.updated(transition: .spring(duration: 0.4)) self.state?.updated(transition: .spring(duration: 0.4))
} else if self.expandedParticipantsVideoState != nil { } else if self.expandedParticipantsVideoState != nil {
self.expandedParticipantsVideoState = nil self.expandedParticipantsVideoState = nil

View File

@ -524,7 +524,7 @@ private func sendUploadedMessageContent(
|> switchToLatest |> switchToLatest
} }
public func standaloneSendMessage(account: Account, peerId: PeerId, text: String, attributes: [MessageAttribute], media: StandaloneMedia?, replyToMessageId: MessageId?) -> Signal<Float, StandaloneSendMessageError> { public func standaloneSendMessage(account: Account, peerId: PeerId, text: String, attributes: [MessageAttribute], media: StandaloneMedia?, replyToMessageId: MessageId?, threadId: Int32? = nil) -> Signal<Float, StandaloneSendMessageError> {
let content: Signal<StandaloneSendMessageEvent, StandaloneSendMessageError> let content: Signal<StandaloneSendMessageEvent, StandaloneSendMessageError>
if let media = media { if let media = media {
switch media { switch media {
@ -561,14 +561,14 @@ public func standaloneSendMessage(account: Account, peerId: PeerId, text: String
case let .progress(progress): case let .progress(progress):
return .single(progress) return .single(progress)
case let .result(result): 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 }) return .single(1.0) |> then(sendContent |> mapError { _ -> StandaloneSendMessageError in })
} }
} }
} }
private func sendMessageContent(account: Account, peerId: PeerId, attributes: [MessageAttribute], content: StandaloneMessageContent) -> Signal<Void, NoError> { private func sendMessageContent(account: Account, peerId: PeerId, attributes: [MessageAttribute], content: StandaloneMessageContent, threadId: Int32?) -> Signal<Void, NoError> {
return account.postbox.transaction { transaction -> Signal<Void, NoError> in return account.postbox.transaction { transaction -> Signal<Void, NoError> in
if peerId.namespace == Namespaces.Peer.SecretChat { if peerId.namespace == Namespaces.Peer.SecretChat {
return .complete() return .complete()
@ -631,9 +631,12 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M
flags |= 1 << 0 flags |= 1 << 0
replyTo = .inputReplyToStory(peer: inputPeer, storyId: replyToStoryId.id) 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<Api.Updates, NoError> in |> `catch` { _ -> Signal<Api.Updates, NoError> in
return .complete() return .complete()
} }
@ -649,6 +652,9 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M
flags |= 1 << 0 flags |= 1 << 0
replyTo = .inputReplyToStory(peer: inputPeer, storyId: replyToStoryId.id) 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)) 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))

View File

@ -731,6 +731,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 struct LinkedDiscussionPeerId: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = EnginePeerCachedInfoItem<EnginePeer.Id?> public typealias Result = EnginePeerCachedInfoItem<EnginePeer.Id?>

View File

@ -191,7 +191,7 @@ func _internal_keepCachedStarGiftsUpdated(postbox: Postbox, network: Network) ->
func managedStarGiftsUpdates(postbox: Postbox, network: Network) -> Signal<Never, NoError> { func managedStarGiftsUpdates(postbox: Postbox, network: Network) -> Signal<Never, NoError> {
let poll = _internal_keepCachedStarGiftsUpdated(postbox: postbox, network: network) 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<Never, NoError> { func _internal_convertStarGift(account: Account, messageId: EngineMessage.Id) -> Signal<Never, NoError> {

View File

@ -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: {}) 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: {}, environment: {},
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),

View File

@ -30,6 +30,8 @@ swift_library(
"//submodules/Markdown", "//submodules/Markdown",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/InvisibleInkDustNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -20,6 +20,8 @@ import ShimmerEffect
import Markdown import Markdown
import ChatMessageBubbleContentNode import ChatMessageBubbleContentNode
import ChatMessageItemCommon import ChatMessageItemCommon
import TextNodeWithEntities
import InvisibleInkDustNode
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? { 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) 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 let mediaBackgroundMaskNode: ASImageNode
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
private let titleNode: TextNode private let titleNode: TextNode
private let subtitleNode: TextNode private let subtitleNode: TextNodeWithEntities
private var dustNode: InvisibleInkDustNode?
private let placeholderNode: StickerShimmerEffectNode private let placeholderNode: StickerShimmerEffectNode
private let animationNode: AnimatedStickerNode private let animationNode: AnimatedStickerNode
@ -60,6 +63,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if wasVisible != isVisible { if wasVisible != isVisible {
self.visibilityStatus = 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.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false self.titleNode.displaysAsynchronously = false
self.subtitleNode = TextNode() self.subtitleNode = TextNodeWithEntities()
self.subtitleNode.isUserInteractionEnabled = false self.subtitleNode.textNode.isUserInteractionEnabled = false
self.subtitleNode.displaysAsynchronously = false self.subtitleNode.textNode.displaysAsynchronously = false
self.buttonNode = HighlightTrackingButtonNode() self.buttonNode = HighlightTrackingButtonNode()
self.buttonNode.clipsToBounds = true self.buttonNode.clipsToBounds = true
@ -120,8 +133,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
self.addSubnode(self.labelNode) self.addSubnode(self.labelNode)
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode) self.addSubnode(self.subtitleNode.textNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.placeholderNode) self.addSubnode(self.placeholderNode)
self.addSubnode(self.animationNode) 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))) { 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 makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) 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 makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode) let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode)
@ -259,6 +271,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
var animationFile: TelegramMediaFile? var animationFile: TelegramMediaFile?
var title = item.presentationData.strings.Notification_PremiumGift_Title var title = item.presentationData.strings.Notification_PremiumGift_Title
var text = "" var text = ""
var entities: [MessageTextEntity] = []
var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View
var ribbonTitle = "" var ribbonTitle = ""
var hasServiceMessage = true var hasServiceMessage = true
@ -329,14 +342,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View
hasServiceMessage = false hasServiceMessage = false
} }
case let .starGift(gift, convertStars, giftText, entities, nameHidden, savedToProfile, converted): case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted):
let _ = nameHidden
//TODO:localize //TODO:localize
if !incoming {
buttonTitle = ""
}
let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
title = "Gift from \(authorName)" title = "Gift from \(authorName)"
if let giftText, !giftText.isEmpty { if let giftText, !giftText.isEmpty {
text = giftText text = giftText
let _ = entities entities = giftEntities ?? []
} else { } else {
if incoming { if incoming {
if converted { if converted {
@ -383,14 +398,19 @@ 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 (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( let attributedText: NSAttributedString
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor), if let _ = animationFile {
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor), 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)
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor), } else {
linkAttribute: { url in attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(
return ("URL", url) body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor),
} bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor),
), textAlignment: .center) 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 (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()))
@ -398,7 +418,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
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())) 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() var labelRects = labelLayout.linesRects()
if labelRects.count > 1 { 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) 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.animationNode.frame = animationFrame
strongSelf.buttonNode.isHidden = buttonTitle.isEmpty
strongSelf.buttonTitleNode.isHidden = buttonTitle.isEmpty
if strongSelf.item == nil { if strongSelf.item == nil {
strongSelf.animationNode.started = { [weak self] in strongSelf.animationNode.started = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
@ -502,7 +528,13 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let _ = labelApply() let _ = labelApply()
let _ = titleApply() 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 _ = buttonTitleApply()
let _ = ribbonTextApply() let _ = ribbonTextApply()
@ -513,7 +545,26 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.titleNode.frame = titleFrame 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) 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) 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 strongSelf.buttonTitleNode.frame = buttonTitleFrame
@ -607,6 +658,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if let (rect, size) = strongSelf.absoluteRect { if let (rect, size) = strongSelf.absoluteRect {
strongSelf.updateAbsoluteRect(rect, within: size) 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() self.updateVisibility()
} }
private var internalPlayedOnce = false
private func updateVisibility() { private func updateVisibility() {
guard let item = self.item else { guard let item = self.item else {
return 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) item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
self.animationNode.playOnce() self.animationNode.playOnce()
self.internalPlayedOnce = true
Queue.mainQueue().after(0.05) { Queue.mainQueue().after(0.05) {
if let itemNode = self.itemNode, let supernode = itemNode.supernode { if let itemNode = self.itemNode, let supernode = itemNode.supernode {

View File

@ -162,7 +162,7 @@ private final class SheetPageContent: CombinedComponent {
transition: .immediate transition: .immediate
) )
context.add(back 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 let constrainedTitleWidth = context.availableSize.width - (back.size.width + 16.0) * 2.0
@ -280,7 +280,8 @@ private final class SheetPageContent: CombinedComponent {
maximumNumberOfLines: 0 maximumNumberOfLines: 0
)), )),
footer: footer, footer: footer,
items: items items: items,
isModal: true
), ),
environment: {}, environment: {},
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
@ -696,12 +697,10 @@ public final class ContentReportScreen: ViewControllerComponentContainer {
switch result { switch result {
case .reported: case .reported:
Queue.mainQueue().after(0.1) {
completed()
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
Queue.mainQueue().after(0.4, { 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) (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)
}) })
} }

View File

@ -1011,14 +1011,15 @@ final class GiftOptionsScreenComponent: Component {
} }
} }
public final class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScreenProtocol { open class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScreenProtocol {
private let context: AccountContext private let context: AccountContext
public init( public init(
context: AccountContext, context: AccountContext,
starsContext: StarsContext, starsContext: StarsContext,
peerId: EnginePeer.Id, peerId: EnginePeer.Id,
premiumOptions: [CachedPremiumGiftOption] premiumOptions: [CachedPremiumGiftOption],
completion: @escaping () -> Void = {}
) { ) {
self.context = context self.context = context

View File

@ -34,10 +34,13 @@ swift_library(
"//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/AppBundle", "//submodules/AppBundle",
"//submodules/WallpaperBackgroundNode", "//submodules/WallpaperBackgroundNode",
"//submodules/TextFormat",
"//submodules/ChatPresentationInterfaceState", "//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/TextFieldComponent", "//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/TelegramUI/Components/ListItemComponentAdaptor", "//submodules/TelegramUI/Components/ListItemComponentAdaptor",
"//submodules/BotPaymentsUI", "//submodules/BotPaymentsUI",
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -28,6 +28,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd
let accountPeer: EnginePeer? let accountPeer: EnginePeer?
let gift: StarGift let gift: StarGift
let text: String let text: String
let entities: [MessageTextEntity]
init( init(
context: AccountContext, context: AccountContext,
@ -42,7 +43,8 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd
nameDisplayOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder,
accountPeer: EnginePeer?, accountPeer: EnginePeer?,
gift: StarGift, gift: StarGift,
text: String text: String,
entities: [MessageTextEntity]
) { ) {
self.context = context self.context = context
self.theme = theme self.theme = theme
@ -57,6 +59,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd
self.accountPeer = accountPeer self.accountPeer = accountPeer
self.gift = gift self.gift = gift
self.text = text 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<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) { func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -130,6 +133,9 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd
if lhs.text != rhs.text { if lhs.text != rhs.text {
return false return false
} }
if lhs.entities != rhs.entities {
return false
}
return true return true
} }
} }
@ -201,7 +207,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
peers[authorPeerId] = item.accountPeer?._asPeer() peers[authorPeerId] = item.accountPeer?._asPeer()
let media: [Media] = [ 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: [:]) 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)) 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.insets = layout.insets
itemNode.frame = nodeFrame itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false itemNode.isUserInteractionEnabled = false
itemNode.visibility = .visible(1.0, .infinite)
Queue.mainQueue().after(0.01) { apply(ListViewItemApply(isOnScreen: true))
apply(ListViewItemApply(isOnScreen: true))
}
}) })
} }
} else { } else {
var messageNodes: [ListViewItemNode] = [] var messageNodes: [ListViewItemNode] = []
for i in 0 ..< items.count { for i in 0 ..< items.count {
var itemNode: ListViewItemNode? 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 itemNode = node
apply().1(ListViewItemApply(isOnScreen: true)) apply().1(ListViewItemApply(isOnScreen: true))
}) })
itemNode!.isUserInteractionEnabled = false itemNode!.isUserInteractionEnabled = false
itemNode?.visibility = .visible(1.0, .infinite)
messageNodes.append(itemNode!) messageNodes.append(itemNode!)
self.initialBubbleHeight = itemNode?.frame.height self.initialBubbleHeight = itemNode?.frame.height

View File

@ -22,6 +22,11 @@ import LottieComponent
import TextFieldComponent import TextFieldComponent
import ButtonComponent import ButtonComponent
import BotPaymentsUI import BotPaymentsUI
import ChatEntityKeyboardInputNode
import EmojiSuggestionsComponent
import ChatPresentationInterfaceState
import AudioToolbox
import TextFormat
final class GiftSetupScreenComponent: Component { final class GiftSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -81,9 +86,23 @@ final class GiftSetupScreenComponent: Component {
private let textInputTag = NSObject() private let textInputTag = NSObject()
private var resetText: String? 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<ChatEntityKeyboardInputNode.InputData>()
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
private var hideName = false private var hideName = false
private var previousHadInputHeight: Bool = false private var previousHadInputHeight: Bool = false
private var previousInputHeight: CGFloat?
private var recenterOnTag: NSObject? private var recenterOnTag: NSObject?
private var peerMap: [EnginePeer.Id: EnginePeer] = [:] private var peerMap: [EnginePeer.Id: EnginePeer] = [:]
@ -175,7 +194,8 @@ final class GiftSetupScreenComponent: Component {
guard let self else { guard let self else {
return 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) let inputData = BotCheckoutController.InputData.fetch(context: component.context, source: source)
|> map(Optional.init) |> map(Optional.init)
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in |> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
@ -264,6 +284,108 @@ final class GiftSetupScreenComponent: Component {
self.state?.updated() 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 let environment = environment[EnvironmentType.self].value
@ -317,15 +439,6 @@ final class GiftSetupScreenComponent: Component {
contentHeight += environment.navigationHeight contentHeight += environment.navigationHeight
contentHeight += 26.0 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<Empty>] = [] var introSectionItems: [AnyComponentWithIdentity<Empty>] = []
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(Rectangle(color: .clear, height: 346.0, tag: self.introPlaceholderTag))))
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListMultilineTextFieldItemComponent( introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListMultilineTextFieldItemComponent(
@ -337,13 +450,14 @@ final class GiftSetupScreenComponent: Component {
resetText: self.resetText.flatMap { resetText: self.resetText.flatMap {
return ListMultilineTextFieldItemComponent.ResetText(value: $0) return ListMultilineTextFieldItemComponent.ResetText(value: $0)
}, },
placeholder: environment.strings.Business_Intro_IntroTextPlaceholder, placeholder: "Enter Message",
autocapitalizationType: .none, autocapitalizationType: .none,
autocorrectionType: .no, autocorrectionType: .no,
returnKeyType: .done, returnKeyType: .done,
characterLimit: 70, characterLimit: 255,
displayCharacterLimit: true, displayCharacterLimit: true,
emptyLineHandling: .notAllowed, emptyLineHandling: .notAllowed,
formatMenuAvailability: .available([.bold, .italic, .underline, .strikethrough, .spoiler]),
updated: { _ in updated: { _ in
}, },
returnKeyAction: { [weak self] in returnKeyAction: { [weak self] in
@ -355,10 +469,173 @@ final class GiftSetupScreenComponent: Component {
} }
}, },
textUpdateTransition: .spring(duration: 0.4), 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 tag: self.textInputTag
)))) ))))
self.resetText = nil 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<Empty>
if let current = self.currentEmojiSuggestionView {
currentEmojiSuggestionView = current
} else {
currentEmojiSuggestionView = ComponentHostView<Empty>()
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( let introSectionSize = self.introSection.update(
transition: transition, transition: transition,
component: AnyComponent(ListSectionComponent( component: AnyComponent(ListSectionComponent(
@ -388,63 +665,43 @@ final class GiftSetupScreenComponent: Component {
contentHeight += introSectionSize.height contentHeight += introSectionSize.height
contentHeight += sectionSpacing 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 listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true)
let introContentSize = self.introContent.update( if let accountPeer = self.peerMap[component.context.account.peerId] {
transition: transition, let introContentSize = self.introContent.update(
component: AnyComponent( transition: transition,
ListItemComponentAdaptor( component: AnyComponent(
itemGenerator: ChatGiftPreviewItem( ListItemComponentAdaptor(
context: component.context, itemGenerator: ChatGiftPreviewItem(
theme: environment.theme, context: component.context,
componentTheme: environment.theme, theme: environment.theme,
strings: environment.strings, componentTheme: environment.theme,
sectionId: 0, strings: environment.strings,
fontSize: presentationData.chatFontSize, sectionId: 0,
chatBubbleCorners: presentationData.chatBubbleCorners, fontSize: presentationData.chatFontSize,
wallpaper: presentationData.chatWallpaper, chatBubbleCorners: presentationData.chatBubbleCorners,
dateTimeFormat: environment.dateTimeFormat, wallpaper: presentationData.chatWallpaper,
nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: environment.dateTimeFormat,
accountPeer: self.peerMap[component.context.account.peerId], nameDisplayOrder: presentationData.nameDisplayOrder,
gift: component.gift, accountPeer: accountPeer,
text: self.textInputState.text.string gift: component.gift,
), text: self.textInputState.text.string,
params: listItemParams entities: generateChatInputTextEntities(self.textInputState.text)
) ),
), params: listItemParams
environment: {}, )
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) ),
) environment: {},
if let introContentView = self.introContent.view { containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
if introContentView.superview == nil { )
if let placeholderView = self.introSection.findTaggedView(tag: self.introPlaceholderTag) { if let introContentView = self.introContent.view {
placeholderView.addSubview(introContentView) 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))
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
} }
} }
self.previousHadInputHeight = environment.inputHeight > 0.0
let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? "" let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? ""
let hideSectionSize = self.hideSection.update( let hideSectionSize = self.hideSection.update(
@ -498,11 +755,9 @@ final class GiftSetupScreenComponent: Component {
contentHeight += bottomContentInset contentHeight += bottomContentInset
let inputHeight: CGFloat = environment.inputHeight
let combinedBottomInset = max(inputHeight, environment.safeInsets.bottom) let combinedBottomInset = max(inputHeight, environment.safeInsets.bottom)
contentHeight += combinedBottomInset contentHeight += combinedBottomInset
if self.starImage == nil || self.starImage?.1 !== environment.theme { 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) 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 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 self.ignoreScrolling = true
let contentSize = CGSize(width: availableSize.width, height: contentHeight) let contentSize = CGSize(width: availableSize.width, height: contentHeight)
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
@ -592,6 +863,152 @@ final class GiftSetupScreenComponent: Component {
return availableSize 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 { func makeView() -> View {

View File

@ -82,7 +82,10 @@ private final class GiftViewSheetContent: CombinedComponent {
super.init() super.init()
if let arguments = subject.arguments { 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( self.disposable = (context.engine.data.get(
EngineDataMap( EngineDataMap(
peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in 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 >]()" descriptionText = "You converted this gift to \(convertStars) Stars. [More About Stars >]()"
} }
} else if let peerId = component.subject.arguments?.peerId, let peer = state.peerMap[peerId] { } 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 { } else {
descriptionText = "" descriptionText = ""
} }
@ -273,10 +280,10 @@ private final class GiftViewSheetContent: CombinedComponent {
let tableLinkColor = theme.list.itemAccentColor let tableLinkColor = theme.list.itemAccentColor
var tableItems: [TableComponent.Item] = [] 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( tableItems.append(.init(
id: "to", id: "from",
title: incoming ? strings.Stars_Transaction_From : strings.Stars_Transaction_To, title: strings.Stars_Transaction_From,
component: AnyComponent( component: AnyComponent(
Button( Button(
content: AnyComponent( content: AnyComponent(
@ -287,24 +294,24 @@ private final class GiftViewSheetContent: CombinedComponent {
) )
), ),
action: { action: {
if "".isEmpty { // if "".isEmpty {
component.openPeer(peer) // component.openPeer(peer)
Queue.mainQueue().after(1.0, { // Queue.mainQueue().after(1.0, {
component.cancel(false) // component.cancel(false)
}) // })
} else { // } else {
if let controller = controller() as? GiftViewScreen, let navigationController = controller.navigationController, let chatController = navigationController.viewControllers.first(where: { $0 is ChatController }) as? ChatController { if let controller = controller() as? GiftViewScreen, let navigationController = controller.navigationController, let chatController = navigationController.viewControllers.first(where: { $0 is ChatController }) as? ChatController {
chatController.playShakeAnimation() chatController.playShakeAnimation()
} }
component.cancel(true) component.cancel(true)
} // }
} }
) )
) )
)) ))
} else { } else {
tableItems.append(.init( tableItems.append(.init(
id: "from", id: "from_anon",
title: strings.Stars_Transaction_From, title: strings.Stars_Transaction_From,
component: AnyComponent( component: AnyComponent(
PeerCellComponent( 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)) .position(CGPoint(x: context.availableSize.width / 2.0, y: descriptionOrigin + description.size.height / 2.0))
) )
originY += description.size.height + 10.0 originY += description.size.height + 10.0
} else {
originY += 11.0
} }
let amountSpacing: CGFloat = 1.0 let amountSpacing: CGFloat = 1.0
@ -439,7 +448,11 @@ private final class GiftViewSheetContent: CombinedComponent {
var amountOrigin = originY var amountOrigin = originY
if "".isEmpty { if "".isEmpty {
amountOrigin -= descriptionSize.height + 10.0 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 { } else {
originY += amount.size.height + 20.0 originY += amount.size.height + 20.0
} }
@ -696,14 +709,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
case message(EngineMessage) case message(EngineMessage)
case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) 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 { switch self {
case let .message(message): 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 { 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): 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 return nil
} }
@ -792,9 +805,25 @@ public class GiftViewScreen: ViewControllerComponentContainer {
presentationData: presentationData, 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), 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, elevatedLayout: lastController is ChatController,
action: { action in action: { [weak navigationController] action in
if case .info = action { 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 return true
} }
@ -825,6 +854,10 @@ public class GiftViewScreen: ViewControllerComponentContainer {
self?.dismissAnimated() self?.dismissAnimated()
if let navigationController { if let navigationController {
if let starsContext = context.starsContext {
navigationController.pushViewController(context.sharedContext.makeStarsTransactionsScreen(context: context, starsContext: starsContext), animated: true)
}
Queue.mainQueue().after(0.5) { Queue.mainQueue().after(0.5) {
if let lastController = navigationController.viewControllers.last as? ViewController { if let lastController = navigationController.viewControllers.last as? ViewController {
let resultController = UndoOverlayController( let resultController = UndoOverlayController(

View File

@ -10,12 +10,15 @@ swift_library(
"-warnings-as-errors", "-warnings-as-errors",
], ],
deps = [ deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Display", "//submodules/Display",
"//submodules/ComponentFlow", "//submodules/ComponentFlow",
"//submodules/TelegramPresentationData", "//submodules/TelegramPresentationData",
"//submodules/Components/MultilineTextComponent", "//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/TextFieldComponent", "//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/AccountContext", "//submodules/AccountContext",
], ],
visibility = [ visibility = [

View File

@ -2,10 +2,13 @@ import Foundation
import UIKit import UIKit
import Display import Display
import ComponentFlow import ComponentFlow
import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import MultilineTextComponent import MultilineTextComponent
import ListSectionComponent import ListSectionComponent
import TextFieldComponent import TextFieldComponent
import LottieComponent
import PlainButtonComponent
import AccountContext import AccountContext
public final class ListMultilineTextFieldItemComponent: Component { 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 text: NSAttributedString = NSAttributedString()
public fileprivate(set) var isEditing: Bool = false public fileprivate(set) var isEditing: Bool = false
public var hasTrackingView = false
public var currentEmojiSuggestion: TextFieldComponent.EmojiSuggestion?
public var dismissedEmojiSuggestionPosition: TextFieldComponent.EmojiSuggestion.Position?
public init() { public init() {
} }
} }
@ -30,6 +38,11 @@ public final class ListMultilineTextFieldItemComponent: Component {
} }
} }
public enum InputMode {
case keyboard
case emoji
}
public enum EmptyLineHandling { public enum EmptyLineHandling {
case allowed case allowed
case oneConsecutive case oneConsecutive
@ -49,10 +62,13 @@ public final class ListMultilineTextFieldItemComponent: Component {
public let characterLimit: Int? public let characterLimit: Int?
public let displayCharacterLimit: Bool public let displayCharacterLimit: Bool
public let emptyLineHandling: EmptyLineHandling public let emptyLineHandling: EmptyLineHandling
public let formatMenuAvailability: TextFieldComponent.FormatMenuAvailability
public let updated: ((String) -> Void)? public let updated: ((String) -> Void)?
public let returnKeyAction: (() -> Void)? public let returnKeyAction: (() -> Void)?
public let backspaceKeyAction: (() -> Void)? public let backspaceKeyAction: (() -> Void)?
public let textUpdateTransition: ComponentTransition public let textUpdateTransition: ComponentTransition
public let inputMode: InputMode?
public let toggleInputMode: (() -> Void)?
public let tag: AnyObject? public let tag: AnyObject?
public init( public init(
@ -69,10 +85,13 @@ public final class ListMultilineTextFieldItemComponent: Component {
characterLimit: Int? = nil, characterLimit: Int? = nil,
displayCharacterLimit: Bool = false, displayCharacterLimit: Bool = false,
emptyLineHandling: EmptyLineHandling = .allowed, emptyLineHandling: EmptyLineHandling = .allowed,
formatMenuAvailability: TextFieldComponent.FormatMenuAvailability = .none,
updated: ((String) -> Void)? = nil, updated: ((String) -> Void)? = nil,
returnKeyAction: (() -> Void)? = nil, returnKeyAction: (() -> Void)? = nil,
backspaceKeyAction: (() -> Void)? = nil, backspaceKeyAction: (() -> Void)? = nil,
textUpdateTransition: ComponentTransition = .immediate, textUpdateTransition: ComponentTransition = .immediate,
inputMode: InputMode? = nil,
toggleInputMode: (() -> Void)? = nil,
tag: AnyObject? = nil tag: AnyObject? = nil
) { ) {
self.externalState = externalState self.externalState = externalState
@ -88,10 +107,13 @@ public final class ListMultilineTextFieldItemComponent: Component {
self.characterLimit = characterLimit self.characterLimit = characterLimit
self.displayCharacterLimit = displayCharacterLimit self.displayCharacterLimit = displayCharacterLimit
self.emptyLineHandling = emptyLineHandling self.emptyLineHandling = emptyLineHandling
self.formatMenuAvailability = formatMenuAvailability
self.updated = updated self.updated = updated
self.returnKeyAction = returnKeyAction self.returnKeyAction = returnKeyAction
self.backspaceKeyAction = backspaceKeyAction self.backspaceKeyAction = backspaceKeyAction
self.textUpdateTransition = textUpdateTransition self.textUpdateTransition = textUpdateTransition
self.inputMode = inputMode
self.toggleInputMode = toggleInputMode
self.tag = tag self.tag = tag
} }
@ -135,9 +157,15 @@ public final class ListMultilineTextFieldItemComponent: Component {
if lhs.emptyLineHandling != rhs.emptyLineHandling { if lhs.emptyLineHandling != rhs.emptyLineHandling {
return false return false
} }
if lhs.formatMenuAvailability != rhs.formatMenuAvailability {
return false
}
if (lhs.updated == nil) != (rhs.updated == nil) { if (lhs.updated == nil) != (rhs.updated == nil) {
return false return false
} }
if lhs.inputMode != rhs.inputMode {
return false
}
return true return true
} }
@ -145,6 +173,8 @@ public final class ListMultilineTextFieldItemComponent: Component {
private let textField = ComponentView<Empty>() private let textField = ComponentView<Empty>()
private let textFieldExternalState = TextFieldComponent.ExternalState() private let textFieldExternalState = TextFieldComponent.ExternalState()
private var modeSelector: ComponentView<Empty>?
private let placeholder = ComponentView<Empty>() private let placeholder = ComponentView<Empty>()
private var customPlaceholder: ComponentView<Empty>? private var customPlaceholder: ComponentView<Empty>?
@ -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<Empty>, transition: ComponentTransition) -> CGSize { func update(component: ListMultilineTextFieldItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true self.isUpdating = true
defer { defer {
self.isUpdating = false self.isUpdating = false
} }
let previousComponent = self.component
self.component = component self.component = component
self.state = state self.state = state
let verticalInset: CGFloat = 12.0 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) let textLimitFont = Font.regular(15.0)
var measureTextLimitInset: CGFloat = 0.0 var measureTextLimitInset: CGFloat = 0.0
@ -258,8 +311,8 @@ public final class ListMultilineTextFieldItemComponent: Component {
fontSize: 17.0, fontSize: 17.0,
textColor: component.theme.list.itemPrimaryTextColor, textColor: component.theme.list.itemPrimaryTextColor,
accentColor: component.theme.list.itemPrimaryTextColor, accentColor: component.theme.list.itemPrimaryTextColor,
insets: UIEdgeInsets(top: verticalInset, left: sideInset - 8.0, bottom: verticalInset, right: sideInset - 8.0 + measureTextLimitInset), insets: UIEdgeInsets(top: verticalInset, left: leftInset - 8.0, bottom: verticalInset, right: rightInset - 8.0 + measureTextLimitInset),
hideKeyboard: false, hideKeyboard: component.inputMode == .emoji,
customInputView: nil, customInputView: nil,
resetText: component.resetText.flatMap { resetText in resetText: component.resetText.flatMap { resetText in
return NSAttributedString(string: resetText.value, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor) 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, isOneLineWhenUnfocused: false,
characterLimit: component.characterLimit, characterLimit: component.characterLimit,
emptyLineHandling: mappedEmptyLineHandling, emptyLineHandling: mappedEmptyLineHandling,
formatMenuAvailability: .none, formatMenuAvailability: component.formatMenuAvailability,
returnKeyType: component.returnKeyType, returnKeyType: component.returnKeyType,
lockedFormatAction: { 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)) text: .plain(NSAttributedString(string: component.placeholder.isEmpty ? " " : component.placeholder, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor))
)), )),
environment: {}, 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 let placeholderView = self.placeholder.view {
if placeholderView.superview == nil { if placeholderView.superview == nil {
placeholderView.layer.anchorPoint = CGPoint() placeholderView.layer.anchorPoint = CGPoint()
@ -329,6 +382,9 @@ public final class ListMultilineTextFieldItemComponent: Component {
component.externalState?.hasText = self.textFieldExternalState.hasText component.externalState?.hasText = self.textFieldExternalState.hasText
component.externalState?.text = self.textFieldExternalState.text component.externalState?.text = self.textFieldExternalState.text
component.externalState?.isEditing = self.textFieldExternalState.isEditing 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? var displayRemainingLimit: Int?
if let characterLimit = component.characterLimit, component.displayCharacterLimit { if let characterLimit = component.characterLimit, component.displayCharacterLimit {
@ -357,7 +413,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
environment: {}, environment: {},
containerSize: CGSize(width: 100.0, height: 100.0) 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 let textLimitLabelView = textLimitLabel.view {
if textLimitLabelView.superview == nil { if textLimitLabelView.superview == nil {
textLimitLabelView.isUserInteractionEnabled = false textLimitLabelView.isUserInteractionEnabled = false
@ -374,6 +430,91 @@ public final class ListMultilineTextFieldItemComponent: Component {
} }
} }
if let inputMode = component.inputMode {
var modeSelectorTransition = transition
let modeSelector: ComponentView<Empty>
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 return size
} }

View File

@ -41,17 +41,20 @@ public final class ListSectionContentView: UIView {
public final class Configuration { public final class Configuration {
public let theme: PresentationTheme public let theme: PresentationTheme
public let isModal: Bool
public let displaySeparators: Bool public let displaySeparators: Bool
public let extendsItemHighlightToSection: Bool public let extendsItemHighlightToSection: Bool
public let background: ListSectionComponent.Background public let background: ListSectionComponent.Background
public init( public init(
theme: PresentationTheme, theme: PresentationTheme,
isModal: Bool = false,
displaySeparators: Bool, displaySeparators: Bool,
extendsItemHighlightToSection: Bool, extendsItemHighlightToSection: Bool,
background: ListSectionComponent.Background background: ListSectionComponent.Background
) { ) {
self.theme = theme self.theme = theme
self.isModal = isModal
self.displaySeparators = displaySeparators self.displaySeparators = displaySeparators
self.extendsItemHighlightToSection = extendsItemHighlightToSection self.extendsItemHighlightToSection = extendsItemHighlightToSection
self.background = background self.background = background
@ -116,7 +119,7 @@ public final class ListSectionContentView: UIView {
backgroundColor = configuration.theme.list.itemHighlightedBackgroundColor backgroundColor = configuration.theme.list.itemHighlightedBackgroundColor
} else { } else {
transition = .easeInOut(duration: 0.2) 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) self.externalContentBackgroundView.updateColor(color: backgroundColor, transition: transition)
@ -144,7 +147,7 @@ public final class ListSectionContentView: UIView {
if self.highlightedItemId != nil && configuration.extendsItemHighlightToSection { if self.highlightedItemId != nil && configuration.extendsItemHighlightToSection {
backgroundColor = configuration.theme.list.itemHighlightedBackgroundColor backgroundColor = configuration.theme.list.itemHighlightedBackgroundColor
} else { } else {
backgroundColor = configuration.theme.list.itemBlocksBackgroundColor backgroundColor = configuration.isModal ? configuration.theme.list.itemModalBlocksBackgroundColor : configuration.theme.list.itemBlocksBackgroundColor
} }
self.externalContentBackgroundView.updateColor(color: backgroundColor, transition: transition) self.externalContentBackgroundView.updateColor(color: backgroundColor, transition: transition)
@ -305,6 +308,7 @@ public final class ListSectionComponent: Component {
public let header: AnyComponent<Empty>? public let header: AnyComponent<Empty>?
public let footer: AnyComponent<Empty>? public let footer: AnyComponent<Empty>?
public let items: [AnyComponentWithIdentity<Empty>] public let items: [AnyComponentWithIdentity<Empty>]
public let isModal: Bool
public let displaySeparators: Bool public let displaySeparators: Bool
public let extendsItemHighlightToSection: Bool public let extendsItemHighlightToSection: Bool
@ -314,6 +318,7 @@ public final class ListSectionComponent: Component {
header: AnyComponent<Empty>?, header: AnyComponent<Empty>?,
footer: AnyComponent<Empty>?, footer: AnyComponent<Empty>?,
items: [AnyComponentWithIdentity<Empty>], items: [AnyComponentWithIdentity<Empty>],
isModal: Bool = false,
displaySeparators: Bool = true, displaySeparators: Bool = true,
extendsItemHighlightToSection: Bool = false extendsItemHighlightToSection: Bool = false
) { ) {
@ -322,6 +327,7 @@ public final class ListSectionComponent: Component {
self.header = header self.header = header
self.footer = footer self.footer = footer
self.items = items self.items = items
self.isModal = isModal
self.displaySeparators = displaySeparators self.displaySeparators = displaySeparators
self.extendsItemHighlightToSection = extendsItemHighlightToSection self.extendsItemHighlightToSection = extendsItemHighlightToSection
} }
@ -342,6 +348,9 @@ public final class ListSectionComponent: Component {
if lhs.items != rhs.items { if lhs.items != rhs.items {
return false return false
} }
if lhs.isModal != rhs.isModal {
return false
}
if lhs.displaySeparators != rhs.displaySeparators { if lhs.displaySeparators != rhs.displaySeparators {
return false return false
} }
@ -448,6 +457,7 @@ public final class ListSectionComponent: Component {
let contentResult = self.contentView.update( let contentResult = self.contentView.update(
configuration: ListSectionContentView.Configuration( configuration: ListSectionContentView.Configuration(
theme: component.theme, theme: component.theme,
isModal: component.isModal,
displaySeparators: component.displaySeparators, displaySeparators: component.displaySeparators,
extendsItemHighlightToSection: component.extendsItemHighlightToSection, extendsItemHighlightToSection: component.extendsItemHighlightToSection,
background: component.background background: component.background
@ -522,17 +532,20 @@ public final class ListSubSectionComponent: Component {
public let theme: PresentationTheme public let theme: PresentationTheme
public let leftInset: CGFloat public let leftInset: CGFloat
public let items: [AnyComponentWithIdentity<Empty>] public let items: [AnyComponentWithIdentity<Empty>]
public let isModal: Bool
public let displaySeparators: Bool public let displaySeparators: Bool
public init( public init(
theme: PresentationTheme, theme: PresentationTheme,
leftInset: CGFloat, leftInset: CGFloat,
items: [AnyComponentWithIdentity<Empty>], items: [AnyComponentWithIdentity<Empty>],
isModal: Bool = false,
displaySeparators: Bool = true displaySeparators: Bool = true
) { ) {
self.theme = theme self.theme = theme
self.leftInset = leftInset self.leftInset = leftInset
self.items = items self.items = items
self.isModal = isModal
self.displaySeparators = displaySeparators self.displaySeparators = displaySeparators
} }
@ -546,6 +559,9 @@ public final class ListSubSectionComponent: Component {
if lhs.items != rhs.items { if lhs.items != rhs.items {
return false return false
} }
if lhs.isModal != rhs.isModal {
return false
}
if lhs.displaySeparators != rhs.displaySeparators { if lhs.displaySeparators != rhs.displaySeparators {
return false return false
} }
@ -615,6 +631,7 @@ public final class ListSubSectionComponent: Component {
let contentResult = self.contentView.update( let contentResult = self.contentView.update(
configuration: ListSectionContentView.Configuration( configuration: ListSectionContentView.Configuration(
theme: component.theme, theme: component.theme,
isModal: component.isModal,
displaySeparators: component.displaySeparators, displaySeparators: component.displaySeparators,
extendsItemHighlightToSection: false, extendsItemHighlightToSection: false,
background: .none(clipped: false) background: .none(clipped: false)

View File

@ -8250,9 +8250,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
} }
private func openReport(type: PeerInfoReportType, contextController: ContextControllerProtocol?, backAction: ((ContextControllerProtocol) -> Void)?) { private func openReport(type: PeerInfoReportType, contextController: ContextControllerProtocol?, backAction: ((ContextControllerProtocol) -> Void)?) {
guard let controller = self.controller else {
return
}
self.view.endEditing(true) self.view.endEditing(true)
switch type { switch type {
@ -8291,20 +8288,28 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
]) ])
self.controller?.present(actionSheet, in: .window(.root)) self.controller?.present(actionSheet, in: .window(.root))
default: default:
let options: [PeerReportOption] contextController?.dismiss()
if case .user = type {
options = [.spam, .fake, .violence, .pornography, .childAbuse] self.context.sharedContext.makeContentReportScreen(context: self.context, subject: .peer(self.peerId), forceDark: false, present: { [weak self] controller in
} else { self?.controller?.push(controller)
options = [.spam, .fake, .violence, .pornography, .childAbuse, .copyright, .other] }, completion: {
}
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)
}
}
}) })
// 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.view.endEditing(true)
strongSelf.controller?.present(peerReportOptionsController(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), passthrough: false, present: { c, a in strongSelf.context.sharedContext.makeContentReportScreen(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), forceDark: false, present: { [weak self] controller in
self?.controller?.present(c, in: .window(.root), with: a) self?.controller?.push(controller)
}, push: { c in }, completion: {})
self?.controller?.push(c)
}, completion: { _, _ in }), in: .window(.root))
}, displayCopyProtectionTip: { [weak self] node, save in }, displayCopyProtectionTip: { [weak self] node, save in
if let strongSelf = self, let peer = strongSelf.data?.peer, let messageIds = strongSelf.state.selectedMessageIds, !messageIds.isEmpty { if let strongSelf = self, let peer = strongSelf.data?.peer, let messageIds = strongSelf.state.selectedMessageIds, !messageIds.isEmpty {
let _ = (strongSelf.context.engine.data.get(EngineDataMap( let _ = (strongSelf.context.engine.data.get(EngineDataMap(
@ -12284,6 +12285,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
private weak var requestsContext: PeerInvitationImportersContext? private weak var requestsContext: PeerInvitationImportersContext?
fileprivate let starsContext: StarsContext? fileprivate let starsContext: StarsContext?
private let switchToRecommendedChannels: Bool private let switchToRecommendedChannels: Bool
private let switchToGifts: Bool
private let chatLocation: ChatLocation private let chatLocation: ChatLocation
private let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil) private let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
@ -12340,7 +12342,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, 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<PresentationData, NoError>)?, 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.context = context
self.updatedPresentationData = updatedPresentationData self.updatedPresentationData = updatedPresentationData
self.peerId = peerId self.peerId = peerId
@ -12354,6 +12356,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
self.hintGroupInCommon = hintGroupInCommon self.hintGroupInCommon = hintGroupInCommon
self.requestsContext = requestsContext self.requestsContext = requestsContext
self.switchToRecommendedChannels = switchToRecommendedChannels self.switchToRecommendedChannels = switchToRecommendedChannels
self.switchToGifts = switchToGifts
if let forumTopicThread = forumTopicThread { if let forumTopicThread = forumTopicThread {
self.chatLocation = .replyThread(message: forumTopicThread) self.chatLocation = .replyThread(message: forumTopicThread)
@ -12694,7 +12697,13 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
} }
override public func loadDisplayNode() { 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.accountsAndPeers.set(self.accountsAndPeers.get() |> map { $0.1 })
self.controllerNode.activeSessionsContextAndCount.set(self.activeSessionsContextAndCount.get()) self.controllerNode.activeSessionsContextAndCount.set(self.activeSessionsContextAndCount.get())
self.cachedDataPromise.set(self.controllerNode.cachedDataPromise.get()) self.cachedDataPromise.set(self.controllerNode.cachedDataPromise.get())

View File

@ -87,6 +87,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
guard let self else { guard let self else {
return return
} }
let isFirstTime = starsProducts == nil
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } 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.statusPromise.set(.single(PeerInfoStatusData(text: presentationData.strings.SharedMedia_GiftCount(state.count ?? 0), isActivity: true, key: .gifts)))
self.starsProducts = state.gifts self.starsProducts = state.gifts
@ -96,7 +97,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
self.ready.set(.single(true)) 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) { 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 { if let starsProducts = self.starsProducts, let params = self.currentParams {
let optionSpacing: CGFloat = 10.0 let optionSpacing: CGFloat = 10.0
let sideInset = params.sideInset + 16.0 let sideInset = params.sideInset + 16.0
@ -140,13 +141,14 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let itemId = AnyHashable(product.date) let itemId = AnyHashable(product.date)
validIds.append(itemId) validIds.append(itemId)
let itemTransition = ComponentTransition.immediate var itemTransition = transition
let visibleItem: ComponentView<Empty> let visibleItem: ComponentView<Empty>
if let current = self.starsItems[itemId] { if let current = self.starsItems[itemId] {
visibleItem = current visibleItem = current
} else { } else {
visibleItem = ComponentView() visibleItem = ComponentView()
self.starsItems[itemId] = visibleItem self.starsItems[itemId] = visibleItem
itemTransition = .immediate
} }
var isVisible = false 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 var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * starsOptionSize.height + 60.0 + 16.0
if self.peerId == self.context.account.peerId { if self.peerId == self.context.account.peerId {
@ -354,7 +376,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
} }
self.scrollNode.view.isScrollEnabled = !isScrollingLockedAtTop self.scrollNode.view.isScrollEnabled = !isScrollingLockedAtTop
self.updateScrolling() self.updateScrolling(transition: ComponentTransition(transition))
} }
public func findLoadedMessage(id: MessageId) -> Message? { public func findLoadedMessage(id: MessageId) -> Message? {

View File

@ -17,7 +17,7 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit", "//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AccountContext", "//submodules/AccountContext",
"//submodules/AttachmentUI", "//submodules/AttachmentUI",
"//submodules/PremiumUI", "//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -5,15 +5,17 @@ import AsyncDisplayKit
import ComponentFlow import ComponentFlow
import SwiftSignalKit import SwiftSignalKit
import AccountContext import AccountContext
import PremiumUI
import AttachmentUI import AttachmentUI
import GiftOptionsScreen
public class PremiumGiftAttachmentScreen: PremiumGiftScreen, AttachmentContainable { public class PremiumGiftAttachmentScreen: GiftOptionsScreen, AttachmentContainable {
public var requestAttachmentMenuExpansion: () -> Void = {} public var requestAttachmentMenuExpansion: () -> Void = {}
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in }
public var parentController: () -> ViewController? = { public var parentController: () -> ViewController? = {
return nil return nil
} }
public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in }
public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in }
public var cancelPanGesture: () -> Void = { } public var cancelPanGesture: () -> Void = { }
public var isContainerPanning: () -> Bool = { return false } public var isContainerPanning: () -> Bool = { return false }
public var isContainerExpanded: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false }
@ -25,17 +27,16 @@ public class PremiumGiftAttachmentScreen: PremiumGiftScreen, AttachmentContainab
} }
private final class PremiumGiftContext: AttachmentMediaPickerContext { private final class PremiumGiftContext: AttachmentMediaPickerContext {
private weak var controller: PremiumGiftScreen? private weak var controller: GiftOptionsScreen?
public var mainButtonState: Signal<AttachmentMainButtonState?, NoError> { public var mainButtonState: Signal<AttachmentMainButtonState?, NoError> {
return self.controller?.mainButtonStatePromise.get() ?? .single(nil) return .single(nil)
} }
init(controller: PremiumGiftScreen) { init(controller: GiftOptionsScreen) {
self.controller = controller self.controller = controller
} }
func mainButtonAction() { func mainButtonAction() {
self.controller?.mainButtonPressed()
} }
} }

View File

@ -1037,7 +1037,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
guard let self else { guard let self else {
return 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 { guard let self, let peerId = peerIds.first else {
return return
} }

View File

@ -741,6 +741,9 @@ public final class TextFieldComponent: Component {
} }
self.insertText(NSAttributedString(string: insertString)) self.insertText(NSAttributedString(string: insertString))
} else if (range.length == 0 && text == "\n"), let returnKeyAction = component.returnKeyAction {
returnKeyAction()
return false
} }
return false return false
} }

View File

@ -588,19 +588,17 @@ extension ChatControllerImpl {
strongSelf.controllerNavigationDisposable.set(nil) strongSelf.controllerNavigationDisposable.set(nil)
} }
case .gift: 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 let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions
if !premiumGiftOptions.isEmpty { if !premiumGiftOptions.isEmpty {
let controller = PremiumGiftAttachmentScreen(context: context, peerIds: [peer.id], options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in let controller = PremiumGiftAttachmentScreen(context: context, starsContext: starsContext, peerId: peer.id, premiumOptions: premiumGiftOptions, completion: { [weak self] in
if let strongSelf = self { guard let self else {
strongSelf.push(c) return
}
}, completion: { [weak self] in
if let strongSelf = self {
strongSelf.hintPlayNextOutgoingGift()
strongSelf.attachmentController?.dismiss(animated: true)
} }
self.hintPlayNextOutgoingGift()
self.attachmentController?.dismiss(animated: true)
}) })
completion(controller, controller.mediaPickerContext) completion(controller, controller.mediaPickerContext)
strongSelf.controllerNavigationDisposable.set(nil) strongSelf.controllerNavigationDisposable.set(nil)

View File

@ -14,6 +14,7 @@ import AttachmentUI
import SearchBarNode import SearchBarNode
import ChatSendAudioMessageContextPreview import ChatSendAudioMessageContextPreview
import ChatSendMessageActionUI import ChatSendMessageActionUI
import ContextUI
class ContactSelectionControllerImpl: ViewController, ContactSelectionController, PresentableController, AttachmentContainable { class ContactSelectionControllerImpl: ViewController, ContactSelectionController, PresentableController, AttachmentContainable {
private let context: AccountContext private let context: AccountContext
@ -42,6 +43,9 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
private let multipleSelection: Bool private let multipleSelection: Bool
private let requirePhoneNumbers: Bool private let requirePhoneNumbers: Bool
private let openProfile: ((EnginePeer) -> Void)?
private let sendMessage: ((EnginePeer) -> Void)?
private var _ready = Promise<Bool>() private var _ready = Promise<Bool>()
override var ready: Promise<Bool> { override var ready: Promise<Bool> {
return self._ready return self._ready
@ -105,6 +109,9 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
self.multipleSelection = params.multipleSelection self.multipleSelection = params.multipleSelection
self.requirePhoneNumbers = params.requirePhoneNumbers self.requirePhoneNumbers = params.requirePhoneNumbers
self.openProfile = params.openProfile
self.sendMessage = params.sendMessage
self.presentationData = params.updatedPresentationData?.initial ?? params.context.sharedContext.currentPresentationData.with { $0 } self.presentationData = params.updatedPresentationData?.initial ?? params.context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
@ -219,15 +226,15 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
} }
self.contactsNode.requestOpenPeerFromSearch = { [weak self] peer in 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.contactsNode.contactListNode.activateSearch = { [weak self] in
self?.activateSearch() self?.activateSearch()
} }
self.contactsNode.contactListNode.openPeer = { [weak self] peer, action, _, _ in self.contactsNode.contactListNode.openPeer = { [weak self] peer, action, node, gesture in
self?.openPeer(peer: peer, action: action) self?.openPeer(peer: peer, action: action, node: node, gesture: gesture)
} }
self.contactsNode.contactListNode.suppressPermissionWarning = { [weak self] in 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.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
self.confirmationDisposable.set((self.confirmation(peer) |> deliverOnMainQueue).startStrict(next: { [weak self] value in self.confirmationDisposable.set((self.confirmation(peer) |> deliverOnMainQueue).startStrict(next: { [weak self] value in
if let strongSelf = self { if let strongSelf = self {
@ -477,3 +517,17 @@ final class ContactsPickerContext: AttachmentMediaPickerContext {
func mainButtonAction() { 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)
}
}

View File

@ -41,6 +41,7 @@ final class ContactSelectionControllerNode: ASDisplayNode {
var requestMultipleAction: ((_ silent: Bool, _ scheduleTime: Int32?, _ parameters: ChatSendMessageActionSheetController.SendParameters?) -> Void)? var requestMultipleAction: ((_ silent: Bool, _ scheduleTime: Int32?, _ parameters: ChatSendMessageActionSheetController.SendParameters?) -> Void)?
var dismiss: (() -> Void)? var dismiss: (() -> Void)?
var cancelSearch: (() -> Void)? var cancelSearch: (() -> Void)?
var openPeerMore: ((ContactListPeer, ASDisplayNode?, ContextGesture?) -> Void)?
var presentationData: PresentationData { var presentationData: PresentationData {
didSet { didSet {

View File

@ -2204,21 +2204,90 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return PremiumLimitScreen(context: context, subject: mappedSubject, count: count, forceDark: forceDark, cancel: cancel, action: action) 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<AccountPrivacySettings?>
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 { public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var presentBirthdayPickerImpl: (() -> Void)? var presentBirthdayPickerImpl: (() -> Void)?
var starsMode: ContactSelectionControllerMode = .generic var mode: ContactSelectionControllerMode = .generic
var currentBirthdays: [EnginePeer.Id: TelegramBirthday]? var currentBirthdays: [EnginePeer.Id: TelegramBirthday]?
if case let .chatList(birthdays) = source, let birthdays, !birthdays.isEmpty { if case let .chatList(birthdays) = source, let birthdays, !birthdays.isEmpty {
starsMode = .starsGifting(birthdays: birthdays, hasActions: true) mode = .starsGifting(birthdays: birthdays, hasActions: true)
currentBirthdays = birthdays currentBirthdays = birthdays
} else if case let .settings(birthdays) = source, let birthdays, !birthdays.isEmpty { } 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 currentBirthdays = birthdays
} else { } else {
starsMode = .starsGifting(birthdays: nil, hasActions: true) mode = .starsGifting(birthdays: nil, hasActions: true)
} }
let contactOptions: Signal<[ContactListAdditionalOption], NoError> let contactOptions: Signal<[ContactListAdditionalOption], NoError>
@ -2247,121 +2316,34 @@ public final class SharedAccountContextImpl: SharedAccountContext {
var sendMessageImpl: ((EnginePeer) -> Void)? var sendMessageImpl: ((EnginePeer) -> Void)?
//TODO:localize //TODO:localize
let controller: ViewController let options = Promise<[PremiumGiftCodeOption]>()
// if case .stars = source { options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil))
// let options = Promise<[StarsGiftOption]>() let controller = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(
// options.set(context.engine.payments.starsGiftOptions(peerId: nil)) context: context,
let options = Promise<[PremiumGiftCodeOption]>() mode: mode,
options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil)) autoDismiss: false,
let contactsController = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( title: { strings in return "Gift Premium or Stars" },
context: context, options: contactOptions,
mode: starsMode, openProfile: { peer in
autoDismiss: false, openProfileImpl?(peer)
title: { strings in return "Gift Premium or Stars" }, },
options: contactOptions, sendMessage: { peer in
openProfile: { peer in sendMessageImpl?(peer)
openProfileImpl?(peer) }
}, ))
sendMessage: { peer in let _ = combineLatest(queue: Queue.mainQueue(), controller.result, options.get())
sendMessageImpl?(peer) .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 sendMessageImpl = { [weak self, weak controller] peer in
guard let self, let controller, let navigationController = controller.navigationController as? NavigationController else { guard let self, let controller, let navigationController = controller.navigationController as? NavigationController else {
@ -2864,6 +2846,7 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation
var hintGroupInCommon: PeerId? var hintGroupInCommon: PeerId?
var forumTopicThread: ChatReplyThreadMessage? var forumTopicThread: ChatReplyThreadMessage?
var isMyProfile = false var isMyProfile = false
var switchToGifts = false
switch mode { switch mode {
case let .nearbyPeer(distance): case let .nearbyPeer(distance):
@ -2880,10 +2863,13 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation
forumTopicThread = thread forumTopicThread = thread
case .myProfile: case .myProfile:
isMyProfile = true isMyProfile = true
case .myProfileGifts:
isMyProfile = true
switchToGifts = true
default: default:
break 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 { } else if peer is TelegramSecretChat {
return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: []) return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [])
} }

View File

@ -49,6 +49,8 @@ public enum DeviceModel: CaseIterable, Equatable {
.iPhone15Plus, .iPhone15Plus,
.iPhone15Pro, .iPhone15Pro,
.iPhone15ProMax, .iPhone15ProMax,
.iPhone16,
.iPhone16Plus,
.iPhone16Pro, .iPhone16Pro,
.iPhone16ProMax .iPhone16ProMax
] ]
@ -118,6 +120,8 @@ public enum DeviceModel: CaseIterable, Equatable {
case iPhone15Pro case iPhone15Pro
case iPhone15ProMax case iPhone15ProMax
case iPhone16
case iPhone16Plus
case iPhone16Pro case iPhone16Pro
case iPhone16ProMax case iPhone16ProMax
@ -223,6 +227,10 @@ public enum DeviceModel: CaseIterable, Equatable {
return ["iPhone16,1"] return ["iPhone16,1"]
case .iPhone15ProMax: case .iPhone15ProMax:
return ["iPhone16,2"] return ["iPhone16,2"]
case .iPhone16:
return ["iPhone17,3"]
case .iPhone16Plus:
return ["iPhone17,4"]
case .iPhone16Pro: case .iPhone16Pro:
return ["iPhone17,1"] return ["iPhone17,1"]
case .iPhone16ProMax: case .iPhone16ProMax:
@ -332,6 +340,10 @@ public enum DeviceModel: CaseIterable, Equatable {
return "iPhone 15 Pro" return "iPhone 15 Pro"
case .iPhone15ProMax: case .iPhone15ProMax:
return "iPhone 15 Pro Max" return "iPhone 15 Pro Max"
case .iPhone16:
return "iPhone 16"
case .iPhone16Plus:
return "iPhone 16 Plus"
case .iPhone16Pro: case .iPhone16Pro:
return "iPhone 16 Pro" return "iPhone 16 Pro"
case .iPhone16ProMax: case .iPhone16ProMax:

View File

@ -48,7 +48,7 @@ CONFIGURE_FLAGS="--enable-cross-compile --disable-programs \
--enable-libvpx \ --enable-libvpx \
--enable-audiotoolbox \ --enable-audiotoolbox \
--enable-bsf=aac_adtstoasc,vp9_superframe,h264_mp4toannexb \ --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-encoder=libvpx_vp9,aac_at \
--enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska,mpegts \ --enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska,mpegts \
--enable-parser=aac,h264,mp3,libopus \ --enable-parser=aac,h264,mp3,libopus \