From d126717ec10ea02b5b2f911b4865b1e4624faea4 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 29 Sep 2025 06:42:09 +0400 Subject: [PATCH] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../MultilineTextWithEntitiesComponent.swift | 14 ++ .../Components/MessageItemComponent.swift | 128 +++++++++++------ .../Components/MessageListComponent.swift | 17 ++- .../VideoChatParticipantAvatarComponent.swift | 2 + .../Sources/VideoChatScreen.swift | 32 +++-- .../VideoChatScreenInviteMembers.swift | 10 +- .../Sources/VideoChatScreenMoreMenu.swift | 4 +- ...ideoChatScreenParticipantContextMenu.swift | 47 ++++--- .../Sources/VideoChatTitleComponent.swift | 2 +- .../Sources/VideoChatToasts.swift | 3 +- .../Sources/ListSectionComponent.swift | 17 ++- .../Sources/GiftListItemComponent.swift | 10 +- .../PeerNameColorProfilePreviewItem.swift | 26 ++-- .../Sources/UserApperanceScreen.swift | 129 +++++++++++++----- 15 files changed, 306 insertions(+), 137 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 6297a7364f..5ad8b8d5b2 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15148,3 +15148,5 @@ Error: %8$@"; "ProfileColorSetup.NoProfileGiftsPlaceholder" = "You don't have any gifts you can use as styles for your profile."; "ProfileColorSetup.NoNameGiftsPlaceholder" = "You don't have any gifts you can use as styles for your name."; "ProfileColorSetup.BrowseGiftsForPurchase" = "Browse gifts available for purchase >"; + +"VoiceChat.RemovedConferencePeerText" = "You removed %@ from this call. They will no longer be able to join using the invite link."; diff --git a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift index 6701f7f2de..744885c1d0 100644 --- a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift +++ b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift @@ -186,6 +186,20 @@ public final class MultilineTextWithEntitiesComponent: Component { fatalError("init(coder:) has not been implemented") } + public func attributes(at point: CGPoint) -> (Int, [NSAttributedString.Key: Any])? { + if let result = self.textNode.attributesAtPoint(CGPoint(x: point.x - self.textNode.frame.minX, y: point.y - self.textNode.frame.minY)) { + return result + } + return nil + } + + public var isSpoilerConcealed: Bool { + if let dustNode = self.textNode.dustNode, !dustNode.isRevealed { + return true + } + return false + } + public func updateVisibility(_ isVisible: Bool) { self.textNode.visibility = isVisible } diff --git a/submodules/TelegramCallsUI/Sources/Components/MessageItemComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MessageItemComponent.swift index c0c9003fd6..1308755867 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MessageItemComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MessageItemComponent.swift @@ -14,6 +14,7 @@ import TextFormat import TelegramPresentationData import ReactionSelectionNode import BundleIconComponent +import LottieComponent import Markdown private let glassColor = UIColor(rgb: 0x25272e, alpha: 0.72) @@ -22,6 +23,7 @@ final class MessageItemComponent: Component { public enum Icon: Equatable { case peer(EnginePeer) case icon(String) + case animation(String) } private let context: AccountContext @@ -30,7 +32,7 @@ final class MessageItemComponent: Component { private let text: String private let entities: [MessageTextEntity] private let availableReactions: [ReactionItem]? - private let avatarTapped: () -> Void + private let openPeer: ((EnginePeer) -> Void)? init( context: AccountContext, @@ -39,7 +41,7 @@ final class MessageItemComponent: Component { text: String, entities: [MessageTextEntity], availableReactions: [ReactionItem]?, - avatarTapped: @escaping () -> Void = {} + openPeer: ((EnginePeer) -> Void)? ) { self.context = context self.icon = icon @@ -47,7 +49,7 @@ final class MessageItemComponent: Component { self.text = text self.entities = entities self.availableReactions = availableReactions - self.avatarTapped = avatarTapped + self.openPeer = openPeer } static func == (lhs: MessageItemComponent, rhs: MessageItemComponent) -> Bool { @@ -77,6 +79,9 @@ final class MessageItemComponent: Component { private let text: ComponentView weak var standaloneReactionAnimation: StandaloneReactionAnimation? + private var cachedEntities: [MessageTextEntity]? + private var entityFiles: [MediaId: TelegramMediaFile] = [:] + private var component: MessageItemComponent? override init(frame: CGRect) { @@ -95,12 +100,21 @@ final class MessageItemComponent: Component { self.addSubview(self.container) self.container.addSubview(self.background) self.container.addSubview(self.avatarNode.view) + + self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.avatarTapped))) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + @objc private func avatarTapped() { + guard let component = self.component, case let .peer(peer) = component.icon else { + return + } + component.openPeer?(peer) + } + func animateFrom(globalFrame: CGRect, cornerRadius: CGFloat, textSnapshotView: UIView, transition: ComponentTransition) { guard let superview = self.superview?.superview?.superview else { return @@ -143,8 +157,17 @@ final class MessageItemComponent: Component { transition.animateScale(view: self.avatarNode.view, from: 0.01, to: 1.0) } - private var cachedEntities: [MessageTextEntity]? - private var entityFiles: [MediaId: TelegramMediaFile] = [:] + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if !self.avatarNode.isHidden, self.avatarNode.frame.contains(point) { + return true + } + if let textView = self.text.view as? MultilineTextWithEntitiesComponent.View, let (_, attributes) = textView.attributes(at: self.convert(point, to: textView)) { + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], textView.isSpoilerConcealed { + return true + } + } + return false + } func update(component: MessageItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let isFirstTime = self.component == nil @@ -170,38 +193,9 @@ final class MessageItemComponent: Component { let avatarSize = CGSize(width: component.isNotification ? 30.0 : 28.0, height: component.isNotification ? 30.0 : 28.0) let avatarSpacing: CGFloat = 10.0 let iconSpacing: CGFloat = 10.0 - let rightInset: CGFloat = 13.0 + let rightInset: CGFloat = component.isNotification ? 15.0 : 13.0 let avatarFrame = CGRect(origin: CGPoint(x: avatarInset, y: avatarInset), size: avatarSize) - if case let .peer(peer) = component.icon { - if peer.smallProfileImage != nil { - self.avatarNode.setPeerV2( - context: component.context, - theme: theme, - peer: peer, - authorOfMessage: nil, - overrideImage: nil, - emptyColor: nil, - clipStyle: .round, - synchronousLoad: true, - displayDimensions: avatarSize - ) - } else { - self.avatarNode.setPeer( - context: component.context, - theme: theme, - peer: peer, - clipStyle: .round, - synchronousLoad: true, - displayDimensions: avatarSize - ) - } - } - if self.avatarNode.bounds.isEmpty { - self.avatarNode.frame = avatarFrame - } else { - transition.setFrame(view: self.avatarNode.view, frame: avatarFrame) - } var peerName = "" if !component.isNotification, case let .peer(peer) = component.icon { @@ -256,6 +250,8 @@ final class MessageItemComponent: Component { spacing = avatarSpacing case .icon: spacing = iconSpacing + case .animation: + spacing = iconSpacing } let textSize = self.text.update( @@ -268,7 +264,8 @@ final class MessageItemComponent: Component { text: .plain(attributedText), maximumNumberOfLines: 0, lineSpacing: 0.0, - spoilerColor: .white + spoilerColor: .white, + handleSpoilers: true )), environment: {}, containerSize: CGSize(width: availableSize.width - avatarInset - avatarSize.width - spacing - rightInset, height: .greatestFiniteMagnitude) @@ -276,7 +273,37 @@ final class MessageItemComponent: Component { let size = CGSize(width: avatarInset + avatarSize.width + spacing + textSize.width + rightInset, height: max(minimalHeight, textSize.height + 15.0)) - if case let .icon(iconName) = component.icon { + switch component.icon { + case let .peer(peer): + if peer.smallProfileImage != nil { + self.avatarNode.setPeerV2( + context: component.context, + theme: theme, + peer: peer, + authorOfMessage: nil, + overrideImage: nil, + emptyColor: nil, + clipStyle: .round, + synchronousLoad: true, + displayDimensions: avatarSize + ) + } else { + self.avatarNode.setPeer( + context: component.context, + theme: theme, + peer: peer, + clipStyle: .round, + synchronousLoad: true, + displayDimensions: avatarSize + ) + } + if self.avatarNode.bounds.isEmpty { + self.avatarNode.frame = avatarFrame + } else { + transition.setFrame(view: self.avatarNode.view, frame: avatarFrame) + } + self.avatarNode.isHidden = false + case let .icon(iconName): let iconSize = self.icon.update( transition: transition, component: AnyComponent(BundleIconComponent(name: iconName, tintColor: .white)), @@ -290,6 +317,31 @@ final class MessageItemComponent: Component { } transition.setFrame(view: iconView, frame: iconFrame) } + self.avatarNode.isHidden = true + case let .animation(animationName): + let iconSize = self.icon.update( + transition: transition, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent( + name: animationName + ), + placeholderColor: nil, + startingPosition: .end, + size: CGSize(width: 40.0, height: 40.0), + loop: false + )), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: avatarInset - 3.0, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize) + if let iconView = self.icon.view as? LottieComponent.View { + if iconView.superview == nil { + self.container.addSubview(iconView) + iconView.playOnce() + } + transition.setFrame(view: iconView, frame: iconFrame) + } + self.avatarNode.isHidden = true } let textFrame = CGRect(origin: CGPoint(x: avatarInset + avatarSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) @@ -365,8 +417,6 @@ final class MessageItemComponent: Component { } } } - - return size } } diff --git a/submodules/TelegramCallsUI/Sources/Components/MessageListComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MessageListComponent.swift index f7461e267a..8e715cfc62 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MessageListComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MessageListComponent.swift @@ -34,17 +34,20 @@ final class MessageListComponent: Component { private let items: [Item] private let availableReactions: [ReactionItem]? private let sendActionTransition: SendActionTransition? + private let openPeer: (EnginePeer) -> Void init( context: AccountContext, items: [Item], availableReactions: [ReactionItem]?, - sendActionTransition: SendActionTransition? + sendActionTransition: SendActionTransition?, + openPeer: @escaping (EnginePeer) -> Void ) { self.context = context self.items = items self.availableReactions = availableReactions self.sendActionTransition = sendActionTransition + self.openPeer = openPeer } static func == (lhs: MessageListComponent, rhs: MessageListComponent) -> Bool { @@ -130,6 +133,15 @@ final class MessageListComponent: Component { } } + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + for (_, itemView) in self.itemViews { + if let view = itemView.view, view.point(inside: self.convert(point, to: view), with: event) { + return true + } + } + return false + } + func update(component: MessageListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state @@ -167,7 +179,8 @@ final class MessageListComponent: Component { isNotification: item.isNotification, text: item.text, entities: item.entities, - availableReactions: component.availableReactions + availableReactions: component.availableReactions, + openPeer: component.openPeer )), environment: {}, containerSize: CGSize(width: maxWidth, height: .greatestFiniteMagnitude) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift index ec8db9272d..de38c777d0 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift @@ -227,6 +227,8 @@ final class VideoChatParticipantAvatarComponent: Component { self.isUpdating = false } + self.isUserInteractionEnabled = false + let previousComponent = self.component self.component = component diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index 7dc23601ee..1d64f2286b 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -693,8 +693,7 @@ final class VideoChatScreenComponent: Component { } else { text = title.isEmpty ? environment.strings.VoiceChat_EditTitleRemoveSuccess : environment.strings.VoiceChat_EditTitleSuccess(title).string } - - self.presentUndoOverlay(content: .voiceChatFlag(text: text), action: { _ in return false }) + self.presentToast(icon: .animation("anim_vcflag"), text: text, duration: 3) }) environment.controller()?.present(controller, in: .window(.root)) }) @@ -752,9 +751,7 @@ final class VideoChatScreenComponent: Component { switch result { case .linkCopied: let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with { $0 } - self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: presentationData.strings.CallList_ToastCallLinkCopied_Text, customUndoText: presentationData.strings.CallList_ToastCallLinkCopied_Action, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in - return false - }), in: .current) + self.presentToast(icon: .icon("anim_linkcopied"), text: presentationData.strings.CallList_ToastCallLinkCopied_Text, duration: 3) case .openCall: break } @@ -841,8 +838,7 @@ final class VideoChatScreenComponent: Component { } else { text = "" } - - environment.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: isSavedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + self.presentToast(icon: .icon(isSavedMessages ? "anim_savedmessages" : "anim_forward"), text: text, duration: 3) }) } shareController.actionCompleted = { [weak self] in @@ -850,7 +846,7 @@ final class VideoChatScreenComponent: Component { return } let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - environment.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.VoiceChat_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + self.presentToast(icon: .icon("anim_linkcopied"), text: presentationData.strings.VoiceChat_InviteLinkCopiedText, duration: 3) } environment.controller()?.present(shareController, in: .window(.root)) }) @@ -893,8 +889,7 @@ final class VideoChatScreenComponent: Component { } else { text = "" } - - environment.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: isSavedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + self.presentToast(icon: .icon(isSavedMessages ? "anim_savedmessages" : "anim_forward"), text: text, duration: 3) }) } shareController.actionCompleted = { [weak self] in @@ -902,7 +897,7 @@ final class VideoChatScreenComponent: Component { return } let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - environment.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.VoiceChat_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + self.presentToast(icon: .icon("anim_linkcopied"), text: presentationData.strings.VoiceChat_InviteLinkCopiedText, duration: 3) } environment.controller()?.present(shareController, in: .window(.root)) } @@ -1839,7 +1834,7 @@ final class VideoChatScreenComponent: Component { } else { text = environment.strings.VoiceChat_DisplayAsSuccess(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string } - self.displayToast(icon: .peer(peer), text: text, duration: 3) + self.presentToast(icon: .peer(peer), text: text, duration: 3) }) self.memberEventsDisposable?.dispose() @@ -1866,7 +1861,7 @@ final class VideoChatScreenComponent: Component { if displayEvent { let text = environment.strings.VoiceChat_PeerJoinedText("**\(event.peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder))**").string - self.displayToast(icon: .peer(event.peer), text: text, duration: 3) + self.presentToast(icon: .peer(event.peer), text: text, duration: 3) } } } else { @@ -3936,6 +3931,8 @@ final class VideoChatScreenComponent: Component { icon = .peer(peer) case let .icon(name): icon = .icon(name) + case let .animation(name): + icon = .animation(name) } messageItems.insert( MessageListComponent.Item( @@ -3957,7 +3954,13 @@ final class VideoChatScreenComponent: Component { context: call.accountContext, items: messageItems, availableReactions: self.reactionItems, - sendActionTransition: sendActionTransition + sendActionTransition: sendActionTransition, + openPeer: { [weak self] peer in + guard let self else { + return + } + self.openPeer(peer) + } )), environment: {}, containerSize: CGSize(width: isTwoColumnLayout ? mainColumnWidth : min(440.0, availableSize.width - environment.safeInsets.left - environment.safeInsets.right), height: availableSize.height - messagesBottomInset) @@ -3975,7 +3978,6 @@ final class VideoChatScreenComponent: Component { let messagesListFrame = CGRect(origin: CGPoint(x: messageListOriginX, y: availableSize.height - messagesListSize.height - messagesBottomInset), size: messagesListSize) if let messagesListView = self.messagesList.view { if messagesListView.superview == nil { - messagesListView.isUserInteractionEnabled = false self.containerView.addSubview(messagesListView) } transition.setFrame(view: messagesListView, frame: messagesListFrame) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift index c5273563ca..d35d00074b 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift @@ -136,7 +136,7 @@ extension VideoChatScreenComponent.View { } else { text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string } - self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: EnginePeer(participant.peer), title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) + self.presentToast(icon: .peer(EnginePeer(participant.peer)), text: text, duration: 3) } } else { if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) { @@ -154,7 +154,7 @@ extension VideoChatScreenComponent.View { guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { return } - self.presentUndoOverlay(content: .forward(savedMessages: false, text: environment.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true }) + self.presentToast(icon: .animation("anim_savedmessages"), text: environment.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string, duration: 3) }) })]), in: .window(.root)) } else { @@ -248,7 +248,7 @@ extension VideoChatScreenComponent.View { } else { text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string } - self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) + self.presentToast(icon: .peer(peer), text: text, duration: 3) } })) } else if case let .legacyGroup(groupPeer) = groupPeer { @@ -320,7 +320,7 @@ extension VideoChatScreenComponent.View { } else { text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string } - self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) + self.presentToast(icon: .peer(peer), text: text, duration: 3) } })) } @@ -361,7 +361,7 @@ extension VideoChatScreenComponent.View { if let link { UIPasteboard.general.string = link - self.presentUndoOverlay(content: .linkCopied(title: nil, text: environment.strings.VoiceChat_InviteLinkCopiedText), action: { _ in return false }) + self.presentToast(icon: .animation("anim_linkcopied"), text: environment.strings.VoiceChat_InviteLinkCopiedText, duration: 3) } }) } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift index e174f60440..e77e72bf50 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift @@ -249,7 +249,7 @@ extension VideoChatScreenComponent.View { iconName = "Call/ToastMessagesDisabled" text = environment.strings.VoiceChat_ToastMessagesDisabled } - self.displayToast(icon: .icon(iconName), text: text, duration: 3) + self.presentToast(icon: .icon(iconName), text: text, duration: 3) }))) } @@ -483,7 +483,7 @@ extension VideoChatScreenComponent.View { text = environment.strings.VoiceChat_RecordingStarted } - self.presentUndoOverlay(content: .voiceChatRecording(text: text), action: { _ in return false }) + self.presentToast(icon: .animation("anim_vcrecord"), text: text, duration: 3) groupCall.playTone(.recordingStarted) }) environment.controller()?.present(controller, in: .window(.root)) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift index 71084b168a..dff33a3bfa 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift @@ -133,7 +133,7 @@ extension VideoChatScreenComponent.View { }).start() } - self.presentUndoOverlay(content: .info(title: nil, text: environment.strings.VoiceChat_EditBioSuccess, timeout: nil, customUndoText: nil), action: { _ in return false }) + self.presentToast(icon: .animation("anim_infotip"), text: environment.strings.VoiceChat_EditBioSuccess, duration: 4) }) environment.controller()?.present(controller, in: .window(.root)) } @@ -155,7 +155,7 @@ extension VideoChatScreenComponent.View { } let _ = currentCall.accountContext.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).startStandalone() - self.presentUndoOverlay(content: .info(title: nil, text: environment.strings.VoiceChat_EditNameSuccess, timeout: nil, customUndoText: nil), action: { _ in return false }) + self.presentToast(icon: .animation("anim_infotip"), text: environment.strings.VoiceChat_EditNameSuccess, duration: 4) }) environment.controller()?.present(controller, in: .window(.root)) } @@ -190,7 +190,8 @@ extension VideoChatScreenComponent.View { f(.default) if let participantPeer = participant.peer { - self.presentUndoOverlay(content: .voiceChatCanSpeak(text: environment.strings.VoiceChat_UserCanNowSpeak(participantPeer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true }) + let text = environment.strings.VoiceChat_UserCanNowSpeak(participantPeer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string + self.presentToast(icon: .animation("anim_vcspeak"), text: text, duration: 3) } }))) } else { @@ -248,24 +249,11 @@ extension VideoChatScreenComponent.View { items.append(.action(ContextMenuActionItem(text: openTitle, icon: { theme in return generateTintedImage(image: openIcon, color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in - guard let self, let environment = self.environment, let currentCall = self.currentCall else { + guard let self else { return } + self.openPeer(peer) - guard let controller = environment.controller() as? VideoChatScreenV2Impl, let navigationController = controller.parentNavigationController else { - return - } - - let context = currentCall.accountContext - controller.dismiss(completion: { [weak navigationController] in - Queue.mainQueue().after(0.1) { - guard let navigationController else { - return - } - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil)) - } - }) - f(.dismissWithoutContent) }))) @@ -314,15 +302,14 @@ extension VideoChatScreenComponent.View { if groupCall.isConference { groupCall.kickPeer(id: peer.id) - //TODO:localize - self.presentUndoOverlay(content: .banned(text: "You removed \(peer.displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)) from this call. They will no longer be able to join using the invite link."), action: { _ in return false }) + self.presentToast(icon: .animation("anim_banned"), text: environment.strings.VoiceChat_RemovedConferencePeerText(peer.displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string, duration: 3) } else { if let callPeerId = groupCall.peerId { let _ = groupCall.accountContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: groupCall.accountContext.engine, peerId: callPeerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start() groupCall.removedPeer(peer.id) } - self.presentUndoOverlay(content: .banned(text: environment.strings.VoiceChat_RemovedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string), action: { _ in return false }) + self.presentToast(icon: .animation("anim_banned"), text: environment.strings.VoiceChat_RemovedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string, duration: 3) } })) @@ -425,6 +412,24 @@ extension VideoChatScreenComponent.View { environment.controller()?.presentInGlobalOverlay(contextController) } + func openPeer(_ peer: EnginePeer) { + guard let environment = self.environment, let currentCall = self.currentCall else { + return + } + guard let controller = environment.controller() as? VideoChatScreenV2Impl, let navigationController = controller.parentNavigationController else { + return + } + let context = currentCall.accountContext + controller.dismiss(completion: { [weak navigationController] in + Queue.mainQueue().after(0.1) { + guard let navigationController else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil)) + } + }) + } + private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) { guard let currentCall = self.currentCall else { return diff --git a/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift index a758001b86..41c9574897 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift @@ -161,7 +161,7 @@ final class VideoChatTitleComponent: Component { let _ = activityStatusNode.transitionToState(.recordingVoice(NSAttributedString(string: value, font: Font.regular(13.0), textColor: UIColor(rgb: 0x34c759)), UIColor(rgb: 0x34c759)), animation: .none) let activityStatusSize = activityStatusNode.updateLayout(CGSize(width: currentSize.width, height: 100.0), alignment: .center) - let activityStatusFrame = CGRect(origin: CGPoint(x: floor((currentSize.width - activityStatusSize.width) * 0.5), y: statusView.center.y - activityStatusSize.height * 0.5), size: activityStatusSize) + let activityStatusFrame = CGRect(origin: CGPoint(x: floor((currentSize.width - activityStatusSize.width) * 0.5), y: statusView.center.y - activityStatusSize.height * 0.5 + 7.0), size: activityStatusSize) let activityStatusNodeView = activityStatusNode.view activityStatusNodeView.center = activityStatusFrame.center diff --git a/submodules/TelegramCallsUI/Sources/VideoChatToasts.swift b/submodules/TelegramCallsUI/Sources/VideoChatToasts.swift index 86de23fcc3..2bb7e39389 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatToasts.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatToasts.swift @@ -5,10 +5,11 @@ import TelegramCore enum VideoChatNotificationIcon { case peer(EnginePeer) case icon(String) + case animation(String) } extension VideoChatScreenComponent.View { - func displayToast(icon: VideoChatNotificationIcon, text: String, duration: Int32) { + func presentToast(icon: VideoChatNotificationIcon, text: String, duration: Int32) { let id = Int64.random(in: 0 ..< .max) let expiresOn = Int32(CFAbsoluteTimeGetCurrent()) + duration diff --git a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift index f21859ed8e..10ad6baf5d 100644 --- a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift +++ b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift @@ -193,11 +193,12 @@ public final class ListSectionContentView: UIView { } } var separatorInset: CGFloat = 0.0 + let separatorRightInset: CGFloat = configuration.style == .glass ? 16.0 : 0.0 if let itemComponentView = itemComponentView as? ListSectionComponentChildView { separatorInset = itemComponentView.separatorInset } - let itemSeparatorFrame = CGRect(origin: CGPoint(x: separatorInset, y: itemFrame.maxY - UIScreenPixel), size: CGSize(width: width - separatorInset, height: UIScreenPixel)) + let itemSeparatorFrame = CGRect(origin: CGPoint(x: separatorInset, y: itemFrame.maxY - UIScreenPixel), size: CGSize(width: width - separatorInset - separatorRightInset, height: UIScreenPixel)) if isAdded && itemComponentView is ListSubSectionComponent.View { readyItem.itemView.frame = itemFrame @@ -311,6 +312,14 @@ public final class ListSectionContentView: UIView { public final class ListSectionComponent: Component { public typealias ChildView = ListSectionComponentChildView + public final class TransitionHint { + public let forceUpdate: Bool + + public init(forceUpdate: Bool) { + self.forceUpdate = forceUpdate + } + } + public enum Background: Equatable { case none(clipped: Bool) case all @@ -417,6 +426,11 @@ public final class ListSectionComponent: Component { func update(component: ListSectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component + var forceUpdate = false + if let hint = transition.userData(TransitionHint.self) { + forceUpdate = hint.forceUpdate + } + let headerSideInset: CGFloat = 16.0 var contentHeight: CGFloat = 0.0 @@ -472,6 +486,7 @@ public final class ListSectionComponent: Component { transition: itemTransition, component: item.component, environment: {}, + forceUpdate: forceUpdate, containerSize: CGSize(width: availableSize.width, height: availableSize.height) ) diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift index 0c65bd1725..3db8c9a322 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift @@ -29,6 +29,7 @@ final class GiftListItemComponent: Component { let starGifts: [StarGift] let selectedId: Int64? let selectionUpdated: (StarGift.UniqueGift) -> Void + let onTabChange: () -> Void let tag: AnyObject? let updated: (ComponentTransition) -> Void @@ -41,6 +42,7 @@ final class GiftListItemComponent: Component { starGifts: [StarGift], selectedId: Int64?, selectionUpdated: @escaping (StarGift.UniqueGift) -> Void, + onTabChange: @escaping () -> Void, tag: AnyObject?, updated: @escaping (ComponentTransition) -> Void ) { @@ -52,6 +54,7 @@ final class GiftListItemComponent: Component { self.starGifts = starGifts self.selectedId = selectedId self.selectionUpdated = selectionUpdated + self.onTabChange = onTabChange self.tag = tag self.updated = updated } @@ -150,12 +153,17 @@ final class GiftListItemComponent: Component { return } + let previousGiftId = self.selectedGiftId self.selectedGiftId = id if id == 0 { self.resaleGiftsState = nil self.resaleGiftsDisposable.set(nil) } else { + if previousGiftId == 0 { + component.onTabChange() + } + let resaleGiftsContext: ResaleGiftsContext if let current = self.resaleGiftsContexts[id] { resaleGiftsContext = current @@ -173,7 +181,6 @@ final class GiftListItemComponent: Component { self.resaleGiftsState = state if !self.isUpdating { let transition: ComponentTransition = isFirstTime ? .easeInOut(duration: 0.25) : .immediate - self.state?.updated(transition: transition) component.updated(transition) } isFirstTime = false @@ -182,7 +189,6 @@ final class GiftListItemComponent: Component { if !self.isUpdating { let transition: ComponentTransition = .easeInOut(duration: 0.25) - self.state?.updated(transition: transition) component.updated(transition) } } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift index eb90a1c5f8..475187589b 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift @@ -188,6 +188,18 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { self.addSubnode(self.maskNode) } + var hasBackground = false + let subject: PeerInfoCoverComponent.Subject? + if let status = item.peer?.emojiStatus, case .starGift = status.content { + subject = .status(status) + hasBackground = true + } else if let peer = item.peer { + subject = .peer(peer) + hasBackground = peer.profileColor != nil + } else { + subject = nil + } + if params.isStandalone { let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) transition.updateAlpha(node: self.backgroundNode, alpha: item.showBackground ? 1.0 : 0.0) @@ -195,7 +207,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { self.backgroundNode.isHidden = false self.topStripeNode.isHidden = true - self.bottomStripeNode.isHidden = false + self.bottomStripeNode.isHidden = hasBackground self.maskNode.isHidden = true self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width, height: separatorHeight)) @@ -225,7 +237,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { hasBottomCorners = true self.bottomStripeNode.isHidden = hasCorners } - + self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil self.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) @@ -239,14 +251,6 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { let avatarSize: CGFloat = 104.0 let avatarFrame = CGRect(origin: CGPoint(x: floor((coverFrame.width - avatarSize) * 0.5), y: coverFrame.minY + item.topInset + 24.0), size: CGSize(width: avatarSize, height: avatarSize)) - let subject: PeerInfoCoverComponent.Subject? - if let status = item.peer?.emojiStatus, case .starGift = status.content { - subject = .status(status) - } else if let peer = item.peer { - subject = .peer(peer) - } else { - subject = nil - } let _ = self.background.update( transition: .immediate, component: AnyComponent(PeerInfoCoverComponent( @@ -314,7 +318,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { credibilityIcon = .emojiStatus(emojiStatus) } else if peer.isVerified { credibilityIcon = .verified - } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled && (peer.id != item.context.account.peerId) { + } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { credibilityIcon = .premium } else { credibilityIcon = .none diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift index f3388a61f0..e0e3515768 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift @@ -46,9 +46,11 @@ final class UserAppearanceScreenComponent: Component { public final class TransitionHint { public let animateTabChange: Bool + public let forceGiftsUpdate: Bool - public init(animateTabChange: Bool) { + public init(animateTabChange: Bool = false, forceGiftsUpdate: Bool = false) { self.animateTabChange = animateTabChange + self.forceGiftsUpdate = forceGiftsUpdate } } @@ -173,6 +175,8 @@ final class UserAppearanceScreenComponent: Component { } private var currentSection: Section = .profile + private let previewShadowView = UIImageView(image: generatePreviewShadowImage()) + private let profilePreview = ComponentView() private let profileColorSection = ComponentView() private let profileResetColorSection = ComponentView() @@ -183,7 +187,6 @@ final class UserAppearanceScreenComponent: Component { private let nameGiftsSection = ComponentView() private var isUpdating: Bool = false - private var forceNextUpdate = false private var component: UserAppearanceScreenComponent? private(set) weak var state: EmptyComponentState? @@ -246,6 +249,7 @@ final class UserAppearanceScreenComponent: Component { self.scrollView.alwaysBounceVertical = true self.edgeEffectView = EdgeEffectView() + self.edgeEffectView.isUserInteractionEnabled = false super.init(frame: frame) @@ -256,6 +260,8 @@ final class UserAppearanceScreenComponent: Component { self.scrollView.layer.addSublayer(self.topOverscrollLayer) + self.containerView.addSubview(self.previewShadowView) + self.addSubview(self.edgeEffectView) self.backButton.action = { [weak self] _, _ in @@ -946,13 +952,7 @@ final class UserAppearanceScreenComponent: Component { defer { self.isUpdating = false } - - var forceUpdate = false - if self.forceNextUpdate { - self.forceNextUpdate = false - forceUpdate = true - } - + let environment = environment[EnvironmentType.self].value let themeUpdated = self.environment?.theme !== environment.theme self.environment = environment @@ -963,8 +963,10 @@ final class UserAppearanceScreenComponent: Component { let theme = environment.theme var animateTabChange = false + var forceGiftsUpdate = false if let hint = transition.userData(TransitionHint.self) { animateTabChange = hint.animateTabChange + forceGiftsUpdate = hint.forceGiftsUpdate } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } @@ -1130,11 +1132,11 @@ final class UserAppearanceScreenComponent: Component { self.selectedProfileGift = nil self.updatedPeerProfileColor = nil self.updatedPeerProfileEmoji = nil + self.updatedPeerStatus = nil } - } else { - self.currentSection = updatedSection - self.state?.updated(transition: .easeInOut(duration: 0.3).withUserData(TransitionHint(animateTabChange: true))) } + self.currentSection = updatedSection + self.state?.updated(transition: .easeInOut(duration: 0.3).withUserData(TransitionHint(animateTabChange: true))) } } } @@ -1162,6 +1164,8 @@ final class UserAppearanceScreenComponent: Component { let itemCornerRadius: CGFloat = 26.0 + transition.setTintColor(view: self.previewShadowView, color: environment.theme.list.itemBlocksBackgroundColor) + switch self.currentSection { case .profile: var transition = transition @@ -1209,7 +1213,10 @@ final class UserAppearanceScreenComponent: Component { } transition.setFrame(view: profilePreviewView, frame: profilePreviewFrame) } - contentHeight += profilePreviewSize.height - 18.0 + contentHeight += profilePreviewSize.height - 38.0 + + transition.setFrame(view: self.previewShadowView, frame: profilePreviewFrame.insetBy(dx: -30.0, dy: -30.0)) + previewTransition.setAlpha(view: self.previewShadowView, alpha: !self.scrolledUp ? 1.0 : 0.0) var profileLogoContents: [AnyComponentWithIdentity] = [] profileLogoContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( @@ -1257,11 +1264,13 @@ final class UserAppearanceScreenComponent: Component { return } if self.selectedProfileGift != nil { - - } else { - self.currentSection = .name - self.state?.updated(transition: .easeInOut(duration: 0.3).withUserData(TransitionHint(animateTabChange: true))) + self.selectedProfileGift = nil + self.updatedPeerProfileColor = nil + self.updatedPeerProfileEmoji = nil + self.updatedPeerStatus = nil } + self.currentSection = .name + self.state?.updated(transition: .easeInOut(duration: 0.3).withUserData(TransitionHint(animateTabChange: true))) } )), items: [ @@ -1391,8 +1400,10 @@ final class UserAppearanceScreenComponent: Component { if let status = resolvedState.emojiStatus, case let .starGift(id, _, _, _, _, _, _, _, _) = status.content { selectedGiftId = id } + + let listTransition = transition.withUserData(ListSectionComponent.TransitionHint(forceUpdate: forceGiftsUpdate)) let giftsSectionSize = self.profileGiftsSection.update( - transition: transition, + transition: listTransition, component: AnyComponent(ListSectionComponent( theme: environment.theme, style: .glass, @@ -1454,13 +1465,18 @@ final class UserAppearanceScreenComponent: Component { self.state?.updated(transition: .spring(duration: 0.4)) } }, + onTabChange: { [weak self] in + guard let self else { + return + } + if let sectionView = self.profileGiftsSection.view { + self.scrollView.setContentOffset(CGPoint(x: 0.0, y: sectionView.frame.minY - 240.0), animated: true) + } + }, tag: giftListTag, updated: { [weak self] transition in - if let self { - self.forceNextUpdate = true - if !self.isUpdating { - self.state?.updated(transition: transition) - } + if let self, !self.isUpdating { + self.state?.updated(transition: transition.withUserData(TransitionHint(forceGiftsUpdate: true))) } } ) @@ -1469,8 +1485,8 @@ final class UserAppearanceScreenComponent: Component { displaySeparators: false )), environment: {}, - forceUpdate: forceUpdate, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + forceUpdate: forceGiftsUpdate, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude) ) let giftsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: giftsSectionSize) if let giftsSectionView = self.profileGiftsSection.view { @@ -1569,7 +1585,10 @@ final class UserAppearanceScreenComponent: Component { } transition.setFrame(view: namePreviewView, frame: namePreviewFrame) } - contentHeight += namePreviewSize.height - 18.0 + contentHeight += namePreviewSize.height - 38.0 + + transition.setFrame(view: self.previewShadowView, frame: namePreviewFrame.insetBy(dx: -30.0, dy: -30.0)) + previewTransition.setAlpha(view: self.previewShadowView, alpha: !self.scrolledUp ? 1.0 : 0.0) let nameColorSectionSize = self.nameColorSection.update( transition: transition, @@ -1655,8 +1674,9 @@ final class UserAppearanceScreenComponent: Component { } } + let listTransition = transition.withUserData(ListSectionComponent.TransitionHint(forceUpdate: forceGiftsUpdate)) let giftsSectionSize = self.nameGiftsSection.update( - transition: transition, + transition: listTransition, component: AnyComponent(ListSectionComponent( theme: environment.theme, style: .glass, @@ -1692,12 +1712,18 @@ final class UserAppearanceScreenComponent: Component { self.updatedPeerNameEmoji = peerColor.backgroundEmojiId self.state?.updated(transition: .spring(duration: 0.4)) }, + onTabChange: { [weak self] in + guard let self else { + return + } + if let sectionView = self.nameGiftsSection.view { + self.scrollView.setContentOffset(CGPoint(x: 0.0, y: sectionView.frame.minY - 240.0), animated: true) + } + }, tag: giftListTag, updated: { [weak self] transition in - if let self { - if !self.isUpdating { - self.state?.updated(transition: transition) - } + if let self, !self.isUpdating { + self.state?.updated(transition: transition.withUserData(TransitionHint(forceGiftsUpdate: true))) } } ) @@ -1706,8 +1732,8 @@ final class UserAppearanceScreenComponent: Component { displaySeparators: false )), environment: {}, - forceUpdate: forceUpdate, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + forceUpdate: forceGiftsUpdate, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude) ) let giftsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: giftsSectionSize) if let giftsSectionView = self.nameGiftsSection.view { @@ -1830,14 +1856,14 @@ final class UserAppearanceScreenComponent: Component { transition.setAlpha(view: buttonView, alpha: 1.0) } - let edgeEffectHeight: CGFloat = availableSize.height - buttonY + 24.0 + let edgeEffectHeight: CGFloat = availableSize.height - buttonY + 36.0 let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - edgeEffectHeight), size: CGSize(width: availableSize.width, height: edgeEffectHeight)) transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) - self.edgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, rect: edgeEffectFrame, edge: .bottom, edgeSize: edgeEffectFrame.height, transition: transition) + self.edgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, alpha: 1.0, rect: edgeEffectFrame, edge: .bottom, edgeSize: edgeEffectFrame.height, transition: transition) let previousBounds = self.scrollView.bounds let contentSize = CGSize(width: availableSize.width, height: contentHeight) - let scrollViewFrame = CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight + 30.0), size: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight - 30.0)) + let scrollViewFrame = CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight + 50.0), size: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight - 50.0)) if self.scrollView.frame != scrollViewFrame { self.scrollView.frame = scrollViewFrame } @@ -1856,7 +1882,7 @@ final class UserAppearanceScreenComponent: Component { } self.topOverscrollLayer.backgroundColor = environment.theme.list.itemBlocksBackgroundColor.cgColor - self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: sideInset, y: -1000.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: 1315.0)) + self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: sideInset, y: -1000.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: 1340.0)) self.updateScrolling(transition: transition) @@ -2044,3 +2070,32 @@ final class TopBottomCornersComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +private func generatePreviewShadowImage() -> UIImage { + let cornerRadius: CGFloat = 26.0 + let shadowInset: CGFloat = 30.0 + + let side = (cornerRadius + 5.0) * 2.0 + let fullSide = shadowInset * 2.0 + side + + return generateImage(CGSize(width: fullSide, height: fullSide), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let edgeHeight = shadowInset + cornerRadius + 11.0 + context.clip(to: CGRect(x: shadowInset, y: size.height - edgeHeight, width: side, height: edgeHeight)) + + let rect = CGRect(origin: .zero, size: CGSize(width: fullSide, height: fullSide)).insetBy(dx: shadowInset + 1.0, dy: shadowInset + 2.0) + let path = CGPath(roundedRect: rect, cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil) + + let drawShadow = { + context.addPath(path) + context.setShadow(offset: CGSize(), blur: 65.0, color: UIColor.black.cgColor) + context.setFillColor(UIColor.black.cgColor) + context.fillPath() + } + + drawShadow() + drawShadow() + drawShadow() + })!.stretchableImage(withLeftCapWidth: Int(shadowInset + cornerRadius + 5), topCapHeight: Int(shadowInset + cornerRadius + 5)).withRenderingMode(.alwaysTemplate) +}