mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Story optimizations
This commit is contained in:
parent
a35f38c0f6
commit
4353823603
@ -1027,6 +1027,8 @@ public protocol AccountContext: AnyObject {
|
||||
|
||||
var userLimits: EngineConfiguration.UserLimits { get }
|
||||
|
||||
var imageCache: AnyObject? { get }
|
||||
|
||||
func storeSecureIdPassword(password: String)
|
||||
func getStoredSecureIdPassword() -> String?
|
||||
|
||||
|
@ -23,6 +23,7 @@ swift_library(
|
||||
"//submodules/FastBlur:FastBlur",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
|
||||
"//submodules/DirectMediaImageCache",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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,
|
||||
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user