Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Mike Renoir 2023-06-30 16:29:48 +02:00
commit 8c56458aea
16 changed files with 756 additions and 100 deletions

View File

@ -94,7 +94,7 @@ func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, con
items.append(.action(ContextMenuActionItem(text: "Move to Chats", icon: { theme in items.append(.action(ContextMenuActionItem(text: "Move to Chats", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MoveToChats"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MoveToChats"), color: theme.contextMenu.primaryColor)
}, action: { _, f in }, action: { _, f in
f(.default) f(.dismissWithoutContent)
context.engine.peers.updatePeerStoriesHidden(id: peerId, isHidden: false) context.engine.peers.updatePeerStoriesHidden(id: peerId, isHidden: false)

View File

@ -96,7 +96,7 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po
return .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaStory(userId: inputUser, id: media.storyId.id), ""), reuploadInfo: nil, cacheReferenceKey: nil)) return .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaStory(userId: inputUser, id: media.storyId.id), ""), reuploadInfo: nil, cacheReferenceKey: nil))
} }
|> castError(PendingMessageUploadError.self), .text) |> castError(PendingMessageUploadError.self), .text)
} else if let media = media.first, let mediaResult = mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: false, peerId: peerId, media: media, text: text, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, messageId: messageId, attributes: attributes) { } else if let media = media.first, let mediaResult = mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, peerId: peerId, media: media, text: text, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, messageId: messageId, attributes: attributes) {
return .signal(mediaResult, .media) return .signal(mediaResult, .media)
} else { } else {
return .signal(.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .text(text), reuploadInfo: nil, cacheReferenceKey: nil))), .text) return .signal(.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .text(text), reuploadInfo: nil, cacheReferenceKey: nil))), .text)

View File

@ -607,7 +607,7 @@ private func prepareUploadStoryContent(account: Account, media: EngineStoryInput
} }
} }
private func uploadedStoryContent(postbox: Postbox, network: Network, media: Media, accountPeerId: PeerId, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods) -> (signal: Signal<PendingMessageUploadedContentResult?, NoError>, media: Media) { private func uploadedStoryContent(postbox: Postbox, network: Network, media: Media, accountPeerId: PeerId, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods, passFetchProgress: Bool) -> (signal: Signal<PendingMessageUploadedContentResult?, NoError>, media: Media) {
let originalMedia: Media = media let originalMedia: Media = media
let contentToUpload: MessageContentToUpload let contentToUpload: MessageContentToUpload
@ -621,7 +621,7 @@ private func uploadedStoryContent(postbox: Postbox, network: Network, media: Med
revalidationContext: revalidationContext, revalidationContext: revalidationContext,
forceReupload: true, forceReupload: true,
isGrouped: false, isGrouped: false,
passFetchProgress: false, passFetchProgress: passFetchProgress,
peerId: accountPeerId, peerId: accountPeerId,
messageId: nil, messageId: nil,
attributes: [], attributes: [],
@ -758,7 +758,8 @@ private func _internal_putPendingStoryIdMapping(accountPeerId: PeerId, stableId:
} }
func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods, stableId: Int32, media: Media, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> { func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods, stableId: Int32, media: Media, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> {
let (contentSignal, originalMedia) = uploadedStoryContent(postbox: postbox, network: network, media: media, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods) let passFetchProgress = media is TelegramMediaFile
let (contentSignal, originalMedia) = uploadedStoryContent(postbox: postbox, network: network, media: media, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods, passFetchProgress: passFetchProgress)
return contentSignal return contentSignal
|> mapToSignal { result -> Signal<StoryUploadResult, NoError> in |> mapToSignal { result -> Signal<StoryUploadResult, NoError> in
switch result { switch result {
@ -896,7 +897,11 @@ func _internal_editStory(account: Account, media: EngineStoryInputMedia?, id: In
let contentSignal: Signal<PendingMessageUploadedContentResult?, NoError> let contentSignal: Signal<PendingMessageUploadedContentResult?, NoError>
let originalMedia: Media? let originalMedia: Media?
if let media = media { if let media = media {
(contentSignal, originalMedia) = uploadedStoryContent(postbox: account.postbox, network: account.network, media: prepareUploadStoryContent(account: account, media: media), accountPeerId: account.peerId, messageMediaPreuploadManager: account.messageMediaPreuploadManager, revalidationContext: account.mediaReferenceRevalidationContext, auxiliaryMethods: account.auxiliaryMethods) var passFetchProgress = false
if case .video = media {
passFetchProgress = true
}
(contentSignal, originalMedia) = uploadedStoryContent(postbox: account.postbox, network: account.network, media: prepareUploadStoryContent(account: account, media: media), accountPeerId: account.peerId, messageMediaPreuploadManager: account.messageMediaPreuploadManager, revalidationContext: account.mediaReferenceRevalidationContext, auxiliaryMethods: account.auxiliaryMethods, passFetchProgress: passFetchProgress)
} else { } else {
contentSignal = .single(nil) contentSignal = .single(nil)
originalMedia = nil originalMedia = nil

View File

@ -359,11 +359,20 @@ private final class CameraScreenComponent: CombinedComponent {
action.invoke(Void()) action.invoke(Void())
} }
private var lastDualCameraTimestamp: Double?
func toggleDualCamera() { func toggleDualCamera() {
let currentTimestamp = CACurrentMediaTime()
if let lastDualCameraTimestamp = self.lastDualCameraTimestamp, currentTimestamp - lastDualCameraTimestamp < 1.5 {
return
}
self.lastDualCameraTimestamp = currentTimestamp
let isEnabled = !self.cameraState.isDualCamEnabled let isEnabled = !self.cameraState.isDualCamEnabled
self.camera.setDualCamEnabled(isEnabled) self.camera.setDualCamEnabled(isEnabled)
self.cameraState = self.cameraState.updatedIsDualCamEnabled(isEnabled) self.cameraState = self.cameraState.updatedIsDualCamEnabled(isEnabled)
self.updated(transition: .easeInOut(duration: 0.1)) self.updated(transition: .easeInOut(duration: 0.1))
self.hapticFeedback.impact(.light)
} }
func updateSwipeHint(_ hint: CaptureControlsComponent.SwipeHint) { func updateSwipeHint(_ hint: CaptureControlsComponent.SwipeHint) {
@ -1600,13 +1609,11 @@ public class CameraScreen: ViewController {
} }
func presentDraftTooltip() { func presentDraftTooltip() {
guard let sourceView = self.componentHost.findTaggedView(tag: galleryButtonTag) else { guard let sourceView = self.componentHost.findTaggedView(tag: galleryButtonTag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: self.view) else {
return return
} }
let parentFrame = self.view.convert(self.bounds, to: nil) let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 29.0), size: CGSize())
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 4.0), size: CGSize())
let controller = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: "Draft Saved"), location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _ in let controller = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: "Draft Saved"), location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _ in
return .ignore return .ignore

View File

@ -772,6 +772,9 @@ final class CaptureControlsComponent: Component {
self.component?.swipeHintUpdated(.flip) self.component?.swipeHintUpdated(.flip)
if location.x > self.frame.width / 2.0 + 60.0 { if location.x > self.frame.width / 2.0 + 60.0 {
self.panBlobState = .transientToFlip self.panBlobState = .transientToFlip
if self.didFlip && location.x < self.frame.width - 100.0 {
self.didFlip = false
}
if !self.didFlip && location.x > self.frame.width - 70.0 { if !self.didFlip && location.x > self.frame.width - 70.0 {
self.didFlip = true self.didFlip = true
self.hapticFeedback.impact(.light) self.hapticFeedback.impact(.light)

View File

@ -15,6 +15,22 @@ import TelegramUIPreferences
public final class EmojiSuggestionsComponent: Component { public final class EmojiSuggestionsComponent: Component {
public typealias EnvironmentType = Empty public typealias EnvironmentType = Empty
public struct Theme: Equatable {
let backgroundColor: UIColor
let textColor: UIColor
let placeholderColor: UIColor
public init(
backgroundColor: UIColor,
textColor: UIColor,
placeholderColor: UIColor
) {
self.backgroundColor = backgroundColor
self.textColor = textColor
self.placeholderColor = placeholderColor
}
}
public static func suggestionData(context: AccountContext, isSavedMessages: Bool, query: String) -> Signal<[TelegramMediaFile], NoError> { public static func suggestionData(context: AccountContext, isSavedMessages: Bool, query: String) -> Signal<[TelegramMediaFile], NoError> {
let hasPremium: Signal<Bool, NoError> let hasPremium: Signal<Bool, NoError>
if isSavedMessages { if isSavedMessages {
@ -98,7 +114,7 @@ public final class EmojiSuggestionsComponent: Component {
} }
public let context: AccountContext public let context: AccountContext
public let theme: PresentationTheme public let theme: Theme
public let animationCache: AnimationCache public let animationCache: AnimationCache
public let animationRenderer: MultiAnimationRenderer public let animationRenderer: MultiAnimationRenderer
public let files: [TelegramMediaFile] public let files: [TelegramMediaFile]
@ -107,7 +123,7 @@ public final class EmojiSuggestionsComponent: Component {
public init( public init(
context: AccountContext, context: AccountContext,
userLocation: MediaResourceUserLocation, userLocation: MediaResourceUserLocation,
theme: PresentationTheme, theme: Theme,
animationCache: AnimationCache, animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer, animationRenderer: MultiAnimationRenderer,
files: [TelegramMediaFile], files: [TelegramMediaFile],
@ -125,7 +141,7 @@ public final class EmojiSuggestionsComponent: Component {
if lhs.context !== rhs.context { if lhs.context !== rhs.context {
return false return false
} }
if lhs.theme !== rhs.theme { if lhs.theme != rhs.theme {
return false return false
} }
if lhs.animationCache !== rhs.animationCache { if lhs.animationCache !== rhs.animationCache {
@ -305,7 +321,7 @@ public final class EmojiSuggestionsComponent: Component {
let itemLayer: InlineStickerItemLayer let itemLayer: InlineStickerItemLayer
if let current = self.visibleLayers[item.fileId] { if let current = self.visibleLayers[item.fileId] {
itemLayer = current itemLayer = current
itemLayer.dynamicColor = component.theme.list.itemPrimaryTextColor itemLayer.dynamicColor = component.theme.textColor
} else { } else {
itemLayer = InlineStickerItemLayer( itemLayer = InlineStickerItemLayer(
context: component.context, context: component.context,
@ -315,9 +331,9 @@ public final class EmojiSuggestionsComponent: Component {
file: item, file: item,
cache: component.animationCache, cache: component.animationCache,
renderer: component.animationRenderer, renderer: component.animationRenderer,
placeholderColor: component.theme.list.mediaPlaceholderColor, placeholderColor: component.theme.placeholderColor,
pointSize: itemFrame.size, pointSize: itemFrame.size,
dynamicColor: component.theme.list.itemPrimaryTextColor dynamicColor: component.theme.textColor
) )
self.visibleLayers[item.fileId] = itemLayer self.visibleLayers[item.fileId] = itemLayer
self.scrollView.layer.addSublayer(itemLayer) self.scrollView.layer.addSublayer(itemLayer)
@ -382,10 +398,10 @@ public final class EmojiSuggestionsComponent: Component {
func update(component: EmojiSuggestionsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize { func update(component: EmojiSuggestionsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
let height: CGFloat = 54.0 let height: CGFloat = 54.0
if self.component?.theme !== component.theme { if self.component?.theme.backgroundColor != component.theme.backgroundColor {
//self.backgroundLayer.fillColor = component.theme.list.plainBackgroundColor.cgColor //self.backgroundLayer.fillColor = component.theme.list.plainBackgroundColor.cgColor
self.backgroundLayer.fillColor = UIColor.black.cgColor self.backgroundLayer.fillColor = UIColor.black.cgColor
self.blurView.updateColor(color: component.theme.list.plainBackgroundColor.withMultipliedAlpha(0.88), transition: .immediate) self.blurView.updateColor(color: component.theme.backgroundColor, transition: .immediate)
} }
var resetScrollingPosition = false var resetScrollingPosition = false
if self.component?.files != component.files { if self.component?.files != component.files {
@ -427,3 +443,11 @@ public final class EmojiSuggestionsComponent: Component {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
} }
} }
public extension EmojiSuggestionsComponent.Theme {
init(theme: PresentationTheme) {
self.backgroundColor = theme.list.plainBackgroundColor.withMultipliedAlpha(0.88)
self.textColor = theme.list.itemPrimaryTextColor
self.placeholderColor = theme.list.mediaPlaceholderColor
}
}

View File

@ -641,9 +641,13 @@ final class VideoInputScalePass: RenderPass {
} }
func process(input: MTLTexture, secondInput: MTLTexture?, timestamp: CMTime, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { func process(input: MTLTexture, secondInput: MTLTexture?, timestamp: CMTime, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
#if targetEnvironment(simulator)
#else
guard max(input.width, input.height) > 1920 || secondInput != nil else { guard max(input.width, input.height) > 1920 || secondInput != nil else {
return input return input
} }
#endif
let scaledSize = CGSize(width: input.width, height: input.height).fitted(CGSize(width: 1920.0, height: 1920.0)) let scaledSize = CGSize(width: input.width, height: input.height).fitted(CGSize(width: 1920.0, height: 1920.0))
let width: Int let width: Int
@ -691,8 +695,11 @@ final class VideoInputScalePass: RenderPass {
renderCommandEncoder.setRenderPipelineState(self.mainPipelineState!) renderCommandEncoder.setRenderPipelineState(self.mainPipelineState!)
#if targetEnvironment(simulator)
let secondInput = input
#endif
let (mainVideoState, additionalVideoState, transitionVideoState) = self.transitionState(for: timestamp, mainInput: input, additionalInput: secondInput) let (mainVideoState, additionalVideoState, transitionVideoState) = self.transitionState(for: timestamp, mainInput: input, additionalInput: secondInput)
if let transitionVideoState { if let transitionVideoState {
self.encodeVideo( self.encodeVideo(

View File

@ -28,6 +28,7 @@ import CameraButtonComponent
import UndoUI import UndoUI
import ChatEntityKeyboardInputNode import ChatEntityKeyboardInputNode
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
import TextFormat
enum DrawingScreenType { enum DrawingScreenType {
case drawing case drawing
@ -694,13 +695,18 @@ final class MediaEditorScreenComponent: Component {
transition.setAlpha(view: cancelButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) transition.setAlpha(view: cancelButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0)
} }
var doneButtonTitle = "NEXT"
if let controller = environment.controller() as? MediaEditorScreen, controller.isEditingStory {
doneButtonTitle = "DONE"
}
let doneButtonSize = self.doneButton.update( let doneButtonSize = self.doneButton.update(
transition: transition, transition: transition,
component: AnyComponent(Button( component: AnyComponent(Button(
content: AnyComponent(DoneButtonComponent( content: AnyComponent(DoneButtonComponent(
backgroundColor: UIColor(rgb: 0x007aff), backgroundColor: UIColor(rgb: 0x007aff),
icon: UIImage(bundleImageName: "Media Editor/Next")!, icon: UIImage(bundleImageName: "Media Editor/Next")!,
title: "NEXT")), title: doneButtonTitle)),
action: { action: {
guard let controller = environment.controller() as? MediaEditorScreen else { guard let controller = environment.controller() as? MediaEditorScreen else {
return return
@ -1857,25 +1863,25 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
} }
} }
} }
//#if DEBUG #if targetEnvironment(simulator)
// if case let .asset(asset) = subject, asset.mediaType == .video { if case let .asset(asset) = subject, asset.mediaType == .video {
// let videoEntity = DrawingStickerEntity(content: .dualVideoReference) let videoEntity = DrawingStickerEntity(content: .dualVideoReference)
// videoEntity.referenceDrawingSize = storyDimensions videoEntity.referenceDrawingSize = storyDimensions
// videoEntity.scale = 1.49 videoEntity.scale = 1.49
// videoEntity.position = PIPPosition.bottomRight.getPosition(storyDimensions) videoEntity.position = PIPPosition.bottomRight.getPosition(storyDimensions)
// self.entitiesView.add(videoEntity, announce: false) self.entitiesView.add(videoEntity, announce: false)
//
// mediaEditor.setAdditionalVideo("", positionChanges: [VideoPositionChange(additional: false, timestamp: 0.0), VideoPositionChange(additional: true, timestamp: 3.0)]) mediaEditor.setAdditionalVideo("", positionChanges: [VideoPositionChange(additional: false, timestamp: 0.0), VideoPositionChange(additional: true, timestamp: 3.0)])
// mediaEditor.setAdditionalVideoPosition(videoEntity.position, scale: videoEntity.scale, rotation: videoEntity.rotation) mediaEditor.setAdditionalVideoPosition(videoEntity.position, scale: videoEntity.scale, rotation: videoEntity.rotation)
// if let entityView = self.entitiesView.getView(for: videoEntity.uuid) as? DrawingStickerEntityView { if let entityView = self.entitiesView.getView(for: videoEntity.uuid) as? DrawingStickerEntityView {
// entityView.updated = { [weak videoEntity, weak self] in entityView.updated = { [weak videoEntity, weak self] in
// if let self, let videoEntity { if let self, let videoEntity {
// self.mediaEditor?.setAdditionalVideoPosition(videoEntity.position, scale: videoEntity.scale, rotation: videoEntity.rotation) self.mediaEditor?.setAdditionalVideoPosition(videoEntity.position, scale: videoEntity.scale, rotation: videoEntity.rotation)
// } }
// } }
// } }
// } }
//#endif #endif
self.gradientColorsDisposable = mediaEditor.gradientColors.start(next: { [weak self] colors in self.gradientColorsDisposable = mediaEditor.gradientColors.start(next: { [weak self] colors in
if let self, let colors { if let self, let colors {
@ -3098,6 +3104,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
let privacy = privacy ?? self.state.privacy let privacy = privacy ?? self.state.privacy
let text = self.getCaption().string
let mentions = generateTextEntities(text, enabledTypes: [.mention], currentEntities: []).map { (text as NSString).substring(with: NSRange(location: $0.range.lowerBound + 1, length: $0.range.upperBound - $0.range.lowerBound - 1)) }
let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .stories(editing: false), initialPeerIds: Set(privacy.privacy.additionallyIncludePeers)) let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .stories(editing: false), initialPeerIds: Set(privacy.privacy.additionallyIncludePeers))
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self else { guard let self else {
@ -3112,6 +3121,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
allowScreenshots: !privacy.isForwardingDisabled, allowScreenshots: !privacy.isForwardingDisabled,
pin: privacy.pin, pin: privacy.pin,
timeout: privacy.timeout, timeout: privacy.timeout,
mentions: mentions,
stateContext: stateContext, stateContext: stateContext,
completion: { [weak self] privacy, allowScreenshots, pin in completion: { [weak self] privacy, allowScreenshots, pin in
guard let self else { guard let self else {
@ -3333,6 +3343,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
func requestDismiss(saveDraft: Bool, animated: Bool) { func requestDismiss(saveDraft: Bool, animated: Bool) {
self.dismissAllTooltips() self.dismissAllTooltips()
var showDraftTooltip = saveDraft
if let subject = self.node.subject, case .draft = subject {
showDraftTooltip = false
}
if saveDraft { if saveDraft {
self.saveDraft(id: nil) self.saveDraft(id: nil)
} else { } else {
@ -3346,7 +3360,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
} }
self.node.entitiesView.invalidate() self.node.entitiesView.invalidate()
self.cancelled(saveDraft) self.cancelled(showDraftTooltip)
self.willDismiss() self.willDismiss()
@ -3973,8 +3987,10 @@ final class DoneButtonComponent: CombinedComponent {
) )
let backgroundHeight: CGFloat = 33.0 let backgroundHeight: CGFloat = 33.0
var backgroundSize = CGSize(width: backgroundHeight, height: backgroundHeight)
let textSpacing: CGFloat = 7.0
var textWidth: CGFloat = 0.0
var title: _UpdatedChildComponent? var title: _UpdatedChildComponent?
if let titleText = context.component.title { if let titleText = context.component.title {
title = text.update( title = text.update(
@ -3986,14 +4002,15 @@ final class DoneButtonComponent: CombinedComponent {
availableSize: CGSize(width: 180.0, height: 100.0), availableSize: CGSize(width: 180.0, height: 100.0),
transition: .immediate transition: .immediate
) )
textWidth = title!.size.width
let updatedBackgroundWidth = backgroundSize.width + textSpacing + title!.size.width
if updatedBackgroundWidth < 126.0 {
backgroundSize.width = updatedBackgroundWidth
} else {
title = nil
}
} }
var backgroundSize = CGSize(width: 33.0, height: backgroundHeight)
if !textWidth.isZero {
backgroundSize.width += textWidth + 7.0
}
let background = background.update( let background = background.update(
component: RoundedRectangle(color: context.component.backgroundColor, cornerRadius: backgroundHeight / 2.0), component: RoundedRectangle(color: context.component.backgroundColor, cornerRadius: backgroundHeight / 2.0),
availableSize: backgroundSize, availableSize: backgroundSize,

View File

@ -13,6 +13,7 @@ swift_library(
"//submodules/Display", "//submodules/Display",
"//submodules/ComponentFlow", "//submodules/ComponentFlow",
"//submodules/AppBundle", "//submodules/AppBundle",
"//submodules/TelegramCore",
"//submodules/TelegramUI/Components/TextFieldComponent", "//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/Components/BundleIconComponent", "//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton", "//submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton",
@ -28,6 +29,7 @@ swift_library(
"//submodules/TextFormat", "//submodules/TextFormat",
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent", "//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
"//submodules/TelegramUI/Components/MoreHeaderButton", "//submodules/TelegramUI/Components/MoreHeaderButton",
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import SwiftSignalKit import SwiftSignalKit
import TelegramCore
import TextFieldComponent import TextFieldComponent
import ChatContextQuery import ChatContextQuery
import AccountContext import AccountContext
@ -129,6 +130,110 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, inp
|> castError(ChatContextQueryError.self) |> castError(ChatContextQueryError.self)
return signal |> then(peers) return signal |> then(peers)
case let .emojiSearch(query, languageCode, range):
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}
|> distinctUntilChanged
if query.isSingleEmoji {
return combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
hasPremium
)
|> map { view, hasPremium -> [(String, TelegramMediaFile?, String)] in
var result: [(String, TelegramMediaFile?, String)] = []
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
}
for attribute in item.file.attributes {
switch attribute {
case let .CustomEmoji(_, _, alt, _):
if alt == query {
if !item.file.isPremiumEmoji || hasPremium {
result.append((alt, item.file, alt))
}
}
default:
break
}
}
}
return result
}
|> map { result -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
return { _ in return .emojis(result, range) }
}
|> castError(ChatContextQueryError.self)
} else {
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: query.count < 2)
if !languageCode.lowercased().hasPrefix("en") {
signal = signal
|> mapToSignal { keywords in
return .single(keywords)
|> then(
context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3)
|> map { englishKeywords in
return keywords + englishKeywords
}
)
}
}
return signal
|> castError(ChatContextQueryError.self)
|> mapToSignal { keywords -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> in
return combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
hasPremium
)
|> map { view, hasPremium -> [(String, TelegramMediaFile?, String)] in
var result: [(String, TelegramMediaFile?, String)] = []
var allEmoticons: [String: String] = [:]
for keyword in keywords {
for emoticon in keyword.emoticons {
allEmoticons[emoticon] = keyword.keyword
}
}
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
}
for attribute in item.file.attributes {
switch attribute {
case let .CustomEmoji(_, _, alt, _):
if !alt.isEmpty, let keyword = allEmoticons[alt] {
if !item.file.isPremiumEmoji || hasPremium {
result.append((alt, item.file, keyword))
}
}
default:
break
}
}
}
for keyword in keywords {
for emoticon in keyword.emoticons {
result.append((emoticon, nil, keyword.keyword))
}
}
return result
}
|> map { result -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
return { _ in return .emojis(result, range) }
}
|> castError(ChatContextQueryError.self)
}
}
default: default:
return .complete() return .complete()
} }

View File

@ -3,6 +3,7 @@ import UIKit
import Display import Display
import ComponentFlow import ComponentFlow
import SwiftSignalKit import SwiftSignalKit
import TelegramCore
import AppBundle import AppBundle
import TextFieldComponent import TextFieldComponent
import BundleIconComponent import BundleIconComponent
@ -12,6 +13,8 @@ import ChatPresentationInterfaceState
import LottieComponent import LottieComponent
import ChatContextQuery import ChatContextQuery
import TextFormat import TextFormat
import EmojiSuggestionsComponent
import AudioToolbox
public final class MessageInputPanelComponent: Component { public final class MessageInputPanelComponent: Component {
public enum Style { public enum Style {
@ -210,7 +213,7 @@ public final class MessageInputPanelComponent: Component {
public enum SendMessageInput { public enum SendMessageInput {
case text(NSAttributedString) case text(NSAttributedString)
} }
public final class View: UIView { public final class View: UIView {
private let fieldBackgroundView: BlurredBackgroundView private let fieldBackgroundView: BlurredBackgroundView
private let vibrancyEffectView: UIVisualEffectView private let vibrancyEffectView: UIVisualEffectView
@ -240,13 +243,15 @@ public final class MessageInputPanelComponent: Component {
private var currentMediaInputIsVoice: Bool = true private var currentMediaInputIsVoice: Bool = true
private var mediaCancelFraction: CGFloat = 0.0 private var mediaCancelFraction: CGFloat = 0.0
private var currentInputMode: InputMode?
private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:] private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:]
private var contextQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] = [:] private var contextQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] = [:]
private var contextQueryResultPanel: ComponentView<Empty>? private var contextQueryResultPanel: ComponentView<Empty>?
private var contextQueryResultPanelExternalState: ContextResultPanelComponent.ExternalState? private var contextQueryResultPanelExternalState: ContextResultPanelComponent.ExternalState?
private var currentInputMode: InputMode? private var viewForOverlayContent: ViewForOverlayContent?
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
private var component: MessageInputPanelComponent? private var component: MessageInputPanelComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
@ -272,6 +277,28 @@ public final class MessageInputPanelComponent: Component {
self.addSubview(self.gradientView) self.addSubview(self.gradientView)
self.fieldBackgroundView.addSubview(self.vibrancyEffectView) self.fieldBackgroundView.addSubview(self.vibrancyEffectView)
self.addSubview(self.fieldBackgroundView) self.addSubview(self.fieldBackgroundView)
self.viewForOverlayContent = ViewForOverlayContent(
ignoreHit: { [weak self] view, point in
guard let self else {
return false
}
if self.hitTest(view.convert(point, to: self), with: nil) != nil {
return true
}
if view.convert(point, to: self).y > self.bounds.maxY {
return true
}
return false
},
dismissSuggestions: { [weak self] in
guard let self else {
return
}
self.textFieldExternalState.dismissedEmojiSuggestionPosition = self.textFieldExternalState.currentEmojiSuggestion?.position
self.state?.updated()
}
)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -351,6 +378,17 @@ public final class MessageInputPanelComponent: Component {
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event) let result = super.hitTest(point, with: event)
if let _ = self.textField.view, let currentEmojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion, let currentEmojiSuggestionView = self.currentEmojiSuggestionView {
if let result = currentEmojiSuggestionView.hitTest(self.convert(point, to: currentEmojiSuggestionView), with: event) {
return result
}
self.textFieldExternalState.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position
if let textFieldView = self.textField.view as? TextFieldComponent.View {
textFieldView.updateEmojiSuggestion(transition: .immediate)
}
self.state?.updated()
}
if result == nil, let contextQueryResultPanel = self.contextQueryResultPanel?.view, let panelResult = contextQueryResultPanel.hitTest(self.convert(point, to: contextQueryResultPanel), with: event), panelResult !== contextQueryResultPanel { if result == nil, let contextQueryResultPanel = self.contextQueryResultPanel?.view, let panelResult = contextQueryResultPanel.hitTest(self.convert(point, to: contextQueryResultPanel), with: event), panelResult !== contextQueryResultPanel {
return panelResult return panelResult
} }
@ -513,9 +551,18 @@ public final class MessageInputPanelComponent: Component {
if let textFieldView = self.textField.view { if let textFieldView = self.textField.view {
if textFieldView.superview == nil { if textFieldView.superview == nil {
self.addSubview(textFieldView) self.addSubview(textFieldView)
if let viewForOverlayContent = self.viewForOverlayContent {
self.addSubview(viewForOverlayContent)
}
} }
transition.setFrame(view: textFieldView, frame: CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX, y: fieldBackgroundFrame.maxY - textFieldSize.height), size: textFieldSize)) let textFieldFrame = CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX, y: fieldBackgroundFrame.maxY - textFieldSize.height), size: textFieldSize)
transition.setFrame(view: textFieldView, frame: textFieldFrame)
transition.setAlpha(view: textFieldView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil) ? 0.0 : 1.0) transition.setAlpha(view: textFieldView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil) ? 0.0 : 1.0)
if let viewForOverlayContent = self.viewForOverlayContent {
transition.setFrame(view: viewForOverlayContent, frame: textFieldFrame)
}
} }
if let disabledPlaceholderText = component.disabledPlaceholder { if let disabledPlaceholderText = component.disabledPlaceholder {
@ -1123,6 +1170,9 @@ public final class MessageInputPanelComponent: Component {
} }
self.updateContextQueries() self.updateContextQueries()
let panelLeftInset: CGFloat = max(insets.left, 7.0)
let panelRightInset: CGFloat = max(insets.right, 41.0)
if let result = self.contextQueryResults[.mention], result.count > 0 && self.textFieldExternalState.isEditing { if let result = self.contextQueryResults[.mention], result.count > 0 && self.textFieldExternalState.isEditing {
let availablePanelHeight: CGFloat = 413.0 let availablePanelHeight: CGFloat = 413.0
@ -1142,8 +1192,6 @@ public final class MessageInputPanelComponent: Component {
animateIn = true animateIn = true
transition = .immediate transition = .immediate
} }
let panelLeftInset: CGFloat = max(insets.left, 7.0)
let panelRightInset: CGFloat = max(insets.right, 41.0)
let panelSize = panel.update( let panelSize = panel.update(
transition: transition, transition: transition,
component: AnyComponent(ContextResultPanelComponent( component: AnyComponent(ContextResultPanelComponent(
@ -1209,6 +1257,143 @@ public final class MessageInputPanelComponent: Component {
}) })
} }
if let emojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion, emojiSuggestion.disposable == nil {
emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value)
|> deliverOnMainQueue).start(next: { [weak self, weak emojiSuggestion] result in
guard let self, let emojiSuggestion, self.textFieldExternalState.currentEmojiSuggestion === emojiSuggestion else {
return
}
emojiSuggestion.value = result
self.state?.updated()
})
}
var hasTrackingView = self.textFieldExternalState.hasTrackingView
if let currentEmojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion, let value = currentEmojiSuggestion.value as? [TelegramMediaFile], value.isEmpty {
hasTrackingView = false
}
if !self.textFieldExternalState.isEditing {
hasTrackingView = false
}
if !hasTrackingView {
if let currentEmojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion {
self.textFieldExternalState.currentEmojiSuggestion = nil
currentEmojiSuggestion.disposable?.dispose()
}
if let currentEmojiSuggestionView = self.currentEmojiSuggestionView {
self.currentEmojiSuggestionView = nil
currentEmojiSuggestionView.alpha = 0.0
currentEmojiSuggestionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak currentEmojiSuggestionView] _ in
currentEmojiSuggestionView?.removeFromSuperview()
})
}
}
if let currentEmojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion, let value = currentEmojiSuggestion.value as? [TelegramMediaFile] {
let currentEmojiSuggestionView: ComponentHostView<Empty>
if let current = self.currentEmojiSuggestionView {
currentEmojiSuggestionView = current
} else {
currentEmojiSuggestionView = ComponentHostView<Empty>()
self.currentEmojiSuggestionView = currentEmojiSuggestionView
self.addSubview(currentEmojiSuggestionView)
currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
//self.installEmojiSuggestionPreviewGesture(hostView: currentEmojiSuggestionView)
}
let globalPosition: CGPoint
if let textView = self.textField.view {
globalPosition = textView.convert(currentEmojiSuggestion.localPosition, to: self)
} else {
globalPosition = .zero
}
let sideInset: CGFloat = 7.0
let viewSize = currentEmojiSuggestionView.update(
transition: .immediate,
component: AnyComponent(EmojiSuggestionsComponent(
context: component.context,
userLocation: .other,
theme: EmojiSuggestionsComponent.Theme(
backgroundColor: UIColor(white: 0.0, alpha: 0.5),
textColor: .white,
placeholderColor: UIColor(rgb: 0xffffff).mixedWith(UIColor(rgb: 0x1c1c1d), alpha: 0.9)
),
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
files: value,
action: { [weak self] file in
guard let self, let textView = self.textField.view as? TextFieldComponent.View, let currentEmojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion else {
return
}
AudioServicesPlaySystemSound(0x450)
let inputState = textView.getInputState()
let inputText = NSMutableAttributedString(attributedString: inputState.inputText)
var text: String?
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, _, displayText, _):
text = displayText
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file)
break loop
default:
break
}
}
if let emojiAttribute = emojiAttribute, let text = text {
let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])
let range = currentEmojiSuggestion.position.range
let previousText = inputText.attributedSubstring(from: range)
inputText.replaceCharacters(in: range, with: replacementText)
var replacedUpperBound = range.lowerBound
while true {
if inputText.attributedSubstring(from: NSRange(location: 0, length: replacedUpperBound)).string.hasSuffix(previousText.string) {
let replaceRange = NSRange(location: replacedUpperBound - previousText.length, length: previousText.length)
if replaceRange.location < 0 {
break
}
let adjacentString = inputText.attributedSubstring(from: replaceRange)
if adjacentString.string != previousText.string || adjacentString.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) != nil {
break
}
inputText.replaceCharacters(in: replaceRange, with: NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: emojiAttribute.interactivelySelectedFromPackId, fileId: emojiAttribute.fileId, file: emojiAttribute.file)]))
replacedUpperBound = replaceRange.lowerBound
} else {
break
}
}
let selectionPosition = range.lowerBound + (replacementText.string as NSString).length
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
}
}
)),
environment: {},
containerSize: CGSize(width: self.bounds.width - panelLeftInset - panelRightInset, height: 100.0)
)
let viewFrame = CGRect(origin: CGPoint(x: min(self.bounds.width - sideInset - viewSize.width, max(panelLeftInset, floor(globalPosition.x - viewSize.width / 2.0))), y: globalPosition.y - 4.0 - viewSize.height), size: viewSize)
currentEmojiSuggestionView.frame = viewFrame
if let componentView = currentEmojiSuggestionView.componentView as? EmojiSuggestionsComponent.View {
componentView.adjustBackground(relativePositionX: floor(globalPosition.x - viewFrame.minX))
}
}
return size return size
} }
} }
@ -1221,3 +1406,44 @@ public final class MessageInputPanelComponent: Component {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
} }
} }
final class ViewForOverlayContent: UIView {
let ignoreHit: (UIView, CGPoint) -> Bool
let dismissSuggestions: () -> Void
init(ignoreHit: @escaping (UIView, CGPoint) -> Bool, dismissSuggestions: @escaping () -> Void) {
self.ignoreHit = ignoreHit
self.dismissSuggestions = dismissSuggestions
super.init(frame: CGRect())
}
required init(coder: NSCoder) {
preconditionFailure()
}
func maybeDismissContent(point: CGPoint) {
for subview in self.subviews.reversed() {
if let _ = subview.hitTest(self.convert(point, to: subview), with: nil) {
return
}
}
self.dismissSuggestions()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for subview in self.subviews.reversed() {
if let result = subview.hitTest(self.convert(point, to: subview), with: event) {
return result
}
}
if event == nil || self.ignoreHit(self, point) {
return nil
}
self.dismissSuggestions()
return nil
}
}

View File

@ -239,7 +239,7 @@ final class CategoryListItemComponent: Component {
text: .plain(NSAttributedString(string: labelData.0, font: Font.regular(15.0), textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor)) text: .plain(NSAttributedString(string: labelData.0, font: Font.regular(15.0), textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor))
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) containerSize: CGSize(width: availableSize.width - leftInset - rightInset - 14.0, height: 100.0)
) )
let labelArrowSize = self.labelArrow.update( let labelArrowSize = self.labelArrow.update(

View File

@ -9,6 +9,7 @@ import ComponentDisplayAdapters
import TelegramPresentationData import TelegramPresentationData
import AccountContext import AccountContext
import TelegramCore import TelegramCore
import Postbox
import MultilineTextComponent import MultilineTextComponent
import SolidRoundedButtonComponent import SolidRoundedButtonComponent
import PresentationDataUtils import PresentationDataUtils
@ -31,6 +32,7 @@ final class ShareWithPeersScreenComponent: Component {
let screenshot: Bool let screenshot: Bool
let pin: Bool let pin: Bool
let timeout: Int let timeout: Int
let mentions: [String]
let categoryItems: [CategoryItem] let categoryItems: [CategoryItem]
let optionItems: [OptionItem] let optionItems: [OptionItem]
let completion: (EngineStoryPrivacy, Bool, Bool) -> Void let completion: (EngineStoryPrivacy, Bool, Bool) -> Void
@ -43,6 +45,7 @@ final class ShareWithPeersScreenComponent: Component {
screenshot: Bool, screenshot: Bool,
pin: Bool, pin: Bool,
timeout: Int, timeout: Int,
mentions: [String],
categoryItems: [CategoryItem], categoryItems: [CategoryItem],
optionItems: [OptionItem], optionItems: [OptionItem],
completion: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, completion: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void,
@ -54,6 +57,7 @@ final class ShareWithPeersScreenComponent: Component {
self.screenshot = screenshot self.screenshot = screenshot
self.pin = pin self.pin = pin
self.timeout = timeout self.timeout = timeout
self.mentions = mentions
self.categoryItems = categoryItems self.categoryItems = categoryItems
self.optionItems = optionItems self.optionItems = optionItems
self.completion = completion self.completion = completion
@ -79,6 +83,9 @@ final class ShareWithPeersScreenComponent: Component {
if lhs.timeout != rhs.timeout { if lhs.timeout != rhs.timeout {
return false return false
} }
if lhs.mentions != rhs.mentions {
return false
}
if lhs.categoryItems != rhs.categoryItems { if lhs.categoryItems != rhs.categoryItems {
return false return false
} }
@ -1508,7 +1515,7 @@ final class ShareWithPeersScreenComponent: Component {
guard let self, let component = self.component, let controller = self.environment?.controller() as? ShareWithPeersScreen else { guard let self, let component = self.component, let controller = self.environment?.controller() as? ShareWithPeersScreen else {
return return
} }
let base: EngineStoryPrivacy.Base let base: EngineStoryPrivacy.Base
if self.selectedCategories.contains(.everyone) { if self.selectedCategories.contains(.everyone) {
base = .everyone base = .everyone
@ -1522,17 +1529,126 @@ final class ShareWithPeersScreenComponent: Component {
base = .nobody base = .nobody
} }
component.completion( let proceed = {
EngineStoryPrivacy( component.completion(
base: base, EngineStoryPrivacy(
additionallyIncludePeers: self.selectedPeers base: base,
), additionallyIncludePeers: self.selectedPeers
self.selectedOptions.contains(.screenshot), ),
self.selectedOptions.contains(.pin) self.selectedOptions.contains(.screenshot),
) self.selectedOptions.contains(.pin)
)
controller.dismissAllTooltips() controller.dismissAllTooltips()
controller.dismiss() controller.dismiss()
}
let presentAlert: ([String]) -> Void = { usernames in
let usernamesString = String(usernames.map { "@\($0)" }.joined(separator: ", "))
let alertController = textAlertController(
context: component.context,
forceTheme: defaultDarkColorPresentationTheme,
title: "Privacy Restrictions",
text: "The privacy settings of your story will prevent some users you tagged (\( usernamesString )) from viewing it.",
actions: [
TextAlertAction(type: .defaultAction, title: "Proceed Anyway", action: {
proceed()
}),
TextAlertAction(type: .genericAction, title: "Cancel", action: {})
],
actionLayout: .vertical
)
controller.present(alertController, in: .window(.root))
}
func matchingUsername(user: TelegramUser, usernames: Set<String>) -> String? {
for username in user.usernames {
if usernames.contains(username.username) {
return username.username
}
}
if let username = user.username {
if usernames.contains(username) {
return username
}
}
return nil
}
let context = component.context
let selectedPeerIds = self.selectedPeers
if case .stories = component.stateContext.subject {
if component.mentions.isEmpty {
proceed()
} else if case .nobody = base {
if selectedPeerIds.isEmpty {
presentAlert(component.mentions)
} else {
let _ = (context.account.postbox.transaction { transaction in
var filteredMentions = Set(component.mentions)
for peerId in selectedPeerIds {
if let user = transaction.getPeer(peerId) as? TelegramUser, let username = matchingUsername(user: user, usernames: filteredMentions) {
filteredMentions.remove(username)
}
}
return Array(filteredMentions)
}
|> deliverOnMainQueue).start(next: { mentions in
if mentions.isEmpty {
proceed()
} else {
presentAlert(mentions)
}
})
}
} else if case .contacts = base {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false))
|> map { contacts -> [String] in
var filteredMentions = Set(component.mentions)
let peers = contacts.peers
for peer in peers {
if selectedPeerIds.contains(peer.id) {
continue
}
if case let .user(user) = peer, let username = matchingUsername(user: user, usernames: filteredMentions) {
filteredMentions.remove(username)
}
}
return Array(filteredMentions)
}
|> deliverOnMainQueue).start(next: { mentions in
if mentions.isEmpty {
proceed()
} else {
presentAlert(mentions)
}
})
} else if case .closeFriends = base {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false))
|> map { contacts -> [String] in
var filteredMentions = Set(component.mentions)
let peers = contacts.peers
for peer in peers {
if case let .user(user) = peer, user.flags.contains(.isCloseFriend), let username = matchingUsername(user: user, usernames: filteredMentions) {
filteredMentions.remove(username)
}
}
return Array(filteredMentions)
}
|> deliverOnMainQueue).start(next: { mentions in
if mentions.isEmpty {
proceed()
} else {
presentAlert(mentions)
}
})
} else {
proceed()
}
} else {
proceed()
}
} }
)), )),
environment: {}, environment: {},
@ -1665,11 +1781,27 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
switch subject { switch subject {
case .stories: case .stories:
let state = State(peers: [], presences: [:]) var signals: [Signal<EnginePeer?, NoError>] = []
self.stateValue = state if initialPeerIds.count < 3 {
self.stateSubject.set(.single(state)) for peerId in initialPeerIds {
self.readySubject.set(true) signals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)))
self.initialPeerIds = initialPeerIds }
}
self.stateDisposable = (combineLatest(signals)
|> deliverOnMainQueue).start(next: { [weak self] peers in
guard let self else {
return
}
let state = State(
peers: peers.compactMap { $0 },
presences: [:]
)
self.stateValue = state
self.stateSubject.set(.single(state))
self.readySubject.set(true)
})
case .chats: case .chats:
self.stateDisposable = (context.engine.messages.chatList(group: .root, count: 200) self.stateDisposable = (context.engine.messages.chatList(group: .root, count: 200)
|> deliverOnMainQueue).start(next: { [weak self] chatList in |> deliverOnMainQueue).start(next: { [weak self] chatList in
@ -1805,12 +1937,15 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
allowScreenshots: Bool = true, allowScreenshots: Bool = true,
pin: Bool = false, pin: Bool = false,
timeout: Int = 0, timeout: Int = 0,
mentions: [String] = [],
stateContext: StateContext, stateContext: StateContext,
completion: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, completion: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void,
editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void
) { ) {
self.context = context self.context = context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = [] var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = []
var optionItems: [ShareWithPeersScreenComponent.OptionItem] = [] var optionItems: [ShareWithPeersScreenComponent.OptionItem] = []
if case let .stories(editing) = stateContext.subject { if case let .stories(editing) = stateContext.subject {
@ -1822,12 +1957,25 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
actionTitle: nil actionTitle: nil
)) ))
var peerNames = ""
if let peers = stateContext.stateValue?.peers, !peers.isEmpty {
peerNames = String(peers.map { $0.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) }.joined(separator: ", "))
}
var contactsSubtitle = "exclude people" var contactsSubtitle = "exclude people"
if initialPrivacy.base == .contacts, initialPrivacy.additionallyIncludePeers.count > 0 { if initialPrivacy.base == .contacts, initialPrivacy.additionallyIncludePeers.count > 0 {
if initialPrivacy.additionallyIncludePeers.count == 1 { if initialPrivacy.additionallyIncludePeers.count == 1 {
contactsSubtitle = "except 1 person" if !peerNames.isEmpty {
contactsSubtitle = "except \(peerNames)"
} else {
contactsSubtitle = "except 1 person"
}
} else { } else {
contactsSubtitle = "except \(initialPrivacy.additionallyIncludePeers.count) people" if !peerNames.isEmpty {
contactsSubtitle = "except \(peerNames)"
} else {
contactsSubtitle = "except \(initialPrivacy.additionallyIncludePeers.count) people"
}
} }
} }
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
@ -1849,9 +1997,17 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
var selectedContactsSubtitle = "choose" var selectedContactsSubtitle = "choose"
if initialPrivacy.base == .nobody, initialPrivacy.additionallyIncludePeers.count > 0 { if initialPrivacy.base == .nobody, initialPrivacy.additionallyIncludePeers.count > 0 {
if initialPrivacy.additionallyIncludePeers.count == 1 { if initialPrivacy.additionallyIncludePeers.count == 1 {
selectedContactsSubtitle = "1 person" if !peerNames.isEmpty {
selectedContactsSubtitle = peerNames
} else {
selectedContactsSubtitle = "1 person"
}
} else { } else {
selectedContactsSubtitle = "\(initialPrivacy.additionallyIncludePeers.count) people" if !peerNames.isEmpty {
selectedContactsSubtitle = peerNames
} else {
selectedContactsSubtitle = "\(initialPrivacy.additionallyIncludePeers.count) people"
}
} }
} }
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
@ -1882,6 +2038,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
screenshot: allowScreenshots, screenshot: allowScreenshots,
pin: pin, pin: pin,
timeout: timeout, timeout: timeout,
mentions: mentions,
categoryItems: categoryItems, categoryItems: categoryItems,
optionItems: optionItems, optionItems: optionItems,
completion: completion, completion: completion,

View File

@ -439,6 +439,8 @@ public final class StoryContentContextImpl: StoryContentContext {
hasMoreToken: nil hasMoreToken: nil
) )
var preFilterOrder = false
let startedWithUnseen: Bool let startedWithUnseen: Bool
if let current = self.startedWithUnseen { if let current = self.startedWithUnseen {
startedWithUnseen = current startedWithUnseen = current
@ -473,11 +475,17 @@ public final class StoryContentContextImpl: StoryContentContext {
self.startedWithUnseen = startedWithUnseenValue self.startedWithUnseen = startedWithUnseenValue
startedWithUnseen = startedWithUnseenValue startedWithUnseen = startedWithUnseenValue
preFilterOrder = true
} }
var sortedItems: [EngineStorySubscriptions.Item] = [] var sortedItems: [EngineStorySubscriptions.Item] = []
for peerId in self.fixedSubscriptionOrder { for peerId in self.fixedSubscriptionOrder {
if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == peerId }) { if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == peerId }) {
if preFilterOrder {
if startedWithUnseen && !storySubscriptions.items[index].hasUnseen {
continue
}
}
sortedItems.append(storySubscriptions.items[index]) sortedItems.append(storySubscriptions.items[index])
} }
} }
@ -507,45 +515,64 @@ public final class StoryContentContextImpl: StoryContentContext {
return return
} }
var preFilterOrder = false
let startedWithUnseen: Bool let startedWithUnseen: Bool
if let current = self.startedWithUnseen { if let current = self.startedWithUnseen {
startedWithUnseen = current startedWithUnseen = current
} else { } else {
var startedWithUnseenValue = false var startedWithUnseenValue = false
var centralIndex: Int? if let (focusedPeerId, _) = self.focusedItem, focusedPeerId == self.context.account.peerId, let accountItem = storySubscriptions.accountItem {
if let (focusedPeerId, _) = self.focusedItem { startedWithUnseenValue = accountItem.hasUnseen || accountItem.hasPending
if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == focusedPeerId }) { } else {
centralIndex = index var centralIndex: Int?
if let (focusedPeerId, _) = self.focusedItem {
if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == focusedPeerId }) {
centralIndex = index
}
} }
} if centralIndex == nil {
if centralIndex == nil { if let index = storySubscriptions.items.firstIndex(where: { $0.hasUnseen }) {
if let index = storySubscriptions.items.firstIndex(where: { $0.hasUnseen }) { centralIndex = index
centralIndex = index }
} }
} if centralIndex == nil {
if centralIndex == nil { if !storySubscriptions.items.isEmpty {
if !storySubscriptions.items.isEmpty { centralIndex = 0
centralIndex = 0 }
} }
}
if let centralIndex {
if let centralIndex { if storySubscriptions.items[centralIndex].hasUnseen {
if storySubscriptions.items[centralIndex].hasUnseen { startedWithUnseenValue = true
startedWithUnseenValue = true }
} }
} }
self.startedWithUnseen = startedWithUnseenValue self.startedWithUnseen = startedWithUnseenValue
startedWithUnseen = startedWithUnseenValue startedWithUnseen = startedWithUnseenValue
preFilterOrder = true
} }
var sortedItems: [EngineStorySubscriptions.Item] = [] var sortedItems: [EngineStorySubscriptions.Item] = []
if !startedWithUnseen, let accountItem = storySubscriptions.accountItem, accountItem.storyCount != 0 { if let accountItem = storySubscriptions.accountItem {
sortedItems.append(accountItem) if startedWithUnseen {
if accountItem.hasUnseen || accountItem.hasPending {
sortedItems.append(accountItem)
}
} else {
sortedItems.append(accountItem)
}
} }
for peerId in self.fixedSubscriptionOrder { for peerId in self.fixedSubscriptionOrder {
if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == peerId }) { if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == peerId }) {
if preFilterOrder {
if startedWithUnseen && !storySubscriptions.items[index].hasUnseen {
continue
}
}
sortedItems.append(storySubscriptions.items[index]) sortedItems.append(storySubscriptions.items[index])
} }
} }

View File

@ -23,10 +23,34 @@ public final class TextFieldComponent: Component {
public fileprivate(set) var hasText: Bool = false public fileprivate(set) var hasText: Bool = false
public var initialText: NSAttributedString? public var initialText: NSAttributedString?
public var hasTrackingView = false
public var currentEmojiSuggestion: EmojiSuggestion?
public var dismissedEmojiSuggestionPosition: EmojiSuggestion.Position?
public init() { public init() {
} }
} }
public final class EmojiSuggestion {
public struct Position: Equatable {
public var range: NSRange
public var value: String
}
public var localPosition: CGPoint
public var position: Position
public var disposable: Disposable?
public var value: Any?
init(localPosition: CGPoint, position: Position) {
self.localPosition = localPosition
self.position = position
self.disposable = nil
self.value = nil
}
}
public final class AnimationHint { public final class AnimationHint {
public enum Kind { public enum Kind {
case textChanged case textChanged
@ -116,7 +140,7 @@ public final class TextFieldComponent: Component {
private var spoilerView: InvisibleInkDustView? private var spoilerView: InvisibleInkDustView?
private var customEmojiContainerView: CustomEmojiContainerView? private var customEmojiContainerView: CustomEmojiContainerView?
private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
private var inputState: InputState { private var inputState: InputState {
let selectionRange: Range<Int> = self.textView.selectedRange.location ..< (self.textView.selectedRange.location + self.textView.selectedRange.length) let selectionRange: Range<Int> = self.textView.selectedRange.location ..< (self.textView.selectedRange.location + self.textView.selectedRange.length)
return InputState(inputText: stateAttributedStringForText(self.textView.attributedText ?? NSAttributedString()), selectionRange: selectionRange) return InputState(inputText: stateAttributedStringForText(self.textView.attributedText ?? NSAttributedString()), selectionRange: selectionRange)
@ -223,6 +247,7 @@ public final class TextFieldComponent: Component {
} }
self.updateSpoilersRevealed() self.updateSpoilersRevealed()
self.updateEmojiSuggestion(transition: .immediate)
} }
public func textViewDidBeginEditing(_ textView: UITextView) { public func textViewDidBeginEditing(_ textView: UITextView) {
@ -335,11 +360,6 @@ public final class TextFieldComponent: Component {
} }
self.textView.becomeFirstResponder() self.textView.becomeFirstResponder()
} }
// strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, {
// return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({
// $0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex))
// })
// })
} }
}) })
component.present(controller) component.present(controller)
@ -547,6 +567,60 @@ public final class TextFieldComponent: Component {
} }
} }
public func updateEmojiSuggestion(transition: Transition) {
guard let component = self.component else {
return
}
var hasTracking = false
var hasTrackingView = false
if self.textView.selectedRange.length == 0 && self.textView.selectedRange.location > 0 {
let selectedSubstring = self.textView.attributedText.attributedSubstring(from: NSRange(location: 0, length: self.textView.selectedRange.location))
if let lastCharacter = selectedSubstring.string.last, String(lastCharacter).isSingleEmoji {
let queryLength = (String(lastCharacter) as NSString).length
if selectedSubstring.attribute(ChatTextInputAttributes.customEmoji, at: selectedSubstring.length - queryLength, effectiveRange: nil) == nil {
let beginning = self.textView.beginningOfDocument
let characterRange = NSRange(location: selectedSubstring.length - queryLength, length: queryLength)
let start = self.textView.position(from: beginning, offset: selectedSubstring.length - queryLength)
let end = self.textView.position(from: beginning, offset: selectedSubstring.length)
if let start = start, let end = end, let textRange = self.textView.textRange(from: start, to: end) {
let selectionRects = self.textView.selectionRects(for: textRange)
let emojiSuggestionPosition = EmojiSuggestion.Position(range: characterRange, value: String(lastCharacter))
hasTracking = true
if let trackingRect = selectionRects.first?.rect {
let trackingPosition = CGPoint(x: trackingRect.midX, y: trackingRect.minY)
if component.externalState.dismissedEmojiSuggestionPosition == emojiSuggestionPosition {
} else {
hasTrackingView = true
let emojiSuggestion: EmojiSuggestion
if let current = component.externalState.currentEmojiSuggestion, current.position.value == emojiSuggestionPosition.value {
emojiSuggestion = current
} else {
emojiSuggestion = EmojiSuggestion(localPosition: trackingPosition, position: emojiSuggestionPosition)
component.externalState.currentEmojiSuggestion = emojiSuggestion
}
emojiSuggestion.localPosition = trackingPosition
emojiSuggestion.position = emojiSuggestionPosition
component.externalState.dismissedEmojiSuggestionPosition = nil
}
}
}
}
}
}
if !hasTracking {
component.externalState.dismissedEmojiSuggestionPosition = nil
}
component.externalState.hasTrackingView = hasTrackingView
}
func update(component: TextFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize { func update(component: TextFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component self.component = component
self.state = state self.state = state
@ -584,6 +658,8 @@ public final class TextFieldComponent: Component {
self.textView.frame = CGRect(origin: CGPoint(), size: size) self.textView.frame = CGRect(origin: CGPoint(), size: size)
self.textView.panGestureRecognizer.isEnabled = isEditing self.textView.panGestureRecognizer.isEnabled = isEditing
self.updateEmojiSuggestion(transition: .immediate)
if refreshScrolling { if refreshScrolling {
if isEditing { if isEditing {
if wasEditing { if wasEditing {

View File

@ -2859,7 +2859,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
component: AnyComponent(EmojiSuggestionsComponent( component: AnyComponent(EmojiSuggestionsComponent(
context: context, context: context,
userLocation: .other, userLocation: .other,
theme: theme, theme: EmojiSuggestionsComponent.Theme(theme: theme),
animationCache: presentationContext.animationCache, animationCache: presentationContext.animationCache,
animationRenderer: presentationContext.animationRenderer, animationRenderer: presentationContext.animationRenderer,
files: value, files: value,