diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index cf91a69342..a26c171db3 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1027,6 +1027,8 @@ public protocol AccountContext: AnyObject { var userLimits: EngineConfiguration.UserLimits { get } + var imageCache: AnyObject? { get } + func storeSecureIdPassword(password: String) func getStoredSecureIdPassword() -> String? diff --git a/submodules/AvatarNode/BUILD b/submodules/AvatarNode/BUILD index 38dc9f4680..3aad72c509 100644 --- a/submodules/AvatarNode/BUILD +++ b/submodules/AvatarNode/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/FastBlur:FastBlur", "//submodules/ComponentFlow", "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", + "//submodules/DirectMediaImageCache", ], visibility = [ "//visibility:public", diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 17e4f68aa4..554f931678 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -13,6 +13,7 @@ import Emoji import Accelerate import ComponentFlow import AvatarStoryIndicatorComponent +import DirectMediaImageCache private let deletedIcon = UIImage(bundleImageName: "Avatar/DeletedIcon")?.precomposed() private let phoneIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/PhoneIcon"), color: .white) @@ -228,6 +229,19 @@ public final class AvatarNode: ASDisplayNode { ] public final class ContentNode: ASDisplayNode { + private struct Params: Equatable { + let peerId: EnginePeer.Id? + let resourceId: String? + + init( + peerId: EnginePeer.Id?, + resourceId: String? + ) { + self.peerId = peerId + self.resourceId = resourceId + } + } + public var font: UIFont { didSet { if oldValue.pointSize != font.pointSize { @@ -255,6 +269,9 @@ public final class AvatarNode: ASDisplayNode { public var unroundedImage: UIImage? private var currentImage: UIImage? + private var params: Params? + private var loadDisposable: Disposable? + public var badgeView: AvatarBadgeView? { didSet { if self.badgeView !== oldValue { @@ -319,6 +336,10 @@ public final class AvatarNode: ASDisplayNode { } } + deinit { + self.loadDisposable?.dispose() + } + override public func didLoad() { super.didLoad() @@ -496,6 +517,58 @@ public final class AvatarNode: ASDisplayNode { } } + func setPeerV2( + context genericContext: AccountContext, + account: Account? = nil, + theme: PresentationTheme, + peer: EnginePeer?, + authorOfMessage: MessageReference? = nil, + overrideImage: AvatarNodeImageOverride? = nil, + emptyColor: UIColor? = nil, + clipStyle: AvatarNodeClipStyle = .round, + synchronousLoad: Bool = false, + displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), + storeUnrounded: Bool = false + ) { + let smallProfileImage = peer?.smallProfileImage + let params = Params( + peerId: peer?.id, + resourceId: smallProfileImage?.resource.id.stringRepresentation + ) + if self.params == params { + return + } + self.params = params + + switch clipStyle { + case .none: + self.imageNode.clipsToBounds = false + self.imageNode.cornerRadius = 0.0 + case .round: + self.imageNode.clipsToBounds = true + self.imageNode.cornerRadius = displayDimensions.height * 0.5 + case .roundedRect: + self.imageNode.clipsToBounds = true + self.imageNode.cornerRadius = displayDimensions.height * 0.25 + } + + if let imageCache = genericContext.imageCache as? DirectMediaImageCache, let peer, let smallProfileImage = peer.smallProfileImage, let peerReference = PeerReference(peer._asPeer()) { + if let result = imageCache.getAvatarImage(peer: peerReference, resource: MediaResourceReference.avatar(peer: peerReference, resource: smallProfileImage.resource), immediateThumbnail: peer.profileImageRepresentations.first?.immediateThumbnailData, size: Int(displayDimensions.width * UIScreenScale), synchronous: synchronousLoad) { + if let image = result.image { + self.imageNode.contents = image.cgImage + } + if let loadSignal = result.loadSignal { + self.loadDisposable = (loadSignal |> deliverOnMainQueue).start(next: { [weak self] image in + guard let self else { + return + } + self.imageNode.contents = image?.cgImage + }) + } + } + } + } + public func setPeer( context genericContext: AccountContext, account: Account? = nil, @@ -657,6 +730,10 @@ public final class AvatarNode: ASDisplayNode { context.fill(bounds) } + if !(parameters is AvatarNodeParameters) { + return + } + let colors: [UIColor] if let parameters = parameters as? AvatarNodeParameters { colors = parameters.colors @@ -925,6 +1002,32 @@ public final class AvatarNode: ASDisplayNode { ) } + public func setPeerV2( + context genericContext: AccountContext, + theme: PresentationTheme, + peer: EnginePeer?, + authorOfMessage: MessageReference? = nil, + overrideImage: AvatarNodeImageOverride? = nil, + emptyColor: UIColor? = nil, + clipStyle: AvatarNodeClipStyle = .round, + synchronousLoad: Bool = false, + displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), + storeUnrounded: Bool = false + ) { + self.contentNode.setPeerV2( + context: genericContext, + theme: theme, + peer: peer, + authorOfMessage: authorOfMessage, + overrideImage: overrideImage, + emptyColor: emptyColor, + clipStyle: clipStyle, + synchronousLoad: synchronousLoad, + displayDimensions: displayDimensions, + storeUnrounded: storeUnrounded + ) + } + public func setPeer( context: AccountContext, account: Account? = nil, diff --git a/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift b/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift index 00fc79847d..a2c39fdd83 100644 --- a/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift +++ b/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift @@ -491,6 +491,33 @@ public final class DirectMediaImageCache { return GetMediaResult(image: resultImage, blurredImage: blurredImage, loadSignal: self.getLoadSignal(width: width, aspectRatio: aspectRatio, userLocation: userLocation, userContentType: .image, resource: resource.resource, resourceSizeLimit: resource.size)) } + + private func getAvatarImageSynchronous(peer: PeerReference, resource: MediaResourceReference, immediateThumbnail: Data?, size: Int, includeBlurred: Bool) -> GetMediaResult? { + let immediateThumbnailData: Data? = immediateThumbnail + + var blurredImage: UIImage? + if includeBlurred, let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data), let blurredImageValue = generateBlurredThumbnail(image: image, adjustSaturation: true) { + blurredImage = blurredImageValue + } + + var resultImage: UIImage? + if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.id, imageType: .square(width: size, aspectRatio: 1.0)))), let image = loadImage(data: data) { + return GetMediaResult(image: image, blurredImage: blurredImage, loadSignal: nil) + } + + if resultImage == nil { + if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.id, imageType: .blurredThumbnail))), let image = loadImage(data: data) { + resultImage = image + } else if let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data) { + if let blurredImageValue = generateBlurredThumbnail(image: image) { + resultImage = blurredImageValue + } + } + } + + return GetMediaResult(image: resultImage, blurredImage: blurredImage, loadSignal: self.getLoadSignal(width: size, aspectRatio: 1.0, userLocation: .other, userContentType: .avatar, resource: resource, resourceSizeLimit: 1 * 1024 * 1024)) + } + public func getImage(peer: PeerReference, story: EngineStoryItem, media: Media, width: Int, aspectRatio: CGFloat, possibleWidths: [Int], includeBlurred: Bool = false, synchronous: Bool) -> GetMediaResult? { if synchronous { @@ -530,4 +557,37 @@ public final class DirectMediaImageCache { |> runOn(.concurrentDefaultQueue())) } } + + public func getAvatarImage(peer: PeerReference, resource: MediaResourceReference, immediateThumbnail: Data?, size: Int, includeBlurred: Bool = false, synchronous: Bool) -> GetMediaResult? { + if synchronous { + return self.getAvatarImageSynchronous(peer: peer, resource: resource, immediateThumbnail: immediateThumbnail, size: size, includeBlurred: includeBlurred) + } else { + var blurredImage: UIImage? + if includeBlurred, let data = immediateThumbnail.flatMap(decodeTinyThumbnail), let image = loadImage(data: data), let blurredImageValue = generateBlurredThumbnail(image: image, adjustSaturation: true) { + blurredImage = blurredImageValue + } + return GetMediaResult(image: nil, blurredImage: blurredImage, loadSignal: Signal { subscriber in + let result = self.getAvatarImageSynchronous(peer: peer, resource: resource, immediateThumbnail: immediateThumbnail, size: size, includeBlurred: includeBlurred) + guard let result = result else { + subscriber.putNext(nil) + subscriber.putCompletion() + + return EmptyDisposable + } + + if let image = result.image { + subscriber.putNext(image) + } + + if let signal = result.loadSignal { + return signal.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) + } else { + subscriber.putCompletion() + + return EmptyDisposable + } + } + |> runOn(.concurrentDefaultQueue())) + } + } } diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 0a00ad8193..0a040452af 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -451,6 +451,14 @@ public final class MessageInputPanelComponent: Component { fatalError("init(coder:) has not been implemented") } + public func hasFirstResponder() -> Bool { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + return textFieldView.hasFirstResponder() + } else { + return false + } + } + public func getSendMessageInput() -> SendMessageInput { guard let textFieldView = self.textField.view as? TextFieldComponent.View else { return .text(NSAttributedString()) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 18bd108744..b58b9aff04 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -574,8 +574,8 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { return self.chatPresentationData.theme.theme.list.itemPlainSeparatorColor } - func createLayer() -> SparseItemGridLayer? { - if self.captureProtected { + func createLayer(item: SparseItemGrid.Item) -> SparseItemGridLayer? { + if let item = item as? VisualMediaItem, item.story.isForwardingDisabled { return CaptureProtectedItemLayer() } else { return GenericItemLayer() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index 3d91718548..68e5ebf52e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -941,7 +941,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme return self.chatPresentationData.theme.theme.list.itemPlainSeparatorColor } - func createLayer() -> SparseItemGridLayer? { + func createLayer(item: SparseItemGrid.Item) -> SparseItemGridLayer? { if self.useListItems { return nil } diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index 3c443c050c..1289eaf99c 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -501,7 +501,22 @@ public final class PeerListItemComponent: Component { } let _ = clipStyle let _ = synchronousLoad - self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + + if peer.smallProfileImage != nil { + self.avatarNode.setPeerV2( + context: component.context, + theme: component.theme, + peer: peer, + authorOfMessage: nil, + overrideImage: nil, + emptyColor: nil, + clipStyle: .round, + synchronousLoad: synchronousLoad, + displayDimensions: CGSize(width: avatarSize, height: avatarSize) + ) + } else { + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + } self.avatarNode.setStoryStats(storyStats: component.storyStats.flatMap { storyStats -> AvatarNode.StoryStats in return AvatarNode.StoryStats( totalCount: storyStats.totalCount == 0 ? 0 : 1, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index a3f9b820cb..24805fae99 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -1210,7 +1210,9 @@ private final class StoryContainerScreenComponent: Component { if component.content.stateValue?.slice == nil { self.environment?.controller()?.dismiss() } else { + let startTime = CFAbsoluteTimeGetCurrent() self.state?.updated(transition: .immediate) + print("update time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") } } else { DispatchQueue.main.async { [weak self] in diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 015bba459e..8fbd333c3e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2324,6 +2324,8 @@ public final class StoryItemSetContainerComponent: Component { func update(component: StoryItemSetContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let isFirstTime = self.component == nil + + let startTime1 = CFAbsoluteTimeGetCurrent() if self.component == nil { self.sendMessageContext.setup(context: component.context, view: self, inputPanelExternalState: self.inputPanelExternalState, keyboardInputData: component.keyboardInputData) @@ -2470,6 +2472,8 @@ public final class StoryItemSetContainerComponent: Component { component.externalState.dismissFraction = dismissFraction + let startTime2 = CFAbsoluteTimeGetCurrent() + transition.setPosition(view: self.componentContainerView, position: CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5 + dismissPanOffset)) transition.setBounds(view: self.componentContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) transition.setScale(view: self.componentContainerView, scale: dismissPanScale) @@ -2481,6 +2485,8 @@ public final class StoryItemSetContainerComponent: Component { transition.setBounds(view: self.overlayContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) transition.setScale(view: self.overlayContainerView, scale: dismissPanScale) + let startTime21 = CFAbsoluteTimeGetCurrent() + var bottomContentInset: CGFloat if !component.safeInsets.bottom.isZero { bottomContentInset = component.safeInsets.bottom + 1.0 @@ -2531,220 +2537,242 @@ public final class StoryItemSetContainerComponent: Component { } else { inputPlaceholder = .plain(component.strings.Story_InputPlaceholderReplyPrivately) } + + let startTime22 = CFAbsoluteTimeGetCurrent() + + var currentHasFirstResponder = false + if let reactionContextNode = self.reactionContextNode { + if hasFirstResponder(reactionContextNode.view) { + currentHasFirstResponder = true + } + } + if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View { + if inputPanelView.hasFirstResponder() { + currentHasFirstResponder = true + } + } var keyboardHeight = component.deviceMetrics.standardInputHeight(inLandscape: false) let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden - let inputNodeVisible = self.sendMessageContext.currentInputMode == .media || hasFirstResponder(self) + let inputNodeVisible = self.sendMessageContext.currentInputMode == .media || currentHasFirstResponder self.inputPanel.parentState = state - let inputPanelSize = self.inputPanel.update( - transition: inputPanelTransition, - component: AnyComponent(MessageInputPanelComponent( - externalState: self.inputPanelExternalState, - context: component.context, - theme: component.theme, - strings: component.strings, - style: .story, - placeholder: inputPlaceholder, - maxLength: 4096, - queryTypes: [.mention, .emoji], - alwaysDarkWhenHasText: component.metrics.widthClass == .regular, - resetInputContents: resetInputContents, - nextInputMode: { [weak self] hasText in - if case .media = self?.sendMessageContext.currentInputMode { - return .text - } else { - return hasText ? .emoji : .stickers - } - }, - areVoiceMessagesAvailable: component.slice.additionalPeerData.areVoiceMessagesAvailable, - presentController: { [weak self] c in - guard let self, let component = self.component else { - return - } - component.presentController(c, nil) - }, - presentInGlobalOverlay: { [weak self] c in - guard let self, let component = self.component else { - return - } - component.presentInGlobalOverlay(c, nil) - }, - sendMessageAction: { [weak self] in - guard let self else { - return - } - self.sendMessageContext.performSendMessageAction(view: self) - }, - sendMessageOptionsAction: { [weak self] sourceView, gesture in - guard let self else { - return - } - self.sendMessageContext.presentSendMessageOptions(view: self, sourceView: sourceView, gesture: gesture) - }, - sendStickerAction: { [weak self] sticker in - guard let self else { - return - } - self.sendMessageContext.performSendStickerAction(view: self, fileReference: .standalone(media: sticker)) - }, - setMediaRecordingActive: { [weak self] isActive, isVideo, sendAction in - guard let self else { - return - } - self.sendMessageContext.setMediaRecordingActive(view: self, isActive: isActive, isVideo: isVideo, sendAction: sendAction) - }, - lockMediaRecording: { [weak self] in - guard let self else { - return - } - self.sendMessageContext.lockMediaRecording() - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) - }, - stopAndPreviewMediaRecording: { [weak self] in - guard let self else { - return - } - self.sendMessageContext.stopMediaRecording(view: self) - }, - discardMediaRecordingPreview: { [weak self] in - guard let self else { - return - } - self.sendMessageContext.videoRecorderValue?.dismissVideo() - self.sendMessageContext.discardMediaRecordingPreview(view: self) - }, - attachmentAction: component.slice.peer.isService ? nil : { [weak self] in - guard let self else { - return - } - self.sendMessageContext.presentAttachmentMenu(view: self, subject: .default) - }, - myReaction: component.slice.item.storyItem.myReaction.flatMap { value -> MessageInputPanelComponent.MyReaction? in - var centerAnimation: TelegramMediaFile? - var animationFileId: Int64? - - switch value { - case .builtin: - if let availableReactions = component.availableReactions { - for availableReaction in availableReactions.reactionItems { - if availableReaction.reaction.rawValue == value { - centerAnimation = availableReaction.listAnimation - break + var inputPanelSize: CGSize? + + let startTime23 = CFAbsoluteTimeGetCurrent() + + if component.slice.peer.id != component.context.account.peerId { + inputPanelSize = self.inputPanel.update( + transition: inputPanelTransition, + component: AnyComponent(MessageInputPanelComponent( + externalState: self.inputPanelExternalState, + context: component.context, + theme: component.theme, + strings: component.strings, + style: .story, + placeholder: inputPlaceholder, + maxLength: 4096, + queryTypes: [.mention, .emoji], + alwaysDarkWhenHasText: component.metrics.widthClass == .regular, + resetInputContents: resetInputContents, + nextInputMode: { [weak self] hasText in + if case .media = self?.sendMessageContext.currentInputMode { + return .text + } else { + return hasText ? .emoji : .stickers + } + }, + areVoiceMessagesAvailable: component.slice.additionalPeerData.areVoiceMessagesAvailable, + presentController: { [weak self] c in + guard let self, let component = self.component else { + return + } + component.presentController(c, nil) + }, + presentInGlobalOverlay: { [weak self] c in + guard let self, let component = self.component else { + return + } + component.presentInGlobalOverlay(c, nil) + }, + sendMessageAction: { [weak self] in + guard let self else { + return + } + self.sendMessageContext.performSendMessageAction(view: self) + }, + sendMessageOptionsAction: { [weak self] sourceView, gesture in + guard let self else { + return + } + self.sendMessageContext.presentSendMessageOptions(view: self, sourceView: sourceView, gesture: gesture) + }, + sendStickerAction: { [weak self] sticker in + guard let self else { + return + } + self.sendMessageContext.performSendStickerAction(view: self, fileReference: .standalone(media: sticker)) + }, + setMediaRecordingActive: { [weak self] isActive, isVideo, sendAction in + guard let self else { + return + } + self.sendMessageContext.setMediaRecordingActive(view: self, isActive: isActive, isVideo: isVideo, sendAction: sendAction) + }, + lockMediaRecording: { [weak self] in + guard let self else { + return + } + self.sendMessageContext.lockMediaRecording() + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + }, + stopAndPreviewMediaRecording: { [weak self] in + guard let self else { + return + } + self.sendMessageContext.stopMediaRecording(view: self) + }, + discardMediaRecordingPreview: { [weak self] in + guard let self else { + return + } + self.sendMessageContext.videoRecorderValue?.dismissVideo() + self.sendMessageContext.discardMediaRecordingPreview(view: self) + }, + attachmentAction: component.slice.peer.isService ? nil : { [weak self] in + guard let self else { + return + } + self.sendMessageContext.presentAttachmentMenu(view: self, subject: .default) + }, + myReaction: component.slice.item.storyItem.myReaction.flatMap { value -> MessageInputPanelComponent.MyReaction? in + var centerAnimation: TelegramMediaFile? + var animationFileId: Int64? + + switch value { + case .builtin: + if let availableReactions = component.availableReactions { + for availableReaction in availableReactions.reactionItems { + if availableReaction.reaction.rawValue == value { + centerAnimation = availableReaction.listAnimation + break + } } } + case let .custom(fileId): + animationFileId = fileId } - case let .custom(fileId): - animationFileId = fileId - } - - if animationFileId == nil && centerAnimation == nil { - return nil - } - - return MessageInputPanelComponent.MyReaction(reaction: value, file: centerAnimation, animationFileId: animationFileId) - }, - likeAction: component.slice.peer.isService ? nil : { [weak self] in - guard let self else { - return - } - self.performLikeAction() - }, - likeOptionsAction: component.slice.peer.isService ? nil : { [weak self] sourceView, gesture in - gesture?.cancel() - - guard let self else { - return - } - self.performLikeOptionsAction(sourceView: sourceView) - }, - inputModeAction: { [weak self] in - guard let self else { - return - } - self.sendMessageContext.toggleInputMode() - if !hasFirstResponder(self) { - self.state?.updated(transition: .spring(duration: 0.4)) - } else { - self.state?.updated(transition: .immediate) - } - }, - timeoutAction: nil, - forwardAction: component.slice.item.storyItem.isPublic ? { [weak self] in - guard let self else { - return - } - self.sendMessageContext.performShareAction(view: self) - } : nil, - moreAction: { [weak self] sourceView, gesture in - guard let self else { - return - } - self.performMoreAction(sourceView: sourceView, gesture: gesture) - }, - presentVoiceMessagesUnavailableTooltip: { [weak self] view in - guard let self, let component = self.component, self.voiceMessagesRestrictedTooltipController == nil else { - return - } - let rect = view.convert(view.bounds, to: nil) - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let text = presentationData.strings.Conversation_VoiceMessagesRestricted(component.slice.peer.compactDisplayTitle).string - let controller = TooltipController(content: .text(text), baseFontSize: presentationData.listsFontSize.baseDisplaySize, padding: 2.0) - controller.dismissed = { [weak self] _ in - if let self { - self.voiceMessagesRestrictedTooltipController = nil - self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + + if animationFileId == nil && centerAnimation == nil { + return nil } - } - component.presentController(controller, TooltipControllerPresentationArguments(sourceViewAndRect: { [weak self] in - if let self { - return (self, rect) + + return MessageInputPanelComponent.MyReaction(reaction: value, file: centerAnimation, animationFileId: animationFileId) + }, + likeAction: component.slice.peer.isService ? nil : { [weak self] in + guard let self else { + return } - return nil - })) - self.voiceMessagesRestrictedTooltipController = controller - self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) - }, - presentTextLengthLimitTooltip: nil, - presentTextFormattingTooltip: nil, - paste: { [weak self] data in - guard let self else { - return - } - switch data { - case let .images(images): - self.sendMessageContext.presentMediaPasteboard(view: self, subjects: images.map { .image($0) }) - case let .video(data): - let tempFilePath = NSTemporaryDirectory() + "\(Int64.random(in: 0...Int64.max)).mp4" - let url = NSURL(fileURLWithPath: tempFilePath) as URL - try? data.write(to: url) - self.sendMessageContext.presentMediaPasteboard(view: self, subjects: [.video(url)]) - case let .gif(data): - self.sendMessageContext.enqueueGifData(view: self, data: data) - case let .sticker(image, isMemoji): - self.sendMessageContext.enqueueStickerImage(view: self, image: image, isMemoji: isMemoji) - case .text: - break - } - }, - audioRecorder: self.sendMessageContext.audioRecorderValue, - videoRecordingStatus: !self.sendMessageContext.hasRecordedVideoPreview ? self.sendMessageContext.videoRecorderValue?.audioStatus : nil, - isRecordingLocked: self.sendMessageContext.isMediaRecordingLocked, - recordedAudioPreview: self.sendMessageContext.recordedAudioPreview, - hasRecordedVideoPreview: self.sendMessageContext.hasRecordedVideoPreview, - wasRecordingDismissed: self.sendMessageContext.wasRecordingDismissed, - timeoutValue: nil, - timeoutSelected: false, - displayGradient: false, - bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset, - isFormattingLocked: false, - hideKeyboard: self.sendMessageContext.currentInputMode == .media, - forceIsEditing: self.sendMessageContext.currentInputMode == .media, - disabledPlaceholder: disabledPlaceholder, - storyId: component.slice.item.storyItem.id - )), - environment: {}, - containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0) - ) + self.performLikeAction() + }, + likeOptionsAction: component.slice.peer.isService ? nil : { [weak self] sourceView, gesture in + gesture?.cancel() + + guard let self else { + return + } + self.performLikeOptionsAction(sourceView: sourceView) + }, + inputModeAction: { [weak self] in + guard let self else { + return + } + self.sendMessageContext.toggleInputMode() + if !hasFirstResponder(self) { + self.state?.updated(transition: .spring(duration: 0.4)) + } else { + self.state?.updated(transition: .immediate) + } + }, + timeoutAction: nil, + forwardAction: component.slice.item.storyItem.isPublic ? { [weak self] in + guard let self else { + return + } + self.sendMessageContext.performShareAction(view: self) + } : nil, + moreAction: { [weak self] sourceView, gesture in + guard let self else { + return + } + self.performMoreAction(sourceView: sourceView, gesture: gesture) + }, + presentVoiceMessagesUnavailableTooltip: { [weak self] view in + guard let self, let component = self.component, self.voiceMessagesRestrictedTooltipController == nil else { + return + } + let rect = view.convert(view.bounds, to: nil) + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let text = presentationData.strings.Conversation_VoiceMessagesRestricted(component.slice.peer.compactDisplayTitle).string + let controller = TooltipController(content: .text(text), baseFontSize: presentationData.listsFontSize.baseDisplaySize, padding: 2.0) + controller.dismissed = { [weak self] _ in + if let self { + self.voiceMessagesRestrictedTooltipController = nil + self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + } + } + component.presentController(controller, TooltipControllerPresentationArguments(sourceViewAndRect: { [weak self] in + if let self { + return (self, rect) + } + return nil + })) + self.voiceMessagesRestrictedTooltipController = controller + self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + }, + presentTextLengthLimitTooltip: nil, + presentTextFormattingTooltip: nil, + paste: { [weak self] data in + guard let self else { + return + } + switch data { + case let .images(images): + self.sendMessageContext.presentMediaPasteboard(view: self, subjects: images.map { .image($0) }) + case let .video(data): + let tempFilePath = NSTemporaryDirectory() + "\(Int64.random(in: 0...Int64.max)).mp4" + let url = NSURL(fileURLWithPath: tempFilePath) as URL + try? data.write(to: url) + self.sendMessageContext.presentMediaPasteboard(view: self, subjects: [.video(url)]) + case let .gif(data): + self.sendMessageContext.enqueueGifData(view: self, data: data) + case let .sticker(image, isMemoji): + self.sendMessageContext.enqueueStickerImage(view: self, image: image, isMemoji: isMemoji) + case .text: + break + } + }, + audioRecorder: self.sendMessageContext.audioRecorderValue, + videoRecordingStatus: !self.sendMessageContext.hasRecordedVideoPreview ? self.sendMessageContext.videoRecorderValue?.audioStatus : nil, + isRecordingLocked: self.sendMessageContext.isMediaRecordingLocked, + recordedAudioPreview: self.sendMessageContext.recordedAudioPreview, + hasRecordedVideoPreview: self.sendMessageContext.hasRecordedVideoPreview, + wasRecordingDismissed: self.sendMessageContext.wasRecordingDismissed, + timeoutValue: nil, + timeoutSelected: false, + displayGradient: false, + bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset, + isFormattingLocked: false, + hideKeyboard: self.sendMessageContext.currentInputMode == .media, + forceIsEditing: self.sendMessageContext.currentInputMode == .media, + disabledPlaceholder: disabledPlaceholder, + storyId: component.slice.item.storyItem.id + )), + environment: {}, + containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0) + ) + } + + let startTime3 = CFAbsoluteTimeGetCurrent() var inputPanelInset: CGFloat = component.containerInsets.bottom var inputHeight = component.inputHeight @@ -2816,7 +2844,7 @@ public final class StoryItemSetContainerComponent: Component { inputPanelBottomInset = bottomContentInset if case .regular = component.metrics.widthClass { bottomContentInset += 60.0 - } else { + } else if let inputPanelSize { bottomContentInset += inputPanelSize.height } inputPanelIsOverlay = false @@ -2832,6 +2860,8 @@ public final class StoryItemSetContainerComponent: Component { let minimizedHeight = max(100.0, availableSize.height - (325.0 + 12.0)) let defaultHeight = 60.0 + component.safeInsets.bottom + 1.0 + let startTime4 = CFAbsoluteTimeGetCurrent() + var validViewListIds: [Int32] = [] if component.slice.peer.id == component.context.account.peerId, let currentIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) { var visibleViewListIds: [Int32] = [component.slice.item.storyItem.id] @@ -3310,6 +3340,8 @@ public final class StoryItemSetContainerComponent: Component { self.viewLists.removeValue(forKey: id) } + let startTime5 = CFAbsoluteTimeGetCurrent() + let itemSize = CGSize(width: availableSize.width, height: ceil(availableSize.width * 1.77778)) let contentDefaultBottomInset: CGFloat = bottomContentInset @@ -3465,6 +3497,8 @@ public final class StoryItemSetContainerComponent: Component { } } + let startTime6 = CFAbsoluteTimeGetCurrent() + let soundButtonState = isSilentVideo || component.isAudioMuted let soundButtonSize = self.soundButton.update( transition: transition, @@ -3748,29 +3782,35 @@ public final class StoryItemSetContainerComponent: Component { } } + let startTime7 = CFAbsoluteTimeGetCurrent() + let topGradientHeight: CGFloat = 90.0 let topContentGradientRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentFrame.width, height: topGradientHeight)) transition.setPosition(view: self.topContentGradientView, position: topContentGradientRect.center) transition.setBounds(view: self.topContentGradientView, bounds: CGRect(origin: CGPoint(), size: topContentGradientRect.size)) - let inputPanelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - inputPanelSize.width) / 2.0), y: availableSize.height - inputPanelBottomInset - inputPanelSize.height), size: inputPanelSize) - var inputPanelAlpha: CGFloat = component.slice.peer.id == component.context.account.peerId || component.hideUI || self.isEditingStory ? 0.0 : 1.0 - if case .regular = component.metrics.widthClass { - inputPanelAlpha *= component.visibilityFraction - } - if let inputPanelView = self.inputPanel.view { - if inputPanelView.superview == nil { - self.componentContainerView.addSubview(inputPanelView) + var inputPanelFrameValue: CGRect? + if let inputPanelSize { + let inputPanelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - inputPanelSize.width) / 2.0), y: availableSize.height - inputPanelBottomInset - inputPanelSize.height), size: inputPanelSize) + inputPanelFrameValue = inputPanelFrame + var inputPanelAlpha: CGFloat = component.slice.peer.id == component.context.account.peerId || component.hideUI || self.isEditingStory ? 0.0 : 1.0 + if case .regular = component.metrics.widthClass { + inputPanelAlpha *= component.visibilityFraction } - - var inputPanelOffset: CGFloat = 0.0 - if component.slice.peer.id != component.context.account.peerId && !self.inputPanelExternalState.isEditing { - let bandingOffset = scrollingRubberBandingOffset(offset: verticalPanFraction * availableSize.height, bandingStart: 0.0, range: 10.0) - inputPanelOffset = -max(0.0, min(10.0, bandingOffset)) + if let inputPanelView = self.inputPanel.view { + if inputPanelView.superview == nil { + self.componentContainerView.addSubview(inputPanelView) + } + + var inputPanelOffset: CGFloat = 0.0 + if component.slice.peer.id != component.context.account.peerId && !self.inputPanelExternalState.isEditing { + let bandingOffset = scrollingRubberBandingOffset(offset: verticalPanFraction * availableSize.height, bandingStart: 0.0, range: 10.0) + inputPanelOffset = -max(0.0, min(10.0, bandingOffset)) + } + + inputPanelTransition.setFrame(view: inputPanelView, frame: inputPanelFrame.offsetBy(dx: 0.0, dy: inputPanelOffset)) + transition.setAlpha(view: inputPanelView, alpha: inputPanelAlpha) } - - inputPanelTransition.setFrame(view: inputPanelView, frame: inputPanelFrame.offsetBy(dx: 0.0, dy: inputPanelOffset)) - transition.setAlpha(view: inputPanelView, alpha: inputPanelAlpha) } if let captionItem = self.captionItem, captionItem.itemId != component.slice.item.storyItem.id { @@ -3915,7 +3955,11 @@ public final class StoryItemSetContainerComponent: Component { likeRect.origin.x -= 30.0 reactionsAnchorRect = likeRect } else { - reactionsAnchorRect = CGRect(origin: CGPoint(x: inputPanelFrame.maxX - 40.0, y: inputPanelFrame.minY + 9.0), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0) + if let inputPanelFrameValue { + reactionsAnchorRect = CGRect(origin: CGPoint(x: inputPanelFrameValue.maxX - 40.0, y: inputPanelFrameValue.minY + 9.0), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0) + } else { + reactionsAnchorRect = CGRect() + } } var effectiveDisplayReactions = false @@ -4297,7 +4341,7 @@ public final class StoryItemSetContainerComponent: Component { } } - let bottomGradientHeight = inputPanelSize.height + 32.0 + let bottomGradientHeight = (inputPanelSize?.height ?? 0.0) + 32.0 transition.setFrame(layer: self.bottomContentGradientLayer, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: availableSize.height - inputHeight - bottomGradientHeight), size: CGSize(width: contentFrame.width, height: bottomGradientHeight))) //transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: inputPanelIsOverlay ? 1.0 : 0.0) transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: 0.0) @@ -4354,10 +4398,14 @@ public final class StoryItemSetContainerComponent: Component { } } + let startTime8 = CFAbsoluteTimeGetCurrent() + self.ignoreScrolling = false self.updateScrolling(transition: itemsTransition) + let startTime9 = CFAbsoluteTimeGetCurrent() + if let focusedItem, let visibleItem = self.visibleItems[focusedItem.storyItem.id], let index = focusedItem.position { let navigationStripSideInset: CGFloat = 8.0 let navigationStripTopInset: CGFloat = 8.0 @@ -4396,8 +4444,27 @@ public final class StoryItemSetContainerComponent: Component { component.externalState.derivedMediaSize = contentFrame.size if component.slice.peer.id == component.context.account.peerId { component.externalState.derivedBottomInset = availableSize.height - itemsContainerFrame.maxY + } else if let inputPanelFrameValue { + component.externalState.derivedBottomInset = availableSize.height - min(inputPanelFrameValue.minY, contentFrame.maxY) } else { - component.externalState.derivedBottomInset = availableSize.height - min(inputPanelFrame.minY, contentFrame.maxY) + component.externalState.derivedBottomInset = 0.0 + } + + if !"".isEmpty { + print("inner update time:\n" + + " 1: \((CFAbsoluteTimeGetCurrent() - startTime1) * 1000.0) ms\n" + + " 2: \((CFAbsoluteTimeGetCurrent() - startTime2) * 1000.0) ms\n" + + " 2.1: \((CFAbsoluteTimeGetCurrent() - startTime21) * 1000.0) ms\n" + + " 2.2: \((CFAbsoluteTimeGetCurrent() - startTime22) * 1000.0) ms\n" + + " 2.3: \((CFAbsoluteTimeGetCurrent() - startTime23) * 1000.0) ms\n" + + " 3: \((CFAbsoluteTimeGetCurrent() - startTime3) * 1000.0) ms\n" + + " 4: \((CFAbsoluteTimeGetCurrent() - startTime4) * 1000.0) ms\n" + + " 5: \((CFAbsoluteTimeGetCurrent() - startTime5) * 1000.0) ms\n" + + " 6: \((CFAbsoluteTimeGetCurrent() - startTime6) * 1000.0) ms\n" + + " 7: \((CFAbsoluteTimeGetCurrent() - startTime7) * 1000.0) ms\n" + + " 8: \((CFAbsoluteTimeGetCurrent() - startTime8) * 1000.0) ms\n" + + " 9: \((CFAbsoluteTimeGetCurrent() - startTime9) * 1000.0) ms\n" + ) } return contentSize diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index 3f4fe3e683..8e035b7b07 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -380,7 +380,7 @@ final class StoryItemSetViewListComponent: Component { } let actualBounds = self.scrollView.bounds - let visibleBounds = actualBounds.insetBy(dx: 0.0, dy: -200.0) + let visibleBounds = actualBounds//.insetBy(dx: 0.0, dy: -200.0) var synchronousLoad = false if let hint = transition.userData(PeerListItemComponent.TransitionHint.self) { @@ -402,6 +402,12 @@ final class StoryItemSetViewListComponent: Component { } #endif + /*if "".isEmpty { + if index > range.lowerBound - 1 { + break + } + }*/ + let itemFrame = itemLayout.itemFrame(for: index) if index >= viewListState.items.count { diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 4e2d143e65..82ab128d91 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -264,6 +264,10 @@ public final class TextFieldComponent: Component { self.updateEntities() } + public func hasFirstResponder() -> Bool { + return self.textView.isFirstResponder + } + public func insertText(_ text: NSAttributedString) { self.updateInputState { state in return state.insertText(text)