mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
d8c061df1c
commit
9d5f5c137d
@ -2440,6 +2440,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
||||
self.requestUpdate(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
},
|
||||
onTextEditingEnded: { _ in },
|
||||
getCurrentImage: { [weak controller] in
|
||||
return controller?.getCurrentImage()
|
||||
},
|
||||
@ -2954,6 +2955,8 @@ public final class DrawingToolsInteraction {
|
||||
private let updateColor: (DrawingColor) -> Void
|
||||
|
||||
private let onInteractionUpdated: (Bool) -> Void
|
||||
private let onTextEditingEnded: (Bool) -> Void
|
||||
|
||||
private let getCurrentImage: () -> UIImage?
|
||||
private let getControllerNode: () -> ASDisplayNode?
|
||||
private let present: (ViewController, PresentationContextType, Any?) -> Void
|
||||
@ -2980,6 +2983,7 @@ public final class DrawingToolsInteraction {
|
||||
updateVideoPlayback: @escaping (Bool) -> Void,
|
||||
updateColor: @escaping (DrawingColor) -> Void,
|
||||
onInteractionUpdated: @escaping (Bool) -> Void,
|
||||
onTextEditingEnded: @escaping (Bool) -> Void,
|
||||
getCurrentImage: @escaping () -> UIImage?,
|
||||
getControllerNode: @escaping () -> ASDisplayNode?,
|
||||
present: @escaping (ViewController, PresentationContextType, Any?) -> Void,
|
||||
@ -2994,6 +2998,7 @@ public final class DrawingToolsInteraction {
|
||||
self.updateVideoPlayback = updateVideoPlayback
|
||||
self.updateColor = updateColor
|
||||
self.onInteractionUpdated = onInteractionUpdated
|
||||
self.onTextEditingEnded = onTextEditingEnded
|
||||
self.getCurrentImage = getCurrentImage
|
||||
self.getControllerNode = getControllerNode
|
||||
self.present = present
|
||||
@ -3123,6 +3128,7 @@ public final class DrawingToolsInteraction {
|
||||
public func endTextEditing(reset: Bool) {
|
||||
if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView {
|
||||
entityView.endEditing(reset: reset)
|
||||
self.onTextEditingEnded(reset)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -504,7 +504,7 @@ public class StickerPickerScreen: ViewController {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let file = item.itemFile {
|
||||
if let file = item.itemFile {
|
||||
strongSelf.controller?.completion(.file(file))
|
||||
} else if case let .staticEmoji(emoji) = item.content {
|
||||
if let image = generateImage(CGSize(width: 256.0, height: 256.0), scale: 1.0, rotatedContext: { size, context in
|
||||
@ -870,12 +870,42 @@ public class StickerPickerScreen: ViewController {
|
||||
}
|
||||
|
||||
content.stickers?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
|
||||
performItemAction: { [weak self] _, item, _, _, _, _ in
|
||||
guard let strongSelf = self, let file = item.itemFile else {
|
||||
performItemAction: { [weak self] groupId, item, _, _, _, _ in
|
||||
guard let self, let controller = self.controller, let file = item.itemFile else {
|
||||
return
|
||||
}
|
||||
strongSelf.controller?.completion(.file(file))
|
||||
strongSelf.controller?.dismiss(animated: true)
|
||||
if groupId == AnyHashable("featuredTop") {
|
||||
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
|
||||
let _ = (controller.context.account.postbox.combinedView(keys: [viewKey])
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] views in
|
||||
guard let self, let controller = self.controller, let view = views.views[viewKey] as? OrderedItemListView else {
|
||||
return
|
||||
}
|
||||
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
|
||||
if featuredStickerPack.topItems.contains(where: { $0.file.fileId == file.fileId }) {
|
||||
controller.push(FeaturedStickersScreen(
|
||||
context: controller.context,
|
||||
highlightedPackId: featuredStickerPack.info.id,
|
||||
forceTheme: defaultDarkPresentationTheme,
|
||||
sendSticker: { [weak self] fileReference, _, _ in
|
||||
guard let self else {
|
||||
return false
|
||||
}
|
||||
self.controller?.completion(.file(fileReference.media))
|
||||
self.controller?.dismiss(animated: true)
|
||||
return true
|
||||
}
|
||||
))
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.controller?.completion(.file(file))
|
||||
self.controller?.dismiss(animated: true)
|
||||
}
|
||||
},
|
||||
deleteBackwards: nil,
|
||||
openStickerSettings: nil,
|
||||
|
@ -841,6 +841,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
if let pagerView = self.entityKeyboardView.componentView as? EntityKeyboardComponent.View, let emojiInputInteraction = self.emojiInputInteraction {
|
||||
pagerView.openCustomSearch(content: EmojiSearchContent(
|
||||
context: self.context,
|
||||
forceTheme: self.interaction?.forceTheme,
|
||||
items: stickerPacks,
|
||||
initialFocusId: featuredStickerPack.info.id,
|
||||
hasPremiumForUse: hasPremium,
|
||||
|
@ -374,7 +374,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
let isTemplate = file.isCustomTemplateEmoji
|
||||
|
||||
let context = self.context
|
||||
if file.isAnimatedSticker || file.isVideoEmoji {
|
||||
if file.isAnimatedSticker || file.isVideoSticker || file.isVideoEmoji {
|
||||
let keyframeOnly = self.pixelSize.width >= 120.0
|
||||
|
||||
self.disposable = renderer.add(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, unique: self.unique, size: self.pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: self.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly, customColor: isTemplate ? .white : nil))
|
||||
|
@ -41,6 +41,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let forceTheme: PresentationTheme?
|
||||
private var initialFocusId: ItemCollectionId?
|
||||
private let hasPremiumForUse: Bool
|
||||
private let hasPremiumForInstallation: Bool
|
||||
@ -70,6 +71,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
forceTheme: PresentationTheme?,
|
||||
items: [FeaturedStickerPackItem],
|
||||
initialFocusId: ItemCollectionId?,
|
||||
hasPremiumForUse: Bool,
|
||||
@ -77,12 +79,17 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
|
||||
parentInputInteraction: EmojiPagerContentComponent.InputInteraction
|
||||
) {
|
||||
self.context = context
|
||||
self.forceTheme = forceTheme
|
||||
self.initialFocusId = initialFocusId
|
||||
self.hasPremiumForUse = hasPremiumForUse
|
||||
self.hasPremiumForInstallation = hasPremiumForInstallation
|
||||
self.parentInputInteraction = parentInputInteraction
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
if let forceTheme {
|
||||
presentationData = presentationData.withUpdated(theme: forceTheme)
|
||||
}
|
||||
self.presentationData = presentationData
|
||||
|
||||
self.panelHostView = PagerExternalTopPanelContainer()
|
||||
self.inputInteractionHolder = EmojiPagerContentComponent.InputInteractionHolder()
|
||||
|
@ -916,7 +916,6 @@ public final class EntityKeyboardComponent: Component {
|
||||
|
||||
if component.useExternalSearchContainer, let containerNode = component.makeSearchContainerNode(contentType) {
|
||||
let controller = EntitySearchContainerController(containerNode: containerNode)
|
||||
|
||||
self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.pushController(controller)
|
||||
} else {
|
||||
self.searchComponent = EntitySearchContentComponent(
|
||||
@ -941,14 +940,19 @@ public final class EntityKeyboardComponent: Component {
|
||||
return
|
||||
}
|
||||
|
||||
self.searchComponent = EntitySearchContentComponent(
|
||||
makeContainerNode: {
|
||||
return content
|
||||
},
|
||||
dismissSearch: { [weak self] in
|
||||
self?.closeSearch()
|
||||
}
|
||||
)
|
||||
if component.useExternalSearchContainer {
|
||||
let controller = EntitySearchContainerController(containerNode: content)
|
||||
self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.pushController(controller)
|
||||
} else {
|
||||
self.searchComponent = EntitySearchContentComponent(
|
||||
makeContainerNode: {
|
||||
return content
|
||||
},
|
||||
dismissSearch: { [weak self] in
|
||||
self?.closeSearch()
|
||||
}
|
||||
)
|
||||
}
|
||||
component.hideInputUpdated(true, true, Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
||||
}
|
||||
|
||||
|
@ -227,9 +227,6 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
let cropRect = CGRect(origin: CGPoint(x: floor((ciImage.extent.size.width - minSide) / 2.0), y: floor((ciImage.extent.size.height - minSide) / 2.0)), size: CGSize(width: minSide, height: minSide))
|
||||
ciImage = ciImage.cropped(to: cropRect).samplingLinear()
|
||||
ciImage = ciImage.transformed(by: CGAffineTransform(translationX: 0.0, y: -420.0))
|
||||
// ciImage = ciImage.transformed(by: CGAffineTransform(translationX: -ciImage.extent.midX, y: -ciImage.extent.midY))
|
||||
// ciImage = ciImage.transformed(by: CGAffineTransform(rotationAngle: -.pi / 2.0))
|
||||
// ciImage = ciImage.transformed(by: CGAffineTransform(translationX: ciImage.extent.midX, y: ciImage.extent.midY))
|
||||
|
||||
var circleMaskFilter: CIFilter?
|
||||
if let current = self.circleMaskFilter {
|
||||
|
@ -363,7 +363,13 @@ final class MediaEditorScreenComponent: Component {
|
||||
self.environment?.controller()?.presentInGlobalOverlay(c, with: a)
|
||||
}
|
||||
},
|
||||
getNavigationController: { return nil },
|
||||
getNavigationController: { [weak self] in
|
||||
if let self {
|
||||
return self.environment?.controller()?.navigationController as? NavigationController
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
requestLayout: { [weak self] transition in
|
||||
if let self {
|
||||
(self.environment?.controller() as? MediaEditorScreen)?.node.requestLayout(forceUpdate: true, transition: Transition(transition))
|
||||
@ -985,6 +991,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
stateContext: self.inputMediaNodeStateContext
|
||||
)
|
||||
inputMediaNode.externalTopPanelContainerImpl = nil
|
||||
inputMediaNode.useExternalSearchContainer = true
|
||||
if let inputPanelView = self.inputPanel.view, inputMediaNode.view.superview == nil {
|
||||
self.insertSubview(inputMediaNode.view, belowSubview: inputPanelView)
|
||||
}
|
||||
@ -1135,6 +1142,9 @@ final class MediaEditorScreenComponent: Component {
|
||||
forwardAction: nil,
|
||||
moreAction: nil,
|
||||
presentVoiceMessagesUnavailableTooltip: nil,
|
||||
paste: { data in
|
||||
let _ = data
|
||||
},
|
||||
audioRecorder: nil,
|
||||
videoRecordingStatus: nil,
|
||||
isRecordingLocked: false,
|
||||
@ -2057,6 +2067,18 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
self.requestUpdate(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
},
|
||||
onTextEditingEnded: { [weak self] reset in
|
||||
if let self, !reset, let entity = self.entitiesView.selectedEntityView?.entity as? DrawingTextEntity, !entity.text.string.isEmpty {
|
||||
let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in
|
||||
let textSettings = MediaEditorStoredTextSettings(style: entity.style, font: entity.font, fontSize: entity.fontSize, alignment: entity.alignment)
|
||||
if let current {
|
||||
return current.withUpdatedTextSettings(textSettings)
|
||||
} else {
|
||||
return MediaEditorStoredState(privacy: nil, textSettings: textSettings)
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
},
|
||||
getCurrentImage: {
|
||||
return nil
|
||||
},
|
||||
@ -2202,13 +2224,33 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
if let layout = self.validLayout, (layout.inputHeight ?? 0.0) > 0.0 {
|
||||
self.view.endEditing(true)
|
||||
} else {
|
||||
let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .filled, animation: .none, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white))
|
||||
self.interaction?.insertEntity(textEntity)
|
||||
self.insertTextEntity()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func insertTextEntity() {
|
||||
let _ = (mediaEditorStoredState(engine: self.context.engine)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var style: DrawingTextEntity.Style = .filled
|
||||
var font: DrawingTextEntity.Font = .sanFrancisco
|
||||
var alignment: DrawingTextEntity.Alignment = .center
|
||||
var fontSize: CGFloat = 1.0
|
||||
if let textSettings = state?.textSettings {
|
||||
style = textSettings.style
|
||||
font = textSettings.font
|
||||
alignment = textSettings.alignment
|
||||
fontSize = textSettings.fontSize
|
||||
}
|
||||
let textEntity = DrawingTextEntity(text: NSAttributedString(), style: style, animation: .none, font: font, alignment: alignment, fontSize: fontSize, color: DrawingColor(color: .white))
|
||||
self.interaction?.insertEntity(textEntity)
|
||||
})
|
||||
}
|
||||
|
||||
private func setupTransitionImage(_ image: UIImage) {
|
||||
self.previewContainerView.alpha = 1.0
|
||||
|
||||
@ -2804,8 +2846,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
self.controller?.present(controller, in: .window(.root))
|
||||
return
|
||||
case .text:
|
||||
let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .filled, animation: .none, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white))
|
||||
self.interaction?.insertEntity(textEntity)
|
||||
self.insertTextEntity()
|
||||
|
||||
self.hasAnyChanges = true
|
||||
self.controller?.isSavingAvailable = true
|
||||
@ -2957,6 +2998,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
self.interaction?.containerLayoutUpdated(layout: layout, transition: transition)
|
||||
|
||||
var layout = layout
|
||||
layout.intrinsicInsets.top = topInset
|
||||
layout.intrinsicInsets.bottom = bottomInset + 60.0
|
||||
controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
@ -3172,7 +3214,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
let initialPrivacy = privacy.privacy
|
||||
let timeout = privacy.timeout
|
||||
|
||||
let controller = ShareWithPeersScreen(
|
||||
let controller = ShareWithPeersScreen(
|
||||
context: self.context,
|
||||
initialPrivacy: initialPrivacy,
|
||||
allowScreenshots: !privacy.isForwardingDisabled,
|
||||
@ -3569,7 +3611,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
|
||||
if !self.isEditingStory {
|
||||
let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, state: MediaEditorStoredState(privacy: self.state.privacy)).start()
|
||||
let privacy = self.state.privacy
|
||||
let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in
|
||||
if let current {
|
||||
return current.withUpdatedPrivacy(privacy)
|
||||
} else {
|
||||
return MediaEditorStoredState(privacy: privacy, textSettings: nil)
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
if mediaEditor.resultIsVideo {
|
||||
|
@ -5,15 +5,62 @@ import TelegramCore
|
||||
import TelegramUIPreferences
|
||||
import MediaEditor
|
||||
|
||||
public final class MediaEditorStoredTextSettings: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case style
|
||||
case font
|
||||
case fontSize
|
||||
case alignment
|
||||
}
|
||||
|
||||
public let style: DrawingTextEntity.Style
|
||||
public let font: DrawingTextEntity.Font
|
||||
public let fontSize: CGFloat
|
||||
public let alignment: DrawingTextEntity.Alignment
|
||||
|
||||
public init(
|
||||
style: DrawingTextEntity.Style,
|
||||
font: DrawingTextEntity.Font,
|
||||
fontSize: CGFloat,
|
||||
alignment: DrawingTextEntity.Alignment
|
||||
) {
|
||||
self.style = style
|
||||
self.font = font
|
||||
self.fontSize = fontSize
|
||||
self.alignment = alignment
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.style = try container.decode(DrawingTextEntity.Style.self, forKey: .style)
|
||||
self.font = try container.decode(DrawingTextEntity.Font.self, forKey: .font)
|
||||
self.fontSize = try container.decode(CGFloat.self, forKey: .fontSize)
|
||||
self.alignment = try container.decode(DrawingTextEntity.Alignment.self, forKey: .alignment)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(self.style, forKey: .style)
|
||||
try container.encode(self.font, forKey: .font)
|
||||
try container.encode(self.fontSize, forKey: .fontSize)
|
||||
try container.encode(self.alignment, forKey: .alignment)
|
||||
}
|
||||
}
|
||||
|
||||
public final class MediaEditorStoredState: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case privacy
|
||||
case textSettings
|
||||
}
|
||||
|
||||
public let privacy: MediaEditorResultPrivacy?
|
||||
public let textSettings: MediaEditorStoredTextSettings?
|
||||
|
||||
public init(privacy: MediaEditorResultPrivacy?) {
|
||||
public init(privacy: MediaEditorResultPrivacy?, textSettings: MediaEditorStoredTextSettings?) {
|
||||
self.privacy = privacy
|
||||
self.textSettings = textSettings
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@ -24,12 +71,17 @@ public final class MediaEditorStoredState: Codable {
|
||||
} else {
|
||||
self.privacy = nil
|
||||
}
|
||||
if let data = try container.decodeIfPresent(Data.self, forKey: .textSettings), let privacy = try? JSONDecoder().decode(MediaEditorStoredTextSettings.self, from: data) {
|
||||
self.textSettings = privacy
|
||||
} else {
|
||||
self.textSettings = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
if let privacy = self .privacy {
|
||||
if let privacy = self.privacy {
|
||||
if let data = try? JSONEncoder().encode(privacy) {
|
||||
try container.encode(data, forKey: .privacy)
|
||||
} else {
|
||||
@ -38,6 +90,24 @@ public final class MediaEditorStoredState: Codable {
|
||||
} else {
|
||||
try container.encodeNil(forKey: .privacy)
|
||||
}
|
||||
|
||||
if let textSettings = self.textSettings {
|
||||
if let data = try? JSONEncoder().encode(textSettings) {
|
||||
try container.encode(data, forKey: .textSettings)
|
||||
} else {
|
||||
try container.encodeNil(forKey: .textSettings)
|
||||
}
|
||||
} else {
|
||||
try container.encodeNil(forKey: .textSettings)
|
||||
}
|
||||
}
|
||||
|
||||
public func withUpdatedPrivacy(_ privacy: MediaEditorResultPrivacy) -> MediaEditorStoredState {
|
||||
return MediaEditorStoredState(privacy: privacy, textSettings: self.textSettings)
|
||||
}
|
||||
|
||||
public func withUpdatedTextSettings(_ textSettings: MediaEditorStoredTextSettings) -> MediaEditorStoredState {
|
||||
return MediaEditorStoredState(privacy: self.privacy, textSettings: textSettings)
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,13 +121,19 @@ func mediaEditorStoredState(engine: TelegramEngine) -> Signal<MediaEditorStoredS
|
||||
}
|
||||
}
|
||||
|
||||
func updateMediaEditorStoredStateInteractively(engine: TelegramEngine, state: MediaEditorStoredState?) -> Signal<Never, NoError> {
|
||||
func updateMediaEditorStoredStateInteractively(engine: TelegramEngine, _ f: @escaping (MediaEditorStoredState?) -> MediaEditorStoredState?) -> Signal<Never, NoError> {
|
||||
let key = EngineDataBuffer(length: 4)
|
||||
key.setInt32(0, value: 0)
|
||||
|
||||
if let state = state {
|
||||
return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.mediaEditorState, id: key, item: state)
|
||||
} else {
|
||||
return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.mediaEditorState, id: key)
|
||||
return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.mediaEditorState, id: key))
|
||||
|> map { entry -> MediaEditorStoredState? in
|
||||
return entry?.get(MediaEditorStoredState.self)
|
||||
}
|
||||
|> mapToSignal { state -> Signal<Never, NoError> in
|
||||
if let updatedState = f(state) {
|
||||
return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.mediaEditorState, id: key, item: updatedState)
|
||||
} else {
|
||||
return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.mediaEditorState, id: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -268,6 +268,7 @@ final class StoryPreviewComponent: Component {
|
||||
forwardAction: {},
|
||||
moreAction: { _, _ in },
|
||||
presentVoiceMessagesUnavailableTooltip: nil,
|
||||
paste: { _ in },
|
||||
audioRecorder: nil,
|
||||
videoRecordingStatus: nil,
|
||||
isRecordingLocked: false,
|
||||
|
@ -82,6 +82,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let forwardAction: (() -> Void)?
|
||||
public let moreAction: ((UIView, ContextGesture?) -> Void)?
|
||||
public let presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?
|
||||
public let paste: (TextFieldComponent.PasteData) -> Void
|
||||
public let audioRecorder: ManagedAudioRecorder?
|
||||
public let videoRecordingStatus: InstantVideoControllerRecordingStatus?
|
||||
public let isRecordingLocked: Bool
|
||||
@ -121,6 +122,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
forwardAction: (() -> Void)?,
|
||||
moreAction: ((UIView, ContextGesture?) -> Void)?,
|
||||
presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?,
|
||||
paste: @escaping (TextFieldComponent.PasteData) -> Void,
|
||||
audioRecorder: ManagedAudioRecorder?,
|
||||
videoRecordingStatus: InstantVideoControllerRecordingStatus?,
|
||||
isRecordingLocked: Bool,
|
||||
@ -159,6 +161,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.forwardAction = forwardAction
|
||||
self.moreAction = moreAction
|
||||
self.presentVoiceMessagesUnavailableTooltip = presentVoiceMessagesUnavailableTooltip
|
||||
self.paste = paste
|
||||
self.audioRecorder = audioRecorder
|
||||
self.videoRecordingStatus = videoRecordingStatus
|
||||
self.isRecordingLocked = isRecordingLocked
|
||||
@ -521,6 +524,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
hideKeyboard: component.hideKeyboard,
|
||||
present: { c in
|
||||
component.presentController(c)
|
||||
},
|
||||
paste: { data in
|
||||
component.paste(data)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
|
@ -79,7 +79,8 @@ final class StickersResultPanelComponent: Component {
|
||||
self.itemsPerRow = itemsPerRow
|
||||
self.itemCount = itemCount
|
||||
|
||||
self.contentSize = CGSize(width: containerSize.width, height: topInset + CGFloat(itemCount) * itemSize.height + bottomInset)
|
||||
let rowsCount = ceil(CGFloat(itemCount) / CGFloat(itemsPerRow))
|
||||
self.contentSize = CGSize(width: containerSize.width, height: topInset + rowsCount * (itemSize.height + itemSpacing) - itemSpacing + bottomInset)
|
||||
}
|
||||
|
||||
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
||||
@ -445,7 +446,7 @@ final class StickersResultPanelComponent: Component {
|
||||
|
||||
let itemLayout = ItemLayout(
|
||||
containerSize: CGSize(width: availableSize.width, height: minimizedHeight),
|
||||
bottomInset: 9.0,
|
||||
bottomInset: 40.0,
|
||||
topInset: 9.0,
|
||||
sideInset: sideInset,
|
||||
itemSize: CGSize(width: itemSize, height: itemSize),
|
||||
|
@ -367,14 +367,17 @@ public final class PeerListItemComponent: Component {
|
||||
)
|
||||
|
||||
let titleSpacing: CGFloat = 2.0
|
||||
var titleVerticalOffset: CGFloat = 0.0
|
||||
let centralContentHeight: CGFloat
|
||||
if labelSize.height > 0.0, case .generic = component.style {
|
||||
centralContentHeight = titleSize.height + labelSize.height + titleSpacing
|
||||
titleVerticalOffset = -1.0
|
||||
} else {
|
||||
centralContentHeight = titleSize.height
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: -1.0 + floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleVerticalOffset + floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
@ -396,46 +399,8 @@ public final class PeerListItemComponent: Component {
|
||||
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
|
||||
}
|
||||
}
|
||||
if let labelView = self.label.view {
|
||||
var iconLabelOffset: CGFloat = 0.0
|
||||
|
||||
if case .checks = component.subtitleAccessory {
|
||||
let iconView: UIImageView
|
||||
if let current = self.iconView {
|
||||
iconView = current
|
||||
} else {
|
||||
iconView = UIImageView(image: readIconImage)
|
||||
iconView.tintColor = component.theme.list.itemSecondaryTextColor
|
||||
self.iconView = iconView
|
||||
self.containerButton.addSubview(iconView)
|
||||
}
|
||||
|
||||
if let image = iconView.image {
|
||||
iconLabelOffset = image.size.width + 4.0
|
||||
transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing + 3.0 + floor((labelSize.height - image.size.height) * 0.5)), size: image.size))
|
||||
}
|
||||
} else if let iconView = self.iconView {
|
||||
self.iconView = nil
|
||||
iconView.removeFromSuperview()
|
||||
}
|
||||
|
||||
if labelView.superview == nil {
|
||||
labelView.isUserInteractionEnabled = false
|
||||
self.containerButton.addSubview(labelView)
|
||||
}
|
||||
|
||||
let labelFrame: CGRect
|
||||
switch component.style {
|
||||
case .generic:
|
||||
labelFrame = CGRect(origin: CGPoint(x: titleFrame.minX + iconLabelOffset, y: titleFrame.maxY + titleSpacing), size: labelSize)
|
||||
case .compact:
|
||||
labelFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: labelSize)
|
||||
}
|
||||
|
||||
transition.setFrame(view: labelView, frame: labelFrame)
|
||||
}
|
||||
|
||||
if let statusIcon {
|
||||
if let statusIcon, case .generic = component.style {
|
||||
let animationCache = component.context.animationCache
|
||||
let animationRenderer = component.context.animationRenderer
|
||||
|
||||
@ -477,6 +442,45 @@ public final class PeerListItemComponent: Component {
|
||||
avatarIcon.view?.removeFromSuperview()
|
||||
}
|
||||
|
||||
if let labelView = self.label.view {
|
||||
var iconLabelOffset: CGFloat = 0.0
|
||||
|
||||
if case .checks = component.subtitleAccessory {
|
||||
let iconView: UIImageView
|
||||
if let current = self.iconView {
|
||||
iconView = current
|
||||
} else {
|
||||
iconView = UIImageView(image: readIconImage)
|
||||
iconView.tintColor = component.theme.list.itemSecondaryTextColor
|
||||
self.iconView = iconView
|
||||
self.containerButton.addSubview(iconView)
|
||||
}
|
||||
|
||||
if let image = iconView.image {
|
||||
iconLabelOffset = image.size.width + 4.0
|
||||
transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing + 3.0 + floor((labelSize.height - image.size.height) * 0.5)), size: image.size))
|
||||
}
|
||||
} else if let iconView = self.iconView {
|
||||
self.iconView = nil
|
||||
iconView.removeFromSuperview()
|
||||
}
|
||||
|
||||
if labelView.superview == nil {
|
||||
labelView.isUserInteractionEnabled = false
|
||||
self.containerButton.addSubview(labelView)
|
||||
}
|
||||
|
||||
let labelFrame: CGRect
|
||||
switch component.style {
|
||||
case .generic:
|
||||
labelFrame = CGRect(origin: CGPoint(x: titleFrame.minX + iconLabelOffset, y: titleFrame.maxY + titleSpacing), size: labelSize)
|
||||
case .compact:
|
||||
labelFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: labelSize)
|
||||
}
|
||||
|
||||
transition.setFrame(view: labelView, frame: labelFrame)
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
||||
}
|
||||
|
@ -843,7 +843,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
context: component.context,
|
||||
chatPeerId: nil,
|
||||
areCustomEmojiEnabled: true,
|
||||
hasTrending: false,
|
||||
hasTrending: true,
|
||||
hasSearch: true,
|
||||
hideBackground: true,
|
||||
sendGif: nil
|
||||
|
@ -1795,6 +1795,8 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.voiceMessagesRestrictedTooltipController = controller
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
|
||||
},
|
||||
paste: { _ in
|
||||
},
|
||||
audioRecorder: self.sendMessageContext.audioRecorderValue,
|
||||
videoRecordingStatus: !self.sendMessageContext.hasRecordedVideoPreview ? self.sendMessageContext.videoRecorderValue?.audioStatus : nil,
|
||||
isRecordingLocked: self.sendMessageContext.isMediaRecordingLocked,
|
||||
|
@ -159,8 +159,12 @@ final class StoryItemSetContainerSendMessage {
|
||||
self.view?.component?.controller()?.presentInGlobalOverlay(c, with: a)
|
||||
}
|
||||
},
|
||||
getNavigationController: {
|
||||
return self.view?.component?.controller()?.navigationController as? NavigationController
|
||||
getNavigationController: { [weak self] in
|
||||
if let self {
|
||||
return self.view?.component?.controller()?.navigationController as? NavigationController
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
requestLayout: { [weak self] transition in
|
||||
if let self {
|
||||
|
@ -18,7 +18,9 @@ swift_library(
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/InvisibleInkDustNode",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||
"//submodules/ChatTextLinkEditUI"
|
||||
"//submodules/ChatTextLinkEditUI",
|
||||
"//submodules/Pasteboard",
|
||||
"//submodules/ImageTransparency",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -9,7 +9,10 @@ import InvisibleInkDustNode
|
||||
import EmojiTextAttachmentView
|
||||
import AccountContext
|
||||
import TextFormat
|
||||
import Pasteboard
|
||||
import ChatTextLinkEditUI
|
||||
import MobileCoreServices
|
||||
import ImageTransparency
|
||||
|
||||
public final class EmptyInputView: UIView, UIInputViewAudioFeedback {
|
||||
public var enableInputClicksWhenVisible: Bool {
|
||||
@ -51,6 +54,14 @@ public final class TextFieldComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public enum PasteData {
|
||||
case sticker(image: UIImage, isMemoji: Bool)
|
||||
case images([UIImage])
|
||||
case video(Data)
|
||||
case gif(Data)
|
||||
}
|
||||
|
||||
|
||||
public final class AnimationHint {
|
||||
public enum Kind {
|
||||
case textChanged
|
||||
@ -72,6 +83,7 @@ public final class TextFieldComponent: Component {
|
||||
public let insets: UIEdgeInsets
|
||||
public let hideKeyboard: Bool
|
||||
public let present: (ViewController) -> Void
|
||||
public let paste: (PasteData) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
@ -81,7 +93,8 @@ public final class TextFieldComponent: Component {
|
||||
textColor: UIColor,
|
||||
insets: UIEdgeInsets,
|
||||
hideKeyboard: Bool,
|
||||
present: @escaping (ViewController) -> Void
|
||||
present: @escaping (ViewController) -> Void,
|
||||
paste: @escaping (PasteData) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.strings = strings
|
||||
@ -91,6 +104,7 @@ public final class TextFieldComponent: Component {
|
||||
self.insets = insets
|
||||
self.hideKeyboard = hideKeyboard
|
||||
self.present = present
|
||||
self.paste = paste
|
||||
}
|
||||
|
||||
public static func ==(lhs: TextFieldComponent, rhs: TextFieldComponent) -> Bool {
|
||||
@ -131,11 +145,21 @@ public final class TextFieldComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
final class TextView: UITextView {
|
||||
var onPaste: () -> Bool = { return true }
|
||||
|
||||
override func paste(_ sender: Any?) {
|
||||
if self.onPaste() {
|
||||
super.paste(sender)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView, UITextViewDelegate, UIScrollViewDelegate {
|
||||
private let textContainer: NSTextContainer
|
||||
private let textStorage: NSTextStorage
|
||||
private let layoutManager: NSLayoutManager
|
||||
private let textView: UITextView
|
||||
private let textView: TextView
|
||||
|
||||
private var spoilerView: InvisibleInkDustView?
|
||||
private var customEmojiContainerView: CustomEmojiContainerView?
|
||||
@ -163,7 +187,7 @@ public final class TextFieldComponent: Component {
|
||||
self.layoutManager.addTextContainer(self.textContainer)
|
||||
self.textStorage.addLayoutManager(self.layoutManager)
|
||||
|
||||
self.textView = UITextView(frame: CGRect(), textContainer: self.textContainer)
|
||||
self.textView = TextView(frame: CGRect(), textContainer: self.textContainer)
|
||||
self.textView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.textView.backgroundColor = nil
|
||||
self.textView.layer.isOpaque = false
|
||||
@ -185,6 +209,10 @@ public final class TextFieldComponent: Component {
|
||||
NSAttributedString.Key.font: Font.regular(17.0),
|
||||
NSAttributedString.Key.foregroundColor: UIColor.white
|
||||
]
|
||||
|
||||
self.textView.onPaste = { [weak self] in
|
||||
return self?.onPaste() ?? false
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -224,6 +252,81 @@ public final class TextFieldComponent: Component {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
|
||||
}
|
||||
|
||||
private func onPaste() -> Bool {
|
||||
guard let component = self.component else {
|
||||
return false
|
||||
}
|
||||
let pasteboard = UIPasteboard.general
|
||||
|
||||
var attributedString: NSAttributedString?
|
||||
if let data = pasteboard.data(forPasteboardType: kUTTypeRTF as String) {
|
||||
attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtf)
|
||||
} else if let data = pasteboard.data(forPasteboardType: "com.apple.flat-rtfd") {
|
||||
attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtfd)
|
||||
}
|
||||
|
||||
if let attributedString = attributedString {
|
||||
self.updateInputState { current in
|
||||
if let inputText = current.inputText.mutableCopy() as? NSMutableAttributedString {
|
||||
inputText.replaceCharacters(in: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count), with: attributedString)
|
||||
let updatedRange = current.selectionRange.lowerBound + attributedString.length
|
||||
return InputState(inputText: inputText, selectionRange: updatedRange ..< updatedRange)
|
||||
} else {
|
||||
return InputState(inputText: attributedString)
|
||||
}
|
||||
}
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
|
||||
return false
|
||||
}
|
||||
|
||||
var images: [UIImage] = []
|
||||
if let data = pasteboard.data(forPasteboardType: "com.compuserve.gif") {
|
||||
component.paste(.gif(data))
|
||||
return false
|
||||
} else if let data = pasteboard.data(forPasteboardType: "public.mpeg-4") {
|
||||
component.paste(.video(data))
|
||||
return false
|
||||
} else {
|
||||
var isPNG = false
|
||||
var isMemoji = false
|
||||
for item in pasteboard.items {
|
||||
if let image = item["com.apple.png-sticker"] as? UIImage {
|
||||
images.append(image)
|
||||
isPNG = true
|
||||
isMemoji = true
|
||||
} else if let image = item[kUTTypePNG as String] as? UIImage {
|
||||
images.append(image)
|
||||
isPNG = true
|
||||
} else if let image = item["com.apple.uikit.image"] as? UIImage {
|
||||
images.append(image)
|
||||
isPNG = true
|
||||
} else if let image = item[kUTTypeJPEG as String] as? UIImage {
|
||||
images.append(image)
|
||||
} else if let image = item[kUTTypeGIF as String] as? UIImage {
|
||||
images.append(image)
|
||||
}
|
||||
}
|
||||
|
||||
if isPNG && images.count == 1, let image = images.first, let cgImage = image.cgImage {
|
||||
let maxSide = max(image.size.width, image.size.height)
|
||||
if maxSide.isZero {
|
||||
return false
|
||||
}
|
||||
let aspectRatio = min(image.size.width, image.size.height) / maxSide
|
||||
if isMemoji || (imageHasTransparency(cgImage) && aspectRatio > 0.2) {
|
||||
component.paste(.sticker(image: image, isMemoji: isMemoji))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if !images.isEmpty {
|
||||
component.paste(.images(images))
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public func textViewDidChange(_ textView: UITextView) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
@ -257,7 +360,7 @@ public final class TextFieldComponent: Component {
|
||||
public func textViewDidEndEditing(_ textView: UITextView) {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
|
||||
}
|
||||
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
public func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
|
||||
let filteredActions: Set<String> = Set([
|
||||
|
Loading…
x
Reference in New Issue
Block a user