mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Merge commit '4b2a826682'
This commit is contained in:
commit
bd8886b0f0
594 changed files with 24232 additions and 23505 deletions
|
|
@ -1587,6 +1587,8 @@ plist_fragment(
|
|||
<string>here-location</string>
|
||||
<string>yandexmaps</string>
|
||||
<string>yandexnavi</string>
|
||||
<string>yandextaxi</string>
|
||||
<string>yangoride</string>
|
||||
<string>comgooglemaps</string>
|
||||
<string>youtube</string>
|
||||
<string>twitter</string>
|
||||
|
|
@ -1615,6 +1617,7 @@ plist_fragment(
|
|||
<string>dolphin</string>
|
||||
<string>instagram-stories</string>
|
||||
<string>yangomaps</string>
|
||||
<string>vivaldi</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
|
|
|
|||
|
|
@ -2661,6 +2661,8 @@ Unused sets are archived when you add more.";
|
|||
"Channel.EditAdmin.PermissionInviteViaLink" = "Invite Users via Link";
|
||||
"Channel.EditAdmin.PermissionPinMessages" = "Pin Messages";
|
||||
"Channel.EditAdmin.PermissionAddAdmins" = "Add New Admins";
|
||||
"Channel.EditAdmin.PermissionProcessJoinRequests" = "Process Join Requests";
|
||||
"Channel.EditAdmin.PermissionProcessJoinRequestsInfo" = "Allow this bot to approve new members using its own interfaces, such as captchas.";
|
||||
|
||||
"Channel.EditAdmin.PermissinAddAdminOn" = "This Admin will be able to add new admins with the same (or more limited) permissions.";
|
||||
"Channel.EditAdmin.PermissinAddAdminOff" = "This Admin will not be able to add new admins.";
|
||||
|
|
@ -2874,6 +2876,13 @@ Unused sets are archived when you add more.";
|
|||
"Call.ReportSend" = "Send";
|
||||
|
||||
"Channel.EditAdmin.CannotEdit" = "You cannot edit the rights of this admin.";
|
||||
"Channel.EditAdmin.GuardBotEnableMembersText" = "New members will now need approval from **%@** before they can send messages in this group";
|
||||
"Channel.EditAdmin.GuardBotEnableSubscribersText" = "New subscribers will now need approval from **%@** before they can join this channel";
|
||||
"Channel.EditAdmin.GuardBotEnable" = "Enable";
|
||||
"Channel.EditAdmin.GuardBotReplaceTitle" = "Replace Bot";
|
||||
"Channel.EditAdmin.GuardBotReplaceText" = "**%@** is currently processing join requests. Switch to **%@** instead?";
|
||||
"Channel.EditAdmin.GuardBotReplaceKeep" = "Keep %@";
|
||||
"Channel.EditAdmin.GuardBotReplaceUse" = "Use %@";
|
||||
"Call.RateCall" = "Rate This Call";
|
||||
"Call.ShareStats" = "Share Statistics";
|
||||
|
||||
|
|
@ -7020,6 +7029,9 @@ Sorry for the inconvenience.";
|
|||
"InviteLink.Create.RequestApprovalOffInfoChannel" = "New users will be able to join the channel without being approved by the admins.";
|
||||
"InviteLink.Create.RequestApprovalOnInfoGroup" = "New users will be able to join the group only after having been approved by the admins.";
|
||||
"InviteLink.Create.RequestApprovalOnInfoChannel" = "New users will be able to join the channel only after having been approved by the admins.";
|
||||
"InviteLink.Create.RequestApprovalInfo" = "Request admin approval for people joining through this link.";
|
||||
"InviteLink.Create.RequestApprovalPublicGroupUnavailable" = "This option is unavailable because anyone can join and send messages through the public group link.";
|
||||
"InviteLink.ApprovalRequired" = "approval required";
|
||||
|
||||
"InviteLink.Create.LinkNameTitle" = "Link Name";
|
||||
"InviteLink.Create.LinkName" = "Link Name (Optional)";
|
||||
|
|
@ -7606,6 +7618,15 @@ Sorry for the inconvenience.";
|
|||
|
||||
"Group.Setup.ApproveNewMembers" = "Approve New Members";
|
||||
"Group.Setup.ApproveNewMembersInfo" = "Turn this on if you want users to be able to send messages only after they are approved by an admin.";
|
||||
"Group.Setup.ApproveNewMembersManagedBy" = "Managed by %@";
|
||||
"Group.Setup.ApproveNewMembersApplyToExistingInviteLinksTitle" = "Apply to existing invite links?";
|
||||
"Group.Setup.ApproveNewMembersApplyToExistingInviteLinksText" = "Also %@ **%@** for **%@ existing invite links** in this %@?";
|
||||
"Group.Setup.ApproveNewMembersApplyToExistingInviteLinksEnable" = "enable";
|
||||
"Group.Setup.ApproveNewMembersApplyToExistingInviteLinksDisable" = "disable";
|
||||
"Group.Setup.ApproveNewMembersApplyToExistingInviteLinksPeerGroup" = "group";
|
||||
"Group.Setup.ApproveNewMembersApplyToExistingInviteLinksPeerChannel" = "channel";
|
||||
"Channel.Setup.ApproveNewSubscribers" = "Approve New Subscribers";
|
||||
"Channel.Setup.ApproveNewSubscribersInfo" = "Turn this on if you want users to be able to join only after they are approved by an admin.";
|
||||
|
||||
"Gallery.GifSaved" = "GIF Saved";
|
||||
|
||||
|
|
@ -16306,3 +16327,18 @@ Error: %8$@";
|
|||
|
||||
"VideoChat.StatusPeerJoined" = "%@ joined";
|
||||
"VideoChat.StatusPeerLeft" = "%@ left";
|
||||
|
||||
"ChatList.BotConnectionReview.PanelTitle" = "A bot just got access to your messages!";
|
||||
"ChatList.BotConnectionReview.PanelText" = "%1$@ connected to your account from %2$@ (%3$@). Was this you?";
|
||||
|
||||
"RecentSession.ConnectedBot.Subtitle" = "Chat Automation Bot";
|
||||
"RecentSession.ConnectedBot.ConnectedFrom" = "CONNECTED FROM";
|
||||
"RecentSession.ConnectedBot.Date" = "Date";
|
||||
"RecentSession.ConnectedBot.Device" = "Device";
|
||||
"RecentSessions.ConnectedBot.ConnectedTime" = "connected %@";
|
||||
"RecentSessions.ConnectedBot.TerminateCheckbox" = "Also terminate %@";
|
||||
|
||||
"Chat.GuestChatMessageTooltip" = "This bot can't read the chat – only the messages where it was mentioned.";
|
||||
|
||||
"MediaPicker.SetNewGroupPhoto" = "Set new group photo";
|
||||
"MediaPicker.SetNewChannelPhoto" = "Set new channel photo";
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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, *) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/Utils/DeviceModel",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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?()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)?
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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?()
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,12 @@ final class ActionSheetControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
}
|
||||
|
||||
func performHighlightedAction() {
|
||||
self.itemGroupsContainerNode.performHighlightedAction()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)]))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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] {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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?()
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: []))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue