Story optimizations

This commit is contained in:
Ali 2023-08-22 23:45:57 +04:00
parent a35f38c0f6
commit 4353823603
12 changed files with 498 additions and 230 deletions

View File

@ -1027,6 +1027,8 @@ public protocol AccountContext: AnyObject {
var userLimits: EngineConfiguration.UserLimits { get }
var imageCache: AnyObject? { get }
func storeSecureIdPassword(password: String)
func getStoredSecureIdPassword() -> String?

View File

@ -23,6 +23,7 @@ swift_library(
"//submodules/FastBlur:FastBlur",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
"//submodules/DirectMediaImageCache",
],
visibility = [
"//visibility:public",

View File

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

View File

@ -492,6 +492,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 {
return self.getImageSynchronous(peer: peer, story: story, userLocation: .peer(peer.id), media: media, width: width, aspectRatio: aspectRatio, possibleWidths: possibleWidths, includeBlurred: includeBlurred)
@ -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()))
}
}
}

View File

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

View File

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

View File

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

View File

@ -501,7 +501,22 @@ public final class PeerListItemComponent: Component {
}
let _ = clipStyle
let _ = synchronousLoad
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,

View File

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

View File

@ -2325,6 +2325,8 @@ public final class StoryItemSetContainerComponent: Component {
func update(component: StoryItemSetContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, 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
@ -2532,11 +2538,30 @@ public final class StoryItemSetContainerComponent: Component {
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(
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,
@ -2745,6 +2770,9 @@ public final class StoryItemSetContainerComponent: Component {
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,12 +3782,17 @@ 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))
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
@ -3772,6 +3811,7 @@ public final class StoryItemSetContainerComponent: Component {
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 {
self.captionItem = nil
@ -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

View File

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

View File

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