mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-06 14:25:04 +00:00
Merge commit '14c595b53ff5ab97fc9ea80a79d5e783c77a2302'
This commit is contained in:
commit
f1075ca8bd
@ -1243,7 +1243,7 @@ public final class ChatListNode: ListView {
|
|||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
//self.useMainQueueTransactions = true
|
self.useMainQueueTransactions = true
|
||||||
|
|
||||||
self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
|
self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
|
||||||
self.verticalScrollIndicatorFollowsOverscroll = true
|
self.verticalScrollIndicatorFollowsOverscroll = true
|
||||||
|
|||||||
@ -2440,6 +2440,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
|||||||
self.requestUpdate(transition: .easeInOut(duration: 0.2))
|
self.requestUpdate(transition: .easeInOut(duration: 0.2))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onTextEditingEnded: { _ in },
|
||||||
getCurrentImage: { [weak controller] in
|
getCurrentImage: { [weak controller] in
|
||||||
return controller?.getCurrentImage()
|
return controller?.getCurrentImage()
|
||||||
},
|
},
|
||||||
@ -2954,6 +2955,8 @@ public final class DrawingToolsInteraction {
|
|||||||
private let updateColor: (DrawingColor) -> Void
|
private let updateColor: (DrawingColor) -> Void
|
||||||
|
|
||||||
private let onInteractionUpdated: (Bool) -> Void
|
private let onInteractionUpdated: (Bool) -> Void
|
||||||
|
private let onTextEditingEnded: (Bool) -> Void
|
||||||
|
|
||||||
private let getCurrentImage: () -> UIImage?
|
private let getCurrentImage: () -> UIImage?
|
||||||
private let getControllerNode: () -> ASDisplayNode?
|
private let getControllerNode: () -> ASDisplayNode?
|
||||||
private let present: (ViewController, PresentationContextType, Any?) -> Void
|
private let present: (ViewController, PresentationContextType, Any?) -> Void
|
||||||
@ -2980,6 +2983,7 @@ public final class DrawingToolsInteraction {
|
|||||||
updateVideoPlayback: @escaping (Bool) -> Void,
|
updateVideoPlayback: @escaping (Bool) -> Void,
|
||||||
updateColor: @escaping (DrawingColor) -> Void,
|
updateColor: @escaping (DrawingColor) -> Void,
|
||||||
onInteractionUpdated: @escaping (Bool) -> Void,
|
onInteractionUpdated: @escaping (Bool) -> Void,
|
||||||
|
onTextEditingEnded: @escaping (Bool) -> Void,
|
||||||
getCurrentImage: @escaping () -> UIImage?,
|
getCurrentImage: @escaping () -> UIImage?,
|
||||||
getControllerNode: @escaping () -> ASDisplayNode?,
|
getControllerNode: @escaping () -> ASDisplayNode?,
|
||||||
present: @escaping (ViewController, PresentationContextType, Any?) -> Void,
|
present: @escaping (ViewController, PresentationContextType, Any?) -> Void,
|
||||||
@ -2994,6 +2998,7 @@ public final class DrawingToolsInteraction {
|
|||||||
self.updateVideoPlayback = updateVideoPlayback
|
self.updateVideoPlayback = updateVideoPlayback
|
||||||
self.updateColor = updateColor
|
self.updateColor = updateColor
|
||||||
self.onInteractionUpdated = onInteractionUpdated
|
self.onInteractionUpdated = onInteractionUpdated
|
||||||
|
self.onTextEditingEnded = onTextEditingEnded
|
||||||
self.getCurrentImage = getCurrentImage
|
self.getCurrentImage = getCurrentImage
|
||||||
self.getControllerNode = getControllerNode
|
self.getControllerNode = getControllerNode
|
||||||
self.present = present
|
self.present = present
|
||||||
@ -3123,6 +3128,7 @@ public final class DrawingToolsInteraction {
|
|||||||
public func endTextEditing(reset: Bool) {
|
public func endTextEditing(reset: Bool) {
|
||||||
if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView {
|
if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView {
|
||||||
entityView.endEditing(reset: reset)
|
entityView.endEditing(reset: reset)
|
||||||
|
self.onTextEditingEnded(reset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -500,12 +500,70 @@ public class StickerPickerScreen: ViewController {
|
|||||||
self.content = content
|
self.content = content
|
||||||
|
|
||||||
content.emoji.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
|
content.emoji.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
|
||||||
performItemAction: { [weak self] _, item, _, _, _, _ in
|
performItemAction: { [weak self] groupId, item, _, _, _, _ in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self, let controller = strongSelf.controller else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if let file = item.itemFile {
|
let context = controller.context
|
||||||
|
if groupId == AnyHashable("featuredTop"), let file = item.itemFile {
|
||||||
|
let _ = (
|
||||||
|
combineLatest(
|
||||||
|
ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: controller.context.account.peerId, premiumIfSavedMessages: true),
|
||||||
|
ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: controller.context.account.peerId, premiumIfSavedMessages: false)
|
||||||
|
)
|
||||||
|
|> take(1)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] hasPremium, hasGlobalPremium in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)
|
||||||
|
let _ = (combineLatest(
|
||||||
|
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
|
||||||
|
context.account.postbox.combinedView(keys: [viewKey])
|
||||||
|
)
|
||||||
|
|> take(1)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] emojiPacksView, views in
|
||||||
|
guard let view = views.views[viewKey] as? OrderedItemListView else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var installedCollectionIds = Set<ItemCollectionId>()
|
||||||
|
for (id, _, _) in emojiPacksView.collectionInfos {
|
||||||
|
installedCollectionIds.insert(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
let stickerPacks = view.items.map({ $0.contents.get(FeaturedStickerPackItem.self)! }).filter({
|
||||||
|
!installedCollectionIds.contains($0.info.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
for featuredStickerPack in stickerPacks {
|
||||||
|
if featuredStickerPack.topItems.contains(where: { $0.file.fileId == file.fileId }) {
|
||||||
|
if let componentView = self.hostView.componentView as? StickerSelectionComponent.View {
|
||||||
|
if let pagerView = componentView.keyboardView.view as? EntityKeyboardComponent.View, let emojiInputInteraction = self.content?.emoji.inputInteractionHolder.inputInteraction {
|
||||||
|
pagerView.openCustomSearch(content: EmojiSearchContent(
|
||||||
|
context: context,
|
||||||
|
forceTheme: defaultDarkPresentationTheme,
|
||||||
|
items: stickerPacks,
|
||||||
|
initialFocusId: featuredStickerPack.info.id,
|
||||||
|
hasPremiumForUse: hasPremium,
|
||||||
|
hasPremiumForInstallation: hasGlobalPremium,
|
||||||
|
parentInputInteraction: emojiInputInteraction
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else if let file = item.itemFile {
|
||||||
strongSelf.controller?.completion(.file(file))
|
strongSelf.controller?.completion(.file(file))
|
||||||
|
strongSelf.controller?.dismiss(animated: true)
|
||||||
} else if case let .staticEmoji(emoji) = item.content {
|
} 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
|
if let image = generateImage(CGSize(width: 256.0, height: 256.0), scale: 1.0, rotatedContext: { size, context in
|
||||||
context.clear(CGRect(origin: .zero, size: size))
|
context.clear(CGRect(origin: .zero, size: size))
|
||||||
@ -528,8 +586,8 @@ public class StickerPickerScreen: ViewController {
|
|||||||
}) {
|
}) {
|
||||||
strongSelf.controller?.completion(.image(image))
|
strongSelf.controller?.completion(.image(image))
|
||||||
}
|
}
|
||||||
|
strongSelf.controller?.dismiss(animated: true)
|
||||||
}
|
}
|
||||||
strongSelf.controller?.dismiss(animated: true)
|
|
||||||
},
|
},
|
||||||
deleteBackwards: nil,
|
deleteBackwards: nil,
|
||||||
openStickerSettings: nil,
|
openStickerSettings: nil,
|
||||||
@ -870,12 +928,42 @@ public class StickerPickerScreen: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content.stickers?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
|
content.stickers?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
|
||||||
performItemAction: { [weak self] _, item, _, _, _, _ in
|
performItemAction: { [weak self] groupId, item, _, _, _, _ in
|
||||||
guard let strongSelf = self, let file = item.itemFile else {
|
guard let self, let controller = self.controller, let file = item.itemFile else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.controller?.completion(.file(file))
|
if groupId == AnyHashable("featuredTop") {
|
||||||
strongSelf.controller?.dismiss(animated: true)
|
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,
|
deleteBackwards: nil,
|
||||||
openStickerSettings: nil,
|
openStickerSettings: nil,
|
||||||
|
|||||||
@ -840,8 +840,8 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||||||
self.controlsContainerNode.addSubnode(self.stripContainerNode)
|
self.controlsContainerNode.addSubnode(self.stripContainerNode)
|
||||||
self.controlsClippingNode.addSubnode(self.controlsContainerNode)
|
self.controlsClippingNode.addSubnode(self.controlsContainerNode)
|
||||||
self.controlsClippingOffsetNode.addSubnode(self.controlsClippingNode)
|
self.controlsClippingOffsetNode.addSubnode(self.controlsClippingNode)
|
||||||
self.addSubnode(self.setByYouNode)
|
self.stripContainerNode.addSubnode(self.setByYouNode)
|
||||||
self.addSubnode(self.setByYouImageNode)
|
self.stripContainerNode.addSubnode(self.setByYouImageNode)
|
||||||
|
|
||||||
self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
|
self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -1435,7 +1435,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||||||
transition.updateAlpha(node: self.setByYouNode, alpha: 0.7)
|
transition.updateAlpha(node: self.setByYouNode, alpha: 0.7)
|
||||||
self.setByYouNode.attributedText = NSAttributedString(string: photoTitle, font: Font.regular(12.0), textColor: UIColor.white)
|
self.setByYouNode.attributedText = NSAttributedString(string: photoTitle, font: Font.regular(12.0), textColor: UIColor.white)
|
||||||
let setByYouSize = self.setByYouNode.updateLayout(size)
|
let setByYouSize = self.setByYouNode.updateLayout(size)
|
||||||
self.setByYouNode.frame = CGRect(origin: CGPoint(x: size.width - setByYouSize.width - 14.0, y: size.height - setByYouSize.height - 18.0), size: setByYouSize)
|
self.setByYouNode.frame = CGRect(origin: CGPoint(x: size.width - setByYouSize.width - 14.0, y: size.height - setByYouSize.height - 58.0), size: setByYouSize)
|
||||||
self.setByYouNode.isUserInteractionEnabled = hasLink
|
self.setByYouNode.isUserInteractionEnabled = hasLink
|
||||||
} else {
|
} else {
|
||||||
transition.updateAlpha(node: self.setByYouNode, alpha: 0.0)
|
transition.updateAlpha(node: self.setByYouNode, alpha: 0.0)
|
||||||
|
|||||||
@ -284,8 +284,6 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
|
|
||||||
public var isReactionSearchActive: Bool = false
|
public var isReactionSearchActive: Bool = false
|
||||||
|
|
||||||
public var reduceMotion: Bool = false
|
|
||||||
|
|
||||||
public static func randomGenericReactionEffect(context: AccountContext) -> Signal<String?, NoError> {
|
public static func randomGenericReactionEffect(context: AccountContext) -> Signal<String?, NoError> {
|
||||||
return context.engine.stickers.loadedStickerPack(reference: .emojiGenericAnimations, forceActualized: false)
|
return context.engine.stickers.loadedStickerPack(reference: .emojiGenericAnimations, forceActualized: false)
|
||||||
|> map { result -> [TelegramMediaFile]? in
|
|> map { result -> [TelegramMediaFile]? in
|
||||||
@ -909,7 +907,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if animateIn {
|
if animateIn {
|
||||||
itemNode.appear(animated: !self.context.sharedContext.currentPresentationData.with({ $0 }).reduceMotion && !self.reduceMotion)
|
itemNode.appear(animated: !self.context.sharedContext.currentPresentationData.with({ $0 }).reduceMotion)
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.getEmojiContent != nil, i == itemLayout.visibleItemCount - 1, let itemNode = itemNode as? ReactionNode {
|
if self.getEmojiContent != nil, i == itemLayout.visibleItemCount - 1, let itemNode = itemNode as? ReactionNode {
|
||||||
@ -1199,7 +1197,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let animateInFromAnchorRect = animateInFromAnchorRect, !self.reduceMotion {
|
if let animateInFromAnchorRect = animateInFromAnchorRect {
|
||||||
let springDuration: Double = 0.5
|
let springDuration: Double = 0.5
|
||||||
let springDamping: CGFloat = 104.0
|
let springDamping: CGFloat = 104.0
|
||||||
let springScaleDelay: Double = 0.1
|
let springScaleDelay: Double = 0.1
|
||||||
@ -1608,13 +1606,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
|
|
||||||
let mainCircleDelay: Double = 0.01
|
let mainCircleDelay: Double = 0.01
|
||||||
|
|
||||||
if !self.presentationData.reduceMotion && !self.reduceMotion {
|
self.backgroundNode.animateIn()
|
||||||
self.backgroundNode.animateIn()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.didAnimateIn = true
|
self.didAnimateIn = true
|
||||||
|
|
||||||
if !self.presentationData.reduceMotion && !self.reduceMotion {
|
if !self.presentationData.reduceMotion {
|
||||||
for i in 0 ..< self.items.count {
|
for i in 0 ..< self.items.count {
|
||||||
guard let itemNode = self.visibleItemNodes[i] else {
|
guard let itemNode = self.visibleItemNodes[i] else {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -466,18 +466,17 @@ public final class PeerStoryListContext {
|
|||||||
self.peerId = peerId
|
self.peerId = peerId
|
||||||
self.isArchived = isArchived
|
self.isArchived = isArchived
|
||||||
|
|
||||||
self.stateValue = State(peerReference: nil, items: [], totalCount: 0, loadMoreToken: 0, isCached: true, allEntityFiles: [:])
|
self.stateValue = State(peerReference: nil, items: [], totalCount: 0, loadMoreToken: 0, isCached: true)
|
||||||
|
|
||||||
let _ = (account.postbox.transaction { transaction -> (PeerReference?, [EngineStoryItem], Int, [MediaId: TelegramMediaFile]) in
|
let _ = (account.postbox.transaction { transaction -> (PeerReference?, [EngineStoryItem], Int) in
|
||||||
let key = ValueBoxKey(length: 8 + 1)
|
let key = ValueBoxKey(length: 8 + 1)
|
||||||
key.setInt64(0, value: peerId.toInt64())
|
key.setInt64(0, value: peerId.toInt64())
|
||||||
key.setInt8(8, value: isArchived ? 1 : 0)
|
key.setInt8(8, value: isArchived ? 1 : 0)
|
||||||
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self)
|
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self)
|
||||||
guard let cached = cached else {
|
guard let cached = cached else {
|
||||||
return (nil, [], 0, [:])
|
return (nil, [], 0)
|
||||||
}
|
}
|
||||||
var items: [EngineStoryItem] = []
|
var items: [EngineStoryItem] = []
|
||||||
var allEntityFiles: [MediaId: TelegramMediaFile] = [:]
|
|
||||||
for storedItem in cached.items {
|
for storedItem in cached.items {
|
||||||
if case let .item(item) = storedItem, let media = item.media {
|
if case let .item(item) = storedItem, let media = item.media {
|
||||||
let mappedItem = EngineStoryItem(
|
let mappedItem = EngineStoryItem(
|
||||||
@ -505,30 +504,19 @@ public final class PeerStoryListContext {
|
|||||||
isEdited: item.isEdited
|
isEdited: item.isEdited
|
||||||
)
|
)
|
||||||
items.append(mappedItem)
|
items.append(mappedItem)
|
||||||
|
|
||||||
for entity in mappedItem.entities {
|
|
||||||
if case let .CustomEmoji(_, fileId) = entity.type {
|
|
||||||
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
|
|
||||||
if allEntityFiles[mediaId] == nil {
|
|
||||||
if let file = transaction.getMedia(mediaId) as? TelegramMediaFile {
|
|
||||||
allEntityFiles[file.fileId] = file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let peerReference = transaction.getPeer(peerId).flatMap(PeerReference.init)
|
let peerReference = transaction.getPeer(peerId).flatMap(PeerReference.init)
|
||||||
|
|
||||||
return (peerReference, items, Int(cached.totalCount), allEntityFiles)
|
return (peerReference, items, Int(cached.totalCount))
|
||||||
}
|
}
|
||||||
|> deliverOn(self.queue)).start(next: { [weak self] peerReference, items, totalCount, allEntityFiles in
|
|> deliverOn(self.queue)).start(next: { [weak self] peerReference, items, totalCount in
|
||||||
guard let `self` = self else {
|
guard let `self` = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.stateValue = State(peerReference: peerReference, items: items, totalCount: totalCount, loadMoreToken: 0, isCached: true, allEntityFiles: allEntityFiles)
|
self.stateValue = State(peerReference: peerReference, items: items, totalCount: totalCount, loadMoreToken: 0, isCached: true)
|
||||||
self.loadMore()
|
self.loadMore()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -840,22 +828,19 @@ public final class PeerStoryListContext {
|
|||||||
public var totalCount: Int
|
public var totalCount: Int
|
||||||
public var loadMoreToken: Int?
|
public var loadMoreToken: Int?
|
||||||
public var isCached: Bool
|
public var isCached: Bool
|
||||||
public var allEntityFiles: [MediaId: TelegramMediaFile]
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
peerReference: PeerReference?,
|
peerReference: PeerReference?,
|
||||||
items: [EngineStoryItem],
|
items: [EngineStoryItem],
|
||||||
totalCount: Int,
|
totalCount: Int,
|
||||||
loadMoreToken: Int?,
|
loadMoreToken: Int?,
|
||||||
isCached: Bool,
|
isCached: Bool
|
||||||
allEntityFiles: [MediaId: TelegramMediaFile]
|
|
||||||
) {
|
) {
|
||||||
self.peerReference = peerReference
|
self.peerReference = peerReference
|
||||||
self.items = items
|
self.items = items
|
||||||
self.totalCount = totalCount
|
self.totalCount = totalCount
|
||||||
self.loadMoreToken = loadMoreToken
|
self.loadMoreToken = loadMoreToken
|
||||||
self.isCached = isCached
|
self.isCached = isCached
|
||||||
self.allEntityFiles = allEntityFiles
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -841,6 +841,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
if let pagerView = self.entityKeyboardView.componentView as? EntityKeyboardComponent.View, let emojiInputInteraction = self.emojiInputInteraction {
|
if let pagerView = self.entityKeyboardView.componentView as? EntityKeyboardComponent.View, let emojiInputInteraction = self.emojiInputInteraction {
|
||||||
pagerView.openCustomSearch(content: EmojiSearchContent(
|
pagerView.openCustomSearch(content: EmojiSearchContent(
|
||||||
context: self.context,
|
context: self.context,
|
||||||
|
forceTheme: self.interaction?.forceTheme,
|
||||||
items: stickerPacks,
|
items: stickerPacks,
|
||||||
initialFocusId: featuredStickerPack.info.id,
|
initialFocusId: featuredStickerPack.info.id,
|
||||||
hasPremiumForUse: hasPremium,
|
hasPremiumForUse: hasPremium,
|
||||||
|
|||||||
@ -145,7 +145,7 @@ public final class ChatListNavigationBar: Component {
|
|||||||
|
|
||||||
public static let searchScrollHeight: CGFloat = 52.0
|
public static let searchScrollHeight: CGFloat = 52.0
|
||||||
public static let storiesScrollHeight: CGFloat = {
|
public static let storiesScrollHeight: CGFloat = {
|
||||||
return 83.0
|
return 79.0
|
||||||
}()
|
}()
|
||||||
|
|
||||||
public final class View: UIView {
|
public final class View: UIView {
|
||||||
@ -404,7 +404,7 @@ public final class ChatListNavigationBar: Component {
|
|||||||
if component.statusBarHeight < 1.0 {
|
if component.statusBarHeight < 1.0 {
|
||||||
headerContentY = 0.0
|
headerContentY = 0.0
|
||||||
} else {
|
} else {
|
||||||
headerContentY = component.statusBarHeight + 5.0
|
headerContentY = component.statusBarHeight + 12.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let headerContentFrame = CGRect(origin: CGPoint(x: 0.0, y: headerContentY), size: headerContentSize)
|
let headerContentFrame = CGRect(origin: CGPoint(x: 0.0, y: headerContentY), size: headerContentSize)
|
||||||
@ -579,7 +579,7 @@ public final class ChatListNavigationBar: Component {
|
|||||||
var contentHeight = component.statusBarHeight
|
var contentHeight = component.statusBarHeight
|
||||||
|
|
||||||
if component.statusBarHeight >= 1.0 {
|
if component.statusBarHeight >= 1.0 {
|
||||||
contentHeight += 3.0
|
contentHeight += 10.0
|
||||||
}
|
}
|
||||||
contentHeight += 44.0
|
contentHeight += 44.0
|
||||||
|
|
||||||
|
|||||||
@ -374,7 +374,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
|||||||
let isTemplate = file.isCustomTemplateEmoji
|
let isTemplate = file.isCustomTemplateEmoji
|
||||||
|
|
||||||
let context = self.context
|
let context = self.context
|
||||||
if file.isAnimatedSticker || file.isVideoEmoji {
|
if file.isAnimatedSticker || file.isVideoSticker || file.isVideoEmoji {
|
||||||
let keyframeOnly = self.pixelSize.width >= 120.0
|
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))
|
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 context: AccountContext
|
||||||
|
private let forceTheme: PresentationTheme?
|
||||||
private var initialFocusId: ItemCollectionId?
|
private var initialFocusId: ItemCollectionId?
|
||||||
private let hasPremiumForUse: Bool
|
private let hasPremiumForUse: Bool
|
||||||
private let hasPremiumForInstallation: Bool
|
private let hasPremiumForInstallation: Bool
|
||||||
@ -70,6 +71,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
|
|||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
|
forceTheme: PresentationTheme?,
|
||||||
items: [FeaturedStickerPackItem],
|
items: [FeaturedStickerPackItem],
|
||||||
initialFocusId: ItemCollectionId?,
|
initialFocusId: ItemCollectionId?,
|
||||||
hasPremiumForUse: Bool,
|
hasPremiumForUse: Bool,
|
||||||
@ -77,12 +79,17 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
|
|||||||
parentInputInteraction: EmojiPagerContentComponent.InputInteraction
|
parentInputInteraction: EmojiPagerContentComponent.InputInteraction
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
self.forceTheme = forceTheme
|
||||||
self.initialFocusId = initialFocusId
|
self.initialFocusId = initialFocusId
|
||||||
self.hasPremiumForUse = hasPremiumForUse
|
self.hasPremiumForUse = hasPremiumForUse
|
||||||
self.hasPremiumForInstallation = hasPremiumForInstallation
|
self.hasPremiumForInstallation = hasPremiumForInstallation
|
||||||
self.parentInputInteraction = parentInputInteraction
|
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.panelHostView = PagerExternalTopPanelContainer()
|
||||||
self.inputInteractionHolder = EmojiPagerContentComponent.InputInteractionHolder()
|
self.inputInteractionHolder = EmojiPagerContentComponent.InputInteractionHolder()
|
||||||
|
|||||||
@ -916,7 +916,6 @@ public final class EntityKeyboardComponent: Component {
|
|||||||
|
|
||||||
if component.useExternalSearchContainer, let containerNode = component.makeSearchContainerNode(contentType) {
|
if component.useExternalSearchContainer, let containerNode = component.makeSearchContainerNode(contentType) {
|
||||||
let controller = EntitySearchContainerController(containerNode: containerNode)
|
let controller = EntitySearchContainerController(containerNode: containerNode)
|
||||||
|
|
||||||
self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.pushController(controller)
|
self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.pushController(controller)
|
||||||
} else {
|
} else {
|
||||||
self.searchComponent = EntitySearchContentComponent(
|
self.searchComponent = EntitySearchContentComponent(
|
||||||
@ -941,14 +940,19 @@ public final class EntityKeyboardComponent: Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.searchComponent = EntitySearchContentComponent(
|
if component.useExternalSearchContainer {
|
||||||
makeContainerNode: {
|
let controller = EntitySearchContainerController(containerNode: content)
|
||||||
return content
|
self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.pushController(controller)
|
||||||
},
|
} else {
|
||||||
dismissSearch: { [weak self] in
|
self.searchComponent = EntitySearchContentComponent(
|
||||||
self?.closeSearch()
|
makeContainerNode: {
|
||||||
}
|
return content
|
||||||
)
|
},
|
||||||
|
dismissSearch: { [weak self] in
|
||||||
|
self?.closeSearch()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
component.hideInputUpdated(true, true, Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
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))
|
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.cropped(to: cropRect).samplingLinear()
|
||||||
ciImage = ciImage.transformed(by: CGAffineTransform(translationX: 0.0, y: -420.0))
|
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?
|
var circleMaskFilter: CIFilter?
|
||||||
if let current = self.circleMaskFilter {
|
if let current = self.circleMaskFilter {
|
||||||
|
|||||||
@ -363,7 +363,13 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
self.environment?.controller()?.presentInGlobalOverlay(c, with: a)
|
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
|
requestLayout: { [weak self] transition in
|
||||||
if let self {
|
if let self {
|
||||||
(self.environment?.controller() as? MediaEditorScreen)?.node.requestLayout(forceUpdate: true, transition: Transition(transition))
|
(self.environment?.controller() as? MediaEditorScreen)?.node.requestLayout(forceUpdate: true, transition: Transition(transition))
|
||||||
@ -985,6 +991,7 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
stateContext: self.inputMediaNodeStateContext
|
stateContext: self.inputMediaNodeStateContext
|
||||||
)
|
)
|
||||||
inputMediaNode.externalTopPanelContainerImpl = nil
|
inputMediaNode.externalTopPanelContainerImpl = nil
|
||||||
|
inputMediaNode.useExternalSearchContainer = true
|
||||||
if let inputPanelView = self.inputPanel.view, inputMediaNode.view.superview == nil {
|
if let inputPanelView = self.inputPanel.view, inputMediaNode.view.superview == nil {
|
||||||
self.insertSubview(inputMediaNode.view, belowSubview: inputPanelView)
|
self.insertSubview(inputMediaNode.view, belowSubview: inputPanelView)
|
||||||
}
|
}
|
||||||
@ -1135,6 +1142,9 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
forwardAction: nil,
|
forwardAction: nil,
|
||||||
moreAction: nil,
|
moreAction: nil,
|
||||||
presentVoiceMessagesUnavailableTooltip: nil,
|
presentVoiceMessagesUnavailableTooltip: nil,
|
||||||
|
paste: { data in
|
||||||
|
let _ = data
|
||||||
|
},
|
||||||
audioRecorder: nil,
|
audioRecorder: nil,
|
||||||
videoRecordingStatus: nil,
|
videoRecordingStatus: nil,
|
||||||
isRecordingLocked: false,
|
isRecordingLocked: false,
|
||||||
@ -2057,6 +2067,18 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
self.requestUpdate(transition: .easeInOut(duration: 0.2))
|
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: {
|
getCurrentImage: {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@ -2202,13 +2224,33 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
if let layout = self.validLayout, (layout.inputHeight ?? 0.0) > 0.0 {
|
if let layout = self.validLayout, (layout.inputHeight ?? 0.0) > 0.0 {
|
||||||
self.view.endEditing(true)
|
self.view.endEditing(true)
|
||||||
} else {
|
} else {
|
||||||
let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .filled, animation: .none, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white))
|
self.insertTextEntity()
|
||||||
self.interaction?.insertEntity(textEntity)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
private func setupTransitionImage(_ image: UIImage) {
|
||||||
self.previewContainerView.alpha = 1.0
|
self.previewContainerView.alpha = 1.0
|
||||||
|
|
||||||
@ -2804,8 +2846,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
self.controller?.present(controller, in: .window(.root))
|
self.controller?.present(controller, in: .window(.root))
|
||||||
return
|
return
|
||||||
case .text:
|
case .text:
|
||||||
let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .filled, animation: .none, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white))
|
self.insertTextEntity()
|
||||||
self.interaction?.insertEntity(textEntity)
|
|
||||||
|
|
||||||
self.hasAnyChanges = true
|
self.hasAnyChanges = true
|
||||||
self.controller?.isSavingAvailable = true
|
self.controller?.isSavingAvailable = true
|
||||||
@ -2957,6 +2998,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
self.interaction?.containerLayoutUpdated(layout: layout, transition: transition)
|
self.interaction?.containerLayoutUpdated(layout: layout, transition: transition)
|
||||||
|
|
||||||
var layout = layout
|
var layout = layout
|
||||||
|
layout.intrinsicInsets.top = topInset
|
||||||
layout.intrinsicInsets.bottom = bottomInset + 60.0
|
layout.intrinsicInsets.bottom = bottomInset + 60.0
|
||||||
controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
|
controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
|
||||||
|
|
||||||
@ -3172,7 +3214,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
let initialPrivacy = privacy.privacy
|
let initialPrivacy = privacy.privacy
|
||||||
let timeout = privacy.timeout
|
let timeout = privacy.timeout
|
||||||
|
|
||||||
let controller = ShareWithPeersScreen(
|
let controller = ShareWithPeersScreen(
|
||||||
context: self.context,
|
context: self.context,
|
||||||
initialPrivacy: initialPrivacy,
|
initialPrivacy: initialPrivacy,
|
||||||
allowScreenshots: !privacy.isForwardingDisabled,
|
allowScreenshots: !privacy.isForwardingDisabled,
|
||||||
@ -3569,7 +3611,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !self.isEditingStory {
|
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 {
|
if mediaEditor.resultIsVideo {
|
||||||
|
|||||||
@ -5,15 +5,62 @@ import TelegramCore
|
|||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import MediaEditor
|
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 {
|
public final class MediaEditorStoredState: Codable {
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case privacy
|
case privacy
|
||||||
|
case textSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
public let privacy: MediaEditorResultPrivacy?
|
public let privacy: MediaEditorResultPrivacy?
|
||||||
|
public let textSettings: MediaEditorStoredTextSettings?
|
||||||
|
|
||||||
public init(privacy: MediaEditorResultPrivacy?) {
|
public init(privacy: MediaEditorResultPrivacy?, textSettings: MediaEditorStoredTextSettings?) {
|
||||||
self.privacy = privacy
|
self.privacy = privacy
|
||||||
|
self.textSettings = textSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
@ -24,12 +71,17 @@ public final class MediaEditorStoredState: Codable {
|
|||||||
} else {
|
} else {
|
||||||
self.privacy = nil
|
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 {
|
public func encode(to encoder: Encoder) throws {
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
if let privacy = self .privacy {
|
if let privacy = self.privacy {
|
||||||
if let data = try? JSONEncoder().encode(privacy) {
|
if let data = try? JSONEncoder().encode(privacy) {
|
||||||
try container.encode(data, forKey: .privacy)
|
try container.encode(data, forKey: .privacy)
|
||||||
} else {
|
} else {
|
||||||
@ -38,6 +90,24 @@ public final class MediaEditorStoredState: Codable {
|
|||||||
} else {
|
} else {
|
||||||
try container.encodeNil(forKey: .privacy)
|
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)
|
let key = EngineDataBuffer(length: 4)
|
||||||
key.setInt32(0, value: 0)
|
key.setInt32(0, value: 0)
|
||||||
|
|
||||||
if let state = state {
|
return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.mediaEditorState, id: key))
|
||||||
return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.mediaEditorState, id: key, item: state)
|
|> map { entry -> MediaEditorStoredState? in
|
||||||
} else {
|
return entry?.get(MediaEditorStoredState.self)
|
||||||
return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.mediaEditorState, id: key)
|
}
|
||||||
|
|> 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: {},
|
forwardAction: {},
|
||||||
moreAction: { _, _ in },
|
moreAction: { _, _ in },
|
||||||
presentVoiceMessagesUnavailableTooltip: nil,
|
presentVoiceMessagesUnavailableTooltip: nil,
|
||||||
|
paste: { _ in },
|
||||||
audioRecorder: nil,
|
audioRecorder: nil,
|
||||||
videoRecordingStatus: nil,
|
videoRecordingStatus: nil,
|
||||||
isRecordingLocked: false,
|
isRecordingLocked: false,
|
||||||
|
|||||||
@ -82,6 +82,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
public let forwardAction: (() -> Void)?
|
public let forwardAction: (() -> Void)?
|
||||||
public let moreAction: ((UIView, ContextGesture?) -> Void)?
|
public let moreAction: ((UIView, ContextGesture?) -> Void)?
|
||||||
public let presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?
|
public let presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?
|
||||||
|
public let paste: (TextFieldComponent.PasteData) -> Void
|
||||||
public let audioRecorder: ManagedAudioRecorder?
|
public let audioRecorder: ManagedAudioRecorder?
|
||||||
public let videoRecordingStatus: InstantVideoControllerRecordingStatus?
|
public let videoRecordingStatus: InstantVideoControllerRecordingStatus?
|
||||||
public let isRecordingLocked: Bool
|
public let isRecordingLocked: Bool
|
||||||
@ -121,6 +122,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
forwardAction: (() -> Void)?,
|
forwardAction: (() -> Void)?,
|
||||||
moreAction: ((UIView, ContextGesture?) -> Void)?,
|
moreAction: ((UIView, ContextGesture?) -> Void)?,
|
||||||
presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?,
|
presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?,
|
||||||
|
paste: @escaping (TextFieldComponent.PasteData) -> Void,
|
||||||
audioRecorder: ManagedAudioRecorder?,
|
audioRecorder: ManagedAudioRecorder?,
|
||||||
videoRecordingStatus: InstantVideoControllerRecordingStatus?,
|
videoRecordingStatus: InstantVideoControllerRecordingStatus?,
|
||||||
isRecordingLocked: Bool,
|
isRecordingLocked: Bool,
|
||||||
@ -159,6 +161,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
self.forwardAction = forwardAction
|
self.forwardAction = forwardAction
|
||||||
self.moreAction = moreAction
|
self.moreAction = moreAction
|
||||||
self.presentVoiceMessagesUnavailableTooltip = presentVoiceMessagesUnavailableTooltip
|
self.presentVoiceMessagesUnavailableTooltip = presentVoiceMessagesUnavailableTooltip
|
||||||
|
self.paste = paste
|
||||||
self.audioRecorder = audioRecorder
|
self.audioRecorder = audioRecorder
|
||||||
self.videoRecordingStatus = videoRecordingStatus
|
self.videoRecordingStatus = videoRecordingStatus
|
||||||
self.isRecordingLocked = isRecordingLocked
|
self.isRecordingLocked = isRecordingLocked
|
||||||
@ -521,6 +524,9 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
hideKeyboard: component.hideKeyboard,
|
hideKeyboard: component.hideKeyboard,
|
||||||
present: { c in
|
present: { c in
|
||||||
component.presentController(c)
|
component.presentController(c)
|
||||||
|
},
|
||||||
|
paste: { data in
|
||||||
|
component.paste(data)
|
||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
|
|||||||
@ -79,7 +79,8 @@ final class StickersResultPanelComponent: Component {
|
|||||||
self.itemsPerRow = itemsPerRow
|
self.itemsPerRow = itemsPerRow
|
||||||
self.itemCount = itemCount
|
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>? {
|
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
||||||
@ -445,7 +446,7 @@ final class StickersResultPanelComponent: Component {
|
|||||||
|
|
||||||
let itemLayout = ItemLayout(
|
let itemLayout = ItemLayout(
|
||||||
containerSize: CGSize(width: availableSize.width, height: minimizedHeight),
|
containerSize: CGSize(width: availableSize.width, height: minimizedHeight),
|
||||||
bottomInset: 9.0,
|
bottomInset: 40.0,
|
||||||
topInset: 9.0,
|
topInset: 9.0,
|
||||||
sideInset: sideInset,
|
sideInset: sideInset,
|
||||||
itemSize: CGSize(width: itemSize, height: itemSize),
|
itemSize: CGSize(width: itemSize, height: itemSize),
|
||||||
|
|||||||
@ -367,14 +367,17 @@ public final class PeerListItemComponent: Component {
|
|||||||
)
|
)
|
||||||
|
|
||||||
let titleSpacing: CGFloat = 2.0
|
let titleSpacing: CGFloat = 2.0
|
||||||
|
var titleVerticalOffset: CGFloat = 0.0
|
||||||
let centralContentHeight: CGFloat
|
let centralContentHeight: CGFloat
|
||||||
if labelSize.height > 0.0, case .generic = component.style {
|
if labelSize.height > 0.0, case .generic = component.style {
|
||||||
centralContentHeight = titleSize.height + labelSize.height + titleSpacing
|
centralContentHeight = titleSize.height + labelSize.height + titleSpacing
|
||||||
|
titleVerticalOffset = -1.0
|
||||||
} else {
|
} else {
|
||||||
centralContentHeight = titleSize.height
|
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 let titleView = self.title.view {
|
||||||
if titleView.superview == nil {
|
if titleView.superview == nil {
|
||||||
titleView.isUserInteractionEnabled = false
|
titleView.isUserInteractionEnabled = false
|
||||||
@ -396,46 +399,8 @@ public final class PeerListItemComponent: Component {
|
|||||||
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
|
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 animationCache = component.context.animationCache
|
||||||
let animationRenderer = component.context.animationRenderer
|
let animationRenderer = component.context.animationRenderer
|
||||||
|
|
||||||
@ -477,6 +442,45 @@ public final class PeerListItemComponent: Component {
|
|||||||
avatarIcon.view?.removeFromSuperview()
|
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 {
|
if themeUpdated {
|
||||||
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,11 +63,9 @@ public final class StoryContentContextImpl: StoryContentContext {
|
|||||||
),
|
),
|
||||||
context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global())
|
context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global())
|
||||||
)
|
)
|
||||||
|> mapToSignal { _, views, globalNotificationSettings -> Signal<(CombinedView, [PeerId: Peer], EngineGlobalNotificationSettings, [MediaId: TelegramMediaFile]), NoError> in
|
|> mapToSignal { _, views, globalNotificationSettings -> Signal<(CombinedView, [PeerId: Peer], EngineGlobalNotificationSettings), NoError> in
|
||||||
return context.account.postbox.transaction { transaction -> (CombinedView, [PeerId: Peer], EngineGlobalNotificationSettings, [MediaId: TelegramMediaFile]) in
|
return context.account.postbox.transaction { transaction -> (CombinedView, [PeerId: Peer], EngineGlobalNotificationSettings) in
|
||||||
var peers: [PeerId: Peer] = [:]
|
var peers: [PeerId: Peer] = [:]
|
||||||
var allEntityFiles: [MediaId: TelegramMediaFile] = [:]
|
|
||||||
|
|
||||||
if let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView {
|
if let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView {
|
||||||
for item in itemsView.items {
|
for item in itemsView.items {
|
||||||
if let item = item.value.get(Stories.StoredItem.self), case let .item(itemValue) = item {
|
if let item = item.value.get(Stories.StoredItem.self), case let .item(itemValue) = item {
|
||||||
@ -78,24 +76,13 @@ public final class StoryContentContextImpl: StoryContentContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for entity in itemValue.entities {
|
|
||||||
if case let .CustomEmoji(_, fileId) = entity.type {
|
|
||||||
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
|
|
||||||
if allEntityFiles[mediaId] == nil {
|
|
||||||
if let file = transaction.getMedia(mediaId) as? TelegramMediaFile {
|
|
||||||
allEntityFiles[file.fileId] = file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return (views, peers, globalNotificationSettings)
|
||||||
return (views, peers, globalNotificationSettings, allEntityFiles)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] views, peers, globalNotificationSettings, allEntityFiles in
|
|> deliverOnMainQueue).start(next: { [weak self] views, peers, globalNotificationSettings in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -270,8 +257,7 @@ public final class StoryContentContextImpl: StoryContentContext {
|
|||||||
return StoryContentItem(
|
return StoryContentItem(
|
||||||
position: nil,
|
position: nil,
|
||||||
peerId: peer.id,
|
peerId: peer.id,
|
||||||
storyItem: item,
|
storyItem: item
|
||||||
entityFiles: extractItemEntityFiles(item: item, allEntityFiles: allEntityFiles)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,8 +268,7 @@ public final class StoryContentContextImpl: StoryContentContext {
|
|||||||
item: StoryContentItem(
|
item: StoryContentItem(
|
||||||
position: mappedFocusedIndex ?? focusedIndex,
|
position: mappedFocusedIndex ?? focusedIndex,
|
||||||
peerId: peer.id,
|
peerId: peer.id,
|
||||||
storyItem: mappedItem,
|
storyItem: mappedItem
|
||||||
entityFiles: extractItemEntityFiles(item: mappedItem, allEntityFiles: allEntityFiles)
|
|
||||||
),
|
),
|
||||||
totalCount: totalCount,
|
totalCount: totalCount,
|
||||||
previousItemId: previousItemId,
|
previousItemId: previousItemId,
|
||||||
@ -922,12 +907,11 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
|||||||
TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: storyId.peerId),
|
TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: storyId.peerId),
|
||||||
TelegramEngine.EngineData.Item.NotificationSettings.Global()
|
TelegramEngine.EngineData.Item.NotificationSettings.Global()
|
||||||
),
|
),
|
||||||
context.account.postbox.transaction { transaction -> (Stories.StoredItem?, [PeerId: Peer], [MediaId: TelegramMediaFile]) in
|
context.account.postbox.transaction { transaction -> (Stories.StoredItem?, [PeerId: Peer]) in
|
||||||
guard let item = transaction.getStory(id: storyId)?.get(Stories.StoredItem.self) else {
|
guard let item = transaction.getStory(id: storyId)?.get(Stories.StoredItem.self) else {
|
||||||
return (nil, [:], [:])
|
return (nil, [:])
|
||||||
}
|
}
|
||||||
var peers: [PeerId: Peer] = [:]
|
var peers: [PeerId: Peer] = [:]
|
||||||
var allEntityFiles: [MediaId: TelegramMediaFile] = [:]
|
|
||||||
if case let .item(item) = item {
|
if case let .item(item) = item {
|
||||||
if let views = item.views {
|
if let views = item.views {
|
||||||
for id in views.seenPeerIds {
|
for id in views.seenPeerIds {
|
||||||
@ -936,18 +920,8 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for entity in item.entities {
|
|
||||||
if case let .CustomEmoji(_, fileId) = entity.type {
|
|
||||||
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
|
|
||||||
if allEntityFiles[mediaId] == nil {
|
|
||||||
if let file = transaction.getMedia(mediaId) as? TelegramMediaFile {
|
|
||||||
allEntityFiles[file.fileId] = file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return (item, peers, allEntityFiles)
|
return (item, peers)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] data, itemAndPeers in
|
|> deliverOnMainQueue).start(next: { [weak self] data, itemAndPeers in
|
||||||
@ -956,7 +930,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (peer, areVoiceMessagesAvailable, notificationSettings, globalNotificationSettings) = data
|
let (peer, areVoiceMessagesAvailable, notificationSettings, globalNotificationSettings) = data
|
||||||
let (item, peers, allEntityFiles) = itemAndPeers
|
let (item, peers) = itemAndPeers
|
||||||
|
|
||||||
guard let peer else {
|
guard let peer else {
|
||||||
return
|
return
|
||||||
@ -1007,8 +981,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
|||||||
let mainItem = StoryContentItem(
|
let mainItem = StoryContentItem(
|
||||||
position: 0,
|
position: 0,
|
||||||
peerId: peer.id,
|
peerId: peer.id,
|
||||||
storyItem: mappedItem,
|
storyItem: mappedItem
|
||||||
entityFiles: extractItemEntityFiles(item: mappedItem, allEntityFiles: allEntityFiles)
|
|
||||||
)
|
)
|
||||||
let stateValue = StoryContentContextState(
|
let stateValue = StoryContentContextState(
|
||||||
slice: StoryContentContextState.FocusedSlice(
|
slice: StoryContentContextState.FocusedSlice(
|
||||||
@ -1157,8 +1130,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
|
|||||||
return StoryContentItem(
|
return StoryContentItem(
|
||||||
position: nil,
|
position: nil,
|
||||||
peerId: peer.id,
|
peerId: peer.id,
|
||||||
storyItem: stateItem,
|
storyItem: stateItem
|
||||||
entityFiles: extractItemEntityFiles(item: stateItem, allEntityFiles: state.allEntityFiles)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1169,8 +1141,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
|
|||||||
item: StoryContentItem(
|
item: StoryContentItem(
|
||||||
position: nil,
|
position: nil,
|
||||||
peerId: peer.id,
|
peerId: peer.id,
|
||||||
storyItem: item,
|
storyItem: item
|
||||||
entityFiles: extractItemEntityFiles(item: item, allEntityFiles: state.allEntityFiles)
|
|
||||||
),
|
),
|
||||||
totalCount: state.totalCount,
|
totalCount: state.totalCount,
|
||||||
previousItemId: focusedIndex == 0 ? nil : state.items[focusedIndex - 1].id,
|
previousItemId: focusedIndex == 0 ? nil : state.items[focusedIndex - 1].id,
|
||||||
@ -1355,16 +1326,3 @@ public func preloadStoryMedia(context: AccountContext, peer: PeerReference, stor
|
|||||||
|
|
||||||
return combineLatest(signals) |> ignoreValues
|
return combineLatest(signals) |> ignoreValues
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractItemEntityFiles(item: EngineStoryItem, allEntityFiles: [MediaId: TelegramMediaFile]) -> [MediaId: TelegramMediaFile] {
|
|
||||||
var result: [MediaId: TelegramMediaFile] = [:]
|
|
||||||
for entity in item.entities {
|
|
||||||
if case let .CustomEmoji(_, fileId) = entity.type {
|
|
||||||
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
|
|
||||||
if let file = allEntityFiles[mediaId] {
|
|
||||||
result[file.fileId] = file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|||||||
@ -843,7 +843,7 @@ private final class StoryContainerScreenComponent: Component {
|
|||||||
context: component.context,
|
context: component.context,
|
||||||
chatPeerId: nil,
|
chatPeerId: nil,
|
||||||
areCustomEmojiEnabled: true,
|
areCustomEmojiEnabled: true,
|
||||||
hasTrending: false,
|
hasTrending: true,
|
||||||
hasSearch: true,
|
hasSearch: true,
|
||||||
hideBackground: true,
|
hideBackground: true,
|
||||||
sendGif: nil
|
sendGif: nil
|
||||||
@ -1123,19 +1123,10 @@ private final class StoryContainerScreenComponent: Component {
|
|||||||
|
|
||||||
switch self.audioMode {
|
switch self.audioMode {
|
||||||
case .ambient:
|
case .ambient:
|
||||||
if self.isMuteSwitchOn {
|
self.audioMode = .on
|
||||||
self.audioMode = .off
|
for (_, itemSetView) in self.visibleItemSetViews {
|
||||||
for (_, itemSetView) in self.visibleItemSetViews {
|
if let componentView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
|
||||||
if let componentView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
|
componentView.leaveAmbientMode()
|
||||||
componentView.enterAmbientMode(ambient: !self.isMuteSwitchOn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.audioMode = .on
|
|
||||||
for (_, itemSetView) in self.visibleItemSetViews {
|
|
||||||
if let componentView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
|
|
||||||
componentView.leaveAmbientMode()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .on:
|
case .on:
|
||||||
|
|||||||
@ -80,18 +80,15 @@ public final class StoryContentItem: Equatable {
|
|||||||
public let position: Int?
|
public let position: Int?
|
||||||
public let peerId: EnginePeer.Id?
|
public let peerId: EnginePeer.Id?
|
||||||
public let storyItem: EngineStoryItem
|
public let storyItem: EngineStoryItem
|
||||||
public let entityFiles: [EngineMedia.Id: TelegramMediaFile]
|
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
position: Int?,
|
position: Int?,
|
||||||
peerId: EnginePeer.Id?,
|
peerId: EnginePeer.Id?,
|
||||||
storyItem: EngineStoryItem,
|
storyItem: EngineStoryItem
|
||||||
entityFiles: [EngineMedia.Id: TelegramMediaFile]
|
|
||||||
) {
|
) {
|
||||||
self.position = position
|
self.position = position
|
||||||
self.peerId = peerId
|
self.peerId = peerId
|
||||||
self.storyItem = storyItem
|
self.storyItem = storyItem
|
||||||
self.entityFiles = entityFiles
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: StoryContentItem, rhs: StoryContentItem) -> Bool {
|
public static func ==(lhs: StoryContentItem, rhs: StoryContentItem) -> Bool {
|
||||||
@ -104,9 +101,6 @@ public final class StoryContentItem: Equatable {
|
|||||||
if lhs.storyItem != rhs.storyItem {
|
if lhs.storyItem != rhs.storyItem {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if lhs.entityFiles != rhs.entityFiles {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,7 +43,6 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let text: String
|
let text: String
|
||||||
let entities: [MessageTextEntity]
|
let entities: [MessageTextEntity]
|
||||||
let entityFiles: [EngineMedia.Id: TelegramMediaFile]
|
|
||||||
let action: (Action) -> Void
|
let action: (Action) -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
@ -51,14 +50,12 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
text: String,
|
text: String,
|
||||||
entities: [MessageTextEntity],
|
entities: [MessageTextEntity],
|
||||||
entityFiles: [EngineMedia.Id: TelegramMediaFile],
|
|
||||||
action: @escaping (Action) -> Void
|
action: @escaping (Action) -> Void
|
||||||
) {
|
) {
|
||||||
self.externalState = externalState
|
self.externalState = externalState
|
||||||
self.context = context
|
self.context = context
|
||||||
self.text = text
|
self.text = text
|
||||||
self.entities = entities
|
self.entities = entities
|
||||||
self.entityFiles = entityFiles
|
|
||||||
self.action = action
|
self.action = action
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,9 +72,6 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
if lhs.entities != rhs.entities {
|
if lhs.entities != rhs.entities {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if lhs.entityFiles != rhs.entityFiles {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,8 +359,7 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
boldItalicFont: Font.semiboldItalic(16.0),
|
boldItalicFont: Font.semiboldItalic(16.0),
|
||||||
fixedFont: Font.monospace(16.0),
|
fixedFont: Font.monospace(16.0),
|
||||||
blockQuoteFont: Font.monospace(16.0),
|
blockQuoteFont: Font.monospace(16.0),
|
||||||
message: nil,
|
message: nil
|
||||||
entityFiles: component.entityFiles
|
|
||||||
)
|
)
|
||||||
|
|
||||||
let makeLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
let makeLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
||||||
|
|||||||
@ -1795,6 +1795,8 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.voiceMessagesRestrictedTooltipController = controller
|
self.voiceMessagesRestrictedTooltipController = controller
|
||||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
|
||||||
},
|
},
|
||||||
|
paste: { _ in
|
||||||
|
},
|
||||||
audioRecorder: self.sendMessageContext.audioRecorderValue,
|
audioRecorder: self.sendMessageContext.audioRecorderValue,
|
||||||
videoRecordingStatus: !self.sendMessageContext.hasRecordedVideoPreview ? self.sendMessageContext.videoRecorderValue?.audioStatus : nil,
|
videoRecordingStatus: !self.sendMessageContext.hasRecordedVideoPreview ? self.sendMessageContext.videoRecorderValue?.audioStatus : nil,
|
||||||
isRecordingLocked: self.sendMessageContext.isMediaRecordingLocked,
|
isRecordingLocked: self.sendMessageContext.isMediaRecordingLocked,
|
||||||
@ -2513,7 +2515,6 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
context: component.context,
|
context: component.context,
|
||||||
text: component.slice.item.storyItem.text,
|
text: component.slice.item.storyItem.text,
|
||||||
entities: component.slice.item.storyItem.entities,
|
entities: component.slice.item.storyItem.entities,
|
||||||
entityFiles: component.slice.item.entityFiles,
|
|
||||||
action: { [weak self] action in
|
action: { [weak self] action in
|
||||||
guard let self, let component = self.component else {
|
guard let self, let component = self.component else {
|
||||||
return
|
return
|
||||||
@ -2645,7 +2646,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
reactionContextNode.displayTail = false
|
reactionContextNode.displayTail = false
|
||||||
self.reactionContextNode = reactionContextNode
|
self.reactionContextNode = reactionContextNode
|
||||||
|
|
||||||
reactionContextNode.reactionSelected = { [weak self, weak reactionContextNode] updateReaction, _ in
|
reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in
|
||||||
guard let self, let component = self.component else {
|
guard let self, let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -2669,24 +2670,23 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
targetView.isUserInteractionEnabled = false
|
targetView.isUserInteractionEnabled = false
|
||||||
self.addSubview(targetView)
|
self.addSubview(targetView)
|
||||||
|
|
||||||
if let reactionContextNode {
|
reactionContextNode.willAnimateOutToReaction(value: updateReaction.reaction)
|
||||||
reactionContextNode.willAnimateOutToReaction(value: updateReaction.reaction)
|
reactionContextNode.animateOutToReaction(value: updateReaction.reaction, targetView: targetView, hideNode: false, animateTargetContainer: nil, addStandaloneReactionAnimation: "".isEmpty ? nil : { [weak self] standaloneReactionAnimation in
|
||||||
reactionContextNode.animateOutToReaction(value: updateReaction.reaction, targetView: targetView, hideNode: false, animateTargetContainer: nil, addStandaloneReactionAnimation: "".isEmpty ? nil : { [weak self] standaloneReactionAnimation in
|
guard let self else {
|
||||||
guard let self else {
|
return
|
||||||
return
|
}
|
||||||
}
|
standaloneReactionAnimation.frame = self.bounds
|
||||||
standaloneReactionAnimation.frame = self.bounds
|
self.addSubview(standaloneReactionAnimation.view)
|
||||||
self.addSubview(standaloneReactionAnimation.view)
|
}, completion: { [weak targetView, weak reactionContextNode] in
|
||||||
}, completion: { [weak targetView, weak reactionContextNode] in
|
targetView?.removeFromSuperview()
|
||||||
targetView?.removeFromSuperview()
|
if let reactionContextNode {
|
||||||
if let reactionContextNode {
|
reactionContextNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, removeOnCompletion: false)
|
||||||
reactionContextNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, removeOnCompletion: false)
|
reactionContextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak reactionContextNode] _ in
|
||||||
reactionContextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak reactionContextNode] _ in
|
reactionContextNode?.view.removeFromSuperview()
|
||||||
reactionContextNode?.view.removeFromSuperview()
|
})
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasFirstResponder(self) {
|
if hasFirstResponder(self) {
|
||||||
self.sendMessageContext.currentInputMode = .text
|
self.sendMessageContext.currentInputMode = .text
|
||||||
@ -2797,7 +2797,6 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
|
|
||||||
if animateReactionsIn {
|
if animateReactionsIn {
|
||||||
reactionContextNode.animateIn(from: reactionsAnchorRect)
|
reactionContextNode.animateIn(from: reactionsAnchorRect)
|
||||||
reactionContextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -159,8 +159,12 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
self.view?.component?.controller()?.presentInGlobalOverlay(c, with: a)
|
self.view?.component?.controller()?.presentInGlobalOverlay(c, with: a)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getNavigationController: {
|
getNavigationController: { [weak self] in
|
||||||
return self.view?.component?.controller()?.navigationController as? NavigationController
|
if let self {
|
||||||
|
return self.view?.component?.controller()?.navigationController as? NavigationController
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
},
|
},
|
||||||
requestLayout: { [weak self] transition in
|
requestLayout: { [weak self] transition in
|
||||||
if let self {
|
if let self {
|
||||||
|
|||||||
@ -345,7 +345,7 @@ public final class StorySetIndicatorComponent: Component {
|
|||||||
if component.hasUnseen {
|
if component.hasUnseen {
|
||||||
borderColors = [component.theme.chatList.storyUnseenColors.topColor.argb, component.theme.chatList.storyUnseenColors.bottomColor.argb]
|
borderColors = [component.theme.chatList.storyUnseenColors.topColor.argb, component.theme.chatList.storyUnseenColors.bottomColor.argb]
|
||||||
} else {
|
} else {
|
||||||
borderColors = [UIColor(white: 1.0, alpha: 0.3).argb, UIColor(white: 1.0, alpha: 0.3).argb]
|
borderColors = [component.theme.chatList.storySeenColors.topColor.argb, component.theme.chatList.storySeenColors.bottomColor.argb]
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageSize = CGSize(width: maxItemsWidth, height: outerDiameter)
|
let imageSize = CGSize(width: maxItemsWidth, height: outerDiameter)
|
||||||
|
|||||||
@ -18,7 +18,9 @@ swift_library(
|
|||||||
"//submodules/AccountContext",
|
"//submodules/AccountContext",
|
||||||
"//submodules/InvisibleInkDustNode",
|
"//submodules/InvisibleInkDustNode",
|
||||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||||
"//submodules/ChatTextLinkEditUI"
|
"//submodules/ChatTextLinkEditUI",
|
||||||
|
"//submodules/Pasteboard",
|
||||||
|
"//submodules/ImageTransparency",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -9,7 +9,10 @@ import InvisibleInkDustNode
|
|||||||
import EmojiTextAttachmentView
|
import EmojiTextAttachmentView
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import TextFormat
|
import TextFormat
|
||||||
|
import Pasteboard
|
||||||
import ChatTextLinkEditUI
|
import ChatTextLinkEditUI
|
||||||
|
import MobileCoreServices
|
||||||
|
import ImageTransparency
|
||||||
|
|
||||||
public final class EmptyInputView: UIView, UIInputViewAudioFeedback {
|
public final class EmptyInputView: UIView, UIInputViewAudioFeedback {
|
||||||
public var enableInputClicksWhenVisible: Bool {
|
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 final class AnimationHint {
|
||||||
public enum Kind {
|
public enum Kind {
|
||||||
case textChanged
|
case textChanged
|
||||||
@ -72,6 +83,7 @@ public final class TextFieldComponent: Component {
|
|||||||
public let insets: UIEdgeInsets
|
public let insets: UIEdgeInsets
|
||||||
public let hideKeyboard: Bool
|
public let hideKeyboard: Bool
|
||||||
public let present: (ViewController) -> Void
|
public let present: (ViewController) -> Void
|
||||||
|
public let paste: (PasteData) -> Void
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
@ -81,7 +93,8 @@ public final class TextFieldComponent: Component {
|
|||||||
textColor: UIColor,
|
textColor: UIColor,
|
||||||
insets: UIEdgeInsets,
|
insets: UIEdgeInsets,
|
||||||
hideKeyboard: Bool,
|
hideKeyboard: Bool,
|
||||||
present: @escaping (ViewController) -> Void
|
present: @escaping (ViewController) -> Void,
|
||||||
|
paste: @escaping (PasteData) -> Void
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
@ -91,6 +104,7 @@ public final class TextFieldComponent: Component {
|
|||||||
self.insets = insets
|
self.insets = insets
|
||||||
self.hideKeyboard = hideKeyboard
|
self.hideKeyboard = hideKeyboard
|
||||||
self.present = present
|
self.present = present
|
||||||
|
self.paste = paste
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: TextFieldComponent, rhs: TextFieldComponent) -> Bool {
|
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 {
|
public final class View: UIView, UITextViewDelegate, UIScrollViewDelegate {
|
||||||
private let textContainer: NSTextContainer
|
private let textContainer: NSTextContainer
|
||||||
private let textStorage: NSTextStorage
|
private let textStorage: NSTextStorage
|
||||||
private let layoutManager: NSLayoutManager
|
private let layoutManager: NSLayoutManager
|
||||||
private let textView: UITextView
|
private let textView: TextView
|
||||||
|
|
||||||
private var spoilerView: InvisibleInkDustView?
|
private var spoilerView: InvisibleInkDustView?
|
||||||
private var customEmojiContainerView: CustomEmojiContainerView?
|
private var customEmojiContainerView: CustomEmojiContainerView?
|
||||||
@ -163,7 +187,7 @@ public final class TextFieldComponent: Component {
|
|||||||
self.layoutManager.addTextContainer(self.textContainer)
|
self.layoutManager.addTextContainer(self.textContainer)
|
||||||
self.textStorage.addLayoutManager(self.layoutManager)
|
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.translatesAutoresizingMaskIntoConstraints = false
|
||||||
self.textView.backgroundColor = nil
|
self.textView.backgroundColor = nil
|
||||||
self.textView.layer.isOpaque = false
|
self.textView.layer.isOpaque = false
|
||||||
@ -185,6 +209,10 @@ public final class TextFieldComponent: Component {
|
|||||||
NSAttributedString.Key.font: Font.regular(17.0),
|
NSAttributedString.Key.font: Font.regular(17.0),
|
||||||
NSAttributedString.Key.foregroundColor: UIColor.white
|
NSAttributedString.Key.foregroundColor: UIColor.white
|
||||||
]
|
]
|
||||||
|
|
||||||
|
self.textView.onPaste = { [weak self] in
|
||||||
|
return self?.onPaste() ?? false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
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)))
|
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) {
|
public func textViewDidChange(_ textView: UITextView) {
|
||||||
guard let component = self.component else {
|
guard let component = self.component else {
|
||||||
return
|
return
|
||||||
@ -257,7 +360,7 @@ public final class TextFieldComponent: Component {
|
|||||||
public func textViewDidEndEditing(_ textView: UITextView) {
|
public func textViewDidEndEditing(_ textView: UITextView) {
|
||||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 16.0, *)
|
@available(iOS 16.0, *)
|
||||||
public func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
|
public func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
|
||||||
let filteredActions: Set<String> = Set([
|
let filteredActions: Set<String> = Set([
|
||||||
|
|||||||
@ -254,10 +254,6 @@ class ChatMessageBubbleContentNode: ASDisplayNode {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func targetForStoryTransition(id: StoryId) -> UIView? {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getStatusNode() -> ASDisplayNode? {
|
func getStatusNode() -> ASDisplayNode? {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4589,11 +4589,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
guard let item = self.item else {
|
guard let item = self.item else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for contentNode in self.contentNodes {
|
|
||||||
if let value = contentNode.targetForStoryTransition(id: id) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let attribute = attribute as? ReplyStoryAttribute {
|
if let attribute = attribute as? ReplyStoryAttribute {
|
||||||
if attribute.storyId == id {
|
if attribute.storyId == id {
|
||||||
|
|||||||
@ -429,10 +429,6 @@ class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
override func targetForStoryTransition(id: StoryId) -> UIView? {
|
|
||||||
return self.interactiveVideoNode.targetForStoryTransition(id: id)
|
|
||||||
}
|
|
||||||
|
|
||||||
override var disablesClipping: Bool {
|
override var disablesClipping: Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -431,8 +431,6 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var replyMessage: Message?
|
|
||||||
var replyStory: StoryId?
|
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let attribute = attribute as? InlineBotMessageAttribute {
|
if let attribute = attribute as? InlineBotMessageAttribute {
|
||||||
var inlineBotNameString: String?
|
var inlineBotNameString: String?
|
||||||
@ -469,34 +467,28 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let replyAttribute = attribute as? ReplyMessageAttribute {
|
if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] {
|
||||||
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId {
|
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId {
|
||||||
} else {
|
} else {
|
||||||
replyMessage = item.message.associatedMessages[replyAttribute.messageId]
|
replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments(
|
||||||
|
presentationData: item.presentationData,
|
||||||
|
strings: item.presentationData.strings,
|
||||||
|
context: item.context,
|
||||||
|
type: .standalone,
|
||||||
|
message: replyMessage,
|
||||||
|
story: nil,
|
||||||
|
parentMessage: item.message,
|
||||||
|
constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude),
|
||||||
|
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||||
|
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer,
|
||||||
|
associatedData: item.associatedData
|
||||||
|
))
|
||||||
}
|
}
|
||||||
} else if let attribute = attribute as? ReplyStoryAttribute {
|
|
||||||
replyStory = attribute.storyId
|
|
||||||
} else if let _ = attribute as? InlineBotMessageAttribute {
|
} else if let _ = attribute as? InlineBotMessageAttribute {
|
||||||
} else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty {
|
} else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty {
|
||||||
replyMarkup = attribute
|
replyMarkup = attribute
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if replyMessage != nil || replyStory != nil {
|
|
||||||
replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments(
|
|
||||||
presentationData: item.presentationData,
|
|
||||||
strings: item.presentationData.strings,
|
|
||||||
context: item.context,
|
|
||||||
type: .standalone,
|
|
||||||
message: replyMessage,
|
|
||||||
story: replyStory,
|
|
||||||
parentMessage: item.message,
|
|
||||||
constrainedSize: CGSize(width: max(0, availableWidth), height: CGFloat.greatestFiniteMagnitude),
|
|
||||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
|
||||||
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer,
|
|
||||||
associatedData: item.associatedData
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
var updatedShareButtonNode: ChatMessageShareButton?
|
var updatedShareButtonNode: ChatMessageShareButton?
|
||||||
if needsShareButton {
|
if needsShareButton {
|
||||||
|
|||||||
@ -316,9 +316,6 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
let availableContentWidth = width - bubbleEdgeInset * 2.0 - bubbleContentInsetsLeft - 20.0
|
let availableContentWidth = width - bubbleEdgeInset * 2.0 - bubbleContentInsetsLeft - 20.0
|
||||||
|
|
||||||
if !ignoreHeaders {
|
if !ignoreHeaders {
|
||||||
var replyMessage: Message?
|
|
||||||
var replyStory: StoryId?
|
|
||||||
|
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let attribute = attribute as? InlineBotMessageAttribute {
|
if let attribute = attribute as? InlineBotMessageAttribute {
|
||||||
var inlineBotNameString: String?
|
var inlineBotNameString: String?
|
||||||
@ -341,32 +338,23 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let replyAttribute = attribute as? ReplyMessageAttribute {
|
if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] {
|
||||||
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId {
|
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId {
|
||||||
} else {
|
} else {
|
||||||
replyMessage = item.message.associatedMessages[replyAttribute.messageId]
|
replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments(
|
||||||
|
presentationData: item.presentationData,
|
||||||
|
strings: item.presentationData.strings,
|
||||||
|
context: item.context,
|
||||||
|
type: .standalone,
|
||||||
|
message: replyMessage,
|
||||||
|
story: nil,
|
||||||
|
parentMessage: item.message,
|
||||||
|
constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude),
|
||||||
|
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||||
|
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer,
|
||||||
|
associatedData: item.associatedData
|
||||||
|
))
|
||||||
}
|
}
|
||||||
} else if let attribute = attribute as? ReplyStoryAttribute {
|
|
||||||
replyStory = attribute.storyId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if replyMessage != nil || replyStory != nil {
|
|
||||||
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyMessage?.id {
|
|
||||||
} else {
|
|
||||||
replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments(
|
|
||||||
presentationData: item.presentationData,
|
|
||||||
strings: item.presentationData.strings,
|
|
||||||
context: item.context,
|
|
||||||
type: .standalone,
|
|
||||||
message: replyMessage,
|
|
||||||
story: replyStory,
|
|
||||||
parentMessage: item.message,
|
|
||||||
constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude),
|
|
||||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
|
||||||
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer,
|
|
||||||
associatedData: item.associatedData
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1271,9 +1259,6 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
if let attribute = attribute as? ReplyMessageAttribute {
|
if let attribute = attribute as? ReplyMessageAttribute {
|
||||||
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId)
|
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId)
|
||||||
return
|
return
|
||||||
} else if let attribute = attribute as? ReplyStoryAttribute {
|
|
||||||
item.controllerInteraction.navigateToStory(item.message, attribute.storyId)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1830,20 +1815,4 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
|
|
||||||
self.canAttachContent = false
|
self.canAttachContent = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func targetForStoryTransition(id: StoryId) -> UIView? {
|
|
||||||
guard let item = self.item else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for attribute in item.message.attributes {
|
|
||||||
if let attribute = attribute as? ReplyStoryAttribute {
|
|
||||||
if attribute.storyId == id {
|
|
||||||
if let replyInfoNode = self.replyInfoNode {
|
|
||||||
return replyInfoNode.mediaTransitionView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,7 +52,7 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M
|
|||||||
return string
|
return string
|
||||||
}
|
}
|
||||||
|
|
||||||
public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:]) -> NSAttributedString {
|
public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?) -> NSAttributedString {
|
||||||
var nsString: NSString?
|
var nsString: NSString?
|
||||||
let string = NSMutableAttributedString(string: text, attributes: [NSAttributedString.Key.font: baseFont, NSAttributedString.Key.foregroundColor: baseColor])
|
let string = NSMutableAttributedString(string: text, attributes: [NSAttributedString.Key.font: baseFont, NSAttributedString.Key.foregroundColor: baseColor])
|
||||||
var skipEntity = false
|
var skipEntity = false
|
||||||
@ -252,14 +252,7 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .CustomEmoji(_, fileId):
|
case let .CustomEmoji(_, fileId):
|
||||||
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
|
string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: message?.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile), range: range)
|
||||||
var emojiFile: TelegramMediaFile?
|
|
||||||
if let file = message?.associatedMedia[mediaId] as? TelegramMediaFile {
|
|
||||||
emojiFile = file
|
|
||||||
} else {
|
|
||||||
emojiFile = entityFiles[mediaId]
|
|
||||||
}
|
|
||||||
string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: emojiFile), range: range)
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user