Merge commit 'a30ab38ce41f3389fee9054eb99875382365a6b7' into beta

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

View File

@ -568,6 +568,7 @@ public enum PeerInfoControllerMode {
case forumTopic(thread: ChatReplyThreadMessage)
case 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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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";

View File

@ -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

View File

@ -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] {

View File

@ -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

View File

@ -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))

View File

@ -731,6 +731,34 @@ public extension TelegramEngine.EngineData.Item {
}
}
public struct StarGiftsCount: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = Int32?
fileprivate var id: EnginePeer.Id
public var mapKey: EnginePeer.Id {
return self.id
}
public init(id: EnginePeer.Id) {
self.id = id
}
var key: PostboxViewKey {
return .cachedPeerData(peerId: self.id)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? CachedPeerDataView else {
preconditionFailure()
}
if let cachedData = view.cachedPeerData as? CachedUserData {
return cachedData.starGiftsCount
} else {
return nil
}
}
}
public struct LinkedDiscussionPeerId: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = EnginePeerCachedInfoItem<EnginePeer.Id?>

View File

@ -191,7 +191,7 @@ func _internal_keepCachedStarGiftsUpdated(postbox: Postbox, network: Network) ->
func managedStarGiftsUpdates(postbox: Postbox, network: Network) -> Signal<Never, NoError> {
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> {

View File

@ -228,7 +228,8 @@ private final class SheetPageContent: CombinedComponent {
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.ReportAd_Help_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
}
)),
items: items
items: items,
isModal: true
),
environment: {},
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),

View File

@ -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",

View File

@ -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 {

View File

@ -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)
})
}

View File

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

View File

@ -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",

View File

@ -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

View File

@ -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 {

View File

@ -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(

View File

@ -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 = [

View File

@ -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
}

View File

@ -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)

View File

@ -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())

View File

@ -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? {

View File

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

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -2204,21 +2204,90 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return PremiumLimitScreen(context: context, subject: mappedSubject, count: count, forceDark: forceDark, cancel: cancel, action: action)
}
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: [])
}

View File

@ -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:

View File

@ -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 \