This commit is contained in:
isaac 2026-05-29 15:47:41 +02:00
commit bd8886b0f0
594 changed files with 24232 additions and 23505 deletions

View file

@ -185,6 +185,22 @@ public enum WallpaperUrlParameter {
case gradient([UInt32], Int32?)
}
public enum PeerType: Equatable {
case user(isBot: Bool)
case group
case channel
public static func getType(for peer: EnginePeer) -> PeerType {
if case let .user(user) = peer {
return .user(isBot: user.botInfo != nil)
} else if case let .channel(channel) = peer, case .broadcast = channel.info {
return .channel
} else {
return .group
}
}
}
public struct ResolvedBotChoosePeerTypes: OptionSet {
public var rawValue: UInt32
@ -391,6 +407,16 @@ public enum ResolveUrlResult {
case result(ResolvedUrl)
}
public struct OpenUserGeneratedUrlConcealedAlertOption {
public let title: String
public let action: () -> Void
public init(title: String, action: @escaping () -> Void) {
self.title = title
self.action = action
}
}
public enum NavigateToChatKeepStack {
case `default`
case always
@ -735,7 +761,6 @@ public enum PeerInfoControllerMode {
case generic
case calls(messages: [EngineMessage])
case nearbyPeer(distance: Int32)
case group(sourceMessageId: EngineMessage.Id)
case reaction(EngineMessage.Id)
case forumTopic(thread: ChatReplyThreadMessage)
@ -1370,7 +1395,6 @@ public protocol SharedAccountContext: AnyObject {
func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController?
func makeChannelAdminController(context: AccountContext, peerId: EnginePeer.Id, adminId: EnginePeer.Id, initialParticipant: ChannelParticipant) -> ViewController?
func makeDeviceContactInfoController(context: ShareControllerAccountContext, environment: ShareControllerEnvironment, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController
func makePeersNearbyController(context: AccountContext) -> ViewController
func makeComposeController(context: AccountContext) -> ViewController
func makeChatListController(context: AccountContext, location: ChatListControllerLocation, controlsHistoryPreload: Bool, hideNetworkActivityStatus: Bool, previewing: Bool, enableDebugActions: Bool) -> ChatListController
func makeChatController(context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, botStart: ChatControllerInitialBotStart?, mode: ChatControllerPresentationMode, params: ChatControllerParams?) -> ChatController
@ -1441,6 +1465,7 @@ public protocol SharedAccountContext: AnyObject {
func resolveUrl(context: AccountContext, peerId: EnginePeer.Id?, url: String, skipUrlAuth: Bool) -> Signal<ResolvedUrl, NoError>
func resolveUrlWithProgress(context: AccountContext, peerId: EnginePeer.Id?, url: String, skipUrlAuth: Bool) -> Signal<ResolveUrlResult, NoError>
func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, forceUpdate: Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView?, CGRect?) -> Bool)?, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((EnginePeer.Id, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?, progress: Promise<Bool>?, completion: (() -> Void)?)
func openUserGeneratedUrl(context: AccountContext, peerId: EnginePeer.Id?, url: String, webpage: TelegramMediaWebpage?, concealed: Bool, forceConcealed: Bool, skipUrlAuth: Bool, skipConcealedAlert: Bool, forceDark: Bool, present: @escaping (ViewController) -> Void, openResolved: @escaping (ResolvedUrl) -> Void, progress: Promise<Bool>?, alertDisplayUpdated: ((ViewController?) -> Void)?, concealedAlertOption: OpenUserGeneratedUrlConcealedAlertOption?) -> Disposable
func openAddContact(context: AccountContext, peer: EnginePeer?, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void)
func openAddPersonContact(context: AccountContext, peerId: EnginePeer.Id, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void)
func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void)
@ -1468,14 +1493,14 @@ public protocol SharedAccountContext: AnyObject {
func makeGiftStoreController(context: AccountContext, peerId: EnginePeer.Id, gift: StarGift.Gift) -> ViewController
func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController
func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController
func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], actionTitle: String?, isEditing: Bool, expandIfNeeded: Bool, parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView?, CGRect?) -> Bool)?, actionPerformed: ((Bool) -> Void)?) -> ViewController
func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], actionTitle: String?, isEditing: Bool, expandIfNeeded: Bool, parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView?, CGRect?) -> Bool)?, actionPerformed: (([StickerPackScreenActionResult]) -> Void)?) -> ViewController
func makeCameraScreen(context: AccountContext, mode: CameraScreenMode, cameraHolder: Any?, transitionIn: CameraScreenTransitionIn?, transitionOut: @escaping (Bool) -> CameraScreenTransitionOut?, completion: @escaping (Any, @escaping () -> Void) -> Void, transitionedOut: (() -> Void)?) -> ViewController
func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController
func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: (url: String, name: String?)?, remainingCount: Int32, completion: @escaping ([MediaEditorScreenResult], MediaEditorTransitionOutExternalState, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController
func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, transitionOut: @escaping () -> BotPreviewEditorTransitionOut?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController
func makeStickerEditorScreen(context: AccountContext, source: Any?, mode: StickerEditorMode, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, [String], @escaping () -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController
func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
func makeAvatarMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> (ViewController?, Any?)
func makeAvatarMediaPickerScreen(context: AccountContext, peerType: PeerType, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> (ViewController?, Any?)
func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController
func makeStickerPickerScreen(context: AccountContext, inputData: Promise<StickerPickerInput>, completion: @escaping (FileMediaReference) -> Void) -> ViewController
func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController
@ -1521,6 +1546,7 @@ public protocol SharedAccountContext: AnyObject {
func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController
func makeIncomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void) -> ViewController
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?, verifyAgeCompletion: ((Int) -> Void)?)
func openJoinChatWebView(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, webView: JoinChatWebView)
func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal<AffiliateProgramSetupScreenInitialData, NoError>
func makeAffiliateProgramSetupScreen(context: AccountContext, initialData: AffiliateProgramSetupScreenInitialData) -> ViewController
func makeAffiliateProgramJoinScreen(context: AccountContext, sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, revenuePerUser: Double, mode: JoinAffiliateProgramScreenMode) -> ViewController
@ -1530,7 +1556,7 @@ public protocol SharedAccountContext: AnyObject {
func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController
func makeSendInviteLinkScreen(context: AccountContext, subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer], theme: PresentationTheme?) -> ViewController
func makeCocoonInfoScreen(context: AccountContext) -> ViewController
func makeLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, text: String, link: String?, apply: @escaping (String?) -> Void) -> ViewController
func makeLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, text: String, link: String?, apply: @escaping (String?, TelegramMediaWebpage?) -> Void) -> ViewController
@available(iOS 13.0, *)
func makePostSuggestionsSettingsScreen(context: AccountContext, peerId: EnginePeer.Id) async -> ViewController
@ -1637,6 +1663,23 @@ public protocol ChatSendMessageActionSheetController: ViewController {
typealias SendParameters = ChatSendMessageActionSheetControllerSendParameters
}
public enum StickerPackScreenActionKind {
case add
case remove(positionInList: Int)
}
public struct StickerPackScreenActionResult {
public let info: StickerPackCollectionInfo
public let items: [StickerPackItem]
public let action: StickerPackScreenActionKind
public init(info: StickerPackCollectionInfo, items: [StickerPackItem], action: StickerPackScreenActionKind) {
self.info = info
self.items = items
self.action = action
}
}
public protocol AccountContext: AnyObject {
var sharedContext: SharedAccountContext { get }
var account: Account { get }

View file

@ -264,6 +264,23 @@ public extension ChatMessageItemAssociatedData {
return false
}
}
func isPollVotingRestricted(poll: TelegramMediaPoll, accountTestingEnvironment: Bool, currentTimestamp: Int32) -> Bool {
if !poll.countries.isEmpty, let accountCountry = self.accountCountry, !poll.countries.contains(accountCountry) {
return true
}
if poll.restrictToSubscribers {
let period: Int32 = accountTestingEnvironment ? 5 * 60 : 24 * 60 * 60
if !self.isParticipant {
return true
} else if let invitedOn = self.invitedOn, invitedOn + period > currentTimestamp {
return true
}
}
return false
}
}
public enum ChatControllerInteractionLongTapAction {

View file

@ -78,7 +78,7 @@ public final class AdInfoScreen: ViewController {
self.scrollNode = ASScrollNode()
self.scrollNode.view.showsVerticalScrollIndicator = true
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.scrollsToTop = true
self.scrollNode.view.scrollsToTop = false
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.canCancelContentTouches = true
if #available(iOS 11.0, *) {

View file

@ -341,6 +341,7 @@
textView.opaque = NO;
}
textView.textContainerInset = self.textContainerInset;
textView.scrollsToTop = false;
// Configure textView with UITextInputTraits
{
@ -366,6 +367,7 @@
_placeholderTextKitComponents.textView = [[ASTextKitComponentsTextView alloc] initWithFrame:CGRectZero textContainer:_placeholderTextKitComponents.textContainer];
_placeholderTextKitComponents.textView.userInteractionEnabled = NO;
_placeholderTextKitComponents.textView.accessibilityElementsHidden = YES;
_placeholderTextKitComponents.textView.scrollsToTop = false;
configureTextView(_placeholderTextKitComponents.textView);
// Create and configure our text view.

View file

@ -26,7 +26,6 @@ swift_library(
"//submodules/DirectionalPanGesture:DirectionalPanGesture",
"//submodules/AttachmentTextInputPanelNode:AttachmentTextInputPanelNode",
"//submodules/ChatSendMessageActionUI:ChatSendMessageActionUI",
"//submodules/ChatTextLinkEditUI:ChatTextLinkEditUI",
"//submodules/ContextUI:ContextUI",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/PhotoResources:PhotoResources",

View file

@ -33,6 +33,7 @@ public enum AttachmentButtonType: Equatable {
case sticker
case emoji
case audio
case link
case standalone
public var key: String {
@ -61,6 +62,8 @@ public enum AttachmentButtonType: Equatable {
return "emoji"
case .audio:
return "audio"
case .link:
return "link"
case .standalone:
return "standalone"
}
@ -140,6 +143,12 @@ public enum AttachmentButtonType: Equatable {
} else {
return false
}
case .link:
if case .link = rhs {
return true
} else {
return false
}
case .standalone:
if case .standalone = rhs {
return true

View file

@ -12,7 +12,6 @@ import AccountContext
import AttachmentTextInputPanelNode
import ChatPresentationInterfaceState
import ChatSendMessageActionUI
import ChatTextLinkEditUI
import PhotoResources
import AnimatedStickerComponent
import SemanticStatusNode
@ -244,6 +243,10 @@ private final class AttachButtonComponent: CombinedComponent {
case .audio:
name = strings.Attachment_Audio
imageName = "Chat/Attach Menu/Audio"
case .link:
//TODO:localize
name = "Link"
imageName = "Chat/Attach Menu/Link"
case let .app(bot):
botPeer = bot.peer
name = bot.shortName
@ -1248,7 +1251,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
}
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let controller = chatTextLinkEditController(context: strongSelf.context, updatedPresentationData: (presentationData, .never()), text: text?.string ?? "", link: link, apply: { [weak self] link in
let controller = strongSelf.context.sharedContext.makeLinkEditController(context: strongSelf.context, updatedPresentationData: (presentationData, .never()), text: text?.string ?? "", link: link, apply: { [weak self] link, _ in
if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange {
if let link = link {
strongSelf.updateChatPresentationInterfaceState(animated: true, { state in
@ -1420,7 +1423,6 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
})
})
}, openScheduledMessages: {
}, openPeersNearby: {
}, displaySearchResultsTooltip: { _, _ in
}, unarchivePeer: {
}, scrollToTop: {
@ -1561,12 +1563,22 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
if let data = view.cachedData as? CachedUserData {
return data.sendPaidMessageStars
} else if let channel = peerViewMainPeer(view) as? TelegramChannel {
if channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = view.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
return nil
} else if let cachedData = view.cachedData as? CachedChannelData, let sendPaidMessageStarsValue = cachedData.sendPaidMessageStars, sendPaidMessageStarsValue == .zero {
return nil
if channel.isMonoForum {
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = view.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
return nil
} else if let cachedData = view.cachedData as? CachedChannelData, let value = cachedData.sendPaidMessageStars, value == .zero {
return nil
} else {
return channel.sendPaidMessageStars
}
} else {
return channel.sendPaidMessageStars
if channel.flags.contains(.isCreator) || channel.adminRights != nil {
return nil
} else if let cachedData = view.cachedData as? CachedChannelData, let value = cachedData.sendPaidMessageStars {
return value == .zero ? nil : value
} else {
return channel.sendPaidMessageStars
}
}
} else {
return nil
@ -1598,6 +1610,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
self.containerNode.layer.cornerCurve = .continuous
}
self.scrollNode.view.scrollsToTop = false
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.showsVerticalScrollIndicator = false
@ -1990,6 +2003,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
var width = layout.size.width
if buttons.count == 3 {
width = smallPanelWidth
} else if buttons.count == 4 {
width = 300
}
var panelSideInset: CGFloat
@ -2001,7 +2016,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
}
var distanceBetweenNodes = floorToScreenPixels((width - panelSideInset * 2.0 - self.buttonSize.width) / CGFloat(max(1, buttons.count - 1)))
if buttons.count == 3 {
if buttons.count == 3 || buttons.count == 4 {
distanceBetweenNodes = floorToScreenPixels((width - panelSideInset * 2.0 - 32.0) / CGFloat(max(1, buttons.count - 1)))
}
let internalWidth = distanceBetweenNodes * CGFloat(max(0, buttons.count - 1))
@ -2017,7 +2032,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
case .legacy:
leftNodeOriginX = (width - internalWidth) / 2.0
}
if buttons.count == 3 {
if buttons.count == 3 || buttons.count == 4 {
leftNodeOriginX = floor((layout.size.width - width) / 2.0) + 16.0
}
@ -2171,6 +2186,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
accessibilityTitle = "Emoji"
case .audio:
accessibilityTitle = self.presentationData.strings.Attachment_Audio
case .link:
//TODO:localize
accessibilityTitle = "Link"
case let .app(bot):
accessibilityTitle = bot.shortName
case .standalone:
@ -2544,6 +2562,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
let buttonsPanelWidth: CGFloat
if buttons.count == 3 {
buttonsPanelWidth = smallPanelWidth
} else if buttons.count == 4 {
buttonsPanelWidth = 300
} else {
buttonsPanelWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - panelSideInset * 2.0
}

View file

@ -47,6 +47,7 @@ swift_library(
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/Utils/DeviceModel",
"//submodules/PresentationDataUtils",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
],
visibility = [
"//visibility:public",

View file

@ -386,7 +386,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
let buttonHeight: CGFloat = 50.0
let buttonHeight: CGFloat = 52.0
let bottomPanelPadding: CGFloat = 12.0
let titleSpacing: CGFloat = -24.0
let listSpacing: CGFloat = 12.0
@ -416,7 +416,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
listView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - listSize.width) / 2.0), y: originY), size: listSize)
}
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 10.0 : bottomPanelPadding
let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset
let priceString: String
@ -463,7 +463,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight)
containerSize: CGSize(width: availableSize.width - 30.0 * 2.0, height: buttonHeight)
)
if let buttonView = self.button.view {
if buttonView.superview == nil {

View file

@ -11,10 +11,21 @@ swift_library(
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/LocalAuth:LocalAuth",
"//submodules/AccountContext:AccountContext",
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/Components/ResizableSheetComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent",
"//submodules/ItemListUI:ItemListUI",
"//submodules/PasswordSetupUI:PasswordSetupUI",
"//submodules/PhotoResources:PhotoResources",

View file

@ -117,6 +117,7 @@ final class BotCheckoutActionButton: HighlightTrackingButtonNode {
} else {
applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black)
}
applePayButton.cornerRadius = BotCheckoutActionButton.height * 0.5
applePayButton.addTarget(self, action: #selector(self.applePayButtonPressed), for: .touchUpInside)
self.view.addSubview(applePayButton)
self.applePayButton = applePayButton

View file

@ -141,9 +141,10 @@ public final class BotCheckoutController: ViewController {
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self._hasGlassStyle = true
var title = self.presentationData.strings.Checkout_Title
if invoice.flags.contains(.isTest) {
@ -151,7 +152,7 @@ public final class BotCheckoutController: ViewController {
}
self.title = title
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed))
}
required public init(coder aDecoder: NSCoder) {

View file

@ -1077,13 +1077,13 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
if methods.isEmpty {
openNewCard(nil, nil)
} else {
strongSelf.present(BotCheckoutPaymentMethodSheetController(context: strongSelf.context, currentMethod: strongSelf.currentPaymentMethod, methods: methods, applyValue: { method in
strongSelf.controller?.push(BotCheckoutPaymentMethodScreen(context: strongSelf.context, currentMethod: strongSelf.currentPaymentMethod, methods: methods, applyValue: { method in
applyPaymentMethod(method)
}, newCard: {
openNewCard(nil, nil)
}, otherMethod: { url, title in
openNewCard(url, title)
}), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}))
}
}
}
@ -1091,14 +1091,14 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
openShippingMethodImpl = { [weak self] in
if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let shippingOptions = strongSelf.currentValidatedFormInfo?.shippingOptions, !shippingOptions.isEmpty {
strongSelf.controller?.view.endEditing(true)
strongSelf.present(BotCheckoutPaymentShippingOptionSheetController(context: strongSelf.context, currency: paymentFormValue.invoice.currency, options: shippingOptions, currentId: strongSelf.currentShippingOptionId, applyValue: { id in
strongSelf.controller?.push(BotCheckoutShippingOptionScreen(context: strongSelf.context, currency: paymentFormValue.invoice.currency, options: shippingOptions, currentId: strongSelf.currentShippingOptionId, applyValue: { id in
if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo {
strongSelf.currentShippingOptionId = id
strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount)))
strongSelf.updateActionButton()
}
}), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}))
}
}

View file

@ -206,7 +206,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
}
let contentSize = CGSize(width: params.width, height: contentHeight)
let insets = itemListNeighborsPlainInsets(neighbors)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)

View file

@ -60,14 +60,16 @@ final class BotCheckoutInfoController: ViewController {
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass))
self._hasGlassStyle = true
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
self.doneItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed))
self.title = self.presentationData.strings.CheckoutInfo_Title
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.rightBarButtonItem = self.doneItem
self.doneItem?.isEnabled = false
}
@ -84,13 +86,13 @@ final class BotCheckoutInfoController: ViewController {
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}, openCountrySelection: { [weak self] in
if let strongSelf = self {
let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false)
let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false, glass: true)
controller.completeWithCountryCode = { _, id in
if let strongSelf = self {
strongSelf.controllerNode.updateCountry(id)
}
}
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
strongSelf.push(controller)
}
}, updateStatus: { [weak self] status in
if let strongSelf = self {

View file

@ -455,7 +455,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, ASScrollVi
let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
var sideInset: CGFloat = 0.0
if layout.size.width >= 375.0 {
if layout.size.width >= 320.0 {
sideInset = inset
}

View file

@ -55,14 +55,16 @@ final class BotCheckoutNativeCardEntryController: ViewController {
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass))
self._hasGlassStyle = true
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
self.doneItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed))
self.title = self.presentationData.strings.Checkout_NewCard_Title
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.rightBarButtonItem = self.doneItem
self.doneItem?.isEnabled = false
}
@ -78,13 +80,13 @@ final class BotCheckoutNativeCardEntryController: ViewController {
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}, openCountrySelection: { [weak self] in
if let strongSelf = self {
let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false)
let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false, glass: true)
controller.completeWithCountryCode = { _, id in
if let strongSelf = self {
strongSelf.controllerNode.updateCountry(id)
}
}
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
strongSelf.push(controller)
}
}, updateStatus: { [weak self] status in
if let strongSelf = self {

View file

@ -468,7 +468,7 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
var sideInset: CGFloat = 0.0
if layout.size.width >= 375.0 {
if layout.size.width >= 320.0 {
sideInset = inset
}

View file

@ -1,374 +1,96 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import ComponentFlow
import AlertComponent
import AlertInputFieldComponent
private final class BotCheckoutPassworInputFieldNode: ASDisplayNode, UITextFieldDelegate {
private var theme: PresentationTheme
private let backgroundNode: ASImageNode
private let textInputNode: TextFieldNode
private let placeholderNode: ASTextNode
var updateHeight: (() -> Void)?
var complete: (() -> Void)?
var textChanged: ((String) -> Void)?
private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0)
private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0)
var text: String {
get {
return self.textInputNode.textField.text ?? ""
}
set {
self.textInputNode.textField.text = newValue
self.placeholderNode.isHidden = !newValue.isEmpty
}
}
var placeholder: String = "" {
didSet {
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
}
}
init(theme: PresentationTheme, placeholder: String) {
self.theme = theme
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
self.textInputNode = TextFieldNode()
self.textInputNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.foregroundColor: theme.actionSheet.inputTextColor]
self.textInputNode.textField.clipsToBounds = true
self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
self.textInputNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.textInputNode.textField.returnKeyType = .done
self.textInputNode.textField.isSecureTextEntry = true
self.textInputNode.textField.tintColor = theme.actionSheet.controlAccentColor
self.textInputNode.textField.textColor = theme.actionSheet.inputTextColor
self.placeholderNode = ASTextNode()
self.placeholderNode.isUserInteractionEnabled = false
self.placeholderNode.displaysAsynchronously = false
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
super.init()
self.textInputNode.textField.delegate = self
self.textInputNode.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textInputNode)
self.addSubnode(self.placeholderNode)
}
func updateTheme(_ theme: PresentationTheme) {
self.theme = theme
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
self.textInputNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
self.textInputNode.textField.tintColor = self.theme.actionSheet.controlAccentColor
self.textInputNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.foregroundColor: theme.actionSheet.inputTextColor]
self.textInputNode.textField.textColor = self.theme.actionSheet.inputTextColor
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
let textFieldHeight = self.calculateTextFieldMetrics(width: width)
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom))
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size)
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height)))
return panelHeight
}
func activateInput() {
self.textInputNode.becomeFirstResponder()
}
func deactivateInput() {
self.textInputNode.resignFirstResponder()
}
func shake() {
self.layer.addShakeAnimation()
func botCheckoutPasswordEntryController(context: AccountContext, strings: PresentationStrings, passwordTip: String?, cartTitle: String, period: Int32, requiresBiometrics: Bool, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let inputState = AlertInputFieldComponent.ExternalState()
let doneInProgressPromise = ValuePromise<Bool>(false)
let doneIsEnabled: Signal<Bool, NoError> = combineLatest(inputState.valueSignal, doneInProgressPromise.get())
|> map { value, isInProgress in
return !value.isEmpty && !isInProgress
}
@objc func textDidChange() {
self.updateTextNodeText(animated: true)
self.textChanged?(self.textInputNode.textField.text ?? "")
self.placeholderNode.isHidden = !(self.textInputNode.textField.text ?? "").isEmpty
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if text == "\n" {
self.complete?()
return false
}
return true
}
private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height))
return min(61.0, max(33.0, unboundTextFieldHeight))
}
private func updateTextNodeText(animated: Bool) {
let backgroundInsets = self.backgroundInsets
let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width)
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
if !self.bounds.size.height.isEqual(to: panelHeight) {
self.updateHeight?()
}
}
@objc func clearPressed() {
self.textInputNode.textField.text = nil
self.deactivateInput()
}
}
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: strings.Checkout_PasswordEntry_Title)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(strings.Checkout_PasswordEntry_Text(cartTitle).string))
)
))
private final class BotCheckoutPasswordAlertContentNode: AlertContentNode {
private let context: AccountContext
private let period: Int32
private let requiresBiometrics: Bool
private let completion: (TemporaryTwoStepPasswordToken) -> Void
private let titleNode: ASTextNode
private let textNode: ASTextNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private let cancelActionNode: TextAlertContentActionNode
private let doneActionNode: TextAlertContentActionNode
let inputFieldNode: BotCheckoutPassworInputFieldNode
private var validLayout: CGSize?
private var isVerifying = false
private let disposable = MetaDisposable()
private let hapticFeedback = HapticFeedback()
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, passwordTip: String?, cardTitle: String, period: Int32, requiresBiometrics: Bool, cancel: @escaping () -> Void, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) {
self.context = context
self.period = period
self.requiresBiometrics = requiresBiometrics
self.completion = completion
let alertTheme = AlertControllerTheme(presentationTheme: theme, fontSize: .regular)
let titleNode = ASTextNode()
titleNode.attributedText = NSAttributedString(string: strings.Checkout_PasswordEntry_Title, font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center)
titleNode.displaysAsynchronously = false
titleNode.isUserInteractionEnabled = false
titleNode.maximumNumberOfLines = 1
titleNode.truncationMode = .byTruncatingTail
self.titleNode = titleNode
self.textNode = ASTextNode()
self.textNode.attributedText = NSAttributedString(string: strings.Checkout_PasswordEntry_Text(cardTitle).string, font: Font.regular(13.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center)
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
self.inputFieldNode = BotCheckoutPassworInputFieldNode(theme: theme, placeholder: passwordTip ?? "")
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodesSeparator.backgroundColor = theme.actionSheet.opaqueItemSeparatorColor
self.cancelActionNode = TextAlertContentActionNode(theme: alertTheme, action: TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: {
cancel()
}))
var doneImpl: (() -> Void)?
self.doneActionNode = TextAlertContentActionNode(theme: alertTheme, action: TextAlertAction(type: .defaultAction, title: strings.Checkout_PasswordEntry_Pay, action: {
doneImpl?()
}))
self.actionNodes = [self.cancelActionNode, self.doneActionNode]
var actionVerticalSeparators: [ASDisplayNode] = []
if self.actionNodes.count > 1 {
for _ in 0 ..< self.actionNodes.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
separatorNode.backgroundColor = theme.actionSheet.opaqueItemSeparatorColor
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.addSubnode(self.inputFieldNode)
self.inputFieldNode.textChanged = { [weak self] _ in
if let strongSelf = self {
strongSelf.updateState()
}
}
self.updateState()
doneImpl = { [weak self] in
self?.verify()
}
var applyImpl: (() -> Void)?
content.append(AnyComponentWithIdentity(
id: "input",
component: AnyComponent(
AlertInputFieldComponent(
context: context,
placeholder: passwordTip ?? "",
isSecureTextEntry: true,
autocapitalizationType: .none,
autocorrectionType: .no,
isInitiallyFocused: true,
externalState: inputState,
returnKeyAction: {
applyImpl?()
}
)
)
))
var isVerifying = false
let disposable = MetaDisposable()
var dismissImpl: (() -> Void)?
let alertController = AlertScreen(
configuration: AlertScreen.Configuration(allowInputInset: true),
content: content,
actions: [
.init(title: strings.Common_Cancel),
.init(title: strings.Checkout_PasswordEntry_Pay, type: .default, action: {
applyImpl?()
}, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgressPromise.get())
],
updatedPresentationData: (initial: presentationData, signal: context.sharedContext.presentationData)
)
alertController.dismissed = { _ in
disposable.dispose()
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let previousLayout = self.validLayout
self.validLayout = size
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let titleSize = titleNode.measure(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude))
let textSize = self.textNode.measure(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude))
let actionsHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionsHeight))
minActionsWidth += actionTitleSize.width + actionTitleInsets
}
let contentWidth = max(max(titleSize.width, textSize.width), minActionsWidth)
let spacing: CGFloat = 6.0
let titleFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - titleSize.width) / 2.0), y: insets.top), size: titleSize)
transition.updateFrame(node: titleNode, frame: titleFrame)
let textFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - textSize.width) / 2.0), y: titleFrame.maxY + spacing), size: textSize)
transition.updateFrame(node: self.textNode, frame: textFrame)
let resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: titleSize.height + spacing + textSize.height + actionsHeight + insets.top + insets.bottom + 46.0)
let inputFieldWidth = resultSize.width
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: resultSize.height - 36.0 - actionsHeight - insets.bottom, width: resultSize.width, height: inputFieldHeight))
self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
}
separatorIndex += 1
let currentActionWidth: CGFloat
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
let actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionsHeight))
actionOffset += currentActionWidth
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
if previousLayout == nil {
self.inputFieldNode.activateInput()
}
return resultSize
}
@objc func textFieldChanged(_ textField: UITextField) {
self.updateState()
}
private func updateState() {
var enabled = true
if self.isVerifying || self.inputFieldNode.text.isEmpty {
enabled = false
}
self.doneActionNode.actionEnabled = enabled
}
private func verify() {
let text = self.inputFieldNode.text
guard !text.isEmpty else {
applyImpl = {
let password = inputState.value
guard !isVerifying, !password.isEmpty else {
return
}
self.isVerifying = true
self.disposable.set((self.context.engine.auth.requestTemporaryTwoStepPasswordToken(password: text, period: self.period, requiresBiometrics: self.requiresBiometrics) |> deliverOnMainQueue).start(next: { [weak self] token in
if let strongSelf = self {
strongSelf.completion(token)
}
}, error: { [weak self] _ in
if let strongSelf = self {
strongSelf.inputFieldNode.shake()
strongSelf.hapticFeedback.error()
strongSelf.isVerifying = false
strongSelf.updateState()
}
}))
self.updateState()
}
}
func botCheckoutPasswordEntryController(context: AccountContext, strings: PresentationStrings, passwordTip: String?, cartTitle: String, period: Int32, requiresBiometrics: Bool, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) -> AlertController {
var dismissImpl: (() -> Void)?
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: BotCheckoutPasswordAlertContentNode(context: context, theme: presentationData.theme, strings: strings, passwordTip: passwordTip, cardTitle: cartTitle, period: period, requiresBiometrics: requiresBiometrics, cancel: {
dismissImpl?()
}, completion: { token in
completion(token)
dismissImpl?()
}))
dismissImpl = { [weak controller] in
controller?.dismissAnimated()
isVerifying = true
doneInProgressPromise.set(true)
disposable.set((context.engine.auth.requestTemporaryTwoStepPasswordToken(password: password, period: period, requiresBiometrics: requiresBiometrics)
|> deliverOnMainQueue).start(next: { token in
completion(token)
dismissImpl?()
}, error: { _ in
inputState.animateError()
isVerifying = false
doneInProgressPromise.set(false)
}))
}
return controller
dismissImpl = { [weak alertController] in
alertController?.dismiss(completion: nil)
}
return alertController
}

View file

@ -0,0 +1,536 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramCore
import AccountContext
import ViewControllerComponent
import ResizableSheetComponent
import TelegramPresentationData
import PresentationDataUtils
import MultilineTextComponent
import ButtonComponent
import ListSectionComponent
import ListActionItemComponent
import GlassBarButtonComponent
import BundleIconComponent
struct BotCheckoutPaymentWebToken: Equatable {
let title: String
let data: String
var saveOnServer: Bool
}
enum BotCheckoutPaymentMethod: Equatable {
case savedCredentials(BotPaymentSavedCredentials)
case webToken(BotCheckoutPaymentWebToken)
case applePay
case other(BotPaymentMethod)
var title: String {
switch self {
case let .savedCredentials(credentials):
switch credentials {
case let .card(_, title):
return title
}
case let .webToken(token):
return token.title
case .applePay:
return "Apple Pay"
case let .other(method):
return method.title
}
}
}
private func splitSavedCardTitle(_ title: String) -> (String, String?) {
guard let separatorIndex = title.lastIndex(of: "*") else {
return (title, nil)
}
let name = String(title[..<separatorIndex]).trimmingCharacters(in: .whitespacesAndNewlines)
let suffix = String(title[title.index(after: separatorIndex)...]).trimmingCharacters(in: .whitespacesAndNewlines)
guard !name.isEmpty, !suffix.isEmpty else {
return (title, nil)
}
return (name, "•••• \(suffix)")
}
private final class BotCheckoutPaymentMethodContentComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let methods: [BotCheckoutPaymentMethod]
let selectedMethod: BotCheckoutPaymentMethod?
let selectMethod: (BotCheckoutPaymentMethod) -> Void
let addCard: () -> Void
init(
methods: [BotCheckoutPaymentMethod],
selectedMethod: BotCheckoutPaymentMethod?,
selectMethod: @escaping (BotCheckoutPaymentMethod) -> Void,
addCard: @escaping () -> Void
) {
self.methods = methods
self.selectedMethod = selectedMethod
self.selectMethod = selectMethod
self.addCard = addCard
}
static func ==(lhs: BotCheckoutPaymentMethodContentComponent, rhs: BotCheckoutPaymentMethodContentComponent) -> Bool {
if lhs.methods != rhs.methods {
return false
}
if lhs.selectedMethod != rhs.selectedMethod {
return false
}
return true
}
final class View: UIView {
private let section = ComponentView<Empty>()
private var component: BotCheckoutPaymentMethodContentComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BotCheckoutPaymentMethodContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let theme = environment.theme.withModalBlocksBackground()
let itemFontSize: CGFloat = 17.0
let sideInset: CGFloat = 16.0
var contentHeight: CGFloat = 76.0 + 9.0
var items: [AnyComponentWithIdentity<Empty>] = []
for i in 0 ..< component.methods.count {
let method = component.methods[i]
let isSelected = method == component.selectedMethod
var title = method.title
var icon: ListActionItemComponent.Icon?
var accessory: ListActionItemComponent.Accessory?
switch method {
case let .savedCredentials(credentials):
switch credentials {
case let .card(_, cardTitle):
let (cardName, cardSuffix) = splitSavedCardTitle(cardTitle)
title = cardName
if let cardSuffix {
accessory = .custom(ListActionItemComponent.CustomAccessory(
component: AnyComponentWithIdentity(
id: AnyHashable("card-suffix-\(i)-\(cardSuffix)"),
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: cardSuffix,
font: Font.regular(itemFontSize),
textColor: theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 1
))
),
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 16.0)
))
}
}
case .applePay:
title = "Apple Pay"
icon = ListActionItemComponent.Icon(
component: AnyComponentWithIdentity(
id: AnyHashable("apple-pay"),
component: AnyComponent(BundleIconComponent(
name: "Bot Payments/ApplePayLogo",
tintColor: nil
))
)
)
case let .webToken(token):
let (cardName, cardSuffix) = splitSavedCardTitle(token.title)
title = cardName
if let cardSuffix {
accessory = .custom(ListActionItemComponent.CustomAccessory(
component: AnyComponentWithIdentity(
id: AnyHashable("card-suffix-\(i)-\(cardSuffix)"),
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: cardSuffix,
font: Font.regular(itemFontSize),
textColor: theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 1
))
),
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 16.0)
))
}
case let .other(method):
title = method.title
}
items.append(AnyComponentWithIdentity(id: AnyHashable("method-\(i)"), component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: title,
font: Font.regular(itemFontSize),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
isSelected: isSelected,
toggle: {
component.selectMethod(method)
}
)),
icon: icon,
accessory: accessory,
action: { _ in
component.selectMethod(method)
}
))))
}
items.append(AnyComponentWithIdentity(id: AnyHashable("add-card"), component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Checkout_PaymentMethod_New,
font: Font.regular(itemFontSize),
textColor: theme.list.itemAccentColor
)),
maximumNumberOfLines: 1
)),
contentInsets: UIEdgeInsets(top: 12.0, left: 45.0, bottom: 12.0, right: 0.0),
leftIcon: nil,
icon: nil,
accessory: nil,
action: { _ in
component.addCard()
}
))))
self.section.parentState = state
let sectionSize = self.section.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: theme,
style: .glass,
header: nil,
footer: nil,
items: items
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let sectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: sectionSize)
if let sectionView = self.section.view {
if sectionView.superview == nil {
self.addSubview(sectionView)
}
transition.setFrame(view: sectionView, frame: sectionFrame)
}
contentHeight += sectionSize.height
contentHeight += 112.0
return CGSize(width: availableSize.width, height: contentHeight)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class BotCheckoutPaymentMethodScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let currentMethod: BotCheckoutPaymentMethod?
let methods: [BotCheckoutPaymentMethod]
let applyValue: (BotCheckoutPaymentMethod) -> Void
let newCard: () -> Void
let otherMethod: (String, String) -> Void
init(
context: AccountContext,
currentMethod: BotCheckoutPaymentMethod?,
methods: [BotCheckoutPaymentMethod],
applyValue: @escaping (BotCheckoutPaymentMethod) -> Void,
newCard: @escaping () -> Void,
otherMethod: @escaping (String, String) -> Void
) {
self.context = context
self.currentMethod = currentMethod
self.methods = methods
self.applyValue = applyValue
self.newCard = newCard
self.otherMethod = otherMethod
}
static func ==(lhs: BotCheckoutPaymentMethodScreenComponent, rhs: BotCheckoutPaymentMethodScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.currentMethod != rhs.currentMethod {
return false
}
if lhs.methods != rhs.methods {
return false
}
return true
}
final class View: UIView {
private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>()
private let animateOut = ActionSlot<Action<Void>>()
private var component: BotCheckoutPaymentMethodScreenComponent?
private weak var state: EmptyComponentState?
private var selectedMethod: BotCheckoutPaymentMethod?
private var isDismissing = false
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BotCheckoutPaymentMethodScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
if self.component == nil {
if let currentMethod = component.currentMethod, component.methods.contains(currentMethod) {
self.selectedMethod = currentMethod
} else {
self.selectedMethod = nil
}
}
self.component = component
self.state = state
let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value
let controller = environmentValue.controller
let theme = environmentValue.theme.withModalBlocksBackground()
let dismiss: (Bool, (() -> Void)?) -> Void = { [weak self] animated, completion in
guard let self, !self.isDismissing else {
return
}
self.isDismissing = true
let performDismiss: () -> Void = {
if let controller = controller() as? BotCheckoutPaymentMethodScreen {
controller.completePendingDismiss()
controller.dismiss(animated: false)
}
completion?()
}
if animated {
self.animateOut.invoke(Action { _ in
performDismiss()
})
} else {
performDismiss()
}
}
let sheetSize = self.sheet.update(
transition: transition,
component: AnyComponent(ResizableSheetComponent<ViewControllerComponentContainer.Environment>(
content: AnyComponent<ViewControllerComponentContainer.Environment>(BotCheckoutPaymentMethodContentComponent(
methods: component.methods,
selectedMethod: self.selectedMethod,
selectMethod: { [weak self] method in
guard let self else {
return
}
self.selectedMethod = method
self.state?.updated(transition: .spring(duration: 0.35))
},
addCard: { [weak self] in
guard let self, let component = self.component else {
return
}
dismiss(true, {
component.newCard()
})
}
)),
titleItem: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environmentValue.strings.Checkout_PaymentMethod,
font: Font.semibold(17.0),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
leftItem: AnyComponent(
GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
dismiss(true, nil)
}
)
),
//TODO:localize
bottomItem: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable("proceed"),
component: AnyComponent(ButtonTextContentComponent(
text: "Proceed",
badge: 0,
textColor: theme.list.itemCheckColors.foregroundColor,
badgeBackground: theme.list.itemCheckColors.foregroundColor,
badgeForeground: theme.list.itemCheckColors.fillColor
))
),
isEnabled: self.selectedMethod != nil,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component, let selectedMethod = self.selectedMethod else {
return
}
dismiss(true, {
switch selectedMethod {
case let .other(method):
component.otherMethod(method.url, method.title)
default:
component.applyValue(selectedMethod)
}
})
}
)),
backgroundColor: .color(theme.list.modalBlocksBackgroundColor),
animateOut: self.animateOut
)),
environment: {
environmentValue
ResizableSheetComponentEnvironment(
theme: theme,
statusBarHeight: environmentValue.statusBarHeight,
safeInsets: environmentValue.safeInsets,
inputHeight: 0.0,
metrics: environmentValue.metrics,
deviceMetrics: environmentValue.deviceMetrics,
isDisplaying: environmentValue.isVisible,
isCentered: environmentValue.metrics.widthClass == .regular,
screenSize: availableSize,
regularMetricsSize: nil,
dismiss: { animated in
dismiss(animated, nil)
}
)
},
forceUpdate: true,
containerSize: availableSize
)
self.sheet.parentState = state
if let sheetView = self.sheet.view {
if sheetView.superview == nil {
self.addSubview(sheetView)
}
transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize))
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class BotCheckoutPaymentMethodScreen: ViewControllerComponentContainer {
private var isDismissed = false
private var dismissCompletion: (() -> Void)?
init(context: AccountContext, currentMethod: BotCheckoutPaymentMethod?, methods: [BotCheckoutPaymentMethod], applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, newCard: @escaping () -> Void, otherMethod: @escaping (String, String) -> Void) {
super.init(
context: context,
component: BotCheckoutPaymentMethodScreenComponent(
context: context,
currentMethod: currentMethod,
methods: methods,
applyValue: applyValue,
newCard: newCard,
otherMethod: otherMethod
),
navigationBarAppearance: .none
)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func completePendingDismiss() {
let dismissCompletion = self.dismissCompletion
self.dismissCompletion = nil
dismissCompletion?()
}
func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
override func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
self.dismissCompletion = completion
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
} else {
self.completePendingDismiss()
self.dismiss(animated: false)
}
}
}
}

View file

@ -1,262 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import AccountContext
import AppBundle
struct BotCheckoutPaymentWebToken: Equatable {
let title: String
let data: String
var saveOnServer: Bool
}
enum BotCheckoutPaymentMethod: Equatable {
case savedCredentials(BotPaymentSavedCredentials)
case webToken(BotCheckoutPaymentWebToken)
case applePay
case other(BotPaymentMethod)
var title: String {
switch self {
case let .savedCredentials(credentials):
switch credentials {
case let .card(_, title):
return title
}
case let .webToken(token):
return token.title
case .applePay:
return "Apple Pay"
case let .other(method):
return method.title
}
}
}
final class BotCheckoutPaymentMethodSheetController: ActionSheetController {
private var presentationDisposable: Disposable?
init(context: AccountContext, currentMethod: BotCheckoutPaymentMethod?, methods: [BotCheckoutPaymentMethod], applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, newCard: @escaping () -> Void, otherMethod: @escaping (String, String) -> Void) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
super.init(theme: ActionSheetControllerTheme(presentationData: presentationData))
self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData)
}
}).strict()
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: strings.Checkout_PaymentMethod))
for method in methods {
let title: String
let icon: UIImage?
switch method {
case let .savedCredentials(credentials):
switch credentials {
case let .card(_, cardTitle):
title = cardTitle
icon = nil
}
case let .webToken(token):
title = token.title
icon = nil
case .applePay:
title = "Apple Pay"
icon = UIImage(bundleImageName: "Bot Payments/ApplePayLogo")?.precomposed()
case let .other(method):
title = method.title
icon = nil
}
let value: Bool?
if let currentMethod = currentMethod {
value = method == currentMethod
} else {
value = nil
}
items.append(BotCheckoutPaymentMethodItem(title: title, icon: icon, value: value, action: { [weak self] _ in
if case let .other(method) = method {
otherMethod(method.url, method.title)
} else {
applyValue(method)
}
self?.dismissAnimated()
}))
}
items.append(ActionSheetButtonItem(title: strings.Checkout_PaymentMethod_New, action: { [weak self] in
self?.dismissAnimated()
newCard()
}))
self.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in
self?.dismissAnimated()
}),
])
])
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDisposable?.dispose()
}
}
public class BotCheckoutPaymentMethodItem: ActionSheetItem {
public let title: String
public let icon: UIImage?
public let value: Bool?
public let action: (Bool) -> Void
public init(title: String, icon: UIImage?, value: Bool?, action: @escaping (Bool) -> Void) {
self.title = title
self.icon = icon
self.value = value
self.action = action
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
let node = BotCheckoutPaymentMethodItemNode(theme: theme)
node.setItem(self)
return node
}
public func updateNode(_ node: ActionSheetItemNode) {
guard let node = node as? BotCheckoutPaymentMethodItemNode else {
assertionFailure()
return
}
node.setItem(self)
node.requestLayoutUpdate()
}
}
public class BotCheckoutPaymentMethodItemNode: ActionSheetItemNode {
private let defaultFont: UIFont
private let theme: ActionSheetControllerTheme
private var item: BotCheckoutPaymentMethodItem?
private let button: HighlightTrackingButton
private let titleNode: ASTextNode
private let iconNode: ASImageNode
private let checkNode: ASImageNode
public override init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0))
self.button = HighlightTrackingButton()
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.checkNode = ASImageNode()
self.checkNode.isUserInteractionEnabled = false
self.checkNode.displayWithoutProcessing = true
self.checkNode.displaysAsynchronously = false
self.checkNode.image = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(theme.controlAccentColor.cgColor)
context.setLineWidth(2.0)
context.move(to: CGPoint(x: 12.0, y: 1.0))
context.addLine(to: CGPoint(x: 4.16482734, y: 9.0))
context.addLine(to: CGPoint(x: 1.0, y: 5.81145833))
context.strokePath()
})
super.init(theme: theme)
self.view.addSubview(self.button)
self.addSubnode(self.titleNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.checkNode)
self.button.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.backgroundNode.backgroundColor = theme.itemHighlightedBackgroundColor
} else {
UIView.animate(withDuration: 0.3, animations: {
strongSelf.backgroundNode.backgroundColor = theme.itemBackgroundColor
})
}
}
}
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
}
func setItem(_ item: BotCheckoutPaymentMethodItem) {
self.item = item
self.titleNode.attributedText = NSAttributedString(string: item.title, font: self.defaultFont, textColor: self.theme.primaryTextColor)
self.iconNode.image = item.icon
if let value = item.value {
self.checkNode.isHidden = !value
} else {
self.checkNode.isHidden = true
}
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 57.0)
self.button.frame = CGRect(origin: CGPoint(), size: size)
var checkInset: CGFloat = 15.0
if let _ = self.item?.value {
checkInset = 44.0
}
let iconSize: CGSize
if let image = self.iconNode.image {
iconSize = image.size
} else {
iconSize = CGSize()
}
let titleSize = self.titleNode.measure(CGSize(width: size.width - 44.0 - iconSize.width - 15.0 - 8.0, height: size.height))
self.titleNode.frame = CGRect(origin: CGPoint(x: checkInset, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
self.iconNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - iconSize.width, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize)
if let image = self.checkNode.image {
self.checkNode.frame = CGRect(origin: CGPoint(x: floor((44.0 - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
}
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
@objc func buttonPressed() {
if let item = self.item {
let updatedValue: Bool
if let value = item.value {
updatedValue = !value
} else {
updatedValue = true
}
item.action(updatedValue)
}
}
}

View file

@ -1,224 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramStringFormatting
final class BotCheckoutPaymentShippingOptionSheetController: ActionSheetController {
private var presentationDisposable: Disposable?
init(context: AccountContext, currency: String, options: [BotPaymentShippingOption], currentId: String?, applyValue: @escaping (String) -> Void) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
super.init(theme: ActionSheetControllerTheme(presentationData: presentationData))
self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData)
}
}).strict()
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: strings.Checkout_ShippingMethod))
let dismissAction: () -> Void = { [weak self] in
self?.dismissAnimated()
}
let toggleCheck: (String, Int) -> Void = { [weak self] id, itemIndex in
for i in 0 ..< options.count {
self?.updateItem(groupIndex: 0, itemIndex: i + 1, { item in
if let item = item as? BotCheckoutPaymentShippingOptionItem, let value = item.value {
return BotCheckoutPaymentShippingOptionItem(title: item.title, label: item.label, value: i == itemIndex ? !value : false, action: item.action)
}
return item
})
}
applyValue(id)
dismissAction()
}
var itemIndex = 0
for option in options {
let index = itemIndex
var totalPrice: Int64 = 0
for price in option.prices {
totalPrice += price.amount
}
let value: Bool?
if let currentId = currentId {
value = option.id == currentId
} else {
value = nil
}
items.append(BotCheckoutPaymentShippingOptionItem(title: option.title, label: formatCurrencyAmount(totalPrice, currency: currency), value: value, action: { value in
toggleCheck(option.id, index)
}))
itemIndex += 1
}
self.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in
self?.dismissAnimated()
}),
])
])
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDisposable?.dispose()
}
}
public class BotCheckoutPaymentShippingOptionItem: ActionSheetItem {
public let title: String
public let label: String
public let value: Bool?
public let action: (Bool) -> Void
public init(title: String, label: String, value: Bool?, action: @escaping (Bool) -> Void) {
self.title = title
self.label = label
self.value = value
self.action = action
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
let node = BotCheckoutPaymentShippingOptionItemNode(theme: theme)
node.setItem(self)
return node
}
public func updateNode(_ node: ActionSheetItemNode) {
guard let node = node as? BotCheckoutPaymentShippingOptionItemNode else {
assertionFailure()
return
}
node.setItem(self)
node.requestLayoutUpdate()
}
}
public class BotCheckoutPaymentShippingOptionItemNode: ActionSheetItemNode {
private let defaultFont: UIFont
private let theme: ActionSheetControllerTheme
private var item: BotCheckoutPaymentShippingOptionItem?
private let button: HighlightTrackingButton
private let titleNode: ASTextNode
private let labelNode: ASTextNode
private let checkNode: ASImageNode
public override init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0))
self.button = HighlightTrackingButton()
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.labelNode = ASTextNode()
self.labelNode.maximumNumberOfLines = 1
self.labelNode.isUserInteractionEnabled = false
self.labelNode.displaysAsynchronously = false
self.checkNode = ASImageNode()
self.checkNode.isUserInteractionEnabled = false
self.checkNode.displayWithoutProcessing = true
self.checkNode.displaysAsynchronously = false
self.checkNode.image = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(theme.controlAccentColor.cgColor)
context.setLineWidth(2.0)
context.move(to: CGPoint(x: 12.0, y: 1.0))
context.addLine(to: CGPoint(x: 4.16482734, y: 9.0))
context.addLine(to: CGPoint(x: 1.0, y: 5.81145833))
context.strokePath()
})
super.init(theme: theme)
self.view.addSubview(self.button)
self.addSubnode(self.titleNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.checkNode)
self.button.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor
} else {
UIView.animate(withDuration: 0.3, animations: {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor
})
}
}
}
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
}
func setItem(_ item: BotCheckoutPaymentShippingOptionItem) {
self.item = item
self.titleNode.attributedText = NSAttributedString(string: item.title, font: self.defaultFont, textColor: self.theme.primaryTextColor)
self.labelNode.attributedText = NSAttributedString(string: item.label, font: self.defaultFont, textColor: self.theme.primaryTextColor)
if let value = item.value {
self.checkNode.isHidden = !value
} else {
self.checkNode.isHidden = true
}
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 57.0)
self.button.frame = CGRect(origin: CGPoint(), size: size)
var checkInset: CGFloat = 15.0
if let _ = self.item?.value {
checkInset = 44.0
}
let labelSize = self.labelNode.measure(CGSize(width: size.width - 44.0 - 15.0 - 8.0, height: size.height))
let titleSize = self.titleNode.measure(CGSize(width: size.width - 44.0 - labelSize.width - 15.0 - 8.0, height: size.height))
self.titleNode.frame = CGRect(origin: CGPoint(x: checkInset, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
self.labelNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - labelSize.width, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
if let image = self.checkNode.image {
self.checkNode.frame = CGRect(origin: CGPoint(x: floor((44.0 - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
}
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
@objc func buttonPressed() {
if let item = self.item {
let updatedValue: Bool
if let value = item.value {
updatedValue = !value
} else {
updatedValue = true
}
item.action(updatedValue)
}
}
}

View file

@ -0,0 +1,418 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramStringFormatting
import ViewControllerComponent
import ResizableSheetComponent
import TelegramPresentationData
import PresentationDataUtils
import MultilineTextComponent
import ButtonComponent
import ListSectionComponent
import ListActionItemComponent
import GlassBarButtonComponent
import BundleIconComponent
private final class BotCheckoutShippingOptionContentComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let currency: String
let options: [BotPaymentShippingOption]
let selectedId: String?
let selectOption: (String) -> Void
init(
currency: String,
options: [BotPaymentShippingOption],
selectedId: String?,
selectOption: @escaping (String) -> Void
) {
self.currency = currency
self.options = options
self.selectedId = selectedId
self.selectOption = selectOption
}
static func ==(lhs: BotCheckoutShippingOptionContentComponent, rhs: BotCheckoutShippingOptionContentComponent) -> Bool {
if lhs.currency != rhs.currency {
return false
}
if lhs.options != rhs.options {
return false
}
if lhs.selectedId != rhs.selectedId {
return false
}
return true
}
final class View: UIView {
private let section = ComponentView<Empty>()
private var component: BotCheckoutShippingOptionContentComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BotCheckoutShippingOptionContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let theme = environment.theme.withModalBlocksBackground()
let itemFontSize: CGFloat = 17.0
let sideInset: CGFloat = 16.0
var contentHeight: CGFloat = 76.0 + 9.0
var items: [AnyComponentWithIdentity<Empty>] = []
for option in component.options {
let totalPrice = option.prices.reduce(Int64(0)) { current, price in
return current + price.amount
}
let priceText = formatCurrencyAmount(totalPrice, currency: component.currency)
let isSelected = option.id == component.selectedId
items.append(AnyComponentWithIdentity(id: option.id, component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: option.title,
font: Font.regular(itemFontSize),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
isSelected: isSelected,
toggle: {
component.selectOption(option.id)
}
)),
icon: nil,
accessory: .custom(ListActionItemComponent.CustomAccessory(
component: AnyComponentWithIdentity(
id: AnyHashable("price-\(option.id)"),
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: priceText,
font: Font.regular(itemFontSize),
textColor: theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 1
))
),
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 16.0)
)),
action: { _ in
component.selectOption(option.id)
}
))))
}
self.section.parentState = state
let sectionSize = self.section.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: theme,
style: .glass,
header: nil,
footer: nil,
items: items
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let sectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: sectionSize)
if let sectionView = self.section.view {
if sectionView.superview == nil {
self.addSubview(sectionView)
}
transition.setFrame(view: sectionView, frame: sectionFrame)
}
contentHeight += sectionSize.height
contentHeight += 112.0
return CGSize(width: availableSize.width, height: contentHeight)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class BotCheckoutShippingOptionScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let currency: String
let options: [BotPaymentShippingOption]
let currentId: String?
let applyValue: (String) -> Void
init(
context: AccountContext,
currency: String,
options: [BotPaymentShippingOption],
currentId: String?,
applyValue: @escaping (String) -> Void
) {
self.context = context
self.currency = currency
self.options = options
self.currentId = currentId
self.applyValue = applyValue
}
static func ==(lhs: BotCheckoutShippingOptionScreenComponent, rhs: BotCheckoutShippingOptionScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.currency != rhs.currency {
return false
}
if lhs.options != rhs.options {
return false
}
if lhs.currentId != rhs.currentId {
return false
}
return true
}
final class View: UIView {
private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>()
private let animateOut = ActionSlot<Action<Void>>()
private var component: BotCheckoutShippingOptionScreenComponent?
private weak var state: EmptyComponentState?
private var selectedId: String?
private var isDismissing = false
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BotCheckoutShippingOptionScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
if self.component == nil {
if let currentId = component.currentId, component.options.contains(where: { $0.id == currentId }) {
self.selectedId = currentId
} else {
self.selectedId = nil
}
}
self.component = component
self.state = state
let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value
let controller = environmentValue.controller
let theme = environmentValue.theme.withModalBlocksBackground()
let dismiss: (Bool) -> Void = { [weak self] animated in
guard let self, !self.isDismissing else {
return
}
self.isDismissing = true
let performDismiss: () -> Void = {
if let controller = controller() as? BotCheckoutShippingOptionScreen {
controller.completePendingDismiss()
controller.dismiss(animated: false)
}
}
if animated {
self.animateOut.invoke(Action { _ in
performDismiss()
})
} else {
performDismiss()
}
}
let sheetSize = self.sheet.update(
transition: transition,
component: AnyComponent(ResizableSheetComponent<ViewControllerComponentContainer.Environment>(
content: AnyComponent<ViewControllerComponentContainer.Environment>(BotCheckoutShippingOptionContentComponent(
currency: component.currency,
options: component.options,
selectedId: self.selectedId,
selectOption: { [weak self] id in
guard let self else {
return
}
self.selectedId = id
self.state?.updated(transition: .spring(duration: 0.35))
}
)),
titleItem: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environmentValue.strings.Checkout_ShippingMethod,
font: Font.semibold(17.0),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
leftItem: AnyComponent(
GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
dismiss(true)
}
)
),
//TODO:localize
bottomItem: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable("proceed"),
component: AnyComponent(ButtonTextContentComponent(
text: "Proceed",
badge: 0,
textColor: theme.list.itemCheckColors.foregroundColor,
badgeBackground: theme.list.itemCheckColors.foregroundColor,
badgeForeground: theme.list.itemCheckColors.fillColor
))
),
isEnabled: self.selectedId != nil,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component, let selectedId = self.selectedId else {
return
}
component.applyValue(selectedId)
dismiss(true)
}
)),
backgroundColor: .color(theme.list.modalBlocksBackgroundColor),
animateOut: self.animateOut
)),
environment: {
environmentValue
ResizableSheetComponentEnvironment(
theme: theme,
statusBarHeight: environmentValue.statusBarHeight,
safeInsets: environmentValue.safeInsets,
inputHeight: 0.0,
metrics: environmentValue.metrics,
deviceMetrics: environmentValue.deviceMetrics,
isDisplaying: environmentValue.isVisible,
isCentered: environmentValue.metrics.widthClass == .regular,
screenSize: availableSize,
regularMetricsSize: nil,
dismiss: { animated in
dismiss(animated)
}
)
},
forceUpdate: true,
containerSize: availableSize
)
self.sheet.parentState = state
if let sheetView = self.sheet.view {
if sheetView.superview == nil {
self.addSubview(sheetView)
}
transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize))
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class BotCheckoutShippingOptionScreen: ViewControllerComponentContainer {
private var isDismissed = false
private var dismissCompletion: (() -> Void)?
init(context: AccountContext, currency: String, options: [BotPaymentShippingOption], currentId: String?, applyValue: @escaping (String) -> Void) {
super.init(
context: context,
component: BotCheckoutShippingOptionScreenComponent(
context: context,
currency: currency,
options: options,
currentId: currentId,
applyValue: applyValue
),
navigationBarAppearance: .none
)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func completePendingDismiss() {
let dismissCompletion = self.dismissCompletion
self.dismissCompletion = nil
dismissCompletion?()
}
func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
override func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
self.dismissCompletion = completion
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
} else {
self.completePendingDismiss()
self.dismiss(animated: false)
}
}
}
}

View file

@ -65,7 +65,7 @@ public final class BrowserBookmarksScreen: ViewController {
}, tapMessage: nil, clickThroughMessage: { _, _ in
}, toggleMessagesSelection: { _, _ in
}, sendCurrentMessage: { _, _ in
}, sendMessage: { _ in
}, sendMessage: { _, _ in
}, sendSticker: { _, _, _, _, _, _, _, _, _ in
return false
}, sendEmoji: { _, _, _ in
@ -83,8 +83,8 @@ public final class BrowserBookmarksScreen: ViewController {
controller.dismiss()
}
}, openExternalInstantPage: { _ in
}, shareCurrentLocation: {
}, shareAccountContact: {
}, shareCurrentLocation: { _ in
}, shareAccountContact: { _ in
}, sendBotCommand: { _, _ in
}, openInstantPage: { message, _ in
if let openMessageImpl = openMessageImpl {
@ -134,7 +134,7 @@ public final class BrowserBookmarksScreen: ViewController {
}, displaySwipeToReplyHint: {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, openPollCreation: { _, _ in
}, openPollMedia: { _, _ in
}, displayPollSolution: { _, _ in
}, displayPsa: { _, _ in
@ -402,7 +402,7 @@ public final class BrowserBookmarksScreen: ViewController {
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed))
self.title = self.presentationData.strings.WebBrowser_Bookmarks_Title
self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, activate: { [weak self] in

View file

@ -166,6 +166,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.delegate = self
self.scrollNode.view.scrollsToTop = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
@ -1532,12 +1533,12 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = OpenInActionSheetController(context: self.context, item: .url(url: baseUrl), openUrl: { [weak self] url in
let actionSheet = OpenInOptionsScreen(context: self.context, item: .url(url: baseUrl), openUrl: { [weak self] url in
if let self {
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
}
})
self.present(actionSheet, nil)
self.push(actionSheet)
}
private func openMedia(_ media: InstantPageMedia) {

View file

@ -34,7 +34,40 @@ public class Readability: NSObject, WKNavigationDelegate {
if let (html, subresources) = extractHtmlString(from: archiveData) {
self.subresources = subresources
self.webView.loadHTMLString(html, baseURL: url.baseURL)
self.sanitizeHtmlString(html) { [weak self] html in
guard let self else {
return
}
self.webView.loadHTMLString(html, baseURL: url.baseURL)
}
}
}
private func sanitizeHtmlString(_ html: String, completion: @escaping (String) -> Void) {
guard let readerModeJS = loadFile(name: "ReaderMode", type: "js") else {
completion(htmlByRemovingScriptTags(html))
return
}
let domPurifyJS = extractDOMPurifyScript(from: readerModeJS) ?? readerModeJS
self.webView.evaluateJavaScript(domPurifyJS) { [weak self] _, error in
guard let self else {
return
}
guard error == nil, let htmlLiteral = javascriptStringLiteral(html) else {
completion(htmlByRemovingScriptTags(html))
return
}
let sanitizeJS = """
(function(html) {
return DOMPurify.sanitize(html, {WHOLE_DOCUMENT: true, ADD_TAGS: ["iframe"]});
})(\(htmlLiteral));
"""
self.webView.evaluateJavaScript(sanitizeJS) { result, _ in
completion((result as? String) ?? htmlByRemovingScriptTags(html))
}
}
}
@ -49,6 +82,7 @@ public class Readability: NSObject, WKNavigationDelegate {
return
}
guard let page = parseJson(result, url: self.url.absoluteString) else {
completion(nil, error)
return
}
completion(page, nil)
@ -95,6 +129,32 @@ func loadFile(name: String, type: String) -> String? {
return userScript
}
private func extractDOMPurifyScript(from readerModeJS: String) -> String? {
guard let range = readerModeJS.range(of: "\n\n(function () {") else {
return nil
}
return String(readerModeJS[..<range.lowerBound])
}
private func javascriptStringLiteral(_ input: String) -> String? {
guard let data = try? JSONSerialization.data(withJSONObject: [input], options: []),
var arrayString = String(data: data, encoding: .utf8),
arrayString.count >= 2 else {
return nil
}
arrayString.removeFirst()
arrayString.removeLast()
return arrayString
}
private func htmlByRemovingScriptTags(_ input: String) -> String {
guard let regex = try? NSRegularExpression(pattern: "<script\\b[^>]*>[\\s\\S]*?</script\\s*>", options: [.caseInsensitive]) else {
return input
}
let range = NSRange(input.startIndex ..< input.endIndex, in: input)
return regex.stringByReplacingMatches(in: input, options: [], range: range, withTemplate: "")
}
private func extractHtmlString(from webArchiveData: Data) -> (String, [Any]?)? {
if let webArchiveDict = try? PropertyListSerialization.propertyList(from: webArchiveData, format: nil) as? [String: Any],
let mainResource = webArchiveDict["WebMainResource"] as? [String: Any],
@ -724,30 +784,38 @@ private func parseVideo(_ input: [String: Any], _ media: inout [EngineMedia.Id:
)
}
private func firstElement(withTag tag: String, in input: [Any], skippingSubtreesWithTag skippedTag: String? = nil) -> [String: Any]? {
for item in input {
guard let item = item as? [String: Any] else {
continue
}
let itemTag = item["tag"] as? String
if itemTag == tag {
return item
}
if itemTag == skippedTag {
continue
}
if let content = item["content"] as? [Any], let result = firstElement(withTag: tag, in: content, skippingSubtreesWithTag: skippedTag) {
return result
}
}
return nil
}
private func parseFigure(_ input: [String: Any], _ media: inout [EngineMedia.Id: EngineRawMedia]) -> InstantPageBlock? {
guard let content = input["content"] as? [Any] else {
return nil
}
var block: InstantPageBlock?
var caption: RichText?
for item in content {
if let item = item as? [String: Any], let tag = item["tag"] as? String {
if tag == "p", let content = item["content"] as? [Any] {
for item in content {
if let item = item as? [String: Any], let tag = item["tag"] as? String {
if tag == "iframe" {
block = parseVideo(item, &media)
}
}
}
} else if tag == "iframe" {
block = parseVideo(item, &media)
} else if tag == "img" {
block = parseImage(item, &media)
} else if tag == "figcaption" {
caption = trim(parseRichText(item, &media))
}
}
if let iframe = firstElement(withTag: "iframe", in: content, skippingSubtreesWithTag: "figcaption") {
block = parseVideo(iframe, &media)
} else if let image = firstElement(withTag: "img", in: content, skippingSubtreesWithTag: "figcaption") {
block = parseImage(image, &media)
}
if let figcaption = firstElement(withTag: "figcaption", in: content) {
caption = trim(parseRichText(figcaption, &media))
}
guard var block else {
return nil

View file

@ -1228,7 +1228,7 @@ public class BrowserScreen: ViewController, MinimizableController {
)
var defaultWebBrowser: String? = settings.defaultWebBrowser
if defaultWebBrowser == nil || defaultWebBrowser == "inAppSafari" {
if defaultWebBrowser == nil || defaultWebBrowser == "inApp" || defaultWebBrowser == "inAppSafari" {
defaultWebBrowser = "safari"
}
@ -1265,6 +1265,7 @@ public class BrowserScreen: ViewController, MinimizableController {
} else {
items.append(.custom(fontItem, false))
items.append(.separator)
if case .webPage = contentState.contentType {
let isAvailable = contentState.hasInstantView
@ -1280,6 +1281,17 @@ public class BrowserScreen: ViewController, MinimizableController {
}
}
if toolbarMode != .markdown && [.webPage, .instantPage].contains(contentState.contentType) {
if !layout.metrics.isTablet && canOpenIn {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in
if let self {
self.context.sharedContext.applicationBindings.openUrl(openInUrl)
}
action(.default)
})))
}
}
if !items.isEmpty {
items.append(.separator)
}
@ -1290,12 +1302,6 @@ public class BrowserScreen: ViewController, MinimizableController {
action(.default)
})))
}
if [.webPage, .document].contains(contentState.contentType) {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_Search, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Search"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
performAction.invoke(.updateSearchActive(true))
action(.default)
})))
}
if canShare && !layout.metrics.isTablet {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
@ -1304,7 +1310,16 @@ public class BrowserScreen: ViewController, MinimizableController {
})))
}
if [.webPage, .document].contains(contentState.contentType) {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_Search, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Search"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
performAction.invoke(.updateSearchActive(true))
action(.default)
})))
}
if toolbarMode != .markdown && [.webPage, .instantPage].contains(contentState.contentType) {
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_AddBookmark, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
performAction.invoke(.addBookmark)
action(.default)
@ -1316,15 +1331,6 @@ public class BrowserScreen: ViewController, MinimizableController {
action(.default)
})))
}
if !layout.metrics.isTablet && canOpenIn {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in
if let self {
self.context.sharedContext.applicationBindings.openUrl(openInUrl)
}
action(.default)
})))
}
}
return ContextController.Items(content: .list(items))
}

View file

@ -1868,7 +1868,7 @@ public final class CalendarMessageScreen: ViewController {
self._hasGlassStyle = true
self.navigationPresentation = .modal
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(dismissPressed)), animated: false)
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(dismissPressed)), animated: false)
self.navigationItem.setTitle(self.presentationData.strings.MessageCalendar_Title, animated: false)
if self.enableMessageRangeDeletion {
@ -1894,7 +1894,7 @@ public final class CalendarMessageScreen: ViewController {
self.node.toggleSelectionMode()
if self.node.selectionState != nil {
self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
self.navigationItem.setRightBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
} else {
self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Select, style: .plain, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
}
@ -1904,7 +1904,7 @@ public final class CalendarMessageScreen: ViewController {
self.node.selectDay(timestamp: timestamp)
if self.node.selectionState != nil {
self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
self.navigationItem.setRightBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
}
}

View file

@ -785,7 +785,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
strongSelf.view.accessibilityCustomActions = [UIAccessibilityCustomAction(name: item.presentationData.strings.Common_Delete, target: strongSelf, selector: #selector(strongSelf.performLocalAccessibilityCustomAction(_:)))]
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)]))
strongSelf.setRevealOptionsOpened(item.revealed, animated: animated)
}
})

View file

@ -213,7 +213,7 @@ public final class CallListController: TelegramBaseController {
if let isEmpty = self.isEmpty, isEmpty {
} else {
if self.editingMode {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed))
} else {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
}
@ -222,7 +222,7 @@ public final class CallListController: TelegramBaseController {
//self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed))
case .navigation:
if self.editingMode {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed))
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
}
@ -679,7 +679,7 @@ public final class CallListController: TelegramBaseController {
switch self.mode {
case .tab:
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)), animated: true)
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)), animated: true)
self.navigationItem.setRightBarButton(UIBarButtonItem(customDisplayNode: buttonNode), animated: true)
self.navigationItem.rightBarButtonItem?.setCustomAction({
@ -691,7 +691,7 @@ public final class CallListController: TelegramBaseController {
pressedImpl?()
})
self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)), animated: true)
self.navigationItem.setRightBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)), animated: true)
}
self.controllerNode.updateState { state in

View file

@ -940,7 +940,7 @@ final class CallListControllerNode: ASDisplayNode {
insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
let inset: CGFloat
if layout.size.width >= 375.0 {
if layout.size.width >= 320.0 {
inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
} else {
inset = 0.0

View file

@ -125,9 +125,11 @@ swift_library(
"//submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode",
"//submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/AlertComponent/AlertHeaderComponent",
"//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent",
"//submodules/TelegramUI/Components/AvatarComponent",
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
"//submodules/TelegramUI/Components/GlassControls",
],
visibility = [
"//visibility:public",

View file

@ -503,14 +503,26 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI
joinChannelDisposable.set(nil)
}
var didJoin = false
joinChannelDisposable.set((createSignal
|> deliverOnMainQueue).start(next: { _ in
|> deliverOnMainQueue).start(next: { result in
switch result {
case .joined:
didJoin = true
case let .webView(webView):
if let chatListController = chatListController {
context.sharedContext.openJoinChatWebView(context: context, parentController: chatListController, updatedPresentationData: nil, webView: webView)
}
}
}, error: { _ in
if let chatListController = chatListController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
chatListController.present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}
}, completed: {
if !didJoin {
return
}
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { peer in
guard let peer = peer else {

View file

@ -59,6 +59,9 @@ import ChatListFilterTabContainerNode
import HeaderPanelContainerComponent
import HorizontalTabsComponent
import GlobalControlPanelsContext
import AlertComponent
import AlertHeaderComponent
import AvatarComponent
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
let controller: ViewController
@ -5199,15 +5202,26 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|> delay(0.8, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
let signal: Signal<Never, JoinChannelError> = self.context.peerChannelMemberCategoriesContextsManager.join(engine: self.context.engine, peerId: peerId, hash: nil)
let signal: Signal<JoinChannelResult, JoinChannelError> = self.context.peerChannelMemberCategoriesContextsManager.join(engine: self.context.engine, peerId: peerId, hash: nil)
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
var didJoin = false
self.joinForumDisposable.set((signal
|> deliverOnMainQueue).startStrict(error: { [weak self] error in
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
guard let self else {
return
}
switch result {
case .joined:
didJoin = true
case let .webView(webView):
self.context.sharedContext.openJoinChatWebView(context: self.context, parentController: self, updatedPresentationData: nil, webView: webView)
}
}, error: { [weak self] error in
guard let strongSelf = self else {
return
}
@ -5241,36 +5255,39 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, completed: { [weak self] in
guard let self else {
return
}
Queue.mainQueue().after(0.5) {
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
guard let self, let peer = peer?._asPeer() else {
return
}
var canEditRank = false
if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.editRank) {
canEditRank = true
} else if let group = peer as? TelegramGroup, !group.hasBannedPermission(.banEditRank) {
canEditRank = true
}
if canEditRank {
let context = self.context
let controller = UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: self.presentationData.strings.Chat_JoinedGroup_Text, cancel: self.presentationData.strings.Chat_JoinedGroup_AddTag, destructive: false), elevatedLayout: true, action: { [weak self] action in
if let self, case .undo = action {
let tagController = context.sharedContext.makeChatCustomRankSetupScreen(context: context, peerId: peerId, participantId: context.account.peerId, rank: nil, role: .member)
self.push(tagController)
}
return true
})
self.present(controller, in: .current)
}
})
}
})
}, completed: { [weak self] in
guard let self else {
return
}
if !didJoin {
return
}
Queue.mainQueue().after(0.5) {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
guard let self, let peer = peer?._asPeer() else {
return
}
var canEditRank = false
if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.editRank) {
canEditRank = true
} else if let group = peer as? TelegramGroup, !group.hasBannedPermission(.banEditRank) {
canEditRank = true
}
if canEditRank {
let context = self.context
let controller = UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: self.presentationData.strings.Chat_JoinedGroup_Text, cancel: self.presentationData.strings.Chat_JoinedGroup_AddTag, destructive: false), elevatedLayout: true, action: { [weak self] action in
if let self, case .undo = action {
let tagController = context.sharedContext.makeChatCustomRankSetupScreen(context: context, peerId: peerId, participantId: context.account.peerId, rank: nil, role: .member)
self.push(tagController)
}
return true
})
self.present(controller, in: .current)
}
})
}
}))
case .savedMessagesChats:
break
@ -5375,21 +5392,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if case .broadcast = channel.info {
canClear = false
deleteTitle = strongSelf.presentationData.strings.Channel_LeaveChannel
if channel.addressName == nil && channel.flags.contains(.isCreator) {
canRemoveGlobally = true
}
} else {
deleteTitle = strongSelf.presentationData.strings.Group_DeleteGroup
if channel.addressName == nil && channel.flags.contains(.isCreator) {
canRemoveGlobally = true
}
}
if strongSelf.canDeletePeerGloballyAsCreator(mainPeer) {
canRemoveGlobally = true
}
if let addressName = channel.addressName, !addressName.isEmpty {
canClear = false
}
}
} else if case let .legacyGroup(group) = chatPeer {
if case .creator = group.role {
} else if case .legacyGroup = chatPeer {
if strongSelf.canDeletePeerGloballyAsCreator(mainPeer) {
canRemoveGlobally = true
}
} else if case let .user(user) = chatPeer, user.botInfo != nil {
@ -5842,30 +5856,32 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
canRemoveGlobally = true
}
if deleteGloballyIfPossible && self.canDeletePeerGloballyAsCreator(mainPeer) {
self.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: true, completion: {
removed()
})
completion(true)
return
}
if canRemoveGlobally {
let actionSheet = ActionSheetController(presentationData: self.presentationData)
var items: [ActionSheetItem] = []
items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder))
var actions: [AlertScreen.Action] = []
if joined || mainPeer.isDeleted {
items.append(ActionSheetButtonItem(title: self.presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
actions.append(.init(title: self.presentationData.strings.Common_Delete, type: .defaultDestructive, action: { [weak self] in
self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
removed()
})
completion(true)
}))
} else {
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
actions.append(.init(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, type: .destructive, action: { [weak self] in
self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
removed()
})
completion(true)
}))
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).string, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
actions.append(.init(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).string, type: .destructive, action: { [weak self] in
guard let strongSelf = self else {
return
}
@ -5882,16 +5898,107 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
], parseMarkdown: true), in: .window(.root))
}))
}
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
completion(false)
})
])
])
self.present(actionSheet, in: .window(.root))
actions.append(.init(title: self.presentationData.strings.Common_Cancel))
//TODO:localize
let title: String = "Delete Chat"
var text: String
if mainPeer.id == self.context.account.peerId {
text = self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmation
} else if case let .legacyGroup(chatPeer) = mainPeer {
text = self.presentationData.strings.ChatList_LeaveGroupConfirmation("**\(chatPeer.title)**").string
} else if case let .channel(chatPeer) = mainPeer {
text = self.presentationData.strings.ChatList_LeaveGroupConfirmation("**\(chatPeer.title)**").string
} else if case .secretChat = chatPeer {
text = self.presentationData.strings.ChatList_DeleteSecretChatConfirmation("**\(chatPeer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder))**").string
} else {
text = self.presentationData.strings.ChatList_DeleteChatConfirmation("**\(chatPeer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder))**").string
}
let alertScreen = AlertScreen(
context: self.context,
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
content: [
AnyComponentWithIdentity(
id: "header",
component: AnyComponent(
AlertHeaderComponent(
component: AnyComponentWithIdentity(id: "user", component: AnyComponent(
AvatarComponent(
context: self.context,
theme: self.presentationData.theme,
peer: mainPeer
)
))
)
)
),
AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: title)
)
),
AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(text))
)
)
],
actions: actions
)
// let actionSheet = ActionSheetController(presentationData: self.presentationData)
// var items: [ActionSheetItem] = []
//
// items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder))
//
// if joined || mainPeer.isDeleted {
// items.append(ActionSheetButtonItem(title: self.presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in
// actionSheet?.dismissAnimated()
// self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
// removed()
// })
// completion(true)
// }))
// } else {
// items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak self, weak actionSheet] in
// actionSheet?.dismissAnimated()
// self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
// removed()
// })
// completion(true)
// }))
// items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).string, color: .destructive, action: { [weak self, weak actionSheet] in
// actionSheet?.dismissAnimated()
// guard let strongSelf = self else {
// return
// }
// strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText, actions: [
// TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
// completion(false)
// }),
// TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: {
// self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
// removed()
// })
// completion(true)
// })
// ], parseMarkdown: true), in: .window(.root))
// }))
// }
// actionSheet.setItemGroups([
// ActionSheetItemGroup(items: items),
// ActionSheetItemGroup(items: [
// ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
// actionSheet?.dismissAnimated()
// completion(false)
// })
// ])
// ])
self.present(alertScreen, in: .window(.root))
} else if peer.peerId == self.context.account.peerId {
self.present(textAlertController(context: self.context, title: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [
TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {
@ -5945,6 +6052,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
private func canDeletePeerGloballyAsCreator(_ peer: EnginePeer) -> Bool {
if case let .channel(channel) = peer {
return !channel.isMonoForum && channel.flags.contains(.isCreator) && channel.addressName == nil
} else if case let .legacyGroup(group) = peer, case .creator = group.role {
return true
} else {
return false
}
}
func archiveChats(peerIds: [PeerId]) {
guard !peerIds.isEmpty else {
return
@ -6126,7 +6243,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if case .chatList(.root) = self.chatListDisplayNode.mainContainerNode.location {
super.setToolbar(toolbar, transition: transition)
} else {
self.chatListDisplayNode.toolbar = toolbar
self.chatListDisplayNode.toolbarData = toolbar
self.requestLayout(transition: transition)
}
}
@ -7046,6 +7163,7 @@ private final class ChatListLocationContext {
if case .chatList(.root) = self.location {
self.rightButton = nil
self.storyButton = nil
self.proxyButton = nil
}
let title = !stateAndFilterId.state.selectedPeerIds.isEmpty ? presentationData.strings.ChatList_SelectedChats(Int32(stateAndFilterId.state.selectedPeerIds.count)) : defaultTitle
@ -7061,6 +7179,7 @@ private final class ChatListLocationContext {
if case .chatList(.root) = self.location {
self.rightButton = nil
self.storyButton = nil
self.proxyButton = nil
}
self.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent(
content: .text(title: presentationData.strings.Common_Done, isBold: true),

View file

@ -28,6 +28,7 @@ import MediaPlaybackHeaderPanelComponent
import LiveLocationHeaderPanelComponent
import ChatListHeaderNoticeComponent
import ChatListFilterTabContainerNode
import GlassControls
public enum ChatListContainerNodeFilter: Equatable {
case all
@ -1134,8 +1135,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
let navigationBarView = ComponentView<Empty>()
weak var controller: ChatListControllerImpl?
var toolbar: Toolbar?
private var toolbarNode: ToolbarNode?
private var toolbar: ComponentView<Empty>?
var toolbarData: Toolbar?
var toolbarActionSelected: ((ToolbarActionOption) -> Void)?
private var isSearchDisplayControllerActive: ChatListNavigationBar.ActiveSearch?
@ -1395,10 +1396,6 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
self.mainContainerNode.updatePresentationData(presentationData)
self.inlineStackContainerNode?.updatePresentationData(presentationData)
self.searchDisplayController?.updatePresentationData(presentationData)
if let toolbarNode = self.toolbarNode {
toolbarNode.updateTheme(ToolbarTheme(rootControllerTheme: self.presentationData.theme))
}
}
private func updateNavigationBar(layout: ContainerViewLayout, deferScrollApplication: Bool, transition: ComponentTransition) -> (navigationHeight: CGFloat, storiesInset: CGFloat) {
@ -1435,6 +1432,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
self.effectiveContainerNode.currentItemNode.interaction?.openPremiumGift(peers, birthdays)
case .reviewLogin:
break
case .reviewBotConnection:
break
case let .starsSubscriptionLowBalance(amount, _):
self.effectiveContainerNode.currentItemNode.interaction?.openStarsTopup(amount.value)
case .setupPhoto:
@ -1458,6 +1457,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
switch notice {
case let .reviewLogin(newSessionReview, _):
self.effectiveContainerNode.currentItemNode.interaction?.performActiveSessionAction(newSessionReview, isPositive)
case let .reviewBotConnection(newBotConnectionReview, _, _):
self.effectiveContainerNode.currentItemNode.interaction?.performBotConnectionReviewAction(newBotConnectionReview, isPositive)
default:
break
}
@ -1838,53 +1839,101 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
insets.left += layout.safeInsets.left
insets.right += layout.safeInsets.right
if let toolbar = self.toolbar {
var tabBarHeight: CGFloat
var options: ContainerViewLayoutInsetOptions = []
if layout.metrics.widthClass == .regular {
options.insert(.input)
if let toolbarData = self.toolbarData {
var panelsBottomInset: CGFloat = layout.insets(options: []).bottom
if layout.metrics.widthClass == .regular, let inputHeight = layout.inputHeight, inputHeight != 0.0 {
panelsBottomInset = inputHeight + 8.0
}
var heightInset: CGFloat = 0.0
if case .forum = self.location {
heightInset = 4.0
}
let bottomInset: CGFloat = layout.insets(options: options).bottom
if !layout.safeInsets.left.isZero {
tabBarHeight = 34.0 + bottomInset
insets.bottom += 34.0
if panelsBottomInset == 0.0 {
panelsBottomInset = 8.0
} else {
tabBarHeight = 49.0 - heightInset + bottomInset
insets.bottom += 49.0 - heightInset
panelsBottomInset = max(panelsBottomInset, 8.0)
}
let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight))
let sideInset: CGFloat = 20.0
let toolbarHeight = 44.0
let toolbarFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - panelsBottomInset - toolbarHeight), size: CGSize(width: layout.size.width - sideInset * 2.0, height: toolbarHeight))
if let toolbarNode = self.toolbarNode {
transition.updateFrame(node: toolbarNode, frame: toolbarFrame)
toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: transition)
let toolbar: ComponentView<Empty>
var toolbarTransition = ComponentTransition(transition)
if let current = self.toolbar {
toolbar = current
} else {
let toolbarNode = ToolbarNode(theme: ToolbarTheme(rootControllerTheme: self.presentationData.theme), displaySeparator: true, left: { [weak self] in
self?.toolbarActionSelected?(.left)
}, right: { [weak self] in
self?.toolbarActionSelected?(.right)
}, middle: { [weak self] in
self?.toolbarActionSelected?(.middle)
})
toolbarNode.frame = toolbarFrame
toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: .immediate)
self.addSubnode(toolbarNode)
self.toolbarNode = toolbarNode
if transition.isAnimated {
toolbarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
toolbar = ComponentView()
self.toolbar = toolbar
toolbarTransition = .immediate
}
let _ = toolbar.update(
transition: toolbarTransition,
component: AnyComponent(GlassControlPanelComponent(
theme: self.presentationData.theme,
leftItem: toolbarData.leftAction.flatMap { value in
return GlassControlPanelComponent.Item(
items: [GlassControlGroupComponent.Item(
id: "left_" + value.title,
content: .text(value.title),
action: value.isEnabled ? { [weak self] in
guard let self else {
return
}
self.toolbarActionSelected?(.left)
} : nil
)],
background: .panel
)
},
centralItem: toolbarData.middleAction.flatMap { value in
return GlassControlPanelComponent.Item(
items: [GlassControlGroupComponent.Item(
id: "right_" + value.title,
content: .text(value.title),
action: value.isEnabled ? { [weak self] in
guard let self else {
return
}
self.toolbarActionSelected?(.middle)
} : nil
)],
background: .panel
)
},
rightItem: toolbarData.rightAction.flatMap { value in
return GlassControlPanelComponent.Item(
items: [GlassControlGroupComponent.Item(
id: "right_" + value.title,
content: .text(value.title),
action: value.isEnabled ? { [weak self] in
guard let self else {
return
}
self.toolbarActionSelected?(.right)
} : nil
)],
background: .panel
)
},
centerAlignmentIfPossible: true
)),
environment: {},
containerSize: toolbarFrame.size
)
if let toolbarView = toolbar.view {
if toolbarView.superview == nil {
self.view.addSubview(toolbarView)
toolbarView.alpha = 0.0
}
toolbarTransition.setFrame(view: toolbarView, frame: toolbarFrame)
ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 1.0)
}
} else if let toolbar = self.toolbar {
self.toolbar = nil
if let toolbarView = toolbar.view {
ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 0.0, completion: { [weak toolbarView] _ in
toolbarView?.removeFromSuperview()
})
}
} else if let toolbarNode = self.toolbarNode {
self.toolbarNode = nil
transition.updateAlpha(node: toolbarNode, alpha: 0.0, completion: { [weak toolbarNode] _ in
toolbarNode?.removeFromSupernode()
})
}
var childrenLayout = layout

View file

@ -21,7 +21,7 @@ enum ChatListFilterCategoryIcon {
case archived
}
final class ChatListFilterPresetCategoryItem: ListViewItem, ItemListItem {
final class ChatListFilterPresetCategoryItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem {
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let title: String
@ -31,6 +31,10 @@ final class ChatListFilterPresetCategoryItem: ListViewItem, ItemListItem {
let sectionId: ItemListSectionId
let updatedRevealedOptions: (Bool) -> Void
let remove: () -> Void
var hasActiveRevealOptions: Bool {
return self.isRevealed
}
init(
presentationData: ItemListPresentationData,
@ -110,6 +114,7 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL
private var item: ChatListFilterPresetCategoryItem?
private var layoutParams: ListViewItemLayoutParams?
private var isHighlighted = false
private var editableControlNode: ItemListEditableControlNode?
@ -177,7 +182,7 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL
var titleAttributedString: NSAttributedString?
let peerRevealOptions: [ItemListRevealOption]
peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]
peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)]
let rightInset: CGFloat = params.rightInset
@ -326,26 +331,29 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
let topStripeIsHidden: Bool
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
topStripeIsHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
topStripeIsHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
let bottomStripeIsHidden: Bool
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset + editingOffset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
bottomStripeIsHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
bottomStripeIsHidden = hasCorners
}
strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions)
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
@ -362,10 +370,9 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL
strongSelf.avatarNode.image = updatedAvatarImage
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)), transition: transition)
strongSelf.backgroundNode.isHidden = false
strongSelf.highlightedBackgroundNode.isHidden = true
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
@ -379,39 +386,8 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
self.isHighlighted = highlighted
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
@ -446,6 +422,12 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + params.leftInset + 15.0, y: self.avatarNode.frame.minY), size: self.avatarNode.bounds.size))
}
override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
}
override func revealOptionsInteractivelyOpened() {
if let item = self.item {

View file

@ -2071,11 +2071,16 @@ public func chatListFilterPresetController(context: AccountContext, currentPrese
)
|> deliverOnMainQueue
|> map { presentationData, stateWithPeers, peerView, premiumLimits, sharedLinks, currentPreset -> (ItemListControllerState, (ItemListNodeState, Any)) in
var presentationData = presentationData
let updatedTheme = presentationData.theme.withModalBlocksBackground()
presentationData = presentationData.withUpdated(theme: updatedTheme)
let (state, includePeers, excludePeers) = stateWithPeers
let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false
let leftNavigationButton = ItemListNavigationButton(content: .text("___close"), style: .regular, enabled: true, action: {
let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: {
if let attemptNavigationImpl {
attemptNavigationImpl({ value in
if value {
@ -2086,7 +2091,7 @@ public func chatListFilterPresetController(context: AccountContext, currentPrese
dismissImpl?()
}
})
let rightNavigationButton = ItemListNavigationButton(content: .text("___done"), style: .bold, enabled: state.isComplete, action: {
let rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: state.isComplete, action: {
applyImpl?(false, {
dismissImpl?()
})

View file

@ -613,6 +613,11 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
)
)
|> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters, peer, allLimits, displayTags -> (ItemListControllerState, (ItemListNodeState, Any)) in
var presentationData = presentationData
let updatedTheme = presentationData.theme.withModalBlocksBackground()
presentationData = presentationData.withUpdated(theme: updatedTheme)
let isPremium = peer?.isPremium ?? false
let limits = allLimits.0
let premiumLimits = allLimits.1
@ -622,13 +627,13 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
case .default:
leftNavigationButton = nil
case .modal:
leftNavigationButton = ItemListNavigationButton(content: .text("___close"), style: .regular, enabled: true, action: {
leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: {
dismissImpl?()
})
}
let rightNavigationButton: ItemListNavigationButton?
if state.isEditing {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: {
let _ = (updatedFilterOrder.get()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak updatedFilterOrder] updatedFilterOrderValue in

View file

@ -16,7 +16,7 @@ struct ChatListFilterPresetListItemEditing: Equatable {
let revealed: Bool
}
final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
final class ChatListFilterPresetListItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
@ -33,6 +33,10 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
let action: () -> Void
let setItemWithRevealedOptions: (Int32?, Int32?) -> Void
let remove: () -> Void
var hasActiveRevealOptions: Bool {
return self.editing.revealed
}
init(
context: AccountContext,
@ -145,6 +149,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
private var item: ChatListFilterPresetListItem?
private var layoutParams: ListViewItemLayoutParams?
private var isHighlighted = false
override var canBeSelected: Bool {
if self.editableControlNode != nil {
@ -242,7 +247,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
let peerRevealOptions: [ItemListRevealOption]
if item.editing.editable && item.canBeDeleted {
peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]
peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)]
} else {
peerRevealOptions = []
}
@ -396,26 +401,29 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
let topStripeIsHidden: Bool
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
topStripeIsHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
topStripeIsHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
let bottomStripeIsHidden: Bool
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset + editingOffset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
bottomStripeIsHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
bottomStripeIsHidden = hasCorners
}
strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions)
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
@ -425,9 +433,9 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset), size: titleLayout.size))
transition.updateFrame(node: strongSelf.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + 1.0), size: titleLayout.size))
let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width + revealOffset, y: verticalInset), size: labelLayout.size)
let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width + revealOffset, y: verticalInset + 1.0), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
transition.updateAlpha(node: strongSelf.labelNode, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0)
@ -496,7 +504,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 0.0), size: CGSize(width: params.width - params.rightInset - 56.0 - (leftInset + revealOffset + editingOffset), height: layout.contentSize.height))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)), transition: transition)
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
@ -517,39 +525,8 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
self.isHighlighted = highlighted
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
@ -605,6 +582,12 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
transition.updateFrame(view: tagIconView, frame: tagIconFrame)
}
}
override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
}
override func revealOptionsInteractivelyOpened() {
if let item = self.item {

View file

@ -215,14 +215,8 @@ public class ChatListFilterPresetListSuggestedItemNode: ListViewItemNode, ItemLi
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor:labelBadgeColor), backgroundColor: nil, maximumNumberOfLines: multilineLabel ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
verticalInset = 15.0
case .legacy:
verticalInset = 11.0
}
let titleSpacing: CGFloat = 3.0
let verticalInset: CGFloat = 11.0
let titleSpacing: CGFloat = 2.0
let height: CGFloat
height = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height

View file

@ -224,7 +224,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
context.engine.accountData.addAppLogEvent(type: "search_global_open_message", data: .dictionary(["msg_id": .number(Double(messageId.id))]))
}
}, openUrl: { [weak self] url in
let _ = openUserGeneratedUrl(context: context, peerId: nil, url: url, concealed: false, present: { c in
let _ = context.sharedContext.openUserGeneratedUrl(context: context, peerId: nil, url: url, webpage: nil, concealed: false, forceConcealed: false, skipUrlAuth: false, skipConcealedAlert: false, forceDark: false, present: { c in
present(c, nil)
}, openResolved: { [weak self] resolved in
context.sharedContext.openResolvedUrl(resolved, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peerId, navigation in
@ -240,7 +240,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
}, dismissInput: {
self?.dismissInput()
}, contentContext: nil, progress: nil, completion: nil)
})
}, progress: nil, alertDisplayUpdated: nil, concealedAlertOption: nil)
}, clearRecentSearch: { [weak self] in
guard let strongSelf = self else {
return

View file

@ -3517,6 +3517,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}, openActiveSessions: {
}, openBirthdaySetup: {
}, performActiveSessionAction: { _, _ in
}, performBotConnectionReviewAction: { _, _ in
}, openChatFolderUpdates: {
}, hideChatFolderUpdates: {
}, openStories: { [weak self] subject, sourceNode in
@ -5814,6 +5815,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {
}, openBirthdaySetup: {
}, performActiveSessionAction: { _, _ in
}, performBotConnectionReviewAction: { _, _ in
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
}, openStories: { _, _ in
}, openStarsTopup: { _ in
@ -6540,10 +6542,10 @@ private final class EmptyResultsButton: Component {
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: component.theme.list.itemCheckColors.fillColor,
foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
cornerRadius: 10.0
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: buttonContent,
isEnabled: isEnabled,
@ -6555,7 +6557,7 @@ private final class EmptyResultsButton: Component {
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 50.0)
containerSize: CGSize(width: availableSize.width, height: 52.0)
)
if let buttonView = self.button.view {
if buttonView.superview == nil {

View file

@ -156,7 +156,7 @@ public final class ChatListShimmerNode: ASDisplayNode {
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, performBotConnectionReviewAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
}, openPhotoSetup: {

View file

@ -185,6 +185,7 @@ class ChatListArchiveInfoItemNode: ListViewItemNode, ASScrollViewDelegate {
self.view.disablesInteractiveTransitionGestureRecognizer = true
self.scrollNode.view.scrollsToTop = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.isPagingEnabled = true
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate

View file

@ -529,10 +529,10 @@ public class ChatListItem: ListViewItem {
public 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) {
async {
let node = ChatListItemNode()
let (first, last, firstWithHeader, nextIsPinned) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
node.insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader)
let (nodeLayout, apply) = node.asyncLayout()(self, params, first, last, firstWithHeader, nextIsPinned)
let (nodeLayout, apply) = node.asyncLayout()(self, params, first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
@ -556,13 +556,13 @@ public class ChatListItem: ListViewItem {
nodeValue.setupItem(item: self, synchronousLoads: false)
let layout = nodeValue.asyncLayout()
async {
let (first, last, firstWithHeader, nextIsPinned) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
var animated = true
if case .None = animation {
animated = false
}
let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader, nextIsPinned)
let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply(false, animated)
@ -600,7 +600,7 @@ public class ChatListItem: ListViewItem {
}
}
static func mergeType(item: ChatListItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool) {
static func mergeType(item: ChatListItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool, nextHasActiveRevealControls: Bool) {
var first = false
var last = false
var firstWithHeader = false
@ -617,14 +617,16 @@ public class ChatListItem: ListViewItem {
firstWithHeader = item.header != nil
}
var nextIsPinned = false
var nextHasActiveRevealControls = false
if let nextItem = nextItem as? ChatListItem {
if case let .chatList(nextIndex) = nextItem.index, nextIndex.pinningIndex != nil {
nextIsPinned = true
}
nextHasActiveRevealControls = nextItem.hasActiveRevealControls
} else {
last = true
}
return (first, last, firstWithHeader, nextIsPinned)
return (first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls)
}
}
@ -638,9 +640,9 @@ private let ungroupIcon = ItemListRevealOptionIcon.animation(animation: "anim_un
private let readIcon = ItemListRevealOptionIcon.animation(animation: "anim_read", scale: 1.0, offset: 0.0, replaceColors: nil, flip: false)
private let unreadIcon = ItemListRevealOptionIcon.animation(animation: "anim_unread", scale: 1.0, offset: 0.0, replaceColors: [0x2194fa], flip: false)
private let archiveIcon = ItemListRevealOptionIcon.animation(animation: "anim_archive", scale: 1.0, offset: 2.0, replaceColors: [0xa9a9ad], flip: false)
private let unarchiveIcon = ItemListRevealOptionIcon.animation(animation: "anim_unarchive", scale: 0.642, offset: -9.0, replaceColors: [0xa9a9ad], flip: false)
private let hideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.0, offset: 2.0, replaceColors: [0xbdbdc2], flip: false)
private let unhideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.0, offset: -20.0, replaceColors: [0xbdbdc2], flip: true)
private let unarchiveIcon = ItemListRevealOptionIcon.animation(animation: "anim_unarchive", scale: 0.52, offset: -6.0, replaceColors: [0xa9a9ad], flip: false)
private let hideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.1, offset: 2.0, replaceColors: [0xbdbdc2], flip: false)
private let unhideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.0, offset: -15.0, replaceColors: [0xbdbdc2], flip: true)
private let startIcon = ItemListRevealOptionIcon.animation(animation: "anim_play", scale: 1.0, offset: 0.0, replaceColors: [0xbdbdc2], flip: false)
private let closeIcon = ItemListRevealOptionIcon.animation(animation: "anim_pause", scale: 1.0, offset: 0.0, replaceColors: [0xbdbdc2], flip: false)
@ -686,28 +688,28 @@ private func revealOptions(strings: PresentationStrings, theme: PresentationThem
if !isEditing {
if case .savedMessagesChats = location {
if isPinned {
options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor))
} else {
options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor))
}
} else if case .chatList(.archive) = location {
if isPinned {
options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor))
} else {
options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor))
}
} else {
if let isMuted = isMuted {
if isMuted {
options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.ChatList_Unmute, icon: unmuteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.ChatList_Unmute, icon: unmuteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, iconColor: theme.list.itemDisclosureActions.neutral2.foregroundColor, textColor: theme.chatList.dateTextColor))
} else {
options.append(ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: strings.ChatList_Mute, icon: muteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: strings.ChatList_Mute, icon: muteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, iconColor: theme.list.itemDisclosureActions.neutral2.foregroundColor, textColor: theme.chatList.dateTextColor))
}
}
}
}
if canDelete {
options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: theme.list.itemDisclosureActions.destructive.fillColor, textColor: theme.list.itemDisclosureActions.destructive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: theme.list.itemDisclosureActions.destructive.fillColor, iconColor: theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: theme.chatList.dateTextColor))
}
if case .savedMessagesChats = location {
} else {
@ -729,10 +731,10 @@ private func revealOptions(strings: PresentationStrings, theme: PresentationThem
}
if canArchive {
if canArchivePeer(id: peerId, accountPeerId: accountPeerId) {
options.append(ItemListRevealOption(key: RevealOptionKey.archive.rawValue, title: strings.ChatList_ArchiveAction, icon: archiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.archive.rawValue, title: strings.ChatList_ArchiveAction, icon: archiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, iconColor: theme.list.itemDisclosureActions.inactive.foregroundColor, textColor: theme.chatList.dateTextColor))
}
} else if canUnarchive {
options.append(ItemListRevealOption(key: RevealOptionKey.unarchive.rawValue, title: strings.ChatList_UnarchiveAction, icon: unarchiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.unarchive.rawValue, title: strings.ChatList_UnarchiveAction, icon: unarchiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, iconColor: theme.list.itemDisclosureActions.inactive.foregroundColor, textColor: theme.chatList.dateTextColor))
}
}
}
@ -743,9 +745,9 @@ private func groupReferenceRevealOptions(strings: PresentationStrings, theme: Pr
var options: [ItemListRevealOption] = []
if !isEditing {
if hiddenByDefault {
options.append(ItemListRevealOption(key: RevealOptionKey.unhide.rawValue, title: strings.ChatList_UnhideAction, icon: unhideIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.unhide.rawValue, title: strings.ChatList_UnhideAction, icon: unhideIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor))
} else {
options.append(ItemListRevealOption(key: RevealOptionKey.hide.rawValue, title: strings.ChatList_HideAction, icon: hideIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.neutral1.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.hide.rawValue, title: strings.ChatList_HideAction, icon: hideIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, iconColor: theme.list.itemDisclosureActions.neutral1.foregroundColor, textColor: theme.chatList.dateTextColor))
}
}
return options
@ -756,9 +758,9 @@ private func forumGeneralRevealOptions(strings: PresentationStrings, theme: Pres
if !isEditing {
if let isMuted = isMuted {
if isMuted {
options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.ChatList_Unmute, icon: unmuteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.ChatList_Unmute, icon: unmuteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, iconColor: theme.list.itemDisclosureActions.neutral2.foregroundColor, textColor: theme.chatList.dateTextColor))
} else {
options.append(ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: strings.ChatList_Mute, icon: muteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: strings.ChatList_Mute, icon: muteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, iconColor: theme.list.itemDisclosureActions.neutral2.foregroundColor, textColor: theme.chatList.dateTextColor))
}
}
}
@ -767,16 +769,16 @@ private func forumGeneralRevealOptions(strings: PresentationStrings, theme: Pres
if !isClosed {
} else {
options.append(ItemListRevealOption(key: RevealOptionKey.open.rawValue, title: strings.ChatList_StartAction, icon: startIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.open.rawValue, title: strings.ChatList_StartAction, icon: startIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor))
}
}
}
if canHide {
if !isEditing {
if hiddenByDefault {
options.append(ItemListRevealOption(key: RevealOptionKey.unhide.rawValue, title: strings.ChatList_ThreadUnhideAction, icon: unhideIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.unhide.rawValue, title: strings.ChatList_ThreadUnhideAction, icon: unhideIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor))
} else {
options.append(ItemListRevealOption(key: RevealOptionKey.hide.rawValue, title: strings.ChatList_ThreadHideAction, icon: hideIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.neutral1.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.hide.rawValue, title: strings.ChatList_ThreadHideAction, icon: hideIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, iconColor: theme.list.itemDisclosureActions.neutral1.foregroundColor, textColor: theme.chatList.dateTextColor))
}
}
}
@ -788,21 +790,21 @@ private func forumThreadRevealOptions(strings: PresentationStrings, theme: Prese
if !isEditing {
if let isMuted = isMuted {
if isMuted {
options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.ChatList_Unmute, icon: unmuteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.ChatList_Unmute, icon: unmuteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, iconColor: theme.list.itemDisclosureActions.neutral2.foregroundColor, textColor: theme.chatList.dateTextColor))
} else {
options.append(ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: strings.ChatList_Mute, icon: muteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: strings.ChatList_Mute, icon: muteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, iconColor: theme.list.itemDisclosureActions.neutral2.foregroundColor, textColor: theme.chatList.dateTextColor))
}
}
}
if canDelete {
options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: theme.list.itemDisclosureActions.destructive.fillColor, textColor: theme.list.itemDisclosureActions.destructive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: theme.list.itemDisclosureActions.destructive.fillColor, iconColor: theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: theme.chatList.dateTextColor))
}
if canOpenClose {
if !isEditing {
if !isClosed {
options.append(ItemListRevealOption(key: RevealOptionKey.close.rawValue, title: strings.ChatList_CloseAction, icon: closeIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.close.rawValue, title: strings.ChatList_CloseAction, icon: closeIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, iconColor: theme.list.itemDisclosureActions.inactive.foregroundColor, textColor: theme.chatList.dateTextColor))
} else {
options.append(ItemListRevealOption(key: RevealOptionKey.open.rawValue, title: strings.ChatList_StartAction, icon: startIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.open.rawValue, title: strings.ChatList_StartAction, icon: startIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor))
}
}
}
@ -815,7 +817,7 @@ private func leftRevealOptions(strings: PresentationStrings, theme: Presentation
if case .root = groupId {
var options: [ItemListRevealOption] = []
if isUnread {
options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.DialogList_Read, icon: readIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.neutral1.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.DialogList_Read, icon: readIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, iconColor: theme.list.itemDisclosureActions.neutral1.foregroundColor, textColor: theme.chatList.dateTextColor))
} else {
var canMarkUnread = true
if case let .channel(channel) = peer, channel.isForumOrMonoForum {
@ -823,15 +825,15 @@ private func leftRevealOptions(strings: PresentationStrings, theme: Presentation
}
if canMarkUnread {
options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.DialogList_Unread, icon: unreadIcon, color: theme.list.itemDisclosureActions.accent.fillColor, textColor: theme.list.itemDisclosureActions.accent.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.DialogList_Unread, icon: unreadIcon, color: theme.list.itemDisclosureActions.accent.fillColor, iconColor: theme.list.itemDisclosureActions.accent.foregroundColor, textColor: theme.chatList.dateTextColor))
}
}
if !isEditing {
if isPinned {
options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor))
} else {
if filterData == nil || peer.id.namespace != Namespaces.Peer.SecretChat {
options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor))
}
}
}
@ -1387,9 +1389,10 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
private var cachedChatListQuoteSearchResult: CachedChatListSearchResult?
private var cachedCustomTextEntities: CachedCustomTextEntities?
var layoutParams: (ChatListItem, first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool, ListViewItemLayoutParams, countersSize: CGFloat)?
var layoutParams: (ChatListItem, first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool, nextHasActiveRevealControls: Bool, ListViewItemLayoutParams, countersSize: CGFloat)?
private var isHighlighted: Bool = false
private var nextHasActiveRevealControls: Bool = false
private var skipFadeout: Bool = false
private var customAnimationInProgress: Bool = false
@ -1686,7 +1689,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in
if let strongSelf = self, let layoutParams = strongSelf.layoutParams {
let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.5, layoutParams.1, layoutParams.2, layoutParams.3, layoutParams.4)
let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.6, layoutParams.1, layoutParams.2, layoutParams.3, layoutParams.4, layoutParams.5)
let _ = apply(false, false)
}
})
@ -2036,8 +2039,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (first, last, firstWithHeader, nextIsPinned) = ChatListItem.mergeType(item: item as! ChatListItem, previousItem: previousItem, nextItem: nextItem)
let (nodeLayout, apply) = layout(item as! ChatListItem, params, first, last, firstWithHeader, nextIsPinned)
let (first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls) = ChatListItem.mergeType(item: item as! ChatListItem, previousItem: previousItem, nextItem: nextItem)
let (nodeLayout, apply) = layout(item as! ChatListItem, params, first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls)
apply(false, false)
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
@ -2056,7 +2059,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
var reallyHighlighted: Bool {
var reallyHighlighted = self.isHighlighted
var reallyHighlighted = self.isHighlighted || self.isRevealOptionsActive
if let item = self.item {
if let itemChatLocation = item.content.chatLocation {
if itemChatLocation == item.interaction.highlightedChatLocation?.location {
@ -2074,6 +2077,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
let highlightProgress: CGFloat = self.item?.interaction.highlightedChatLocation?.progress ?? 1.0
transition.updateCornerRadius(node: self.highlightedBackgroundNode, cornerRadius: self.isRevealOptionsActive ? 26.0 : 0.0)
self.updateSeparatorAlpha(transition: transition)
if self.reallyHighlighted {
if self.highlightedBackgroundNode.supernode == nil {
@ -2136,6 +2141,15 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
}
private func updateSeparatorAlpha(transition: ContainedViewLayoutTransition, inlineNavigationProgress: CGFloat? = nil) {
let revealSeparatorAlpha: CGFloat = (self.isRevealOptionsActive || self.isNextRevealOptionsActive || self.nextHasActiveRevealControls) ? 0.0 : 1.0
if let inlineNavigationProgress = inlineNavigationProgress ?? self.item?.interaction.inlineNavigationLocation?.progress {
transition.updateAlpha(node: self.separatorNode, alpha: (1.0 - inlineNavigationProgress) * revealSeparatorAlpha)
} else {
transition.updateAlpha(node: self.separatorNode, alpha: revealSeparatorAlpha)
}
}
override public func tapped() {
guard let item = self.item, item.editing else {
@ -2153,7 +2167,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
func asyncLayout() -> (_ item: ChatListItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) {
func asyncLayout() -> (_ item: ChatListItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ nextIsPinned: Bool, _ nextHasActiveRevealControls: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) {
let dateLayout = TextNode.asyncLayout(self.dateNode)
let textLayout = TextNodeWithEntities.asyncLayout(self.textNode)
let makeTrailingTextBadgeLayout = TextNode.asyncLayout(self.trailingTextBadgeNode)
@ -2175,8 +2189,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let currentChatListQuoteSearchResult = self.cachedChatListQuoteSearchResult
let currentCustomTextEntities = self.cachedCustomTextEntities
return { item, params, first, last, firstWithHeader, nextIsPinned in
let titleFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0))
return { item, params, first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls in
let titleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0))
let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
let italicTextFont = Font.italic(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
let dateFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
@ -2450,7 +2464,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else if !useChatListLayout {
avatarLeftInset = 50.0
} else {
avatarLeftInset = 18.0 + avatarDiameter
avatarLeftInset = 24.0 + avatarDiameter
}
}
@ -2589,7 +2603,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
isUser = true
}
var isGuestChatAuthor = false
if case let .user(user) = messages.last?.author, let botInfo = user.botInfo, botInfo.flags.contains(.isGuestChat) {
if let message = messages.last, case let .user(user) = message.author, user.id != message.id.peerId, let botInfo = user.botInfo, botInfo.flags.contains(.isGuestChat) {
isGuestChatAuthor = true
}
@ -3503,7 +3517,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let layoutOffset: CGFloat = 0.0
let rawContentWidth = params.width - leftInset - params.rightInset - 10.0 - editingOffset
let rawContentWidth = params.width - leftInset - params.rightInset - 18.0 - editingOffset
let (dateLayout, dateApply) = dateLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
@ -3512,7 +3526,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let (mentionBadgeLayout, mentionBadgeApply) = mentionBadgeLayout(CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), badgeDiameter, badgeFont, currentMentionBadgeImage, mentionBadgeContent)
var actionButtonTitleNodeLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if case .none = badgeContent, case .none = mentionBadgeContent, case let .chat(itemPeer) = contentPeer, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
if !item.editing, case .none = badgeContent, case .none = mentionBadgeContent, case let .chat(itemPeer) = contentPeer, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
actionButtonTitleNodeLayoutAndApply = makeActionButtonTitleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.ChatList_InlineButtonOpenApp, font: Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)), textColor: theme.unreadBadgeActiveTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
}
@ -3640,7 +3654,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
textMaxWidth -= 18.0
}
let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: (authorAttributedString == nil && itemTags.isEmpty && forumThread == nil && topForumTopicItems.isEmpty) ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: textMaxWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: textCutout, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)))
let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(
attributedString: textAttributedString,
backgroundColor: nil,
maximumNumberOfLines: (authorAttributedString == nil && itemTags.isEmpty && forumThread == nil && topForumTopicItems.isEmpty) ? 2 : 1,
truncationType: .end,
constrainedSize: CGSize(width: textMaxWidth, height: .greatestFiniteMagnitude),
alignment: .natural,
lineSpacing: 0.2,
cutout: textCutout,
insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)
))
let maxTitleLines: Int
switch item.index {
@ -3770,15 +3794,15 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
} else if case .psa = promoInfo {
peerRevealOptions = [
ItemListRevealOption(key: RevealOptionKey.hidePsa.rawValue, title: item.presentationData.strings.ChatList_HideAction, icon: deleteIcon, color: item.presentationData.theme.list.itemDisclosureActions.inactive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.neutral1.foregroundColor)
ItemListRevealOption(key: RevealOptionKey.hidePsa.rawValue, title: item.presentationData.strings.ChatList_HideAction, icon: deleteIcon, color: item.presentationData.theme.list.itemDisclosureActions.inactive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.neutral1.foregroundColor, textColor: item.presentationData.theme.chatList.dateTextColor)
]
peerLeftRevealOptions = []
} else if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
peerLeftRevealOptions = []
if customMessageListData.commandPrefix != nil {
peerRevealOptions = [
ItemListRevealOption(key: RevealOptionKey.edit.rawValue, title: item.presentationData.strings.ChatList_ItemMenuEdit, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.neutral2.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.neutral2.foregroundColor),
ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.ChatList_ItemMenuDelete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)
ItemListRevealOption(key: RevealOptionKey.edit.rawValue, title: item.presentationData.strings.ChatList_ItemMenuEdit, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.neutral2.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.neutral2.foregroundColor, textColor: item.presentationData.theme.chatList.dateTextColor),
ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.ChatList_ItemMenuDelete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.chatList.dateTextColor)
]
} else {
peerRevealOptions = []
@ -3800,13 +3824,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
if actions.contains(.toggleUnread) {
if unreadCount.unread {
peerLeftRevealOptions.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: item.presentationData.strings.DialogList_Read, icon: readIcon, color: item.presentationData.theme.list.itemDisclosureActions.inactive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.neutral1.foregroundColor))
peerLeftRevealOptions.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: item.presentationData.strings.DialogList_Read, icon: readIcon, color: item.presentationData.theme.list.itemDisclosureActions.inactive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.neutral1.foregroundColor, textColor: item.presentationData.theme.chatList.dateTextColor))
} else {
peerLeftRevealOptions.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: item.presentationData.strings.DialogList_Unread, icon: unreadIcon, color: item.presentationData.theme.list.itemDisclosureActions.accent.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.accent.foregroundColor))
peerLeftRevealOptions.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: item.presentationData.strings.DialogList_Unread, icon: unreadIcon, color: item.presentationData.theme.list.itemDisclosureActions.accent.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.accent.foregroundColor, textColor: item.presentationData.theme.chatList.dateTextColor))
}
}
if actions.contains(.delete) {
peerRevealOptions.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.Common_Delete, icon: deleteIcon, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor))
peerRevealOptions.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.Common_Delete, icon: deleteIcon, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.chatList.dateTextColor))
}
}
} else {
@ -3864,7 +3888,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
return (layout, { [weak self] synchronousLoads, animated in
if let strongSelf = self {
strongSelf.layoutParams = (item, first, last, firstWithHeader, nextIsPinned, params, countersSize)
strongSelf.layoutParams = (item, first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls, params, countersSize)
strongSelf.nextHasActiveRevealControls = nextHasActiveRevealControls
strongSelf.currentItemHeight = itemHeight
strongSelf.cachedChatListText = chatListText
strongSelf.cachedChatListSearchResult = chatListSearchResult
@ -3998,7 +4023,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + revealOffset, dy: 0.0)
let avatarFrame = CGRect(origin: CGPoint(x: leftInset - avatarLeftInset + editingOffset + 10.0 + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter))
let avatarFrame = CGRect(origin: CGPoint(x: leftInset - avatarLeftInset + editingOffset + 10.0 + 6.0 + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter))
var avatarScaleOffset: CGFloat = 0.0
var avatarScale: CGFloat = 1.0
if let inlineNavigationLocation = item.interaction.inlineNavigationLocation {
@ -4291,7 +4316,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let _ = mentionBadgeApply(animateBadges, true)
let _ = onlineApply(animateContent && animateOnline)
var dateFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size)
var dateFrame = CGRect(origin: CGPoint(x: contentRect.maxX - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size)
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.messageCount != nil, customMessageListData.commandPrefix == nil {
dateFrame.origin.x -= 10.0
@ -4355,16 +4380,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.statusNode.fontSize = item.presentationData.fontSize.itemListBaseFontSize
let _ = strongSelf.statusNode.transitionToState(statusState, animated: animateContent)
let rightAccessoryVerticalOffset: CGFloat = -2.0
var nextBadgeX: CGFloat = contentRect.maxX
if let _ = currentBadgeBackgroundImage {
let badgeFrame = CGRect(x: nextBadgeX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0, width: badgeLayout.width, height: badgeLayout.height)
let badgeFrame = CGRect(x: nextBadgeX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0 + rightAccessoryVerticalOffset, width: badgeLayout.width, height: badgeLayout.height)
transition.updateFrame(node: strongSelf.badgeNode, frame: badgeFrame)
nextBadgeX -= badgeLayout.width + 6.0
}
if currentMentionBadgeImage != nil || currentBadgeBackgroundImage != nil {
let badgeFrame = CGRect(x: nextBadgeX - mentionBadgeLayout.width, y: contentRect.maxY - mentionBadgeLayout.height - 2.0, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height)
let badgeFrame = CGRect(x: nextBadgeX - mentionBadgeLayout.width, y: contentRect.maxY - mentionBadgeLayout.height - 2.0 + rightAccessoryVerticalOffset, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height)
transition.updateFrame(node: strongSelf.mentionBadgeNode, frame: badgeFrame)
nextBadgeX -= mentionBadgeLayout.width + 6.0
@ -4375,7 +4401,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.pinnedIconNode.isHidden = false
let pinnedIconSize = currentPinnedIconImage.size
let pinnedIconFrame = CGRect(x: nextBadgeX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0, width: pinnedIconSize.width, height: pinnedIconSize.height)
let pinnedIconFrame = CGRect(x: nextBadgeX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0 + rightAccessoryVerticalOffset, width: pinnedIconSize.width, height: pinnedIconSize.height)
strongSelf.pinnedIconNode.frame = pinnedIconFrame
nextBadgeX -= pinnedIconSize.width + 6.0
@ -4392,11 +4418,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let actionButtonSize = CGSize(width: actionButtonTitleNodeLayout.size.width + actionButtonSideInset * 2.0, height: actionButtonTitleNodeLayout.size.height + actionButtonTopInset + actionButtonBottomInset)
var actionButtonFrame = CGRect(x: nextBadgeX - actionButtonSize.width, y: contentRect.minY + floor((contentRect.height - actionButtonSize.height) * 0.5), width: actionButtonSize.width, height: actionButtonSize.height)
actionButtonFrame.origin.y = max(actionButtonFrame.origin.y, dateFrame.maxY + floor(item.presentationData.fontSize.itemListBaseFontSize * 4.0 / 17.0))
actionButtonFrame.origin.y += 4.0
let actionButtonNode: HighlightableButtonNode
var animateActionButtonIn = false
if let current = strongSelf.actionButtonNode {
actionButtonNode = current
} else {
animateActionButtonIn = true
actionButtonNode = HighlightableButtonNode()
strongSelf.actionButtonNode = actionButtonNode
strongSelf.mainContentContainerNode.addSubnode(actionButtonNode)
@ -4425,23 +4454,40 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
actionButtonNode.addSubnode(actionButtonTitleNode)
}
actionButtonNode.isUserInteractionEnabled = true
actionButtonNode.frame = actionButtonFrame
actionButtonBackgroundView.frame = CGRect(origin: CGPoint(), size: actionButtonFrame.size)
actionButtonTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((actionButtonFrame.width - actionButtonTitleNodeLayout.size.width) * 0.5), y: actionButtonTopInset), size: actionButtonTitleNodeLayout.size)
if animateActionButtonIn {
actionButtonNode.alpha = 0.0
}
transition.updateAlpha(node: actionButtonNode, alpha: 1.0)
nextBadgeX -= actionButtonSize.width + 6.0
} else {
if let actionButtonTitleNode = strongSelf.actionButtonTitleNode {
actionButtonTitleNode.removeFromSupernode()
strongSelf.actionButtonTitleNode = nil
}
if let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView {
actionButtonBackgroundView.removeFromSuperview()
strongSelf.actionButtonBackgroundView = nil
}
if let actionButtonNode = strongSelf.actionButtonNode {
actionButtonNode.removeFromSupernode()
let actionButtonTitleNode = strongSelf.actionButtonTitleNode
let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView
actionButtonNode.isUserInteractionEnabled = false
strongSelf.actionButtonTitleNode = nil
strongSelf.actionButtonBackgroundView = nil
strongSelf.actionButtonNode = nil
transition.updateAlpha(node: actionButtonNode, alpha: 0.0, completion: { [weak actionButtonNode, weak actionButtonTitleNode, weak actionButtonBackgroundView] _ in
actionButtonTitleNode?.removeFromSupernode()
actionButtonBackgroundView?.removeFromSuperview()
actionButtonNode?.removeFromSupernode()
})
} else {
if let actionButtonTitleNode = strongSelf.actionButtonTitleNode {
actionButtonTitleNode.removeFromSupernode()
strongSelf.actionButtonTitleNode = nil
}
if let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView {
actionButtonBackgroundView.removeFromSuperview()
strongSelf.actionButtonBackgroundView = nil
}
}
}
@ -4470,9 +4516,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let titleFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: contentRect.origin.y + UIScreenPixel), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
let authorNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height), size: authorLayout)
let authorNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 2.0), size: authorLayout)
strongSelf.authorNode.frame = authorNodeFrame
let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 1.0 + UIScreenPixel + (authorLayout.height.isZero ? 0.0 : (authorLayout.height - 3.0))), size: textLayout.size)
let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 2.0 + (authorLayout.height.isZero ? 0.0 : (authorLayout.height - 3.0))), size: textLayout.size)
if let topForumTopicRect, !isSearching {
let compoundHighlightingNode: LinkHighlightingNode
@ -5063,20 +5109,24 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
titleBadge.textNode.removeFromSupernode()
}
let separatorInset: CGFloat
let leftSeparatorInset: CGFloat
let rightSeparatorInset: CGFloat
if case let .groupReference(groupReferenceData) = item.content, groupReferenceData.hiddenByDefault {
separatorInset = 0.0
leftSeparatorInset = 0.0
rightSeparatorInset = 0.0
} else if (!nextIsPinned && isPinned) || last {
separatorInset = 0.0
leftSeparatorInset = 0.0
rightSeparatorInset = 0.0
} else {
separatorInset = editingOffset + leftInset + rawContentRect.origin.x
leftSeparatorInset = editingOffset + leftInset + rawContentRect.origin.x
rightSeparatorInset = 16.0
}
transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, y: layoutOffset + itemHeight - separatorHeight), size: CGSize(width: params.width - separatorInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftSeparatorInset, y: layoutOffset + itemHeight - separatorHeight), size: CGSize(width: params.width - leftSeparatorInset - rightSeparatorInset, height: separatorHeight)))
if let inlineNavigationLocation = item.interaction.inlineNavigationLocation {
transition.updateAlpha(node: strongSelf.separatorNode, alpha: 1.0 - inlineNavigationLocation.progress)
strongSelf.updateSeparatorAlpha(transition: transition, inlineNavigationProgress: inlineNavigationLocation.progress)
} else {
transition.updateAlpha(node: strongSelf.separatorNode, alpha: 1.0)
strongSelf.updateSeparatorAlpha(transition: transition)
}
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
@ -5122,7 +5172,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.highlightedBackgroundNode.backgroundColor = highlightedBackgroundColor
let topNegativeInset: CGFloat = 0.0
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: layoutOffset - separatorHeight - topNegativeInset), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height + separatorHeight + topNegativeInset))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: strongSelf.revealOffset, y: layoutOffset - separatorHeight - topNegativeInset), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height + separatorHeight + topNegativeInset))
transition.updateCornerRadius(node: strongSelf.highlightedBackgroundNode, cornerRadius: strongSelf.isRevealOptionsActive ? 26.0 : 0.0)
if let peerPresence = peerPresence {
strongSelf.peerPresenceManager?.reset(presence: EnginePeer.Presence(status: peerPresence.status, lastActivity: 0), isOnline: online)
@ -5266,8 +5317,23 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
transition.updateBounds(node: self.contextContainer, bounds: self.contextContainer.frame.offsetBy(dx: -offset, dy: 0.0))
let highlightedBackgroundFrame = self.highlightedBackgroundNode.frame
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: offset, y: highlightedBackgroundFrame.minY), size: highlightedBackgroundFrame.size))
}
override public func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
self.updateIsHighlighted(transition: transition)
}
override public func nextRevealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
super.nextRevealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
self.updateSeparatorAlpha(transition: transition)
}
override public func touchesToOtherItemsPrevented() {
@ -5471,7 +5537,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
@objc private func performLocalAccessibilityCustomAction(_ action: UIAccessibilityCustomAction) {
if let action = action as? ChatListItemAccessibilityCustomAction {
self.revealOptionSelected(ItemListRevealOption(key: action.key, title: "", icon: .none, color: .black, textColor: .white), animated: false)
self.revealOptionSelected(ItemListRevealOption(key: action.key, title: "", icon: .none, color: .black, iconColor: .white, textColor: .white), animated: false)
}
}

View file

@ -106,6 +106,7 @@ public final class ChatListNodeInteraction {
let openActiveSessions: () -> Void
let openBirthdaySetup: () -> Void
let performActiveSessionAction: (NewSessionReview, Bool) -> Void
let performBotConnectionReviewAction: (NewBotConnectionReview, Bool) -> Void
let openChatFolderUpdates: () -> Void
let hideChatFolderUpdates: () -> Void
let openStories: (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void
@ -166,6 +167,7 @@ public final class ChatListNodeInteraction {
openActiveSessions: @escaping () -> Void,
openBirthdaySetup: @escaping () -> Void,
performActiveSessionAction: @escaping (NewSessionReview, Bool) -> Void,
performBotConnectionReviewAction: @escaping (NewBotConnectionReview, Bool) -> Void,
openChatFolderUpdates: @escaping () -> Void,
hideChatFolderUpdates: @escaping () -> Void,
openStories: @escaping (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void,
@ -213,6 +215,7 @@ public final class ChatListNodeInteraction {
self.openActiveSessions = openActiveSessions
self.openBirthdaySetup = openBirthdaySetup
self.performActiveSessionAction = performActiveSessionAction
self.performBotConnectionReviewAction = performBotConnectionReviewAction
self.openChatFolderUpdates = openChatFolderUpdates
self.hideChatFolderUpdates = hideChatFolderUpdates
self.openStories = openStories
@ -1796,6 +1799,17 @@ public final class ChatListNode: ListViewImpl {
let _ = self.context.engine.privacy.terminateAnotherSession(id: newSessionReview.id).startStandalone()
#endif
}
}, performBotConnectionReviewAction: { [weak self] newBotConnectionReview, isPositive in
guard let self else {
return
}
if isPositive {
let _ = self.context.engine.accountData.confirmBotConnectionReview(botId: newBotConnectionReview.botId).startStandalone()
} else {
let _ = removeNewBotConnectionReviews(postbox: self.context.account.postbox, botIds: [newBotConnectionReview.botId]).startStandalone()
let _ = self.context.engine.accountData.setAccountConnectedBot(bot: nil).startStandalone()
}
}, openChatFolderUpdates: { [weak self] in
guard let self else {
return

View file

@ -152,7 +152,6 @@ public final class ChatPanelInterfaceInteraction {
public let displaySendMessageOptions: (ASDisplayNode, ContextGesture) -> Void
public let openScheduledMessages: () -> Void
public let displaySearchResultsTooltip: (ASDisplayNode, CGRect) -> Void
public let openPeersNearby: () -> Void
public let unarchivePeer: () -> Void
public let scrollToTop: () -> Void
public let viewReplies: (EngineMessage.Id?, ChatReplyThreadMessage) -> Void
@ -284,7 +283,6 @@ public final class ChatPanelInterfaceInteraction {
displaySlowmodeTooltip: @escaping (UIView, CGRect) -> Void,
displaySendMessageOptions: @escaping (ASDisplayNode, ContextGesture) -> Void,
openScheduledMessages: @escaping () -> Void,
openPeersNearby: @escaping () -> Void,
displaySearchResultsTooltip: @escaping (ASDisplayNode, CGRect) -> Void,
unarchivePeer: @escaping () -> Void,
scrollToTop: @escaping () -> Void,
@ -416,7 +414,6 @@ public final class ChatPanelInterfaceInteraction {
self.displaySlowmodeTooltip = displaySlowmodeTooltip
self.displaySendMessageOptions = displaySendMessageOptions
self.openScheduledMessages = openScheduledMessages
self.openPeersNearby = openPeersNearby
self.displaySearchResultsTooltip = displaySearchResultsTooltip
self.unarchivePeer = unarchivePeer
self.scrollToTop = scrollToTop
@ -556,7 +553,6 @@ public final class ChatPanelInterfaceInteraction {
}, displaySlowmodeTooltip: { _, _ in
}, displaySendMessageOptions: { _, _ in
}, openScheduledMessages: {
}, openPeersNearby: {
}, displaySearchResultsTooltip: { _, _ in
}, unarchivePeer: {
}, scrollToTop: {

View file

@ -20,6 +20,7 @@ swift_library(
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent",
"//submodules/TelegramUI/Components/AlertComponent/AlertWebpagePreviewComponent",
],
visibility = [
"//visibility:public",

View file

@ -10,81 +10,142 @@ import UrlEscaping
import ComponentFlow
import AlertComponent
import AlertMultilineInputFieldComponent
import AlertWebpagePreviewComponent
public func chatTextLinkEditController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
text: String,
link: String?,
apply: @escaping (String?) -> Void
preview: Bool = false,
apply: @escaping (String?, TelegramMediaWebpage?) -> Void
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let inputState = AlertMultilineInputFieldComponent.ExternalState()
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: link != nil ? strings.TextFormat_EditLinkTitle : strings.TextFormat_AddLinkTitle)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(strings.TextFormat_AddLinkText(text).string))
)
))
var applyImpl: (() -> Void)?
content.append(AnyComponentWithIdentity(
id: "input",
component: AnyComponent(
AlertMultilineInputFieldComponent(
context: context,
initialValue: link.flatMap { NSAttributedString(string: $0) },
placeholder: strings.TextFormat_AddLinkPlaceholder,
returnKeyType: .done,
keyboardType: .URL,
autocapitalizationType: .none,
autocorrectionType: .no,
isInitiallyFocused: true,
externalState: inputState,
returnKeyAction: {
applyImpl?()
}
)
)
))
var effectiveUpdatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)
if let updatedPresentationData {
effectiveUpdatedPresentationData = updatedPresentationData
} else {
effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData)
}
let makeContent: (TelegramMediaWebpage?) -> [AnyComponentWithIdentity<AlertComponentEnvironment>] = { webpage in
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: link != nil ? strings.TextFormat_EditLinkTitle : strings.TextFormat_AddLinkTitle)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(text))
)
))
content.append(AnyComponentWithIdentity(
id: "input",
component: AnyComponent(
AlertMultilineInputFieldComponent(
context: context,
initialValue: link.flatMap { NSAttributedString(string: $0) },
placeholder: strings.TextFormat_AddLinkPlaceholder,
returnKeyType: .done,
keyboardType: .URL,
autocapitalizationType: .none,
autocorrectionType: .no,
isInitiallyFocused: true,
externalState: inputState,
returnKeyAction: {
applyImpl?()
}
)
)
))
if let webpage {
content.append(AnyComponentWithIdentity(
id: "webpagePreview",
component: AnyComponent(AlertWebpagePreviewComponent(
context: context,
presentationData: effectiveUpdatedPresentationData.0,
webpage: webpage
))
))
}
return content
}
let contentPromise = Promise<[AnyComponentWithIdentity<AlertComponentEnvironment>]>()
contentPromise.set(.single(makeContent(nil)))
var dismissImpl: (() -> Void)?
let alertController = AlertScreen(
configuration: AlertScreen.Configuration(allowInputInset: true),
content: content,
actions: [
contentSignal: contentPromise.get(),
actionsSignal: .single([
.init(title: strings.Common_Cancel),
.init(title: strings.Common_Done, type: .default, action: {
applyImpl?()
}, autoDismiss: false)
],
]),
updatedPresentationData: effectiveUpdatedPresentationData
)
let previewDisposable = MetaDisposable()
var currentPreview: (link: String, webpage: TelegramMediaWebpage)?
if preview {
var currentDisplayedPreview: (link: String, webpage: TelegramMediaWebpage?)?
previewDisposable.set((inputState.valueSignal
|> map { value -> String in
return explicitUrl(value.string)
}
|> distinctUntilChanged
|> mapToSignal { link -> Signal<(String, TelegramMediaWebpage?), NoError> in
guard !link.isEmpty && isValidUrl(link, validSchemes: ["http": true, "https": true, "tg": false, "ton": false, "tonsite": true]) else {
return .single((link, nil))
}
let previewSignal = webpagePreview(account: context.account, urls: [link])
|> map { result -> (String, TelegramMediaWebpage?) in
guard case let .result(result) = result, let webpage = result?.webpage, case .Loaded = webpage.content else {
return (link, nil)
}
return (link, webpage)
}
return .single((link, nil))
|> then((.complete() |> delay(1.0, queue: Queue.mainQueue())) |> then(previewSignal))
}
|> deliverOnMainQueue).startStrict(next: { link, webpage in
if let currentDisplayedPreview, currentDisplayedPreview.link == link && currentDisplayedPreview.webpage == webpage {
return
}
currentDisplayedPreview = (link, webpage)
if let webpage {
currentPreview = (link, webpage)
} else {
currentPreview = nil
}
contentPromise.set(.single(makeContent(webpage)))
}))
}
alertController.dismissed = { _ in
previewDisposable.dispose()
}
applyImpl = {
let updatedLink = explicitUrl(inputState.value.string)
if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false, "tonsite": true]) {
dismissImpl?()
apply(updatedLink)
apply(updatedLink, currentPreview?.link == updatedLink ? currentPreview?.webpage : nil)
} else if inputState.value.string.isEmpty {
dismissImpl?()
apply("")
apply("", nil)
} else {
inputState.animateError()
}

View file

@ -7,19 +7,22 @@ public final class Image: Component {
public let size: CGSize?
public let contentMode: UIImageView.ContentMode
public let cornerRadius: CGFloat
public let flipHorizontally: Bool
public init(
image: UIImage?,
tintColor: UIColor? = nil,
size: CGSize? = nil,
contentMode: UIImageView.ContentMode = .scaleToFill,
cornerRadius: CGFloat = 0.0
cornerRadius: CGFloat = 0.0,
flipHorizontally: Bool = false
) {
self.image = image
self.tintColor = tintColor
self.size = size
self.contentMode = contentMode
self.cornerRadius = cornerRadius
self.flipHorizontally = flipHorizontally
}
public static func ==(lhs: Image, rhs: Image) -> Bool {
@ -38,6 +41,9 @@ public final class Image: Component {
if lhs.cornerRadius != rhs.cornerRadius {
return false
}
if lhs.flipHorizontally != rhs.flipHorizontally {
return false
}
return true
}
@ -51,7 +57,11 @@ public final class Image: Component {
}
func update(component: Image, availableSize: CGSize, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.image = component.image
if component.flipHorizontally, let cgImage = component.image?.cgImage {
self.image = UIImage(cgImage: cgImage, scale: component.image?.scale ?? 0.0, orientation: .upMirrored)
} else {
self.image = component.image
}
self.contentMode = component.contentMode
self.clipsToBounds = component.cornerRadius > 0.0

View file

@ -11,15 +11,17 @@ public final class BundleIconComponent: Component {
public let scaleFactor: CGFloat
public let shadowColor: UIColor?
public let shadowBlur: CGFloat
public let flipHorizontally: Bool
public let flipVertically: Bool
public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, scaleFactor: CGFloat = 1.0, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0, flipVertically: Bool = false) {
public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, scaleFactor: CGFloat = 1.0, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0, flipHorizontally: Bool = false, flipVertically: Bool = false) {
self.name = name
self.tintColor = tintColor
self.maxSize = maxSize
self.scaleFactor = scaleFactor
self.shadowColor = shadowColor
self.shadowBlur = shadowBlur
self.flipHorizontally = flipHorizontally
self.flipVertically = flipVertically
}
@ -42,6 +44,9 @@ public final class BundleIconComponent: Component {
if lhs.shadowBlur != rhs.shadowBlur {
return false
}
if lhs.flipHorizontally != rhs.flipHorizontally {
return false
}
if lhs.flipVertically != rhs.flipVertically {
return false
}
@ -77,7 +82,9 @@ public final class BundleIconComponent: Component {
}
})
}
if component.flipVertically, let cgImage = image?.cgImage {
if component.flipHorizontally, let cgImage = image?.cgImage {
self.image = UIImage(cgImage: cgImage, scale: image?.scale ?? 0.0, orientation: .upMirrored)
} else if component.flipVertically, let cgImage = image?.cgImage {
self.image = UIImage(cgImage: cgImage, scale: image?.scale ?? 0.0, orientation: .down)
} else {
self.image = image

View file

@ -291,6 +291,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
self.scrollNode.view.scrollsToTop = false
self.itemNodes = reactions.map { reaction, count in
return ItemNode(context: context, availableReactions: availableReactions, reaction: reaction, animationCache: animationCache, animationRenderer: animationRenderer, count: count)
@ -897,6 +898,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.scrollNode.clipsToBounds = false
self.scrollNode.view.scrollsToTop = false
super.init()

View file

@ -184,7 +184,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
private let dimView: UIView
private let scrollView: ScrollView
private let backgroundView: SheetBackgroundView
private var effectView: UIVisualEffectView?
private var effectView: SheetBackgroundBlurView?
private let clipView: SheetBackgroundView
private let contentView: ComponentView<ChildEnvironmentType>
private var headerView: ComponentView<Empty>?
@ -411,13 +411,13 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
}
var backgroundColor: UIColor = .clear
var blurEffectStyle: UIBlurEffect.Style?
switch component.backgroundColor {
case let .blur(style):
blurEffectStyle = style == .dark ? .dark : .light
self.backgroundView.isHidden = true
if self.effectView == nil {
let effectView = UIVisualEffectView(effect: UIBlurEffect(style: style == .dark ? .dark : .light))
effectView.layer.cornerRadius = self.backgroundView.layer.cornerRadius
effectView.layer.masksToBounds = true
let effectView = SheetBackgroundBlurView()
self.backgroundView.superview?.insertSubview(effectView, aboveSubview: self.backgroundView)
self.effectView = effectView
}
@ -471,8 +471,9 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
transition.setFrame(view: self.clipView, frame: clipFrame, completion: nil)
transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: clipFrame.size), completion: nil)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil)
if let effectView = self.effectView {
if let effectView = self.effectView, let blurEffectStyle = blurEffectStyle {
transition.setFrame(view: effectView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil)
effectView.update(style: blurEffectStyle, size: contentSize, topCornerRadius: topCornerRadius, bottomCornerRadius: topCornerRadius, transition: transition)
}
self.backgroundView.update(size: contentSize, color: backgroundColor, topCornerRadius: topCornerRadius, bottomCornerRadius: topCornerRadius, transition: transition)
} else {
@ -483,14 +484,19 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
transition.setFrame(view: self.clipView, frame: clipFrame)
transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height)), completion: nil)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: glassInset, y: -glassInset), size: CGSize(width: contentSize.width, height: contentSize.height)), completion: nil)
if let effectView = self.effectView, let blurEffectStyle = blurEffectStyle {
transition.setFrame(view: effectView, frame: CGRect(origin: CGPoint(x: glassInset, y: -glassInset), size: CGSize(width: contentSize.width, height: contentSize.height)), completion: nil)
effectView.update(style: blurEffectStyle, size: contentSize, topCornerRadius: topCornerRadius + 1.5, bottomCornerRadius: bottomCornerRadius, transition: transition)
}
case .legacy:
let clipFrame = CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 100.0))
self.clipView.update(size: clipFrame.size, color: .clear, topCornerRadius: topCornerRadius, bottomCornerRadius: bottomCornerRadius, transition: transition)
transition.setFrame(view: self.clipView, frame: clipFrame)
transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: clipFrame.size), completion: nil)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil)
if let effectView = self.effectView {
if let effectView = self.effectView, let blurEffectStyle = blurEffectStyle {
transition.setFrame(view: effectView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil)
effectView.update(style: blurEffectStyle, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0), topCornerRadius: topCornerRadius + 1.5, bottomCornerRadius: bottomCornerRadius, transition: transition)
}
}
self.backgroundView.update(size: contentSize, color: backgroundColor, topCornerRadius: topCornerRadius + 1.5, bottomCornerRadius: bottomCornerRadius, transition: transition)
@ -614,3 +620,31 @@ public final class SheetBackgroundView: UIView {
transition.setBackgroundColor(view: self.bottomCornersView, color: color)
}
}
private final class SheetBackgroundBlurView: UIView {
private let clipView = SheetBackgroundView()
private let effectView = UIVisualEffectView()
private var currentStyle: UIBlurEffect.Style?
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.clipView)
self.clipView.bottomCornersView.addSubview(self.effectView)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
func update(style: UIBlurEffect.Style, size: CGSize, topCornerRadius: CGFloat, bottomCornerRadius: CGFloat, transition: ComponentTransition) {
if self.currentStyle != style {
self.currentStyle = style
self.effectView.effect = UIBlurEffect(style: style)
}
self.clipView.update(size: size, color: .clear, topCornerRadius: topCornerRadius, bottomCornerRadius: bottomCornerRadius, transition: transition)
transition.setFrame(view: self.clipView, frame: CGRect(origin: .zero, size: size))
transition.setFrame(view: self.effectView, frame: CGRect(origin: .zero, size: size))
}
}

View file

@ -47,6 +47,7 @@ swift_library(
"//submodules/ContextUI",
"//submodules/TelegramUI/Components/EdgeEffect",
"//submodules/TelegramUI/Components/SearchInputPanelComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/SearchBarNode",
],

View file

@ -375,49 +375,6 @@ public class ContactsController: ViewController {
}
}
self.contactsNode.openPeopleNearby = { [weak self] in
let _ = (DeviceAccess.authorizationStatus(subject: .location(.tracking))
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] status in
guard let strongSelf = self else {
return
}
let presentPeersNearby = {
let controller = strongSelf.context.sharedContext.makePeersNearbyController(context: strongSelf.context)
controller.navigationPresentation = .master
if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController {
var controllers = navigationController.viewControllers.filter { !($0 is PermissionController) }
controllers.append(controller)
navigationController.setViewControllers(controllers, animated: true)
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
}
}
switch status {
case .allowed:
presentPeersNearby()
default:
let controller = PermissionController(context: strongSelf.context, splashScreen: false)
controller.setState(.permission(.nearbyLocation(status: PermissionRequestStatus(accessType: status))), animated: false)
controller.navigationPresentation = .master
controller.proceed = { result in
if result {
presentPeersNearby()
} else {
let _ = (strongSelf.navigationController as? NavigationController)?.popViewController(animated: true)
}
}
if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController {
navigationController.pushViewController(controller, completion: { [weak self] in
if let strongSelf = self {
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
}
})
}
}
})
}
self.contactsNode.openInvite = { [weak self] in
let _ = (DeviceAccess.authorizationStatus(subject: .contacts)
|> take(1)

View file

@ -64,7 +64,6 @@ final class ContactsControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
var requestOpenPeerFromSearch: ((ContactListPeer) -> Void)?
var requestOpenDisabledPeerFromSearch: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?
var requestAddContact: ((String) -> Void)?
var openPeopleNearby: (() -> Void)?
var openInvite: (() -> Void)?
var openQrScan: (() -> Void)?
var openStories: ((EnginePeer, ASDisplayNode) -> Void)?

View file

@ -5,6 +5,7 @@ import Display
import TelegramPresentationData
import ComponentFlow
import ButtonComponent
import AnimatedTextComponent
import EdgeEffect
final class InviteContactsCountPanelNode: ASDisplayNode {
@ -69,15 +70,11 @@ final class InviteContactsCountPanelNode: ASDisplayNode {
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(ButtonTextContentComponent(
text: self.strings.Contacts_InviteContacts(Int32(self.count)),
badge: 0,
textColor: self.theme.list.itemCheckColors.foregroundColor,
badgeBackground: self.theme.list.itemCheckColors.foregroundColor,
badgeForeground: self.theme.list.itemCheckColors.fillColor,
badgeStyle: .roundedRectangle,
badgeIconName: nil,
combinedAlignment: true
component: AnyComponent(AnimatedTextComponent(
font: Font.with(size: 17.0, weight: .semibold, traits: .monospacedNumbers),
color: self.theme.list.itemCheckColors.foregroundColor,
items: [AnimatedTextComponent.Item(id: "text", content: .text(self.strings.Contacts_InviteContacts(Int32(max(1, self.count)))))],
noDelay: true
))
),
isEnabled: true,

View file

@ -1236,7 +1236,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
color = item.presentationData.theme.list.itemDisclosureActions.accent.fillColor
textColor = item.presentationData.theme.list.itemDisclosureActions.accent.foregroundColor
}
mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, textColor: textColor))
mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, iconColor: textColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor))
index += 1
}
peerRevealOptions = mappedOptions
@ -2002,7 +2002,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
if item.editing.editable {
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)]))
strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated)
} else {
strongSelf.setRevealOptions((left: [], right: peerRevealOptions))

View file

@ -1,146 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import TelegramStringFormatting
import SwiftSignalKit
import AccountContext
import UIKitRuntimeUtils
public final class DateSelectionActionSheetController: ActionSheetController {
private var presentationDisposable: Disposable?
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
public init(context: AccountContext, title: String?, currentValue: Int32, minimumDate: Date? = nil, maximumDate: Date? = nil, emptyTitle: String? = nil, applyValue: @escaping (Int32?) -> Void) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
super.init(theme: ActionSheetControllerTheme(presentationData: presentationData))
self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData)
}
}).strict()
self._ready.set(.single(true))
var updatedValue = currentValue
var items: [ActionSheetItem] = []
if let title = title {
items.append(ActionSheetTextItem(title: title))
}
items.append(DateSelectionActionSheetItem(strings: strings, currentValue: currentValue, minimumDate: minimumDate, maximumDate: maximumDate, valueChanged: { value in
updatedValue = value
}))
if let emptyTitle = emptyTitle {
items.append(ActionSheetButtonItem(title: emptyTitle, action: { [weak self] in
self?.dismissAnimated()
applyValue(nil)
}))
}
items.append(ActionSheetButtonItem(title: strings.Wallpaper_Set, action: { [weak self] in
self?.dismissAnimated()
applyValue(updatedValue)
}))
self.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in
self?.dismissAnimated()
}),
])
])
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDisposable?.dispose()
}
}
private final class DateSelectionActionSheetItem: ActionSheetItem {
let strings: PresentationStrings
let currentValue: Int32
let minimumDate: Date?
let maximumDate: Date?
let valueChanged: (Int32) -> Void
init(strings: PresentationStrings, currentValue: Int32, minimumDate: Date?, maximumDate: Date?, valueChanged: @escaping (Int32) -> Void) {
self.strings = strings
self.currentValue = roundDateToDays(currentValue)
self.minimumDate = minimumDate
self.maximumDate = maximumDate
self.valueChanged = valueChanged
}
func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
return DateSelectionActionSheetItemNode(theme: theme, strings: self.strings, currentValue: self.currentValue, minimumDate: self.minimumDate, maximumDate: self.maximumDate, valueChanged: self.valueChanged)
}
func updateNode(_ node: ActionSheetItemNode) {
}
}
private final class DateSelectionActionSheetItemNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
private let strings: PresentationStrings
private let valueChanged: (Int32) -> Void
private let pickerView: UIDatePicker
init(theme: ActionSheetControllerTheme, strings: PresentationStrings, currentValue: Int32, minimumDate: Date?, maximumDate: Date?, valueChanged: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.valueChanged = valueChanged
UILabel.setDateLabel(theme.primaryTextColor)
self.pickerView = UIDatePicker()
self.pickerView.timeZone = TimeZone(secondsFromGMT: 0)
self.pickerView.datePickerMode = .countDownTimer
self.pickerView.datePickerMode = .date
self.pickerView.date = Date(timeIntervalSince1970: Double(roundDateToDays(currentValue)))
self.pickerView.locale = localeWithStrings(strings)
if #available(iOS 13.4, *) {
self.pickerView.preferredDatePickerStyle = .wheels
}
if let minimumDate = minimumDate {
self.pickerView.minimumDate = minimumDate
}
if let maximumDate = maximumDate {
self.pickerView.maximumDate = maximumDate
} else {
self.pickerView.maximumDate = Date(timeIntervalSince1970: Double(Int32.max - 1))
}
self.pickerView.setValue(theme.primaryTextColor, forKey: "textColor")
super.init(theme: theme)
self.view.addSubview(self.pickerView)
self.pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged)
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 216.0)
self.pickerView.frame = CGRect(origin: CGPoint(), size: size)
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
@objc private func datePickerUpdated() {
self.valueChanged(roundDateToDays(Int32(self.pickerView.date.timeIntervalSince1970)))
}
}

View file

@ -1709,7 +1709,7 @@ public func debugController(sharedContext: SharedAccountContext, context: Accoun
var leftNavigationButton: ItemListNavigationButton?
if modal {
leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: {
dismissImpl?()
})
}

View file

@ -104,19 +104,19 @@ public final class DeviceLocationManager: NSObject {
self.currentTopMode = topMode
if let topMode = topMode {
self.log?("setting mode \(topMode)")
switch topMode {
case .preciseForeground:
self.manager.allowsBackgroundLocationUpdates = false
case .preciseAlways:
self.manager.allowsBackgroundLocationUpdates = true
}
if previousTopMode == nil {
if !self.requestedAuthorization {
self.requestedAuthorization = true
self.manager.requestAlwaysAuthorization()
}
switch topMode {
case .preciseForeground:
self.manager.allowsBackgroundLocationUpdates = false
case .preciseAlways:
self.manager.allowsBackgroundLocationUpdates = true
}
self.manager.startUpdatingLocation()
self.manager.startUpdatingHeading()
}

View file

@ -88,6 +88,12 @@ final class ActionSheetControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.scrollsToTop = false
}
func performHighlightedAction() {
self.itemGroupsContainerNode.performHighlightedAction()
}

View file

@ -52,6 +52,7 @@ final class ActionSheetItemGroupNode: ASDisplayNode, ASScrollViewDelegate {
self.scrollNode.view.canCancelContentTouches = true
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.scrollsToTop = false
super.init()

View file

@ -72,8 +72,21 @@ public extension CALayer {
func makeAnimation(from: Any?, to: Any, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) -> CAAnimation {
if timingFunction.hasPrefix(kCAMediaTimingFunctionCustomSpringPrefix) {
let components = timingFunction.components(separatedBy: "_")
let damping = Float(components[1]) ?? 100.0
let initialVelocity = Float(components[2]) ?? 0.0
let mass: Float
let stiffness: Float
let damping: Float
let initialVelocity: Float
if components.count >= 5 {
mass = Float(components[1]) ?? 5.0
stiffness = Float(components[2]) ?? 900.0
damping = Float(components[3]) ?? 100.0
initialVelocity = Float(components[4]) ?? 0.0
} else {
mass = 5.0
stiffness = 900.0
damping = components.count > 1 ? (Float(components[1]) ?? 100.0) : 100.0
initialVelocity = components.count > 2 ? (Float(components[2]) ?? 0.0) : 0.0
}
let animation = CASpringAnimation(keyPath: keyPath)
animation.fromValue = from
@ -83,10 +96,10 @@ public extension CALayer {
if let completion = completion {
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
}
animation.mass = CGFloat(mass)
animation.stiffness = CGFloat(stiffness)
animation.damping = CGFloat(damping)
animation.initialVelocity = CGFloat(initialVelocity)
animation.mass = 5.0
animation.stiffness = 900.0
animation.duration = animation.settlingDuration
animation.timingFunction = CAMediaTimingFunction.init(name: .linear)
let k = Float(UIView.animationDurationFactor())

View file

@ -14,7 +14,7 @@ public enum ContainedViewLayoutTransitionCurve: Equatable, Hashable {
case easeInOut
case easeIn
case spring
case customSpring(damping: CGFloat, initialVelocity: CGFloat)
case customSpring(mass: CGFloat = 5.0, stiffness: CGFloat = 900.0, damping: CGFloat, initialVelocity: CGFloat)
case custom(Float, Float, Float, Float)
public static var slide: ContainedViewLayoutTransitionCurve {
@ -52,8 +52,8 @@ public extension ContainedViewLayoutTransitionCurve {
return CAMediaTimingFunctionName.easeIn.rawValue
case .spring:
return kCAMediaTimingFunctionSpring
case let .customSpring(damping, initialVelocity):
return "\(kCAMediaTimingFunctionCustomSpringPrefix)_\(damping)_\(initialVelocity)"
case let .customSpring(mass, stiffness, damping, initialVelocity):
return "\(kCAMediaTimingFunctionCustomSpringPrefix)_\(mass)_\(stiffness)_\(damping)_\(initialVelocity)"
case .custom:
return CAMediaTimingFunctionName.easeInEaseOut.rawValue
}
@ -124,8 +124,8 @@ private extension CALayer {
let timingFunction: String
let mediaTimingFunction: CAMediaTimingFunction?
switch curve {
case .spring:
timingFunction = kCAMediaTimingFunctionSpring
case .spring, .customSpring:
timingFunction = curve.timingFunction
mediaTimingFunction = nil
default:
timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue

View file

@ -509,6 +509,7 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur
self.scroller.contentSize = CGSize(width: 0.0, height: infiniteScrollSize * 2.0)
self.scroller.isHidden = true
self.scroller.delegate = self.wrappedScrollViewDelegate
self.scroller.scrollsToTop = false
self.view.addSubview(self.scroller)
self.scroller.panGestureRecognizer.cancelsTouchesInView = true
self.view.addGestureRecognizer(self.scroller.panGestureRecognizer)

View file

@ -172,6 +172,7 @@ open class NavigationController: UINavigationController, ContainableController,
private var inCallStatusBar: StatusBar?
private var updateInCallStatusBarState: CallStatusBarNode?
private var globalScrollToTopNode: ScrollToTopNode?
private var windowScrollToTopProxyViews: WindowScrollToTopProxyViews?
private var rootContainer: RootContainer?
private var rootModalFrame: NavigationModalFrame?
private var modalContainers: [NavigationModalContainer] = []
@ -331,6 +332,54 @@ open class NavigationController: UINavigationController, ContainableController,
}
deinit {
self.windowScrollToTopProxyViews?.update(window: nil, mode: .disabled, referenceView: nil)
}
private func getWindowScrollToTopProxyViews() -> WindowScrollToTopProxyViews {
if let windowScrollToTopProxyViews = self.windowScrollToTopProxyViews {
return windowScrollToTopProxyViews
}
let windowScrollToTopProxyViews = WindowScrollToTopProxyViews(scrollToTop: { [weak self] subject in
self?.scrollToTop(subject)
})
self.windowScrollToTopProxyViews = windowScrollToTopProxyViews
return windowScrollToTopProxyViews
}
private func windowScrollToTopReferenceView(window: UIWindow) -> UIView? {
var view: UIView? = self.view
while let currentView = view {
if currentView.superview === window {
return currentView
}
view = currentView.superview
}
return nil
}
private func updateWindowScrollToTopProxyViews(layout: ContainerViewLayout) {
guard let window = self.view.window, let rootContainer = self.rootContainer else {
(self.globalScrollToTopNode?.view as? ScrollToTopView)?.scrollsToTop = true
self.windowScrollToTopProxyViews?.update(window: nil, mode: .disabled, referenceView: nil)
return
}
(self.globalScrollToTopNode?.view as? ScrollToTopView)?.scrollsToTop = false
let referenceView = self.windowScrollToTopReferenceView(window: window)
let proxyViews = self.getWindowScrollToTopProxyViews()
switch rootContainer {
case .flat:
let scrollToTopHeight = max(layout.statusBarHeight ?? layout.safeInsets.top, 1.0)
let frame = self.view.convert(CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: scrollToTopHeight)), to: window)
proxyViews.update(window: window, mode: .flat(frame: frame), referenceView: referenceView)
case let .split(container):
let frames = container.scrollToTopProxyFrames(layout: layout)
proxyViews.update(window: window, mode: .split(
masterFrame: container.view.convert(frames.master, to: window),
detailFrame: container.view.convert(frames.detail, to: window)
), referenceView: referenceView)
}
}
public func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations {
@ -498,7 +547,7 @@ open class NavigationController: UINavigationController, ContainableController,
}
if let globalScrollToTopNode = self.globalScrollToTopNode {
globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1))
globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1.0))
}
var overlayContainerLayout = layout
@ -1018,8 +1067,6 @@ open class NavigationController: UINavigationController, ContainableController,
case let .flat(flatContainer):
let splitContainer = NavigationSplitContainer(theme: self.theme, controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
}, scrollToTop: { [weak self] subject in
self?.scrollToTop(subject)
})
if let detailsPlaceholderNode = self.detailsPlaceholderNode {
self.displayNode.insertSubnode(splitContainer, aboveSubnode: detailsPlaceholderNode)
@ -1053,8 +1100,6 @@ open class NavigationController: UINavigationController, ContainableController,
} else {
let splitContainer = NavigationSplitContainer(theme: self.theme, controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
}, scrollToTop: { [weak self] subject in
self?.scrollToTop(subject)
})
if let detailsPlaceholderNode = self.detailsPlaceholderNode {
self.displayNode.insertSubnode(splitContainer, aboveSubnode: detailsPlaceholderNode)
@ -1385,6 +1430,8 @@ open class NavigationController: UINavigationController, ContainableController,
}
}
self.updateWindowScrollToTopProxyViews(layout: layout)
self.isUpdatingContainers = false
if notifyGlobalOverlayControllersUpdated {

View file

@ -91,6 +91,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
self.scrollNode.view.clipsToBounds = false
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.tag = 0x5C4011
self.scrollNode.view.scrollsToTop = false
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
guard let strongSelf = self, !strongSelf.isDismissed else {

View file

@ -11,8 +11,6 @@ enum NavigationSplitContainerScrollToTop {
final class NavigationSplitContainer: ASDisplayNode {
private var theme: NavigationControllerTheme
private let masterScrollToTopView: ScrollToTopView
private let detailScrollToTopView: ScrollToTopView
private let masterContainer: NavigationContainer
private let detailContainer: NavigationContainer
private let separator: ASDisplayNode
@ -39,18 +37,9 @@ final class NavigationSplitContainer: ASDisplayNode {
self.detailContainer.isInFocus = isInFocus
}
init(theme: NavigationControllerTheme, controllerRemoved: @escaping (ViewController) -> Void, scrollToTop: @escaping (NavigationSplitContainerScrollToTop) -> Void) {
init(theme: NavigationControllerTheme, controllerRemoved: @escaping (ViewController) -> Void) {
self.theme = theme
self.masterScrollToTopView = ScrollToTopView(frame: CGRect())
self.masterScrollToTopView.action = {
scrollToTop(.master)
}
self.detailScrollToTopView = ScrollToTopView(frame: CGRect())
self.detailScrollToTopView.action = {
scrollToTop(.detail)
}
self.masterContainer = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved)
self.masterContainer.clipsToBounds = true
@ -65,8 +54,6 @@ final class NavigationSplitContainer: ASDisplayNode {
self.addSubnode(self.masterContainer)
self.addSubnode(self.detailContainer)
self.addSubnode(self.separator)
self.view.addSubview(self.masterScrollToTopView)
self.view.addSubview(self.detailScrollToTopView)
}
func hasNonReadyControllers() -> Bool {
@ -83,13 +70,21 @@ final class NavigationSplitContainer: ASDisplayNode {
self.separator.backgroundColor = theme.navigationBar.separatorColor
}
func scrollToTopProxyFrames(layout: ContainerViewLayout) -> (master: CGRect, detail: CGRect) {
let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0))
let detailWidth = layout.size.width - masterWidth
let scrollToTopHeight = max(layout.statusBarHeight ?? layout.safeInsets.top, 1.0)
return (
master: CGRect(origin: CGPoint(), size: CGSize(width: masterWidth, height: scrollToTopHeight)),
detail: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: scrollToTopHeight))
)
}
func update(layout: ContainerViewLayout, masterControllers: [ViewController], detailControllers: [ViewController], detailsPlaceholderNode: NavigationDetailsPlaceholderNode?, transition: ContainedViewLayoutTransition) {
let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0))
let detailWidth = layout.size.width - masterWidth
self.masterScrollToTopView.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: masterWidth, height: 10.0))
self.detailScrollToTopView.frame = CGRect(origin: CGPoint(x: masterWidth, y: -1.0), size: CGSize(width: detailWidth, height: 1.0))
transition.updateFrame(node: self.masterContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: masterWidth, height: layout.size.height)))
transition.updateFrame(node: self.detailContainer, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: layout.size.height)))
transition.updateFrame(node: self.separator, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height)))

View file

@ -35,10 +35,6 @@ class ScrollToTopView: UIScrollView, UIScrollViewDelegate {
return false
}
func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
print("scrollViewDidScrollToTop")
}
}
class ScrollToTopNode: ASDisplayNode {
@ -52,3 +48,83 @@ class ScrollToTopNode: ASDisplayNode {
})
}
}
final class WindowScrollToTopProxyViews {
enum Mode {
case disabled
case flat(frame: CGRect)
case split(masterFrame: CGRect, detailFrame: CGRect)
}
private let flatView: ScrollToTopView
private let masterView: ScrollToTopView
private let detailView: ScrollToTopView
init(scrollToTop: @escaping (NavigationSplitContainerScrollToTop) -> Void) {
self.flatView = ScrollToTopView(frame: CGRect())
self.masterView = ScrollToTopView(frame: CGRect())
self.detailView = ScrollToTopView(frame: CGRect())
self.flatView.action = {
scrollToTop(.master)
}
self.masterView.action = {
scrollToTop(.master)
}
self.detailView.action = {
scrollToTop(.detail)
}
}
deinit {
self.disable(self.flatView)
self.disable(self.masterView)
self.disable(self.detailView)
}
func update(window: UIWindow?, mode: Mode, referenceView: UIView?) {
guard let window else {
self.disable(self.flatView)
self.disable(self.masterView)
self.disable(self.detailView)
return
}
switch mode {
case .disabled:
self.disable(self.flatView)
self.disable(self.masterView)
self.disable(self.detailView)
case let .flat(frame):
self.activate(self.flatView, in: window, frame: frame, referenceView: referenceView)
self.disable(self.masterView)
self.disable(self.detailView)
case let .split(masterFrame, detailFrame):
self.disable(self.flatView)
self.activate(self.masterView, in: window, frame: masterFrame, referenceView: referenceView)
self.activate(self.detailView, in: window, frame: detailFrame, referenceView: referenceView)
}
}
private func activate(_ view: ScrollToTopView, in window: UIWindow, frame: CGRect, referenceView: UIView?) {
if let referenceView, referenceView.superview === window {
if view.superview !== window {
view.removeFromSuperview()
}
window.insertSubview(view, aboveSubview: referenceView)
} else if view.superview !== window {
view.removeFromSuperview()
window.addSubview(view)
}
view.isHidden = false
view.scrollsToTop = true
view.frame = frame
}
private func disable(_ view: ScrollToTopView) {
view.scrollsToTop = false
view.isHidden = true
view.removeFromSuperview()
}
}

View file

@ -107,6 +107,11 @@ swift_library(
"//submodules/TelegramUI/Components/DynamicCornerRadiusView",
"//submodules/TelegramUI/Components/StickerPickerScreen",
"//submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation",
"//submodules/TelegramUI/Components/SegmentControlComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/LiquidLens",
"//submodules/TelegramUI/Components/TabSelectionRecognizer",
"//submodules/TelegramUI/Components/GlassControls",
],
visibility = [
"//visibility:public",

View file

@ -10,10 +10,12 @@ import TelegramPresentationData
import SheetComponent
import ViewControllerComponent
import BlurredBackgroundComponent
import SegmentedControlNode
import SegmentControlComponent
import MultilineTextComponent
import HexColor
import MediaEditor
import GlassBarButtonComponent
import BundleIconComponent
private let palleteColors: [UInt32] = [
0xffffff, 0xebebeb, 0xd6d6d6, 0xc2c2c2, 0xadadad, 0x999999, 0x858585, 0x707070, 0x5c5c5c, 0x474747, 0x333333, 0x000000,
@ -1494,73 +1496,6 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor:
})
}
private class SegmentedControlComponent: Component {
let values: [String]
let selectedIndex: Int
let selectionChanged: (Int) -> Void
init(values: [String], selectedIndex: Int, selectionChanged: @escaping (Int) -> Void) {
self.values = values
self.selectedIndex = selectedIndex
self.selectionChanged = selectionChanged
}
static func ==(lhs: SegmentedControlComponent, rhs: SegmentedControlComponent) -> Bool {
if lhs.values != rhs.values {
return false
}
if lhs.selectedIndex != rhs.selectedIndex {
return false
}
return true
}
final class View: UIView {
private let backgroundNode: NavigationBackgroundNode
private let node: SegmentedControlNode
init() {
self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.1))
self.node = SegmentedControlNode(theme: SegmentedControlTheme(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0x6f7075, alpha: 0.6), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)), items: [], selectedIndex: 0)
super.init(frame: CGRect())
self.addSubview(self.backgroundNode.view)
self.addSubview(self.node.view)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: SegmentedControlComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.node.items = component.values.map { SegmentedControlItem(title: $0) }
self.node.selectedIndex = component.selectedIndex
let selectionChanged = component.selectionChanged
self.node.selectedIndexChanged = { [weak self] index in
self?.window?.endEditing(true)
selectionChanged(index)
}
let size = self.node.updateLayout(.stretchToFill(width: availableSize.width), transition: transition.containedViewLayoutTransition)
transition.setFrame(view: self.node.view, frame: CGRect(origin: CGPoint(), size: size))
transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundNode.update(size: size, cornerRadius: 10.0, transition: .immediate)
return size
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
final class ColorSwatchComponent: Component {
enum SwatchType: Equatable {
case main
@ -1886,30 +1821,6 @@ private final class ColorPickerContent: CombinedComponent {
}
final class State: ComponentState {
var cachedEyedropperImage: UIImage?
var eyedropperImage: UIImage {
let eyedropperImage: UIImage
if let image = self.cachedEyedropperImage {
eyedropperImage = image
} else {
eyedropperImage = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Eyedropper"), color: .white)!
self.cachedEyedropperImage = eyedropperImage
}
return eyedropperImage
}
var cachedCloseImage: UIImage?
var closeImage: UIImage {
let closeImage: UIImage
if let image = self.cachedCloseImage {
closeImage = image
} else {
closeImage = generateCloseButtonImage(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xa8aab1))!
self.cachedCloseImage = closeImage
}
return closeImage
}
var selectedMode: Int = 0
var selectedColor: DrawingColor
@ -1951,10 +1862,10 @@ private final class ColorPickerContent: CombinedComponent {
}
static var body: Body {
let eyedropperButton = Child(Button.self)
let closeButton = Child(Button.self)
let eyedropperButton = Child(GlassBarButtonComponent.self)
let closeButton = Child(GlassBarButtonComponent.self)
let title = Child(MultilineTextComponent.self)
let modeControl = Child(SegmentedControlComponent.self)
let modeControl = Child(SegmentControlComponent.self)
let colorGrid = Child(ColorGridComponent.self)
let colorSpectrum = Child(ColorSpectrumComponent.self)
@ -1985,50 +1896,45 @@ private final class ColorPickerContent: CombinedComponent {
let sideInset: CGFloat = 16.0
let eyedropperButton = eyedropperButton.update(
component: Button(
content: AnyComponent(
Image(image: state.eyedropperImage)
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: true,
state: .glass,
component: AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(BundleIconComponent(name: "Media Editor/Eyedropper", tintColor: .white))
),
action: { [weak component] in
component?.eyedropper()
action: { _ in
component.eyedropper()
}
).minSize(CGSize(width: 30.0, height: 30.0)),
availableSize: CGSize(width: 19.0, height: 19.0),
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
context.add(eyedropperButton
.position(CGPoint(x: environment.safeInsets.left + eyedropperButton.size.width + 1.0, y: 29.0))
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - eyedropperButton.size.width / 2.0 - 16.0, y: 16.0 + eyedropperButton.size.height / 2.0))
)
let closeButton = closeButton.update(
component: Button(
content: AnyComponent(ZStack([
AnyComponentWithIdentity(
id: "background",
component: AnyComponent(
BlurredBackgroundComponent(
color: UIColor(rgb: 0x888888, alpha: 0.1)
)
)
),
AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(
Image(image: state.closeImage)
)
),
])),
action: { [weak component] in
component?.dismiss()
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: true,
state: .glass,
component: AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(BundleIconComponent(name: "Navigation/Close", tintColor: .white))
),
action: { _ in
component.dismiss()
}
),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - closeButton.size.width - 1.0, y: 29.0))
.clipsToBounds(true)
.cornerRadius(15.0)
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
let title = title.update(
@ -2046,17 +1952,24 @@ private final class ColorPickerContent: CombinedComponent {
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: 29.0))
.position(CGPoint(x: context.availableSize.width / 2.0, y: 16.0 + 22.0))
)
var contentHeight: CGFloat = 58.0
var contentHeight: CGFloat = 76.0
let modeControl = modeControl.update(
component: SegmentedControlComponent(
values: [strings.Paint_ColorGrid, strings.Paint_ColorSpectrum, strings.Paint_ColorSliders],
selectedIndex: 0,
selectionChanged: { [weak state] index in
state?.updateSelectedMode(index)
component: SegmentControlComponent(
theme: SegmentControlComponent.Theme(backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.07), legacyBackgroundColor: .clear, foregroundColor: UIColor(rgb: 0x6f7075, alpha: 0.6), textColor: .white, dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)),
items: [
.init(id: 0, title: strings.Paint_ColorGrid),
.init(id: 1, title: strings.Paint_ColorSpectrum),
.init(id: 2, title: strings.Paint_ColorSliders),
],
selectedId: state.selectedMode,
action: { [weak state] index in
if let value = index.base as? Int {
state?.updateSelectedMode(value)
}
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
@ -2379,6 +2292,7 @@ private final class ColorPickerSheetComponent: CombinedComponent {
})
}
)),
style: .glass,
backgroundColor: .blur(.dark),
animateOut: animateOut
),

View file

@ -25,6 +25,9 @@ import FastBlur
import MediaEditor
import StickerPickerScreen
import ImageObjectSeparation
import GlassBarButtonComponent
import GlassControls
import BundleIconComponent
public struct DrawingResultData {
public let data: Data?
@ -363,15 +366,17 @@ private final class ReferenceContentSource: ContextReferenceContentSource {
private let sourceView: UIView
private let contentArea: CGRect
private let customPosition: CGPoint
private let actionsPosition: ContextControllerReferenceViewInfo.ActionsPosition
init(sourceView: UIView, contentArea: CGRect, customPosition: CGPoint) {
init(sourceView: UIView, contentArea: CGRect, customPosition: CGPoint, actionsPosition: ContextControllerReferenceViewInfo.ActionsPosition = .top) {
self.sourceView = sourceView
self.contentArea = contentArea
self.customPosition = customPosition
self.actionsPosition = actionsPosition
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: self.contentArea, customPosition: self.customPosition, actionsPosition: .top)
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: self.contentArea, customPosition: self.customPosition, actionsPosition: self.actionsPosition)
}
}
@ -467,13 +472,11 @@ private let bottomGradientTag = GenericComponentViewTag()
private let undoButtonTag = GenericComponentViewTag()
private let redoButtonTag = GenericComponentViewTag()
private let clearAllButtonTag = GenericComponentViewTag()
private let topButtonsTag = GenericComponentViewTag()
private let colorButtonTag = GenericComponentViewTag()
private let addButtonTag = GenericComponentViewTag()
private let toolsTag = GenericComponentViewTag()
private let modeTag = GenericComponentViewTag()
private let flipButtonTag = GenericComponentViewTag()
private let fillButtonTag = GenericComponentViewTag()
private let zoomOutButtonTag = GenericComponentViewTag()
private let textSettingsTag = GenericComponentViewTag()
private let sizeSliderTag = GenericComponentViewTag()
private let fontTag = GenericComponentViewTag()
@ -489,6 +492,12 @@ private let colorTags = [color1Tag, color2Tag, color3Tag, color4Tag, color5Tag,
private let cancelButtonTag = GenericComponentViewTag()
private let doneButtonTag = GenericComponentViewTag()
enum DrawingMode: Int {
case drawing
case sticker
case text
}
private final class DrawingScreenComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -616,7 +625,6 @@ private final class DrawingScreenComponent: CombinedComponent {
final class State: ComponentState {
enum ImageKey: Hashable {
case undo
case redo
case done
case add
case fill
@ -633,8 +641,6 @@ private final class DrawingScreenComponent: CombinedComponent {
switch key {
case .undo:
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Undo"), color: .white)!
case .redo:
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Redo"), color: .white)!
case .done:
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Done"), color: .white)!
case .add:
@ -652,13 +658,7 @@ private final class DrawingScreenComponent: CombinedComponent {
return image
}
}
enum Mode {
case drawing
case sticker
case text
}
private let context: AccountContext
private let updateToolState: ActionSlot<DrawingToolState>
private let insertEntity: ActionSlot<DrawingEntity>
@ -675,7 +675,7 @@ private final class DrawingScreenComponent: CombinedComponent {
private let entityViewForEntity: (DrawingEntity) -> DrawingEntityView?
private let present: (ViewController) -> Void
var currentMode: Mode
var currentMode: DrawingMode
var drawingState: DrawingState
var drawingViewState: DrawingView.NavigationState
var currentColor: DrawingColor
@ -992,7 +992,7 @@ private final class DrawingScreenComponent: CombinedComponent {
self.present(contextController)
}
func updateCurrentMode(_ mode: Mode, update: Bool = true) {
func updateCurrentMode(_ mode: DrawingMode, update: Bool = true) {
self.currentMode = mode
if let selectedEntity = self.selectedEntity {
if selectedEntity is DrawingStickerEntity || selectedEntity is DrawingTextEntity {
@ -1058,15 +1058,13 @@ private final class DrawingScreenComponent: CombinedComponent {
let topGradient = Child(BlurredGradientComponent.self)
let bottomGradient = Child(BlurredGradientComponent.self)
let undoButton = Child(Button.self)
let undoButton = Child(GlassBarButtonComponent.self)
let redoButton = Child(GlassBarButtonComponent.self)
let clearAllButton = Child(GlassBarButtonComponent.self)
let topButtons = Child(GlassControlPanelComponent.self)
let redoButton = Child(Button.self)
let clearAllButton = Child(Button.self)
let zoomOutButton = Child(Button.self)
let tools = Child(ToolsComponent.self)
let modeAndSize = Child(ModeAndSizeComponent.self)
let mode = Child(ModeComponent.self)
let colorButton = Child(ColorSwatchComponent.self)
@ -1082,16 +1080,11 @@ private final class DrawingScreenComponent: CombinedComponent {
let swatch8Button = Child(ColorSwatchComponent.self)
let addButton = Child(Button.self)
let flipButton = Child(Button.self)
let fillButton = Child(Button.self)
let backButton = Child(Button.self)
let doneButton = Child(Button.self)
let backButton = Child(GlassBarButtonComponent.self)
let doneButton = Child(GlassBarButtonComponent.self)
let textSize = Child(TextSizeSliderComponent.self)
let textCancelButton = Child(Button.self)
let textDoneButton = Child(Button.self)
let presetColors: [DrawingColor] = [
DrawingColor(rgb: 0xff453a),
@ -1220,6 +1213,8 @@ private final class DrawingScreenComponent: CombinedComponent {
var additionalBottomInset: CGFloat = 0.0
if component.sourceHint == .storyEditor {
additionalBottomInset = max(0.0, previewBottomInset - environment.safeInsets.bottom - 49.0)
} else {
additionalBottomInset = 8.0
}
if let textEntity = state.selectedEntity as? DrawingTextEntity {
@ -1318,8 +1313,8 @@ private final class DrawingScreenComponent: CombinedComponent {
)
}
let rightButtonPosition = rightEdge - 24.0
var offsetX: CGFloat = leftEdge + 24.0
let rightButtonPosition = rightEdge - 28.0
var offsetX: CGFloat = leftEdge + 28.0
let delta: CGFloat = (rightButtonPosition - offsetX) / 7.0
let applySwatchColor: (DrawingColor) -> Void = { [weak state] color in
@ -1608,6 +1603,7 @@ private final class DrawingScreenComponent: CombinedComponent {
}
var hasTopButtons = false
var centerItems: [GlassControlGroupComponent.Item] = []
if let entity = state.selectedEntity {
var isFilled: Bool?
if let entity = entity as? DrawingSimpleShapeEntity {
@ -1625,12 +1621,13 @@ private final class DrawingScreenComponent: CombinedComponent {
hasTopButtons = isFilled != nil || hasFlip
if let isFilled = isFilled {
let fillButton = fillButton.update(
component: Button(
content: AnyComponent(
Image(image: state.image(isFilled ? .fill : .stroke))
),
if controlsAreVisible {
if let isFilled = isFilled {
centerItems.append(GlassControlGroupComponent.Item(
id: AnyHashable("fill"),
content: .customIcon(id: AnyHashable("fill"), component: AnyComponent(
Image(image: state.image(isFilled ? .fill : .stroke), size: CGSize(width: 30.0, height: 30.0))
), insets: .zero),
action: { [weak state] in
guard let state = state else {
return
@ -1661,25 +1658,15 @@ private final class DrawingScreenComponent: CombinedComponent {
}
state.updated(transition: .easeInOut(duration: 0.2))
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(fillButtonTag),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .immediate
)
context.add(fillButton
.position(CGPoint(x: context.availableSize.width / 2.0 - (hasFlip ? 46.0 : 0.0), y: topInset))
.appear(.default(scale: true))
.disappear(.default(scale: true))
.opacity(!controlsAreVisible ? 0.0 : 1.0)
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
)
}
if hasFlip {
let flipButton = flipButton.update(
component: Button(
content: AnyComponent(
Image(image: state.image(.flip))
),
))
}
if hasFlip {
centerItems.append(GlassControlGroupComponent.Item(
id: AnyHashable("flip"),
content: .customIcon(id: AnyHashable("flip"), component: AnyComponent(
Image(image: state.image(.flip), size: CGSize(width: 30.0, height: 30.0))
), insets: .zero),
action: { [weak state] in
guard let state = state else {
return
@ -1695,17 +1682,8 @@ private final class DrawingScreenComponent: CombinedComponent {
}
state.updated(transition: .easeInOut(duration: 0.2))
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(flipButtonTag),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .immediate
)
context.add(flipButton
.position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 46.0 : 0.0), y: topInset))
.appear(.default(scale: true))
.disappear(.default(scale: true))
.opacity(!controlsAreVisible ? 0.0 : 1.0)
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
)
))
}
}
}
@ -1730,28 +1708,24 @@ private final class DrawingScreenComponent: CombinedComponent {
sizeValue = entity.lineWidth
}
}
if state.drawingViewState.canZoomOut && !hasTopButtons {
let zoomOutButton = zoomOutButton.update(
component: Button(
content: AnyComponent(
ZoomOutButtonContent(
title: strings.Paint_ZoomOut,
image: state.image(.zoomOut)
)
),
action: {
dismissEyedropper.invoke(Void())
performAction.invoke(.zoomOut)
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(zoomOutButtonTag),
availableSize: CGSize(width: 120.0, height: 33.0),
transition: .immediate
)
context.add(zoomOutButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: environment.safeInsets.top + 32.0 - UIScreenPixel))
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
if state.drawingViewState.canZoomOut && !hasTopButtons && controlsAreVisible {
centerItems.append(GlassControlGroupComponent.Item(
id: AnyHashable("zoomOut"),
content: .customIcon(id: AnyHashable("zoomOut"), component: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
Image(image: state.image(.zoomOut), size: CGSize(width: 24.0, height: 24.0))
)),
AnyComponentWithIdentity(id: "label", component: AnyComponent(
Text(text: environment.strings.Paint_ZoomOut, font: Font.regular(17.0), color: .white)
)),
], spacing: 2.0)
), insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)),
action: {
dismissEyedropper.invoke(Void())
performAction.invoke(.zoomOut)
}
))
}
}
if let sizeValue {
@ -1784,111 +1758,170 @@ private final class DrawingScreenComponent: CombinedComponent {
.position(CGPoint(x: textSize.size.width / 2.0, y: topInset + (context.availableSize.height - topInset - bottomInset) / 2.0))
.opacity(sizeSliderVisible && controlsAreVisible ? 1.0 : 0.0)
)
let undoButton = undoButton.update(
component: Button(
content: AnyComponent(
Image(image: state.image(.undo))
),
isEnabled: state.drawingViewState.canUndo,
action: {
var leftItems: [GlassControlGroupComponent.Item] = []
var rightItems: [GlassControlGroupComponent.Item] = []
if !isEditingText && controlsAreVisible {
leftItems.append(GlassControlGroupComponent.Item(
id: "undo",
content: .customIcon(id: "undo", component: AnyComponent(Image(image: state.image(.undo), tintColor: state.drawingViewState.canUndo ? .white : .white.withAlphaComponent(0.4), size: CGSize(width: 30.0, height: 30.0))), insets: .zero),
action: state.drawingViewState.canUndo ? {
dismissEyedropper.invoke(Void())
performAction.invoke(.undo)
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(undoButtonTag),
availableSize: CGSize(width: 24.0, height: 24.0),
} : nil
))
if state.drawingViewState.canRedo {
leftItems.append(GlassControlGroupComponent.Item(
id: "redo",
content: .customIcon(id: "redo", component: AnyComponent(Image(image: state.image(.undo), tintColor: state.drawingViewState.canRedo ? .white : .white.withAlphaComponent(0.4), size: CGSize(width: 30.0, height: 30.0), flipHorizontally: true)), insets: .zero),
action: state.drawingViewState.canRedo ? {
dismissEyedropper.invoke(Void())
performAction.invoke(.redo)
} : nil
))
}
}
let undoButton = undoButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: true,
state: .glass,
component: AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(Image(image: state.image(.undo)))
),
action: { _ in
dismissEyedropper.invoke(Void())
performAction.invoke(.undo)
},
tag: undoButtonTag
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: context.transition
)
context.add(undoButton
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 2.0, y: topInset))
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 16.0, y: topInset))
.scale(isEditingText ? 0.01 : 1.0)
.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0)
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
.opacity(0.0)
//.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0)
)
let redoButton = redoButton.update(
component: Button(
content: AnyComponent(
Image(image: state.image(.redo))
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: true,
state: .glass,
component: AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(Image(image: state.image(.undo), flipHorizontally: true))
),
action: {
action: { _ in
dismissEyedropper.invoke(Void())
performAction.invoke(.redo)
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag),
availableSize: CGSize(width: 24.0, height: 24.0),
},
tag: redoButtonTag
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: context.transition
)
context.add(redoButton
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset))
.scale(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.01)
.opacity(state.drawingViewState.canRedo && !isEditingText && controlsAreVisible ? 1.0 : 0.0)
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
.opacity(0.0)
//.opacity(state.drawingViewState.canRedo && !isEditingText && controlsAreVisible ? 1.0 : 0.0)
)
let clearAllButton = clearAllButton.update(
component: Button(
content: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: strings.Paint_Clear, font: Font.regular(17.0), textColor: .white)),
textShadowColor: component.sourceHint == .storyEditor ? UIColor(rgb: 0x000000, alpha: 0.35) : nil,
textShadowBlur: 2.0
)
component: GlassBarButtonComponent(
size: nil,
backgroundColor: nil,
isDark: true,
state: .glass,
component: AnyComponentWithIdentity(
id: "label",
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: strings.Paint_Clear, font: Font.regular(17.0), textColor: .white))
))
),
isEnabled: state.drawingViewState.canClear,
action: {
action: { _ in
dismissEyedropper.invoke(Void())
performAction.invoke(.clear)
}
).tagged(clearAllButtonTag),
},
tag: clearAllButtonTag
),
availableSize: CGSize(width: 180.0, height: 30.0),
transition: context.transition
)
context.add(clearAllButton
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - clearAllButton.size.width / 2.0 - 13.0, y: topInset))
.scale(isEditingText ? 0.01 : 1.0)
.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0)
.opacity(0.0)
//.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0)
)
let textCancelButton = textCancelButton.update(
component: Button(
content: AnyComponent(
Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: .white)
),
if !isEditingText && controlsAreVisible {
rightItems.append(GlassControlGroupComponent.Item(
id: "clearAll",
content: .text(strings.Paint_Clear),
action: state.drawingViewState.canClear ? {
dismissEyedropper.invoke(Void())
performAction.invoke(.clear)
} : nil
))
}
if isEditingText {
leftItems.append(GlassControlGroupComponent.Item(
id: "cancel",
content: .text(strings.Common_Cancel),
action: { [weak state] in
if let entity = state?.selectedEntity as? DrawingTextEntity {
endEditingTextEntityView.invoke((entity.uuid, true))
}
}
),
availableSize: CGSize(width: 100.0, height: 30.0),
transition: context.transition
)
context.add(textCancelButton
.position(CGPoint(x: environment.safeInsets.left + textCancelButton.size.width / 2.0 + 13.0, y: topInset))
.scale(isEditingText ? 1.0 : 0.01)
.opacity(isEditingText ? 1.0 : 0.0)
)
let textDoneButton = textDoneButton.update(
component: Button(
content: AnyComponent(
Text(text: environment.strings.Common_Done, font: Font.semibold(17.0), color: .white)
),
))
rightItems.append(GlassControlGroupComponent.Item(
id: "done",
content: .text(strings.Common_Done),
action: { [weak state] in
if let entity = state?.selectedEntity as? DrawingTextEntity {
endEditingTextEntityView.invoke((entity.uuid, false))
}
}
))
}
let topButtons = topButtons.update(
component: GlassControlPanelComponent(
theme: defaultDarkPresentationTheme,
leftItem: leftItems.isEmpty ? nil : GlassControlPanelComponent.Item(
items: leftItems,
background: .panel
),
centralItem: centerItems.isEmpty ? nil : GlassControlPanelComponent.Item(
items: centerItems,
background: .panel
),
rightItem: rightItems.isEmpty ? nil : GlassControlPanelComponent.Item(
items: rightItems,
background: .panel
),
centerAlignmentIfPossible: true,
isDark: true,
tag: topButtonsTag
),
availableSize: CGSize(width: 100.0, height: 30.0),
availableSize: CGSize(width: context.availableSize.width - 32.0, height: 44.0),
transition: context.transition
)
context.add(textDoneButton
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - textDoneButton.size.width / 2.0 - 13.0, y: topInset))
.scale(isEditingText ? 1.0 : 0.01)
.opacity(isEditingText ? 1.0 : 0.0)
context.add(topButtons
.position(CGPoint(x: context.availableSize.width / 2.0, y: topInset))
//.opacity(isEditingText ? 1.0 : 0.0)
)
var color: DrawingColor?
@ -1930,7 +1963,7 @@ private final class DrawingScreenComponent: CombinedComponent {
transition: context.transition
)
context.add(colorButton
.position(CGPoint(x: leftEdge + colorButton.size.width / 2.0 + 2.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 89.0 - additionalBottomInset))
.position(CGPoint(x: leftEdge + colorButton.size.width / 2.0 + 6.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 89.0 - additionalBottomInset))
.appear(.default(scale: true))
.disappear(.default(scale: true))
.opacity(controlsAreVisible ? 1.0 : 0.0)
@ -1979,7 +2012,7 @@ private final class DrawingScreenComponent: CombinedComponent {
transition: .immediate
)
context.add(addButton
.position(CGPoint(x: rightEdge - addButton.size.width / 2.0 - 2.0, y: context.availableSize.height - environment.safeInsets.bottom - addButton.size.height / 2.0 - 89.0 - additionalBottomInset))
.position(CGPoint(x: rightEdge - addButton.size.width / 2.0 - 6.0, y: context.availableSize.height - environment.safeInsets.bottom - addButton.size.height / 2.0 - 89.0 - additionalBottomInset))
.appear(.default(scale: true))
.disappear(.default(scale: true))
.cornerRadius(12.0)
@ -1987,27 +2020,38 @@ private final class DrawingScreenComponent: CombinedComponent {
)
let doneButton = doneButton.update(
component: Button(
content: AnyComponent(
Image(image: state.image(.done))
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: UIColor(rgb: 0x0088ff),
isDark: true,
state: .tintedGlass,
component: AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(
BundleIconComponent(name: "Navigation/Done", tintColor: .white)
)
),
action: { [weak state] in
action: { [weak state] _ in
dismissEyedropper.invoke(Void())
state?.saveToolState()
apply.invoke(Void())
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(doneButtonTag),
availableSize: CGSize(width: 33.0, height: 33.0),
},
tag: doneButtonTag
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
var doneButtonPosition = CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel)
var doneButtonPosition = CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 14.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel)
if component.sourceHint == .storyEditor {
doneButtonPosition.x = doneButtonPosition.x - 2.0
if case .regular = environment.metrics.widthClass {
doneButtonPosition.x -= 20.0
}
doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + doneButton.size.height / 2.0) + controlsBottomInset
doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + doneButton.size.height / 2.0) + controlsBottomInset + 5.0
} else {
doneButtonPosition.x = doneButtonPosition.x - 12.0
doneButtonPosition.y -= 3.0 - UIScreenPixel
}
context.add(doneButton
.position(doneButtonPosition)
@ -2026,117 +2070,82 @@ private final class DrawingScreenComponent: CombinedComponent {
})
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
let selectedIndex: Int
switch state.currentMode {
case .drawing:
selectedIndex = 0
case .sticker:
selectedIndex = 1
case .text:
selectedIndex = 2
}
var selectedSize: CGFloat = 0.0
if let entity = state.selectedEntity {
selectedSize = entity.lineWidth
} else {
selectedSize = state.drawingState.toolState(for: state.drawingState.selectedTool).size ?? 0.0
}
let modeAndSize = modeAndSize.update(
component: ModeAndSizeComponent(
values: [ strings.Paint_Draw, strings.Paint_Sticker, strings.Paint_Text],
sizeValue: selectedSize,
isEditing: false,
isEnabled: true,
rightInset: modeRightInset - 57.0,
tag: modeTag,
selectedIndex: selectedIndex,
selectionChanged: { [weak state] index in
dismissEyedropper.invoke(Void())
guard let state = state else {
return
}
switch index {
case 1:
state.presentStickerPicker()
case 2:
state.addTextEntity()
default:
state.updateCurrentMode(.drawing)
}
},
sizeUpdated: { [weak state] size in
if let state = state {
let mode = mode.update(
component: ModeComponent(
isTablet: false,
strings: environment.strings,
tintColor: .white,
availableModes: [.drawing, .sticker, .text],
currentMode: state.currentMode,
updatedMode: { [weak state] mode in
if let state {
dismissEyedropper.invoke(Void())
state.updateBrushSize(size)
if state.selectedEntity == nil {
previewBrushSize.invoke(size)
switch mode {
case .drawing:
state.updateCurrentMode(.drawing)
case .sticker:
state.presentStickerPicker()
case .text:
state.addTextEntity()
}
}
},
sizeReleased: {
previewBrushSize.invoke(nil)
}
tag: modeTag
),
availableSize: CGSize(width: availableWidth - 57.0 - modeRightInset, height: context.availableSize.height),
availableSize: CGSize(width: context.availableSize.width - 66.0 * 2.0, height: 44.0),
transition: context.transition
)
var modeAndSizePosition = CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - modeAndSize.size.height / 2.0 - 9.0)
var modePosition = CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - mode.size.height / 2.0 - 9.0)
if component.sourceHint == .storyEditor {
modeAndSizePosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 8.0 + modeAndSize.size.height / 2.0) + controlsBottomInset
modePosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 8.0 + mode.size.height / 2.0) + controlsBottomInset
} else {
modePosition.y += 4.0
}
context.add(modeAndSize
.position(modeAndSizePosition)
context.add(mode
.position(modePosition)
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
var animatingOut = false
if let appearanceTransition = context.transition.userData(DrawingScreenTransition.self), case .animateOut = appearanceTransition {
animatingOut = true
}
if animatingOut && component.sourceHint == .storyEditor {
} else {
let backButton = backButton.update(
component: Button(
content: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "media_backToCancel",
mode: .animating(loop: false),
range: animatingOut || component.isAvatar ? (0.5, 1.0) : (0.0, 0.5)
),
colors: ["__allcolors__": .white],
size: CGSize(width: 33.0, height: 33.0)
)
),
action: { [weak state] in
if let state = state {
dismissEyedropper.invoke(Void())
state.saveToolState()
dismiss.invoke(Void())
}
let backButton = backButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: true,
state: .glass,
component: AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(
BundleIconComponent(name: "Navigation/Close", tintColor: .white)
)
),
action: { [weak state] _ in
if let state {
dismissEyedropper.invoke(Void())
state.saveToolState()
dismiss.invoke(Void())
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(cancelButtonTag),
availableSize: CGSize(width: 33.0, height: 33.0),
transition: .immediate
)
var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel)
if component.sourceHint == .storyEditor {
backButtonPosition.x = backButtonPosition.x + 2.0
if case .regular = environment.metrics.widthClass {
backButtonPosition.x += 20.0
}
backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0) + controlsBottomInset
},
tag: cancelButtonTag
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 14.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel)
if component.sourceHint == .storyEditor {
backButtonPosition.x = backButtonPosition.x + 2.0
if case .regular = environment.metrics.widthClass {
backButtonPosition.x += 20.0
}
context.add(backButton
.position(backButtonPosition)
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0) + controlsBottomInset + 5.0
} else {
backButtonPosition.x = backButtonPosition.x + 12.0
backButtonPosition.y -= 3.0 - UIScreenPixel
}
context.add(backButton
.position(backButtonPosition)
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
return context.availableSize
}
@ -2224,22 +2233,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
self.performAction.connect { [weak self] action in
if let self {
if case .clear = action {
let actionSheet = ActionSheetController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Paint_ClearConfirm, color: .destructive, action: { [weak actionSheet, weak self] in
actionSheet?.dismissAnimated()
self?._drawingView?.performAction(action)
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
self.controller?.present(actionSheet, in: .window(.root))
let sourceView: UIView
if let topButtonsView = self.componentHost.findTaggedView(tag: topButtonsTag) as? GlassControlPanelComponent.View, let rightItemView = topButtonsView.rightItemView, let clearAllView = rightItemView.itemView(id: AnyHashable("clearAll")) {
sourceView = clearAllView
} else {
sourceView = self.view
}
let items: [ContextMenuItem] = [
.action(ContextMenuActionItem(text: self.presentationData.strings.Paint_ClearConfirm, textColor: .destructive, icon: { _ in
return nil
}, action: { [weak self] f in
f.dismissWithResult(.default)
self?._drawingView?.performAction(.clear)
}))
]
let presentationData = self.presentationData.withUpdated(theme: defaultDarkPresentationTheme)
let contextController = makeContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: UIScreen.main.bounds, customPosition: CGPoint(), actionsPosition: .bottom)), items: .single(ContextController.Items(content: .list(items))))
self.controller?.present(contextController, in: .window(.root))
} else {
self._drawingView?.performAction(action)
}
@ -2403,22 +2414,18 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
self.dismiss.connect { [weak self] _ in
if let strongSelf = self {
if strongSelf.drawingView.canUndo || strongSelf.entitiesView.hasChanges {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.PhotoEditor_DiscardChanges, color: .accent, action: { [weak actionSheet, weak self] in
actionSheet?.dismissAnimated()
self?.controller?.requestDismiss()
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.controller?.present(actionSheet, in: .window(.root))
let sourceView = strongSelf.componentHost.findTaggedView(tag: cancelButtonTag) ?? strongSelf.view
let items: [ContextMenuItem] = [
.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PhotoEditor_DiscardChanges, textColor: .destructive, icon: { _ in
return nil
}, action: { [weak self] f in
f.dismissWithResult(.default)
self?.controller?.requestDismiss()
}))
]
let presentationData = strongSelf.presentationData.withUpdated(theme: defaultDarkPresentationTheme)
let contextController = makeContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: UIScreen.main.bounds, customPosition: CGPoint())), items: .single(ContextController.Items(content: .list(items))))
strongSelf.controller?.present(contextController, in: .window(.root))
} else {
strongSelf.controller?.requestDismiss()
}
@ -2497,17 +2504,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
if let view = self.componentHost.findTaggedView(tag: bottomGradientTag) {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) {
buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
if let topButtonsView = self.componentHost.findTaggedView(tag: topButtonsTag) as? GlassControlPanelComponent.View {
topButtonsView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
if let leftItemView = topButtonsView.leftItemView {
leftItemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
leftItemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
}
if let rightItemView = topButtonsView.rightItemView {
rightItemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
rightItemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
}
}
if let buttonView = self.componentHost.findTaggedView(tag: clearAllButtonTag) {
buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
if let view = self.componentHost.findTaggedView(tag: modeTag) {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
if let buttonView = self.componentHost.findTaggedView(tag: addButtonTag) {
buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
if let view = self.componentHost.findTaggedView(tag: addButtonTag) {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
}
var delay: Double = 0.0
for tag in colorTags {
@ -2520,6 +2534,15 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) {
view.layer.animatePosition(from: CGPoint(x: -33.0, y: 0.0), to: CGPoint(), duration: 0.3, additive: true)
}
if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
}
if let view = self.componentHost.findTaggedView(tag: doneButtonTag) {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
}
}
func animateOut(completion: @escaping () -> Void) {
@ -2536,43 +2559,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
}
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
}
if let buttonView = self.componentHost.findTaggedView(tag: redoButtonTag), buttonView.alpha > 0.0 {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
}
if let buttonView = self.componentHost.findTaggedView(tag: clearAllButtonTag) {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
if let topButtonsView = self.componentHost.findTaggedView(tag: topButtonsTag) as? GlassControlPanelComponent.View {
topButtonsView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
if let view = topButtonsView.leftItemView {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false)
}
if let view = topButtonsView.rightItemView {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false)
}
}
if let view = self.componentHost.findTaggedView(tag: colorButtonTag) as? ColorSwatchComponent.View {
view.animateOut()
}
if let buttonView = self.componentHost.findTaggedView(tag: addButtonTag) {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
}
if let buttonView = self.componentHost.findTaggedView(tag: flipButtonTag) {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
}
if let buttonView = self.componentHost.findTaggedView(tag: fillButtonTag) {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
}
if let buttonView = self.componentHost.findTaggedView(tag: zoomOutButtonTag) {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
if let view = self.componentHost.findTaggedView(tag: addButtonTag) {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false)
}
if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) {
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -33.0, y: 0.0), duration: 0.3, removeOnCompletion: false, additive: true)
@ -2596,12 +2600,18 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
})
}
if let view = self.componentHost.findTaggedView(tag: modeTag) as? ModeAndSizeComponent.View {
view.animateOut()
if let view = self.componentHost.findTaggedView(tag: modeTag) {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
if let buttonView = self.componentHost.findTaggedView(tag: doneButtonTag) {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false)
}
if let view = self.componentHost.findTaggedView(tag: doneButtonTag) {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
}
}

View file

@ -8,7 +8,7 @@ import AVFoundation
public final class EntityVideoRecorder {
private weak var mediaEditor: MediaEditor?
private weak var entitiesView: DrawingEntitiesView?
private let maxDuration: Double
private let camera: Camera
@ -21,6 +21,9 @@ public final class EntityVideoRecorder {
private let micLevelPromise = Promise<Float>()
private var changingPositionDisposable: Disposable?
private var positionDisposable: Disposable?
private var currentCameraPosition: Camera.Position = .front
private var recordingInitialPosition: Camera.Position = .front
public var duration: Signal<Double, NoError> {
return self.durationPromise.get()
@ -35,7 +38,7 @@ public final class EntityVideoRecorder {
public init(mediaEditor: MediaEditor, entitiesView: DrawingEntitiesView) {
self.mediaEditor = mediaEditor
self.entitiesView = entitiesView
self.maxDuration = min(60.0, mediaEditor.duration ?? 60.0)
self.previewView = CameraSimplePreviewView(frame: .zero, main: true)
@ -86,6 +89,10 @@ public final class EntityVideoRecorder {
}
self.micLevelPromise.set(camera.audioLevel)
self.positionDisposable = (camera.position
|> deliverOnMainQueue).start(next: { [weak self] position in
self?.currentCameraPosition = position
})
let start = mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0
mediaEditor.stop()
@ -107,6 +114,7 @@ public final class EntityVideoRecorder {
deinit {
self.recordingDisposable.dispose()
self.changingPositionDisposable?.dispose()
self.positionDisposable?.dispose()
}
public func setup(
@ -145,6 +153,7 @@ public final class EntityVideoRecorder {
mediaEditor.maybeMuteVideo()
mediaEditor.play()
self.recordingInitialPosition = self.currentCameraPosition
self.start = CACurrentMediaTime()
self.recordingDisposable.set((self.camera.startRecording()
|> deliverOnMainQueue).startStrict(next: { [weak self] recordingData in
@ -169,16 +178,20 @@ public final class EntityVideoRecorder {
}
self.recordingDisposable.set((self.camera.stopRecording()
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
guard let self, let mediaEditor = self.mediaEditor, let entitiesView = self.entitiesView, case let .finished(mainResult, _, _, _, _) = result else {
guard let self, let mediaEditor = self.mediaEditor, let entitiesView = self.entitiesView, case let .finished(mainResult, _, _, positionChangeTimestamps, _) = result else {
return
}
if save {
let duration = AVURLAsset(url: URL(fileURLWithPath: mainResult.path)).duration
let mirroringChanges = self.additionalVideoMirroringChanges(
initialPosition: self.recordingInitialPosition,
positionChangeTimestamps: positionChangeTimestamps
)
let start = mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0
mediaEditor.setAdditionalVideoOffset(-start, apply: false)
mediaEditor.setAdditionalVideoTrimRange(0 ..< duration.seconds, apply: true)
mediaEditor.setAdditionalVideo(mainResult.path, positionChanges: [])
mediaEditor.setAdditionalVideo(mainResult.path, mirroringChanges: mirroringChanges, positionChanges: [])
mediaEditor.stop()
Queue.mainQueue().justDispatch {
@ -217,7 +230,7 @@ public final class EntityVideoRecorder {
self.mediaEditor?.maybeUnmuteVideo()
self.entitiesView?.remove(uuid: self.entity.uuid, animated: true)
self.mediaEditor?.setAdditionalVideo(nil, positionChanges: [])
self.mediaEditor?.setAdditionalVideo(nil, mirroringChanges: [], positionChanges: [])
completion()
}
@ -226,4 +239,22 @@ public final class EntityVideoRecorder {
public func togglePosition() {
self.camera.togglePosition()
}
private func additionalVideoMirroringChanges(initialPosition: Camera.Position, positionChangeTimestamps: [(Bool, Double)]) -> [VideoMirroringChange] {
var result: [VideoMirroringChange] = [
VideoMirroringChange(isMirrored: initialPosition == .front, timestamp: 0.0)
]
for (isMirrored, timestamp) in positionChangeTimestamps.sorted(by: { $0.1 < $1.1 }) {
result.append(VideoMirroringChange(isMirrored: isMirrored, timestamp: timestamp))
}
var deduplicated: [VideoMirroringChange] = []
for change in result {
if deduplicated.last?.isMirrored == change.isMirrored {
continue
}
deduplicated.append(change)
}
return deduplicated
}
}

View file

@ -2,95 +2,123 @@ import Foundation
import UIKit
import Display
import ComponentFlow
import LegacyComponents
import TelegramCore
import SegmentedControlNode
import MultilineTextComponent
import TelegramPresentationData
import GlassBackgroundComponent
import LiquidLens
import TabSelectionRecognizer
private func generateMaskPath(size: CGSize, leftRadius: CGFloat, rightRadius: CGFloat) -> UIBezierPath {
let path = UIBezierPath()
path.addArc(withCenter: CGPoint(x: leftRadius, y: size.height / 2.0), radius: leftRadius, startAngle: .pi * 0.5, endAngle: -.pi * 0.5, clockwise: true)
path.addArc(withCenter: CGPoint(x: size.width - rightRadius, y: size.height / 2.0), radius: rightRadius, startAngle: -.pi * 0.5, endAngle: .pi * 0.5, clockwise: true)
path.close()
return path
private let buttonSize = CGSize(width: 55.0, height: 44.0)
private let tabletButtonSize = CGSize(width: 55.0, height: 44.0)
extension DrawingMode {
func title(strings: PresentationStrings) -> String {
switch self {
case .drawing:
return strings.Paint_Draw
case .sticker:
return strings.Paint_Sticker
case .text:
return strings.Paint_Text
}
}
}
private func generateKnobImage() -> UIImage? {
let side: CGFloat = 28.0
let margin: CGFloat = 10.0
let image = generateImage(CGSize(width: side + margin * 2.0, height: side + margin * 2.0), opaque: false, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor)
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: margin, y: margin), size: CGSize(width: side, height: side)))
})
return image?.stretchableImage(withLeftCapWidth: Int(margin + side * 0.5), topCapHeight: Int(margin + side * 0.5))
}
final class ModeAndSizeComponent: Component {
let values: [String]
let sizeValue: CGFloat
let isEditing: Bool
let isEnabled: Bool
let rightInset: CGFloat
final class ModeComponent: Component {
let isTablet: Bool
let strings: PresentationStrings
let tintColor: UIColor
let availableModes: [DrawingMode]
let currentMode: DrawingMode
let updatedMode: (DrawingMode) -> Void
let tag: AnyObject?
let selectedIndex: Int
let selectionChanged: (Int) -> Void
let sizeUpdated: (CGFloat) -> Void
let sizeReleased: () -> Void
init(values: [String], sizeValue: CGFloat, isEditing: Bool, isEnabled: Bool, rightInset: CGFloat, tag: AnyObject?, selectedIndex: Int, selectionChanged: @escaping (Int) -> Void, sizeUpdated: @escaping (CGFloat) -> Void, sizeReleased: @escaping () -> Void) {
self.values = values
self.sizeValue = sizeValue
self.isEditing = isEditing
self.isEnabled = isEnabled
self.rightInset = rightInset
init(
isTablet: Bool,
strings: PresentationStrings,
tintColor: UIColor,
availableModes: [DrawingMode],
currentMode: DrawingMode,
updatedMode: @escaping (DrawingMode) -> Void,
tag: AnyObject?
) {
self.isTablet = isTablet
self.strings = strings
self.tintColor = tintColor
self.availableModes = availableModes
self.currentMode = currentMode
self.updatedMode = updatedMode
self.tag = tag
self.selectedIndex = selectedIndex
self.selectionChanged = selectionChanged
self.sizeUpdated = sizeUpdated
self.sizeReleased = sizeReleased
}
static func ==(lhs: ModeAndSizeComponent, rhs: ModeAndSizeComponent) -> Bool {
if lhs.values != rhs.values {
static func ==(lhs: ModeComponent, rhs: ModeComponent) -> Bool {
if lhs.isTablet != rhs.isTablet {
return false
}
if lhs.sizeValue != rhs.sizeValue {
if lhs.strings !== rhs.strings {
return false
}
if lhs.isEditing != rhs.isEditing {
if lhs.tintColor != rhs.tintColor {
return false
}
if lhs.isEnabled != rhs.isEnabled {
if lhs.availableModes != rhs.availableModes {
return false
}
if lhs.rightInset != rhs.rightInset {
return false
}
if lhs.selectedIndex != rhs.selectedIndex {
if lhs.currentMode != rhs.currentMode {
return false
}
return true
}
final class View: UIView, UIGestureRecognizerDelegate, ComponentTaggedView {
private let backgroundNode: NavigationBackgroundNode
private let node: SegmentedControlNode
final class View: UIView, ComponentTaggedView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
private final class ScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
private var knob: UIImageView
private struct LayoutData {
var containerSize: CGSize
var selectedFrame: CGRect
var cornerRadius: CGFloat?
var isTablet: Bool
}
private let maskLayer = SimpleShapeLayer()
private var component: ModeComponent?
private var state: EmptyComponentState?
private var isEditing: Bool?
private var isControlEnabled: Bool?
private var sliderWidth: CGFloat = 0.0
final class ItemView: HighlightTrackingButton {
init() {
super.init(frame: .zero)
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(isTablet: Bool, value: String, selected: Bool, tintColor: UIColor) -> CGSize {
let title = NSMutableAttributedString(string: value, font: Font.with(size: 15.0, design: .regular, weight: .medium), textColor: UIColor(rgb: 0xffffff), paragraphAlignment: .center)
self.setAttributedTitle(title, for: .normal)
self.sizeToFit()
return CGSize(width: self.titleLabel?.bounds.size.width ?? 0.0, height: buttonSize.height)
}
}
fileprivate var updated: (CGFloat) -> Void = { _ in }
fileprivate var released: () -> Void = { }
private var backgroundView = UIView()
private var backgroundContainer = GlassBackgroundContainerView()
private var liquidLensView: LiquidLensView?
private let scrollView = ScrollView()
private let selectedScrollView = UIView()
private var ignoreScrolling = false
private var layoutData: LayoutData?
private var itemViews: [AnyHashable: ItemView] = [:]
private var selectedItemViews: [AnyHashable: ItemView] = [:]
private var tabSelectionRecognizer: TabSelectionRecognizer?
private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)?
private var component: ModeAndSizeComponent?
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
@ -102,164 +130,350 @@ final class ModeAndSizeComponent: Component {
}
init() {
self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.3))
self.node = SegmentedControlNode(theme: SegmentedControlTheme(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)), items: [], selectedIndex: 0, cornerRadius: 16.0)
self.knob = UIImageView(image: generateKnobImage())
super.init(frame: CGRect())
self.backgroundView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.09)
self.backgroundView.layer.cornerRadius = 22.0
self.layer.allowsGroupOpacity = true
self.addSubview(self.backgroundNode.view)
self.addSubview(self.node.view)
self.addSubview(self.knob)
self.backgroundNode.layer.mask = self.maskLayer
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.contentInsetAdjustmentBehavior = .never
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.alwaysBounceVertical = false
self.scrollView.scrollsToTop = false
self.scrollView.clipsToBounds = true
self.scrollView.delegate = self
self.scrollView.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
guard let self else {
return false
}
return self.scrollView.contentOffset.x > .ulpOfOne
}
let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:)))
pressGestureRecognizer.minimumPressDuration = 0.01
pressGestureRecognizer.delegate = self
self.addGestureRecognizer(pressGestureRecognizer)
self.selectedScrollView.clipsToBounds = true
self.selectedScrollView.isUserInteractionEnabled = false
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
panGestureRecognizer.delegate = self
self.addGestureRecognizer(panGestureRecognizer)
self.addSubview(self.backgroundView)
self.backgroundView.addSubview(self.backgroundContainer)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
@objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) {
let location = gestureRecognizer.location(in: self).offsetBy(dx: -12.0, dy: 0.0)
guard self.frame.width > 0.0, case .began = gestureRecognizer.state else {
return
}
let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0)))
self.updated(value)
private var animatedOut = false
func animateOutToEditor(transition: ComponentTransition) {
self.animatedOut = true
transition.setAlpha(view: self.backgroundView, alpha: 0.0)
transition.setSublayerTransform(view: self, transform: CATransform3DMakeTranslation(0.0, -buttonSize.height, 0.0))
}
@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state {
case .changed:
let location = gestureRecognizer.location(in: self).offsetBy(dx: -12.0, dy: 0.0)
guard self.frame.width > 0.0 else {
return
func animateInFromEditor(transition: ComponentTransition) {
self.animatedOut = false
transition.setAlpha(view: self.backgroundView, alpha: 1.0)
transition.setSublayerTransform(view: self, transform: CATransform3DIdentity)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return self.backgroundView.frame.contains(point)
}
private func item(at point: CGPoint, in view: UIView) -> AnyHashable? {
var closestItem: (AnyHashable, CGFloat)?
for (id, itemView) in self.itemViews {
let itemFrame = itemView.convert(itemView.bounds, to: view)
if itemFrame.contains(point) {
return id
} else {
let distance = abs(point.x - itemFrame.midX)
if let closestItemValue = closestItem {
if closestItemValue.1 > distance {
closestItem = (id, distance)
}
} else {
closestItem = (id, distance)
}
}
}
return closestItem?.0
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if self.ignoreScrolling {
return
}
self.updateScrolling(transition: .immediate)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer === self.tabSelectionRecognizer && otherGestureRecognizer === self.scrollView.panGestureRecognizer {
return true
}
if otherGestureRecognizer === self.tabSelectionRecognizer && gestureRecognizer === self.scrollView.panGestureRecognizer {
return true
}
return false
}
@objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) {
guard let component = self.component else {
return
}
let location = recognizer.location(in: self)
switch recognizer.state {
case .began:
if let itemId = self.item(at: location, in: self), let itemView = self.itemViews[itemId] {
let startX = itemView.frame.minX - 4.0
self.selectionGestureState = (startX, startX, itemId)
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
}
case .changed:
if var selectionGestureState = self.selectionGestureState {
let translation = recognizer.translation(in: self)
if !component.isTablet && self.scrollView.isScrollEnabled && abs(translation.x) > 6.0 && abs(translation.x) > abs(translation.y) {
self.selectionGestureState = nil
recognizer.state = .cancelled
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
return
}
selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x
if let itemId = self.item(at: location, in: self) {
selectionGestureState.itemId = itemId
}
self.selectionGestureState = selectionGestureState
self.state?.updated(transition: .immediate, isLocal: true)
}
let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0)))
self.updated(value)
case .ended, .cancelled:
self.released()
if let selectionGestureState = self.selectionGestureState {
self.selectionGestureState = nil
if case .ended = recognizer.state {
guard let item = component.availableModes.first(where: { AnyHashable($0.rawValue) == selectionGestureState.itemId }) else {
return
}
component.updatedMode(item)
}
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
}
default:
break
}
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let isEditing = self.isEditing, let isControlEnabled = self.isControlEnabled {
return isEditing && isControlEnabled
private func updateScrolling(transition: ComponentTransition) {
guard let component = self.component, let liquidLensView = self.liquidLensView, let layoutData = self.layoutData else {
return
}
let contentOffsetX = layoutData.isTablet ? 0.0 : self.scrollView.bounds.minX
var lensSelection = (origin: layoutData.selectedFrame.origin, size: layoutData.selectedFrame.size)
if let selectionGestureState = self.selectionGestureState, !layoutData.isTablet {
lensSelection.origin = CGPoint(x: selectionGestureState.currentX, y: 0.0)
}
if layoutData.isTablet {
lensSelection.size.width = layoutData.containerSize.width
} else {
return false
lensSelection.origin.x -= contentOffsetX
lensSelection.origin.y = 0.0
lensSelection.size.height = layoutData.containerSize.height
}
let maxSelectionOriginX = max(0.0, layoutData.containerSize.width - lensSelection.size.width)
transition.setFrame(view: self.selectedScrollView, frame: CGRect(origin: .zero, size: layoutData.containerSize))
transition.setBounds(view: self.selectedScrollView, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: layoutData.containerSize))
liquidLensView.update(size: layoutData.containerSize, cornerRadius: layoutData.cornerRadius, selectionOrigin: CGPoint(x: max(0.0, min(lensSelection.origin.x, maxSelectionOriginX)), y: lensSelection.origin.y), selectionSize: lensSelection.size, inset: 3.0, isDark: true, isLifted: self.selectionGestureState != nil && !layoutData.isTablet, isCollapsed: false, transition: transition)
self.backgroundContainer.update(size: layoutData.containerSize, isDark: true, transition: .immediate)
self.scrollView.isScrollEnabled = !component.isTablet && self.scrollView.contentSize.width > self.scrollView.bounds.width + .ulpOfOne
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func animateIn() {
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
func animateOut() {
self.node.alpha = 0.0
self.node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
self.backgroundNode.alpha = 0.0
self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
}
func update(component: ModeAndSizeComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
self.updated = component.sizeUpdated
self.released = component.sizeReleased
let previousIsEditing = self.isEditing
self.isEditing = component.isEditing
self.isControlEnabled = component.isEnabled
if component.isEditing {
self.sliderWidth = availableSize.width
}
self.node.items = component.values.map { SegmentedControlItem(title: $0) }
self.node.setSelectedIndex(component.selectedIndex, animated: !transition.animation.isImmediate)
let selectionChanged = component.selectionChanged
self.node.selectedIndexChanged = { [weak self] index in
self?.window?.endEditing(true)
selectionChanged(index)
}
let nodeSize = self.node.updateLayout(.stretchToFill(width: availableSize.width + component.rightInset), transition: transition.containedViewLayoutTransition)
let size = CGSize(width: availableSize.width, height: nodeSize.height)
transition.setFrame(view: self.node.view, frame: CGRect(origin: CGPoint(), size: nodeSize))
var isDismissingEditing = false
if component.isEditing != previousIsEditing && !component.isEditing {
isDismissingEditing = true
}
self.knob.alpha = component.isEditing ? 1.0 : 0.0
if !isDismissingEditing {
self.knob.frame = CGRect(origin: CGPoint(x: -12.0 + floorToScreenPixels((self.sliderWidth + 24.0 - self.knob.frame.size.width) * component.sizeValue), y: floorToScreenPixels((size.height - self.knob.frame.size.height) / 2.0)), size: self.knob.frame.size)
}
if component.isEditing != previousIsEditing {
let containedTransition = transition.containedViewLayoutTransition
let maskPath: UIBezierPath
if component.isEditing {
maskPath = generateMaskPath(size: size, leftRadius: 2.0, rightRadius: 11.5)
let selectionFrame = self.node.animateSelection(to: self.knob.center, transition: containedTransition)
containedTransition.animateFrame(layer: self.knob.layer, from: selectionFrame.insetBy(dx: -9.0, dy: -9.0))
self.knob.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
func update(component: ModeComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
self.state = state
let isTablet = component.isTablet
let liquidLensView: LiquidLensView
if let current = self.liquidLensView {
liquidLensView = current
} else {
liquidLensView = LiquidLensView(kind: isTablet ? .noContainer : .externalContainer)
self.liquidLensView = liquidLensView
self.backgroundContainer.contentView.addSubview(liquidLensView)
liquidLensView.contentView.addSubview(self.scrollView)
liquidLensView.selectedContentView.addSubview(self.selectedScrollView)
let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:)))
tabSelectionRecognizer.delegate = self
tabSelectionRecognizer.cancelsTouchesInView = false
self.tabSelectionRecognizer = tabSelectionRecognizer
liquidLensView.addGestureRecognizer(tabSelectionRecognizer)
}
if self.scrollView.superview == nil {
liquidLensView.contentView.addSubview(self.scrollView)
}
if self.selectedScrollView.superview == nil {
liquidLensView.selectedContentView.addSubview(self.selectedScrollView)
}
self.backgroundView.backgroundColor = component.isTablet ? .clear : UIColor(rgb: 0xffffff, alpha: 0.11)
var inset: CGFloat = 23.0
let spacing: CGFloat
if isTablet {
spacing = 9.0
} else {
if availableSize.width < 200.0 {
inset = 20.0
spacing = 24.0
} else {
maskPath = generateMaskPath(size: size, leftRadius: 16.0, rightRadius: 16.0)
if previousIsEditing != nil {
let selectionFrame = self.node.animateSelection(from: self.knob.center, transition: containedTransition)
containedTransition.animateFrame(layer: self.knob.layer, from: self.knob.frame, to: selectionFrame.insetBy(dx: -9.0, dy: -9.0))
self.knob.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
spacing = 30.0
}
}
var i = 0
var itemFrame = CGRect(origin: isTablet ? .zero : CGPoint(x: inset, y: 0.0), size: buttonSize)
var selectedFrame = itemFrame
var validKeys: Set<AnyHashable> = Set()
for mode in component.availableModes {
let id = mode.rawValue
validKeys.insert(id)
let itemView: ItemView
let selectedItemView: ItemView
if let current = self.itemViews[id], let currentSelected = self.selectedItemViews[id] {
itemView = current
selectedItemView = currentSelected
} else {
itemView = ItemView()
itemView.isUserInteractionEnabled = false
self.itemViews[id] = itemView
selectedItemView = ItemView()
selectedItemView.isUserInteractionEnabled = false
self.selectedItemViews[id] = selectedItemView
}
if itemView.superview !== self.scrollView {
self.scrollView.addSubview(itemView)
}
if selectedItemView.superview !== self.selectedScrollView {
self.selectedScrollView.addSubview(selectedItemView)
}
let itemSize = itemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: false, tintColor: component.tintColor)
itemView.bounds = CGRect(origin: .zero, size: itemSize)
let _ = selectedItemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: true, tintColor: component.tintColor)
selectedItemView.bounds = CGRect(origin: .zero, size: itemSize)
itemFrame = CGRect(origin: itemFrame.origin, size: itemSize)
if mode == component.currentMode {
selectedFrame = itemFrame
}
if isTablet {
itemView.center = CGPoint(x: availableSize.width / 2.0, y: itemFrame.midY)
selectedItemView.center = itemView.center
itemFrame = itemFrame.offsetBy(dx: 0.0, dy: tabletButtonSize.height + spacing)
} else {
itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
selectedItemView.center = itemView.center
itemFrame = itemFrame.offsetBy(dx: itemFrame.width + spacing, dy: 0.0)
}
i += 1
}
var removeKeys: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validKeys.contains(id) {
removeKeys.append(id)
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
itemView.removeFromSuperview()
})
if let selectedItemView = self.selectedItemViews[id] {
transition.setAlpha(view: selectedItemView, alpha: 0.0, completion: { _ in
selectedItemView.removeFromSuperview()
})
}
}
transition.setShapeLayerPath(layer: self.maskLayer, path: maskPath.cgPath)
}
transition.setFrame(layer: self.maskLayer, frame: CGRect(origin: .zero, size: nodeSize))
for id in removeKeys {
self.itemViews.removeValue(forKey: id)
self.selectedItemViews.removeValue(forKey: id)
}
transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundNode.update(size: size, transition: transition.containedViewLayoutTransition)
let totalSize: CGSize
let size: CGSize
let contentSize: CGSize
var cornerRadius: CGFloat?
if isTablet {
totalSize = CGSize(width: availableSize.width, height: tabletButtonSize.height * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1))
size = CGSize(width: availableSize.width, height: availableSize.height)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: totalSize))
contentSize = totalSize
cornerRadius = 20.0
} else {
size = CGSize(width: availableSize.width, height: buttonSize.height)
totalSize = CGSize(width: itemFrame.minX - spacing + inset, height: buttonSize.height)
let visibleSize = CGSize(width: min(availableSize.width, totalSize.width), height: totalSize.height)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - visibleSize.width) / 2.0), y: 0.0), size: visibleSize))
contentSize = totalSize
}
if let screenTransition = transition.userData(DrawingScreenTransition.self) {
switch screenTransition {
case .animateIn:
self.animateIn()
case .animateOut:
self.animateOut()
let containerFrame = CGRect(origin: .zero, size: self.backgroundView.frame.size)
transition.setFrame(view: self.backgroundContainer, frame: containerFrame)
transition.setFrame(view: liquidLensView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerFrame.size))
let scrollViewFrame = CGRect(origin: .zero, size: containerFrame.size)
transition.setFrame(view: self.scrollView, frame: scrollViewFrame)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
self.scrollView.isScrollEnabled = !isTablet && contentSize.width > scrollViewFrame.width + .ulpOfOne
self.layoutData = LayoutData(containerSize: containerFrame.size, selectedFrame: selectedFrame.insetBy(dx: -inset, dy: 3.0), cornerRadius: cornerRadius, isTablet: isTablet)
self.ignoreScrolling = true
var scrollViewBounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollViewFrame.size)
let maxContentOffsetX = max(0.0, contentSize.width - scrollViewFrame.width)
let shouldFocusOnSelectedItem = previousComponent?.currentMode != component.currentMode || previousComponent?.availableModes != component.availableModes || self.scrollView.bounds.size != scrollViewFrame.size
if self.scrollView.isScrollEnabled && shouldFocusOnSelectedItem {
let scrollLookahead = min(60.0, scrollViewBounds.width * 0.25)
if scrollViewBounds.minX + scrollViewBounds.width - scrollLookahead < selectedFrame.maxX {
scrollViewBounds.origin.x = selectedFrame.maxX - scrollViewBounds.width + scrollLookahead
}
if scrollViewBounds.minX > selectedFrame.minX - scrollLookahead {
scrollViewBounds.origin.x = selectedFrame.minX - scrollLookahead
}
}
scrollViewBounds.origin.x = max(0.0, min(scrollViewBounds.origin.x, maxContentOffsetX))
transition.setBounds(view: self.scrollView, bounds: scrollViewBounds)
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
return size
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
return view.update(component: self, availableSize: availableSize, state: state, transition: transition)
}
}

View file

@ -254,6 +254,70 @@ public final class ChatMediaInputTrendingPane: ChatMediaInputPane {
self.disposable?.dispose()
self.installDisposable.dispose()
}
private func presentStickerPackActionOverlay(_ actions: [StickerPackScreenActionResult]) {
guard let action = actions.first else {
return
}
var animateInAsReplacement = false
if let navigationController = self.interaction.getNavigationController() {
for controller in navigationController.overlayControllers {
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitActionAndReplacementAnimation()
animateInAsReplacement = true
}
}
}
var presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
if let forceTheme = self.forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme)
}
let controller: UndoOverlayController
switch action.action {
case .add:
controller = UndoOverlayController(
presentationData: presentationData,
content: .stickersModified(
title: presentationData.strings.StickerPackActionInfo_AddedTitle,
text: presentationData.strings.StickerPackActionInfo_AddedText(action.info.title).string,
undo: false,
info: action.info,
topItem: action.items.first,
context: self.context
),
elevatedLayout: false,
animateInAsReplacement: animateInAsReplacement,
action: { _ in
return true
}
)
case let .remove(positionInList):
controller = UndoOverlayController(
presentationData: presentationData,
content: .stickersModified(
title: presentationData.strings.StickerPackActionInfo_RemovedTitle,
text: presentationData.strings.StickerPackActionInfo_RemovedText(action.info.title).string,
undo: true,
info: action.info,
topItem: action.items.first,
context: self.context
),
elevatedLayout: false,
animateInAsReplacement: animateInAsReplacement,
action: { [weak self] overlayAction in
if case .undo = overlayAction {
let _ = self?.context.engine.stickers.addStickerPackInteractively(info: action.info, items: action.items, positionInList: positionInList).start()
}
return true
}
)
}
self.interaction.getNavigationController()?.presentOverlay(controller: controller)
}
public func activate() {
if self.isActivated {
@ -373,7 +437,9 @@ public final class ChatMediaInputTrendingPane: ChatMediaInputPane {
return false
}
},
actionPerformed: nil
actionPerformed: { [weak self] actions in
self?.presentStickerPackActionOverlay(actions)
}
)
strongSelf.interaction.presentController(controller, nil)
}

View file

@ -675,6 +675,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
super.didLoad()
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.scrollsToTop = false
let backwardLongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.seekBackwardLongPress(_:)))
backwardLongPressGestureRecognizer.minimumPressDuration = 0.3
@ -1356,7 +1357,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
speed: settingsButtonState.speed,
quality: settingsButtonState.quality,
isOpen: false
))),
)), insets: .zero),
action: { [weak self] in
guard let self, let buttonPanelView = self.buttonPanel.view as? GlassControlPanelComponent.View else {
return
@ -1819,12 +1820,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
if availableOpenInOptions(context: strongSelf.context, item: item).count > 1 {
preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Conversation_FileOpenIn, action: { [weak self] in
if let strongSelf = self {
let openInController = OpenInActionSheetController(context: strongSelf.context, forceTheme: defaultDarkColorPresentationTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in
let openInController = OpenInOptionsScreen(context: strongSelf.context, forceTheme: defaultDarkColorPresentationTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in
if let strongSelf = self {
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
}
})
strongSelf.controllerInteraction?.presentController(openInController, nil)
strongSelf.controllerInteraction?.pushController(openInController)
}
}))
} else {
@ -2124,12 +2125,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
if availableOpenInOptions(context: self.context, item: item).count > 1 {
preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Conversation_FileOpenIn, action: { [weak self] in
if let strongSelf = self {
let openInController = OpenInActionSheetController(context: strongSelf.context, forceTheme: forceTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in
let openInController = OpenInOptionsScreen(context: strongSelf.context, forceTheme: forceTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in
if let strongSelf = self {
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
}
})
strongSelf.controllerInteraction?.presentController(openInController, nil)
strongSelf.controllerInteraction?.pushController(openInController)
}
}))
} else {

View file

@ -83,6 +83,7 @@ public final class GalleryThumbnailContainerNode: ASDisplayNode, ASScrollViewDel
self.scrollNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.scrollsToTop = false
self.addSubnode(self.scrollNode)
}

View file

@ -419,6 +419,9 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN
adView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
adView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in
adView.removeFromSuperview()
if self.adView.view === adView {
self.adView = ComponentView<Empty>()
}
Queue.mainQueue().after(0.1) {
adView.layer.removeAllAnimations()
}
@ -444,7 +447,7 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN
return result
}
}
if let adView = self.adView.view, adView.frame.contains(point) {
if let adView = self.adView.view, adView.superview === self.view, !self.isAnimatingOut, adView.frame.contains(point) {
return super.hitTest(point, with: event)
}
return nil
@ -3892,12 +3895,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
if !presentationData.theme.overallDarkAppearance {
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
}
let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in
let actionSheet = OpenInOptionsScreen(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in
if let strongSelf = self {
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {})
}
})
controller.present(actionSheet, in: .window(.root))
controller.push(actionSheet)
}
})))
break

View file

@ -1,19 +0,0 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GlassButtonNode",
module_name = "GlassButtonNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
],
visibility = [
"//visibility:public",
],
)

View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

View file

@ -307,7 +307,7 @@ final class HashtagSearchRecentQueryItemNode: ItemListRevealOptionsItemNode {
if item.clear {
strongSelf.setRevealOptions((left: [], right: []))
} else {
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.theme.list.itemSecondaryTextColor)]))
}
}
})

View file

@ -110,6 +110,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, ASScroll
self.wrappingScrollNode.view.alwaysBounceVertical = true
self.wrappingScrollNode.view.delaysContentTouches = false
self.wrappingScrollNode.view.canCancelContentTouches = true
self.wrappingScrollNode.view.scrollsToTop = false
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)

View file

@ -139,6 +139,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.addSubnode(self.navigationBar)
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.scrollsToTop = false
self.navigationBar.back = navigateBack
self.navigationBar.share = { [weak self] in
@ -1787,12 +1788,12 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
private func openUrlIn(_ url: InstantPageUrlItem) {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = OpenInActionSheetController(context: self.context, item: .url(url: url.url), openUrl: { [weak self] url in
let actionSheet = OpenInOptionsScreen(context: self.context, item: .url(url: url.url), openUrl: { [weak self] url in
if let strongSelf = self, let navigationController = strongSelf.getNavigationController() {
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
}
})
self.present(actionSheet, nil)
self.pushController(actionSheet)
}
private func mediasFromItems(_ items: [InstantPageItem]) -> [InstantPageMedia] {

View file

@ -309,7 +309,21 @@ public final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode
@objc func joinPressed() {
if let peer = self.peer, case .notJoined = self.joinState {
self.updateJoinState(.inProgress)
self.joinDisposable.set((self.context.engine.peers.joinChannel(peerId: peer.id, hash: nil) |> deliverOnMainQueue).start(error: { [weak self] _ in
self.joinDisposable.set((self.context.engine.peers.joinChannel(peerId: peer.id, hash: nil) |> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
switch result {
case .joined:
break
case let .webView(webView):
if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController, let controller = navigationController.viewControllers.last as? ViewController {
strongSelf.context.sharedContext.openJoinChatWebView(context: strongSelf.context, parentController: controller, updatedPresentationData: nil, webView: webView)
} else if case .inProgress = strongSelf.joinState {
strongSelf.updateJoinState(.notJoined)
}
}
}, error: { [weak self] _ in
if let strongSelf = self {
if case .inProgress = strongSelf.joinState {
strongSelf.updateJoinState(.notJoined)

View file

@ -52,6 +52,7 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, ASScrollVie
self.wrappingScrollNode.view.alwaysBounceVertical = true
self.wrappingScrollNode.view.delaysContentTouches = false
self.wrappingScrollNode.view.canCancelContentTouches = true
self.wrappingScrollNode.view.scrollsToTop = false
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)

View file

@ -60,6 +60,7 @@ swift_library(
"//submodules/PromptUI",
"//submodules/TelegramUI/Components/ItemListDatePickerItem:ItemListDatePickerItem",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/ComponentFlow",
"//submodules/Components/MultilineTextComponent",
],

View file

@ -711,7 +711,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
saveEnabled = true
}
doneButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Save), style: .bold, enabled: !state.selectedPeerIds.isEmpty && saveEnabled, action: {
doneButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: !state.selectedPeerIds.isEmpty && saveEnabled, action: {
applyChangesImpl?()
})
}

View file

@ -479,7 +479,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
}
}
private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, isGroup: Bool, isPublic: Bool, presentationData: PresentationData, configuration: StarsSubscriptionConfiguration) -> [InviteLinksEditEntry] {
private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, isGroup: Bool, isPublic: Bool, globalRequestApproval: Bool, presentationData: PresentationData, configuration: StarsSubscriptionConfiguration) -> [InviteLinksEditEntry] {
var entries: [InviteLinksEditEntry] = []
entries.append(.titleHeader(presentationData.theme, presentationData.strings.InviteLink_Create_LinkNameTitle.uppercased()))
@ -507,17 +507,14 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state:
entries.append(.subscriptionFeeInfo(presentationData.theme, infoText))
}
if !isPublic {
entries.append(.requestApproval(presentationData.theme, presentationData.strings.InviteLink_Create_RequestApproval, state.requestApproval, isEditingEnabled && !isSubscription))
var requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel
if !isPublic || isGroup {
let requestApprovalEnabled = isEditingEnabled && !isSubscription && !(isPublic && isGroup && !globalRequestApproval)
entries.append(.requestApproval(presentationData.theme, isGroup ? presentationData.strings.Group_Setup_ApproveNewMembers : presentationData.strings.Channel_Setup_ApproveNewSubscribers, state.requestApproval, requestApprovalEnabled))
var requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalInfo
if isSubscription {
requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalFeeUnavailable
} else {
if state.requestApproval {
requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOnInfoChannel
} else {
requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel
}
} else if isPublic && isGroup && !globalRequestApproval {
requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalPublicGroupUnavailable
}
entries.append(.requestApprovalInfo(presentationData.theme, requestApprovalInfoText))
}
@ -538,7 +535,7 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state:
}
entries.append(.timeInfo(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimitInfo))
if !state.requestApproval || isPublic {
if !state.requestApproval {
entries.append(.usageHeader(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimit.uppercased()))
entries.append(.usagePicker(presentationData.theme, presentationData.dateTimeFormat, state.usage, isEditingEnabled))
@ -696,7 +693,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
|> map { presentationData, state, peer -> (ItemListControllerState, (ItemListNodeState, Any)) in
let isPublic = !(peer?.addressName?.isEmpty ?? true)
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: {
dismissImpl?()
})
@ -707,7 +704,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
}
}
let rightNavigationButton = ItemListNavigationButton(content: .text(invite == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Save), style: state.updating ? .activity : .bold, enabled: doneIsEnabled, action: {
let rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: state.updating ? .activity : .bold, enabled: doneIsEnabled, action: {
updateState { state in
var updatedState = state
updatedState.updating = true
@ -727,7 +724,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
let titleString = state.title.trimmingCharacters(in: .whitespacesAndNewlines)
let title = titleString.isEmpty ? nil : titleString
var usageLimit = state.usage.value
var requestNeeded: Bool? = state.requestApproval && !isPublic
var requestNeeded: Bool? = state.requestApproval
if invite == nil {
let subscriptionPricing: StarsSubscriptionPricing?
@ -792,14 +789,20 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
}
let isGroup: Bool
let globalRequestApproval: Bool
if case let .channel(channel) = peer, case .broadcast = channel.info {
isGroup = false
globalRequestApproval = channel.flags.contains(.requestToJoin)
} else if case let .channel(channel) = peer {
isGroup = true
globalRequestApproval = channel.flags.contains(.requestToJoin)
} else {
isGroup = true
globalRequestApproval = false
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(invite == nil ? presentationData.strings.InviteLink_Create_Title : presentationData.strings.InviteLink_Create_EditTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, isGroup: isGroup, isPublic: isPublic, presentationData: presentationData, configuration: configuration), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, isGroup: isGroup, isPublic: isPublic, globalRequestApproval: globalRequestApproval, presentationData: presentationData, configuration: configuration), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
}

View file

@ -7,14 +7,12 @@ import AsyncDisplayKit
import TelegramCore
import Display
import AccountContext
import SolidRoundedButtonNode
import ItemListUI
import ItemListPeerItem
import SectionHeaderItem
import TelegramStringFormatting
import MergeLists
import ContextUI
import OverlayStatusController
import PresentationDataUtils
import DirectionalPanGesture

View file

@ -7,14 +7,12 @@ import AsyncDisplayKit
import TelegramCore
import Display
import AccountContext
import SolidRoundedButtonNode
import ItemListUI
import ItemListPeerItem
import SectionHeaderItem
import TelegramStringFormatting
import MergeLists
import ContextUI
import OverlayStatusController
import PresentationDataUtils
import DirectionalPanGesture

View file

@ -6,10 +6,11 @@ import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import ItemListUI
import SolidRoundedButtonNode
import AnimatedAvatarSetNode
import ShimmerEffect
import TelegramCore
import ComponentFlow
import ButtonComponent
private func actionButtonImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 24.0, height: 24.0), contextGenerator: { size, context in
@ -141,8 +142,8 @@ public class ItemListFolderInviteLinkItemNode: ListViewItemNode, ItemListItemNod
private let addressButtonNode: HighlightTrackingButtonNode
private let addressButtonIconNode: ASImageNode
private var addressShimmerNode: ShimmerEffectNode?
private var shareButtonNode: SolidRoundedButtonNode?
private var secondaryButtonNode: SolidRoundedButtonNode?
private var shareButton: ComponentView<Empty>?
private var secondaryButton: ComponentView<Empty>?
private let avatarsButtonNode: HighlightTrackingButtonNode
private let avatarsContext: AnimatedAvatarSetContext
@ -250,11 +251,6 @@ public class ItemListFolderInviteLinkItemNode: ListViewItemNode, ItemListItemNod
}
}
}
self.shareButtonNode?.pressed = { [weak self] in
if let strongSelf = self, let item = strongSelf.item {
item.shareAction?()
}
}
self.avatarsButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
@ -458,64 +454,94 @@ public class ItemListFolderInviteLinkItemNode: ListViewItemNode, ItemListItemNod
strongSelf.referenceContainerNode.frame = strongSelf.containerNode.bounds
strongSelf.addressButtonIconNode.frame = strongSelf.containerNode.bounds
let shareButtonNode: SolidRoundedButtonNode
if let currentShareButtonNode = strongSelf.shareButtonNode {
shareButtonNode = currentShareButtonNode
} else {
let buttonTheme: SolidRoundedButtonTheme
if let buttonColor = item.buttonColor {
buttonTheme = SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
} else {
buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme)
}
shareButtonNode = SolidRoundedButtonNode(theme: buttonTheme, glass: item.systemStyle == .glass, height: buttonHeight, cornerRadius: buttonHeight * 0.5)
shareButtonNode.pressed = { [weak self] in
self?.item?.shareAction?()
}
strongSelf.addSubnode(shareButtonNode)
strongSelf.shareButtonNode = shareButtonNode
}
shareButtonNode.title = item.buttonTitle
let buttonBackgroundColor = item.buttonColor ?? item.presentationData.theme.list.itemCheckColors.fillColor
let buttonForegroundColor = item.presentationData.theme.list.itemCheckColors.foregroundColor
let buttonBackground = ButtonComponent.Background(
style: item.systemStyle == .glass ? .glass : .legacy,
color: buttonBackgroundColor,
foreground: buttonForegroundColor,
pressedColor: buttonBackgroundColor.withMultipliedAlpha(0.8),
cornerRadius: buttonHeight * 0.5
)
if let secondaryButtonTitle = item.secondaryButtonTitle {
let secondaryButtonNode: SolidRoundedButtonNode
if let current = strongSelf.secondaryButtonNode {
secondaryButtonNode = current
} else {
let buttonTheme: SolidRoundedButtonTheme
if let buttonColor = item.buttonColor {
buttonTheme = SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
} else {
buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme)
}
secondaryButtonNode = SolidRoundedButtonNode(theme: buttonTheme, glass: item.systemStyle == .glass, height: buttonHeight, cornerRadius: buttonHeight * 0.5)
secondaryButtonNode.pressed = { [weak self] in
self?.item?.secondaryAction?()
}
strongSelf.addSubnode(secondaryButtonNode)
strongSelf.secondaryButtonNode = secondaryButtonNode
}
secondaryButtonNode.title = secondaryButtonTitle
let shareButton: ComponentView<Empty>
if let currentShareButton = strongSelf.shareButton {
shareButton = currentShareButton
} else {
if let secondaryButtonNode = strongSelf.secondaryButtonNode {
strongSelf.secondaryButtonNode = nil
secondaryButtonNode.removeFromSupernode()
shareButton = ComponentView()
strongSelf.shareButton = shareButton
}
if item.secondaryButtonTitle != nil {
if strongSelf.secondaryButton == nil {
strongSelf.secondaryButton = ComponentView()
}
} else {
if let secondaryButton = strongSelf.secondaryButton {
strongSelf.secondaryButton = nil
secondaryButton.view?.removeFromSuperview()
}
}
var buttonWidth = contentSize.width - leftInset - rightInset
let totalButtonWidth = buttonWidth
let buttonSpacing: CGFloat = 8.0
if strongSelf.secondaryButtonNode != nil {
if strongSelf.secondaryButton != nil {
buttonWidth = floor((buttonWidth - 8.0) / 2.0)
}
let _ = shareButtonNode.updateLayout(width: buttonWidth, transition: .immediate)
shareButtonNode.frame = CGRect(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight)
let shareButtonSize = shareButton.update(
transition: .immediate,
component: AnyComponent(ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(id: AnyHashable(item.buttonTitle), component: AnyComponent(Text(text: item.buttonTitle, font: Font.semibold(17.0), color: buttonForegroundColor))),
isEnabled: item.enableButton,
tintWhenDisabled: false,
action: { [weak self] in
self?.item?.shareAction?()
}
)),
environment: {},
containerSize: CGSize(width: buttonWidth, height: buttonHeight)
)
if let shareButtonView = shareButton.view {
if shareButtonView.superview == nil {
strongSelf.view.addSubview(shareButtonView)
}
shareButtonView.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing), size: shareButtonSize)
shareButtonView.isHidden = !item.displayButton
shareButtonView.alpha = item.enableButton ? 1.0 : 0.4
shareButtonView.isAccessibilityElement = true
shareButtonView.accessibilityLabel = item.buttonTitle
shareButtonView.accessibilityTraits = item.enableButton ? [.button] : [.button, .notEnabled]
}
if let secondaryButtonNode = strongSelf.secondaryButtonNode {
let _ = secondaryButtonNode.updateLayout(width: totalButtonWidth - buttonWidth - buttonSpacing, transition: .immediate)
secondaryButtonNode.frame = CGRect(x: leftInset + buttonWidth + buttonSpacing, y: verticalInset + fieldHeight + fieldSpacing, width: totalButtonWidth - buttonWidth - buttonSpacing, height: buttonHeight)
if let secondaryButton = strongSelf.secondaryButton, let secondaryButtonTitle = item.secondaryButtonTitle {
let secondaryButtonWidth = totalButtonWidth - buttonWidth - buttonSpacing
let secondaryButtonSize = secondaryButton.update(
transition: .immediate,
component: AnyComponent(ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(id: AnyHashable(secondaryButtonTitle), component: AnyComponent(Text(text: secondaryButtonTitle, font: Font.semibold(17.0), color: buttonForegroundColor))),
tintWhenDisabled: false,
action: { [weak self] in
self?.item?.secondaryAction?()
}
)),
environment: {},
containerSize: CGSize(width: secondaryButtonWidth, height: buttonHeight)
)
if let secondaryButtonView = secondaryButton.view {
if secondaryButtonView.superview == nil {
strongSelf.view.addSubview(secondaryButtonView)
}
secondaryButtonView.frame = CGRect(origin: CGPoint(x: leftInset + buttonWidth + buttonSpacing, y: verticalInset + fieldHeight + fieldSpacing), size: secondaryButtonSize)
secondaryButtonView.isHidden = !item.displayButton
secondaryButtonView.alpha = 1.0
secondaryButtonView.isAccessibilityElement = true
secondaryButtonView.accessibilityLabel = secondaryButtonTitle
secondaryButtonView.accessibilityTraits = [.button]
}
}
var totalWidth = invitedPeersLayout.size.width
@ -544,9 +570,6 @@ public class ItemListFolderInviteLinkItemNode: ListViewItemNode, ItemListItemNod
strongSelf.fieldButtonNode.isUserInteractionEnabled = item.invite != nil
strongSelf.addressButtonIconNode.alpha = item.invite != nil ? 1.0 : 0.0
strongSelf.shareButtonNode?.isUserInteractionEnabled = item.enableButton
strongSelf.shareButtonNode?.alpha = item.enableButton ? 1.0 : 0.4
strongSelf.shareButtonNode?.isHidden = !item.displayButton
strongSelf.avatarsButtonNode.isHidden = !item.displayImporters
strongSelf.avatarsNode.isHidden = !item.displayImporters || item.invite == nil
strongSelf.invitedPeersNode.isHidden = !item.displayImporters || item.invite == nil

View file

@ -521,7 +521,7 @@ public class ItemListFolderInviteLinkListItemNode: ItemListRevealOptionsItemNode
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
if item.removeAction != nil {
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.ChatListFilter_LinkActionDelete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.ChatListFilter_LinkActionDelete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)]))
} else {
strongSelf.setRevealOptions((left: [], right: []))
}

View file

@ -377,7 +377,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
var pricingAttributedText: NSMutableAttributedString?
var timerValue: TimerNode.Value?
if let invite = item.invite, case let .link(_, title, _, _, _, _, date, startDate, expireDate, usageLimit, count, requestedCount, subscriptionPricing) = invite {
if let invite = item.invite, case let .link(_, title, _, requestApproval, _, _, date, startDate, expireDate, usageLimit, count, requestedCount, subscriptionPricing) = invite {
if let title = title, !title.isEmpty {
titleText = title
}
@ -405,6 +405,12 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
}
subtitleText += item.presentationData.strings.MemberRequests_PeopleRequestedShort(requestedCount)
}
if requestApproval {
if !subtitleText.isEmpty {
subtitleText += ""
}
subtitleText += item.presentationData.strings.InviteLink_ApprovalRequired
}
if let subscriptionPricing {
let text = NSMutableAttributedString()

View file

@ -721,7 +721,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)

View file

@ -6,13 +6,13 @@ import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import ItemListUI
import SolidRoundedButtonNode
import AnimatedAvatarSetNode
import ShimmerEffect
import TelegramCore
import Markdown
import TextFormat
import ComponentFlow
import ButtonComponent
import MultilineTextComponent
import TextNodeWithEntities
@ -143,8 +143,8 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
private let addressButtonNode: HighlightTrackingButtonNode
private let addressButtonIconNode: ASImageNode
private var addressShimmerNode: ShimmerEffectNode?
private var copyButtonNode: SolidRoundedButtonNode?
private var shareButtonNode: SolidRoundedButtonNode?
private var copyButton: ComponentView<Empty>?
private var shareButton: ComponentView<Empty>?
private let avatarsButtonNode: HighlightTrackingButtonNode
private let avatarsContext: AnimatedAvatarSetContext
@ -257,16 +257,6 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
}
}
}
self.copyButtonNode?.pressed = { [weak self] in
if let strongSelf = self, let item = strongSelf.item {
item.copyAction?()
}
}
self.shareButtonNode?.pressed = { [weak self] in
if let strongSelf = self, let item = strongSelf.item {
item.shareAction?()
}
}
self.avatarsButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
@ -535,46 +525,38 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
effectiveSeparateButtons = false
}
let copyButtonNode: SolidRoundedButtonNode
if let currentCopyButtonNode = strongSelf.copyButtonNode {
copyButtonNode = currentCopyButtonNode
let buttonBackgroundColor = item.buttonColor ?? item.presentationData.theme.list.itemCheckColors.fillColor
let buttonForegroundColor = item.presentationData.theme.list.itemCheckColors.foregroundColor
let buttonBackground = ButtonComponent.Background(
style: item.systemStyle == .glass ? .glass : .legacy,
color: buttonBackgroundColor,
foreground: buttonForegroundColor,
pressedColor: buttonBackgroundColor.withMultipliedAlpha(0.8),
cornerRadius: item.systemStyle == .glass ? 26.0 : 11.0
)
let copyButtonTitle = item.presentationData.strings.InviteLink_CopyShort
let copyButton: ComponentView<Empty>
if let currentCopyButton = strongSelf.copyButton {
copyButton = currentCopyButton
} else {
let buttonTheme: SolidRoundedButtonTheme
if let buttonColor = item.buttonColor {
buttonTheme = SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
} else {
buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme)
}
copyButtonNode = SolidRoundedButtonNode(theme: buttonTheme, glass: item.systemStyle == .glass, height: 52.0, cornerRadius: item.systemStyle == .glass ? 26.0 : 11.0)
copyButtonNode.title = item.presentationData.strings.InviteLink_CopyShort
copyButtonNode.pressed = { [weak self] in
self?.item?.copyAction?()
}
strongSelf.addSubnode(copyButtonNode)
strongSelf.copyButtonNode = copyButtonNode
copyButton = ComponentView()
strongSelf.copyButton = copyButton
}
let shareButtonNode: SolidRoundedButtonNode
if let currentShareButtonNode = strongSelf.shareButtonNode {
shareButtonNode = currentShareButtonNode
let shareButtonTitle: String
if let invite = item.invite, invitationAvailability(invite).isZero {
shareButtonTitle = item.presentationData.strings.InviteLink_ReactivateLink
} else {
let buttonTheme: SolidRoundedButtonTheme
if let buttonColor = item.buttonColor {
buttonTheme = SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
} else {
buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme)
}
shareButtonNode = SolidRoundedButtonNode(theme: buttonTheme, glass: item.systemStyle == .glass, height: 52.0, cornerRadius: item.systemStyle == .glass ? 26.0 : 11.0)
if let invite = item.invite, invitationAvailability(invite).isZero {
shareButtonNode.title = item.presentationData.strings.InviteLink_ReactivateLink
} else {
shareButtonNode.title = effectiveSeparateButtons ? item.presentationData.strings.InviteLink_ShareShort : item.presentationData.strings.InviteLink_Share
}
shareButtonNode.pressed = { [weak self] in
self?.item?.shareAction?()
}
strongSelf.addSubnode(shareButtonNode)
strongSelf.shareButtonNode = shareButtonNode
shareButtonTitle = effectiveSeparateButtons ? item.presentationData.strings.InviteLink_ShareShort : item.presentationData.strings.InviteLink_Share
}
let shareButton: ComponentView<Empty>
if let currentShareButton = strongSelf.shareButton {
shareButton = currentShareButton
} else {
shareButton = ComponentView()
strongSelf.shareButton = shareButton
}
let buttonSpacing: CGFloat = 8.0
@ -585,11 +567,57 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
shareButtonOriginX = leftInset + buttonWidth + buttonSpacing
}
let _ = copyButtonNode.updateLayout(width: buttonWidth, transition: .immediate)
copyButtonNode.frame = CGRect(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight)
let copyButtonSize = copyButton.update(
transition: .immediate,
component: AnyComponent(ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(id: AnyHashable(copyButtonTitle), component: AnyComponent(Text(text: copyButtonTitle, font: Font.semibold(17.0), color: buttonForegroundColor))),
isEnabled: item.invite != nil,
tintWhenDisabled: false,
action: { [weak self] in
self?.item?.copyAction?()
}
)),
environment: {},
containerSize: CGSize(width: buttonWidth, height: buttonHeight)
)
if let copyButtonView = copyButton.view {
if copyButtonView.superview == nil {
strongSelf.view.addSubview(copyButtonView)
}
copyButtonView.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing), size: copyButtonSize)
copyButtonView.isHidden = !item.displayButton || !effectiveSeparateButtons
copyButtonView.alpha = item.invite != nil ? 1.0 : 0.4
copyButtonView.isAccessibilityElement = true
copyButtonView.accessibilityLabel = copyButtonTitle
copyButtonView.accessibilityTraits = item.invite != nil ? [.button] : [.button, .notEnabled]
}
let _ = shareButtonNode.updateLayout(width: buttonWidth, transition: .immediate)
shareButtonNode.frame = CGRect(x: shareButtonOriginX, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight)
let shareButtonSize = shareButton.update(
transition: .immediate,
component: AnyComponent(ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(id: AnyHashable(shareButtonTitle), component: AnyComponent(Text(text: shareButtonTitle, font: Font.semibold(17.0), color: buttonForegroundColor))),
isEnabled: item.invite != nil,
tintWhenDisabled: false,
action: { [weak self] in
self?.item?.shareAction?()
}
)),
environment: {},
containerSize: CGSize(width: buttonWidth, height: buttonHeight)
)
if let shareButtonView = shareButton.view {
if shareButtonView.superview == nil {
strongSelf.view.addSubview(shareButtonView)
}
shareButtonView.frame = CGRect(origin: CGPoint(x: shareButtonOriginX, y: verticalInset + fieldHeight + fieldSpacing), size: shareButtonSize)
shareButtonView.isHidden = !item.displayButton
shareButtonView.alpha = item.invite != nil ? 1.0 : 0.4
shareButtonView.isAccessibilityElement = true
shareButtonView.accessibilityLabel = shareButtonTitle
shareButtonView.accessibilityTraits = item.invite != nil ? [.button] : [.button, .notEnabled]
}
if let justCreatedCallTextNodeLayout {
if let justCreatedCallTextNode = justCreatedCallTextNodeLayout.1(TextNodeWithEntities.Arguments(
@ -608,7 +636,8 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
justCreatedCallTextNode.textNode.view.addGestureRecognizer(UITapGestureRecognizer(target: strongSelf, action: #selector(strongSelf.justCreatedCallTextTap(_:))))
}
let justCreatedCallTextNodeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - justCreatedCallTextNodeLayout.0.size.width) / 2.0), y: shareButtonNode.frame.maxY + justCreatedCallTextSpacing), size: CGSize(width: justCreatedCallTextNodeLayout.0.size.width, height: justCreatedCallTextNodeLayout.0.size.height))
let buttonMaxY = verticalInset + fieldHeight + fieldSpacing + buttonHeight
let justCreatedCallTextNodeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - justCreatedCallTextNodeLayout.0.size.width) / 2.0), y: buttonMaxY + justCreatedCallTextSpacing), size: CGSize(width: justCreatedCallTextNodeLayout.0.size.width, height: justCreatedCallTextNodeLayout.0.size.height))
justCreatedCallTextNode.textNode.frame = justCreatedCallTextNodeFrame
let justCreatedCallSeparatorText: ComponentView<Empty>
@ -648,7 +677,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
environment: {},
containerSize: CGSize(width: params.width - leftInset - rightInset, height: 100.0)
)
let justCreatedCallSeparatorTextFrame = CGRect(origin: CGPoint(x: floor((params.width - justCreatedCallSeparatorTextSize.width) * 0.5), y: shareButtonNode.frame.maxY + justCreatedCallSeparatorSpacing), size: justCreatedCallSeparatorTextSize)
let justCreatedCallSeparatorTextFrame = CGRect(origin: CGPoint(x: floor((params.width - justCreatedCallSeparatorTextSize.width) * 0.5), y: buttonMaxY + justCreatedCallSeparatorSpacing), size: justCreatedCallSeparatorTextSize)
if let justCreatedCallSeparatorTextView = justCreatedCallSeparatorText.view {
if justCreatedCallSeparatorTextView.superview == nil {
strongSelf.view.addSubview(justCreatedCallSeparatorTextView)
@ -703,14 +732,6 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
strongSelf.addressButtonIconNode.alpha = item.invite != nil ? 1.0 : 0.0
strongSelf.copyButtonNode?.isUserInteractionEnabled = item.invite != nil
strongSelf.copyButtonNode?.alpha = item.invite != nil ? 1.0 : 0.4
strongSelf.copyButtonNode?.isHidden = !item.displayButton || !effectiveSeparateButtons
strongSelf.shareButtonNode?.isUserInteractionEnabled = item.invite != nil
strongSelf.shareButtonNode?.alpha = item.invite != nil ? 1.0 : 0.4
strongSelf.shareButtonNode?.isHidden = !item.displayButton
strongSelf.avatarsButtonNode.isHidden = !item.displayImporters
strongSelf.avatarsNode.isHidden = !item.displayImporters || item.invite == nil
strongSelf.invitedPeersNode.isHidden = !item.displayImporters || item.invite == nil

View file

@ -321,7 +321,7 @@ public struct ItemListPeerItemShimmering {
}
}
public final class ItemListPeerItem: ListViewItem, ItemListItem {
public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem {
public enum Context {
public final class Custom {
public let accountPeerId: EnginePeer.Id
@ -468,6 +468,10 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
let disableInteractiveTransitionIfNecessary: Bool
let storyStats: EnginePeerStoryStats?
let openStories: ((UIView) -> Void)?
public var hasActiveRevealOptions: Bool {
return self.editing.revealed == true
}
public init(
presentationData: ItemListPresentationData,
@ -1013,12 +1017,12 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
color = item.presentationData.theme.list.itemDisclosureActions.accent.fillColor
textColor = item.presentationData.theme.list.itemDisclosureActions.accent.foregroundColor
}
mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, textColor: textColor))
mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, iconColor: textColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor))
index += 1
}
peerRevealOptions = mappedOptions
} else {
peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]
peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)]
}
} else {
peerRevealOptions = []
@ -1434,26 +1438,29 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
let hasCorners = itemListHasRoundedBlockLayout(params) && !item.noCorners
var hasTopCorners = false
var hasBottomCorners = false
let topStripeIsHidden: Bool
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
topStripeIsHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = !item.displayDecorations || hasCorners || !item.hasTopStripe
topStripeIsHidden = !item.displayDecorations || hasCorners || !item.hasTopStripe
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
let bottomStripeIsHidden: Bool
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset + editingOffset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = !item.displayDecorations
bottomStripeIsHidden = !item.displayDecorations
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners || !item.displayDecorations
bottomStripeIsHidden = hasCorners || !item.displayDecorations
}
strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions)
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
@ -1778,7 +1785,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
}
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)), transition: transition)
if let presence = item.presence {
strongSelf.peerPresenceManager?.reset(presence: presence)
@ -1860,7 +1867,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
var isHighlighted = false
var reallyHighlighted: Bool {
var reallyHighlighted = self.isHighlighted
var reallyHighlighted = self.isHighlighted || self.isRevealOptionsActive
if let (item, _, _, _) = self.layoutParams, item.highlighted {
reallyHighlighted = true
}
@ -1868,39 +1875,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
}
func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
if self.reallyHighlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if transition.isAnimated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.reallyHighlighted, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
@ -2002,6 +1977,12 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
transition.updateFrame(view: avatarIconComponentView, frame: threadIconFrame)
}
}
override public func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
self.updateIsHighlighted(transition: transition)
}
override public func revealOptionsInteractivelyOpened() {
if let (item, _, _, _) = self.layoutParams {

View file

@ -36,7 +36,7 @@ public enum ItemListStickerPackItemControl: Equatable {
case check(checked: Bool)
}
public final class ItemListStickerPackItem: ListViewItem, ItemListItem {
public final class ItemListStickerPackItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem {
let presentationData: ItemListPresentationData
let context: AccountContext
let systemStyle: ItemListSystemStyle
@ -56,6 +56,10 @@ public final class ItemListStickerPackItem: ListViewItem, ItemListItem {
let removePack: () -> Void
let toggleSelected: () -> Void
public var hasActiveRevealOptions: Bool {
return self.editing.revealed
}
public init(presentationData: ItemListPresentationData, context: AccountContext, systemStyle: ItemListSystemStyle = .legacy, packInfo: StickerPackCollectionInfo.Accessor, itemCount: String, topItem: StickerPackItem?, unread: Bool, control: ItemListStickerPackItemControl, editing: ItemListStickerPackItemEditing, enabled: Bool, playAnimatedStickers: Bool, style: ItemListStyle = .blocks, sectionId: ItemListSectionId, action: (() -> Void)?, setPackIdWithRevealedOptions: @escaping (EngineItemCollectionId?, EngineItemCollectionId?) -> Void, addPack: @escaping () -> Void, removePack: @escaping () -> Void, toggleSelected: @escaping () -> Void) {
self.presentationData = presentationData
self.context = context
@ -180,6 +184,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
private let activateArea: AccessibilityAreaNode
private let fetchDisposable = MetaDisposable()
private var isHighlighted: Bool = false
override var canBeSelected: Bool {
if self.selectableControlNode != nil || self.editableControlNode != nil || self.disabledOverlayNode != nil {
@ -372,7 +377,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
let packRevealOptions: [ItemListRevealOption]
if item.editing.editable && item.enabled && !item.editing.editing {
packRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]
packRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)]
} else {
packRevealOptions = []
}
@ -410,14 +415,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
let leftInset: CGFloat = 65.0 + params.leftInset
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
verticalInset = 13.0
case .legacy:
verticalInset = 11.0
}
let verticalInset: CGFloat = 11.0
let titleSpacing: CGFloat = 2.0
let separatorHeight = UIScreenPixel
@ -764,26 +762,29 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
let topStripeIsHidden: Bool
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
topStripeIsHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
topStripeIsHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
let bottomStripeIsHidden: Bool
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset + editingOffset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
bottomStripeIsHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
bottomStripeIsHidden = hasCorners
}
strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions)
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
@ -877,7 +878,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
strongSelf.imageNode.setSignal(updatedImageSignal)
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel))
strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel)), transition: transition)
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
@ -895,39 +896,8 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
self.isHighlighted = highlighted
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
@ -967,6 +937,12 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
transition.updateFrame(node: animationNode, frame: CGRect(origin: CGPoint(x: params.leftInset + self.revealOffset + editingOffset + 15.0 + floor((boundingSize.width - animationNode.frame.size.width) / 2.0), y: animationNode.frame.minY), size: animationNode.frame.size))
}
}
override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
}
override func revealOptionsInteractivelyOpened() {
if let (item, _, _) = self.layoutParams {

View file

@ -35,6 +35,7 @@ swift_library(
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/GlassControls",
"//submodules/TelegramUI/Components/HorizontalTabsComponent",
],
visibility = [

View file

@ -25,6 +25,8 @@ public enum ItemListNavigationButtonContentIcon {
case search
case add
case action
case close
case done
}
public enum ItemListNavigationButtonContent: Equatable {
@ -426,16 +428,27 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
case let .text(value):
item = UIBarButtonItem(title: value, style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed))
case let .icon(icon):
var image: UIImage?
switch icon {
if [.close, .done].contains(icon) {
switch icon {
case .done:
item = UIBarButtonItem(title: "___done", style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed))
default:
item = UIBarButtonItem(title: "___close", style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed))
}
} else {
var image: UIImage?
switch icon {
case .search:
image = PresentationResourcesRootController.navigationCompactSearchIcon(controllerState.presentationData.theme)
case .add:
image = PresentationResourcesRootController.navigationAddIcon(controllerState.presentationData.theme)
case .action:
image = PresentationResourcesRootController.navigationShareIcon(controllerState.presentationData.theme)
default:
image = nil
}
item = UIBarButtonItem(image: image, style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed))
}
item = UIBarButtonItem(image: image, style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed))
case let .node(node):
item = UIBarButtonItem(customDisplayNode: node)
item.setCustomAction({ [weak self] in
@ -488,16 +501,27 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
case let .text(value):
item = UIBarButtonItem(title: value, style: style.barButtonItemStyle, target: strongSelf, action: action)
case let .icon(icon):
var image: UIImage?
switch icon {
if [.close, .done].contains(icon) {
switch icon {
case .done:
item = UIBarButtonItem(title: "___done", style: style.barButtonItemStyle, target: strongSelf, action: action)
default:
item = UIBarButtonItem(title: "___close", style: style.barButtonItemStyle, target: strongSelf, action: action)
}
} else {
var image: UIImage?
switch icon {
case .search:
image = PresentationResourcesRootController.navigationCompactSearchIcon(controllerState.presentationData.theme)
case .add:
image = PresentationResourcesRootController.navigationAddIcon(controllerState.presentationData.theme)
case .action:
image = PresentationResourcesRootController.navigationShareIcon(controllerState.presentationData.theme)
default:
image = nil
}
item = UIBarButtonItem(image: image, style: style.barButtonItemStyle, target: strongSelf, action: action)
}
item = UIBarButtonItem(image: image, style: style.barButtonItemStyle, target: strongSelf, action: action)
case let .node(node):
item = UIBarButtonItem(customDisplayNode: node)
item.setCustomAction({ [weak self] in
@ -665,6 +689,16 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
}
}
public func itemNode(forTag tag: ItemListItemTag) -> ListViewItemNode? {
var result: ListViewItemNode?
self.forEachItemNode { itemNode in
if result == nil, let taggedItemNode = itemNode as? ItemListItemNode, let itemTag = taggedItemNode.tag, itemTag.isEqual(to: tag) {
result = itemNode
}
}
return result
}
public func ensureItemNodeVisible(_ itemNode: ListViewItemNode, animated: Bool = true, overflow: CGFloat = 0.0, atTop: Bool = false, curve: ListViewAnimationCurve = .Default(duration: 0.25)) {
self.controllerNode.listNode.ensureItemNodeVisible(itemNode, animated: animated, overflow: overflow, atTop: atTop, curve: curve)
}

Some files were not shown because too many files have changed in this diff Show more