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