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

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

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

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

@ -2324,6 +2324,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
@ -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

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)