diff --git a/submodules/ContactListUI/Sources/ContactContextMenus.swift b/submodules/ContactListUI/Sources/ContactContextMenus.swift index f5e7cbcd22..90f63ba04f 100644 --- a/submodules/ContactListUI/Sources/ContactContextMenus.swift +++ b/submodules/ContactListUI/Sources/ContactContextMenus.swift @@ -94,7 +94,7 @@ func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, con items.append(.action(ContextMenuActionItem(text: "Move to Chats", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MoveToChats"), color: theme.contextMenu.primaryColor) }, action: { _, f in - f(.default) + f(.dismissWithoutContent) context.engine.peers.updatePeerStoriesHidden(id: peerId, isHidden: false) diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 855e7ec65e..37b9aa36ad 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -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)) } |> 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) } else { return .signal(.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .text(text), reuploadInfo: nil, cacheReferenceKey: nil))), .text) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index fd85989db6..2fcb4eff13 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -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, media: Media) { +private func uploadedStoryContent(postbox: Postbox, network: Network, media: Media, accountPeerId: PeerId, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods, passFetchProgress: Bool) -> (signal: Signal, media: Media) { let originalMedia: Media = media let contentToUpload: MessageContentToUpload @@ -621,7 +621,7 @@ private func uploadedStoryContent(postbox: Postbox, network: Network, media: Med revalidationContext: revalidationContext, forceReupload: true, isGrouped: false, - passFetchProgress: false, + passFetchProgress: passFetchProgress, peerId: accountPeerId, messageId: nil, 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 { - 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 |> mapToSignal { result -> Signal in switch result { @@ -896,7 +897,11 @@ func _internal_editStory(account: Account, media: EngineStoryInputMedia?, id: In let contentSignal: Signal let originalMedia: 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 { contentSignal = .single(nil) originalMedia = nil diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 892b747940..14fa0a0a75 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -359,11 +359,20 @@ private final class CameraScreenComponent: CombinedComponent { action.invoke(Void()) } + private var lastDualCameraTimestamp: Double? func toggleDualCamera() { + let currentTimestamp = CACurrentMediaTime() + if let lastDualCameraTimestamp = self.lastDualCameraTimestamp, currentTimestamp - lastDualCameraTimestamp < 1.5 { + return + } + self.lastDualCameraTimestamp = currentTimestamp + let isEnabled = !self.cameraState.isDualCamEnabled self.camera.setDualCamEnabled(isEnabled) self.cameraState = self.cameraState.updatedIsDualCamEnabled(isEnabled) self.updated(transition: .easeInOut(duration: 0.1)) + + self.hapticFeedback.impact(.light) } func updateSwipeHint(_ hint: CaptureControlsComponent.SwipeHint) { @@ -1600,13 +1609,11 @@ public class CameraScreen: ViewController { } 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 } - let parentFrame = self.view.convert(self.bounds, to: nil) - 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 location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 29.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 return .ignore diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift index 92be146087..df6e5a93e7 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift @@ -772,6 +772,9 @@ final class CaptureControlsComponent: Component { self.component?.swipeHintUpdated(.flip) if location.x > self.frame.width / 2.0 + 60.0 { 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 { self.didFlip = true self.hapticFeedback.impact(.light) diff --git a/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift index 535b79b95d..143d96eff1 100644 --- a/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift +++ b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift @@ -15,6 +15,22 @@ import TelegramUIPreferences public final class EmojiSuggestionsComponent: Component { 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> { let hasPremium: Signal if isSavedMessages { @@ -98,7 +114,7 @@ public final class EmojiSuggestionsComponent: Component { } public let context: AccountContext - public let theme: PresentationTheme + public let theme: Theme public let animationCache: AnimationCache public let animationRenderer: MultiAnimationRenderer public let files: [TelegramMediaFile] @@ -107,7 +123,7 @@ public final class EmojiSuggestionsComponent: Component { public init( context: AccountContext, userLocation: MediaResourceUserLocation, - theme: PresentationTheme, + theme: Theme, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, files: [TelegramMediaFile], @@ -125,7 +141,7 @@ public final class EmojiSuggestionsComponent: Component { if lhs.context !== rhs.context { return false } - if lhs.theme !== rhs.theme { + if lhs.theme != rhs.theme { return false } if lhs.animationCache !== rhs.animationCache { @@ -305,7 +321,7 @@ public final class EmojiSuggestionsComponent: Component { let itemLayer: InlineStickerItemLayer if let current = self.visibleLayers[item.fileId] { itemLayer = current - itemLayer.dynamicColor = component.theme.list.itemPrimaryTextColor + itemLayer.dynamicColor = component.theme.textColor } else { itemLayer = InlineStickerItemLayer( context: component.context, @@ -315,9 +331,9 @@ public final class EmojiSuggestionsComponent: Component { file: item, cache: component.animationCache, renderer: component.animationRenderer, - placeholderColor: component.theme.list.mediaPlaceholderColor, + placeholderColor: component.theme.placeholderColor, pointSize: itemFrame.size, - dynamicColor: component.theme.list.itemPrimaryTextColor + dynamicColor: component.theme.textColor ) self.visibleLayers[item.fileId] = 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, transition: Transition) -> CGSize { 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 = 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 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) } } + +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 + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift index fb017e0585..480a78a79b 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift @@ -641,9 +641,13 @@ final class VideoInputScalePass: RenderPass { } 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 { return input } +#endif let scaledSize = CGSize(width: input.width, height: input.height).fitted(CGSize(width: 1920.0, height: 1920.0)) let width: Int @@ -691,8 +695,11 @@ final class VideoInputScalePass: RenderPass { renderCommandEncoder.setRenderPipelineState(self.mainPipelineState!) +#if targetEnvironment(simulator) + let secondInput = input +#endif + let (mainVideoState, additionalVideoState, transitionVideoState) = self.transitionState(for: timestamp, mainInput: input, additionalInput: secondInput) - if let transitionVideoState { self.encodeVideo( diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 3d1d2f1f3f..a186c5894e 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -28,6 +28,7 @@ import CameraButtonComponent import UndoUI import ChatEntityKeyboardInputNode import ChatPresentationInterfaceState +import TextFormat enum DrawingScreenType { 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) } + var doneButtonTitle = "NEXT" + if let controller = environment.controller() as? MediaEditorScreen, controller.isEditingStory { + doneButtonTitle = "DONE" + } + let doneButtonSize = self.doneButton.update( transition: transition, component: AnyComponent(Button( content: AnyComponent(DoneButtonComponent( backgroundColor: UIColor(rgb: 0x007aff), icon: UIImage(bundleImageName: "Media Editor/Next")!, - title: "NEXT")), + title: doneButtonTitle)), action: { guard let controller = environment.controller() as? MediaEditorScreen else { return @@ -1857,25 +1863,25 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } } -//#if DEBUG -// if case let .asset(asset) = subject, asset.mediaType == .video { -// let videoEntity = DrawingStickerEntity(content: .dualVideoReference) -// videoEntity.referenceDrawingSize = storyDimensions -// videoEntity.scale = 1.49 -// videoEntity.position = PIPPosition.bottomRight.getPosition(storyDimensions) -// self.entitiesView.add(videoEntity, announce: false) -// -// 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) -// if let entityView = self.entitiesView.getView(for: videoEntity.uuid) as? DrawingStickerEntityView { -// entityView.updated = { [weak videoEntity, weak self] in -// if let self, let videoEntity { -// self.mediaEditor?.setAdditionalVideoPosition(videoEntity.position, scale: videoEntity.scale, rotation: videoEntity.rotation) -// } -// } -// } -// } -//#endif +#if targetEnvironment(simulator) + if case let .asset(asset) = subject, asset.mediaType == .video { + let videoEntity = DrawingStickerEntity(content: .dualVideoReference) + videoEntity.referenceDrawingSize = storyDimensions + videoEntity.scale = 1.49 + videoEntity.position = PIPPosition.bottomRight.getPosition(storyDimensions) + self.entitiesView.add(videoEntity, announce: false) + + 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) + if let entityView = self.entitiesView.getView(for: videoEntity.uuid) as? DrawingStickerEntityView { + entityView.updated = { [weak videoEntity, weak self] in + if let self, let videoEntity { + self.mediaEditor?.setAdditionalVideoPosition(videoEntity.position, scale: videoEntity.scale, rotation: videoEntity.rotation) + } + } + } + } +#endif self.gradientColorsDisposable = mediaEditor.gradientColors.start(next: { [weak self] colors in if let self, let colors { @@ -3098,6 +3104,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate 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.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { @@ -3112,6 +3121,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate allowScreenshots: !privacy.isForwardingDisabled, pin: privacy.pin, timeout: privacy.timeout, + mentions: mentions, stateContext: stateContext, completion: { [weak self] privacy, allowScreenshots, pin in guard let self else { @@ -3333,6 +3343,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate func requestDismiss(saveDraft: Bool, animated: Bool) { self.dismissAllTooltips() + var showDraftTooltip = saveDraft + if let subject = self.node.subject, case .draft = subject { + showDraftTooltip = false + } if saveDraft { self.saveDraft(id: nil) } else { @@ -3346,7 +3360,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } self.node.entitiesView.invalidate() - self.cancelled(saveDraft) + self.cancelled(showDraftTooltip) self.willDismiss() @@ -3973,8 +3987,10 @@ final class DoneButtonComponent: CombinedComponent { ) 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? if let titleText = context.component.title { title = text.update( @@ -3986,14 +4002,15 @@ final class DoneButtonComponent: CombinedComponent { availableSize: CGSize(width: 180.0, height: 100.0), 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( component: RoundedRectangle(color: context.component.backgroundColor, cornerRadius: backgroundHeight / 2.0), availableSize: backgroundSize, diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD index f48ad6e74c..354df30334 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD @@ -13,6 +13,7 @@ swift_library( "//submodules/Display", "//submodules/ComponentFlow", "//submodules/AppBundle", + "//submodules/TelegramCore", "//submodules/TelegramUI/Components/TextFieldComponent", "//submodules/Components/BundleIconComponent", "//submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton", @@ -28,6 +29,7 @@ swift_library( "//submodules/TextFormat", "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", "//submodules/TelegramUI/Components/MoreHeaderButton", + "//submodules/TelegramUI/Components/EmojiSuggestionsComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift index 4a36e6044c..584bd0d883 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift @@ -1,5 +1,6 @@ import Foundation import SwiftSignalKit +import TelegramCore import TextFieldComponent import ChatContextQuery import AccountContext @@ -129,6 +130,110 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, inp |> castError(ChatContextQueryError.self) 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: return .complete() } diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index f2c7d2dc88..06d13fe462 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -3,6 +3,7 @@ import UIKit import Display import ComponentFlow import SwiftSignalKit +import TelegramCore import AppBundle import TextFieldComponent import BundleIconComponent @@ -12,6 +13,8 @@ import ChatPresentationInterfaceState import LottieComponent import ChatContextQuery import TextFormat +import EmojiSuggestionsComponent +import AudioToolbox public final class MessageInputPanelComponent: Component { public enum Style { @@ -210,7 +213,7 @@ public final class MessageInputPanelComponent: Component { public enum SendMessageInput { case text(NSAttributedString) } - + public final class View: UIView { private let fieldBackgroundView: BlurredBackgroundView private let vibrancyEffectView: UIVisualEffectView @@ -240,13 +243,15 @@ public final class MessageInputPanelComponent: Component { private var currentMediaInputIsVoice: Bool = true private var mediaCancelFraction: CGFloat = 0.0 + private var currentInputMode: InputMode? + private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:] private var contextQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] = [:] - private var contextQueryResultPanel: ComponentView? private var contextQueryResultPanelExternalState: ContextResultPanelComponent.ExternalState? - private var currentInputMode: InputMode? + private var viewForOverlayContent: ViewForOverlayContent? + private var currentEmojiSuggestionView: ComponentHostView? private var component: MessageInputPanelComponent? private weak var state: EmptyComponentState? @@ -272,6 +277,28 @@ public final class MessageInputPanelComponent: Component { self.addSubview(self.gradientView) self.fieldBackgroundView.addSubview(self.vibrancyEffectView) 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) { @@ -351,6 +378,17 @@ public final class MessageInputPanelComponent: Component { override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 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 { return panelResult } @@ -513,9 +551,18 @@ public final class MessageInputPanelComponent: Component { if let textFieldView = self.textField.view { if textFieldView.superview == nil { 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) + + if let viewForOverlayContent = self.viewForOverlayContent { + transition.setFrame(view: viewForOverlayContent, frame: textFieldFrame) + } } if let disabledPlaceholderText = component.disabledPlaceholder { @@ -1123,6 +1170,9 @@ public final class MessageInputPanelComponent: Component { } 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 { let availablePanelHeight: CGFloat = 413.0 @@ -1142,8 +1192,6 @@ public final class MessageInputPanelComponent: Component { animateIn = true transition = .immediate } - let panelLeftInset: CGFloat = max(insets.left, 7.0) - let panelRightInset: CGFloat = max(insets.right, 41.0) let panelSize = panel.update( transition: transition, 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 + if let current = self.currentEmojiSuggestionView { + currentEmojiSuggestionView = current + } else { + currentEmojiSuggestionView = ComponentHostView() + 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 } } @@ -1221,3 +1406,44 @@ public final class MessageInputPanelComponent: Component { 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 + } +} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift index a1f23de673..48b059b18f 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift @@ -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)) )), 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( diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 9a64aa4119..2735fc2978 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -9,6 +9,7 @@ import ComponentDisplayAdapters import TelegramPresentationData import AccountContext import TelegramCore +import Postbox import MultilineTextComponent import SolidRoundedButtonComponent import PresentationDataUtils @@ -31,6 +32,7 @@ final class ShareWithPeersScreenComponent: Component { let screenshot: Bool let pin: Bool let timeout: Int + let mentions: [String] let categoryItems: [CategoryItem] let optionItems: [OptionItem] let completion: (EngineStoryPrivacy, Bool, Bool) -> Void @@ -43,6 +45,7 @@ final class ShareWithPeersScreenComponent: Component { screenshot: Bool, pin: Bool, timeout: Int, + mentions: [String], categoryItems: [CategoryItem], optionItems: [OptionItem], completion: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, @@ -54,6 +57,7 @@ final class ShareWithPeersScreenComponent: Component { self.screenshot = screenshot self.pin = pin self.timeout = timeout + self.mentions = mentions self.categoryItems = categoryItems self.optionItems = optionItems self.completion = completion @@ -79,6 +83,9 @@ final class ShareWithPeersScreenComponent: Component { if lhs.timeout != rhs.timeout { return false } + if lhs.mentions != rhs.mentions { + return false + } if lhs.categoryItems != rhs.categoryItems { 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 { return } - + let base: EngineStoryPrivacy.Base if self.selectedCategories.contains(.everyone) { base = .everyone @@ -1522,17 +1529,126 @@ final class ShareWithPeersScreenComponent: Component { base = .nobody } - component.completion( - EngineStoryPrivacy( - base: base, - additionallyIncludePeers: self.selectedPeers - ), - self.selectedOptions.contains(.screenshot), - self.selectedOptions.contains(.pin) - ) + let proceed = { + component.completion( + EngineStoryPrivacy( + base: base, + additionallyIncludePeers: self.selectedPeers + ), + self.selectedOptions.contains(.screenshot), + self.selectedOptions.contains(.pin) + ) - controller.dismissAllTooltips() - controller.dismiss() + controller.dismissAllTooltips() + 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? { + 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: {}, @@ -1665,11 +1781,27 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { switch subject { case .stories: - let state = State(peers: [], presences: [:]) - self.stateValue = state - self.stateSubject.set(.single(state)) - self.readySubject.set(true) - self.initialPeerIds = initialPeerIds + var signals: [Signal] = [] + if initialPeerIds.count < 3 { + for peerId in initialPeerIds { + signals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) + } + } + 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: self.stateDisposable = (context.engine.messages.chatList(group: .root, count: 200) |> deliverOnMainQueue).start(next: { [weak self] chatList in @@ -1805,12 +1937,15 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { allowScreenshots: Bool = true, pin: Bool = false, timeout: Int = 0, + mentions: [String] = [], stateContext: StateContext, completion: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void ) { self.context = context + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = [] var optionItems: [ShareWithPeersScreenComponent.OptionItem] = [] if case let .stories(editing) = stateContext.subject { @@ -1822,12 +1957,25 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { 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" if initialPrivacy.base == .contacts, initialPrivacy.additionallyIncludePeers.count > 0 { if initialPrivacy.additionallyIncludePeers.count == 1 { - contactsSubtitle = "except 1 person" + if !peerNames.isEmpty { + contactsSubtitle = "except \(peerNames)" + } else { + contactsSubtitle = "except 1 person" + } } else { - contactsSubtitle = "except \(initialPrivacy.additionallyIncludePeers.count) people" + if !peerNames.isEmpty { + contactsSubtitle = "except \(peerNames)" + } else { + contactsSubtitle = "except \(initialPrivacy.additionallyIncludePeers.count) people" + } } } categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( @@ -1849,9 +1997,17 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { var selectedContactsSubtitle = "choose" if initialPrivacy.base == .nobody, initialPrivacy.additionallyIncludePeers.count > 0 { if initialPrivacy.additionallyIncludePeers.count == 1 { - selectedContactsSubtitle = "1 person" + if !peerNames.isEmpty { + selectedContactsSubtitle = peerNames + } else { + selectedContactsSubtitle = "1 person" + } } else { - selectedContactsSubtitle = "\(initialPrivacy.additionallyIncludePeers.count) people" + if !peerNames.isEmpty { + selectedContactsSubtitle = peerNames + } else { + selectedContactsSubtitle = "\(initialPrivacy.additionallyIncludePeers.count) people" + } } } categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( @@ -1882,6 +2038,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { screenshot: allowScreenshots, pin: pin, timeout: timeout, + mentions: mentions, categoryItems: categoryItems, optionItems: optionItems, completion: completion, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 59a052caa3..3a6b83158c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -439,6 +439,8 @@ public final class StoryContentContextImpl: StoryContentContext { hasMoreToken: nil ) + var preFilterOrder = false + let startedWithUnseen: Bool if let current = self.startedWithUnseen { startedWithUnseen = current @@ -473,11 +475,17 @@ public final class StoryContentContextImpl: StoryContentContext { self.startedWithUnseen = startedWithUnseenValue startedWithUnseen = startedWithUnseenValue + preFilterOrder = true } var sortedItems: [EngineStorySubscriptions.Item] = [] for peerId in self.fixedSubscriptionOrder { 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]) } } @@ -507,45 +515,64 @@ public final class StoryContentContextImpl: StoryContentContext { return } + var preFilterOrder = false + let startedWithUnseen: Bool if let current = self.startedWithUnseen { startedWithUnseen = current } else { var startedWithUnseenValue = false - var centralIndex: Int? - if let (focusedPeerId, _) = self.focusedItem { - if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == focusedPeerId }) { - centralIndex = index + if let (focusedPeerId, _) = self.focusedItem, focusedPeerId == self.context.account.peerId, let accountItem = storySubscriptions.accountItem { + startedWithUnseenValue = accountItem.hasUnseen || accountItem.hasPending + } else { + 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 let index = storySubscriptions.items.firstIndex(where: { $0.hasUnseen }) { - centralIndex = index + if centralIndex == nil { + if let index = storySubscriptions.items.firstIndex(where: { $0.hasUnseen }) { + centralIndex = index + } } - } - if centralIndex == nil { - if !storySubscriptions.items.isEmpty { - centralIndex = 0 + if centralIndex == nil { + if !storySubscriptions.items.isEmpty { + centralIndex = 0 + } } - } - - if let centralIndex { - if storySubscriptions.items[centralIndex].hasUnseen { - startedWithUnseenValue = true + + if let centralIndex { + if storySubscriptions.items[centralIndex].hasUnseen { + startedWithUnseenValue = true + } } } self.startedWithUnseen = startedWithUnseenValue startedWithUnseen = startedWithUnseenValue + preFilterOrder = true } var sortedItems: [EngineStorySubscriptions.Item] = [] - if !startedWithUnseen, let accountItem = storySubscriptions.accountItem, accountItem.storyCount != 0 { - sortedItems.append(accountItem) + if let accountItem = storySubscriptions.accountItem { + if startedWithUnseen { + if accountItem.hasUnseen || accountItem.hasPending { + sortedItems.append(accountItem) + } + } else { + sortedItems.append(accountItem) + } } for peerId in self.fixedSubscriptionOrder { 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]) } } diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 6cdce3f439..96a9a25363 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -23,10 +23,34 @@ public final class TextFieldComponent: Component { public fileprivate(set) var hasText: Bool = false public var initialText: NSAttributedString? + public var hasTrackingView = false + + public var currentEmojiSuggestion: EmojiSuggestion? + public var dismissedEmojiSuggestionPosition: EmojiSuggestion.Position? + 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 enum Kind { case textChanged @@ -116,7 +140,7 @@ public final class TextFieldComponent: Component { private var spoilerView: InvisibleInkDustView? private var customEmojiContainerView: CustomEmojiContainerView? private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? - + private var inputState: InputState { let selectionRange: Range = self.textView.selectedRange.location ..< (self.textView.selectedRange.location + self.textView.selectedRange.length) return InputState(inputText: stateAttributedStringForText(self.textView.attributedText ?? NSAttributedString()), selectionRange: selectionRange) @@ -223,6 +247,7 @@ public final class TextFieldComponent: Component { } self.updateSpoilersRevealed() + self.updateEmojiSuggestion(transition: .immediate) } public func textViewDidBeginEditing(_ textView: UITextView) { @@ -335,11 +360,6 @@ public final class TextFieldComponent: Component { } 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) @@ -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, transition: Transition) -> CGSize { self.component = component self.state = state @@ -584,6 +658,8 @@ public final class TextFieldComponent: Component { self.textView.frame = CGRect(origin: CGPoint(), size: size) self.textView.panGestureRecognizer.isEnabled = isEditing + self.updateEmojiSuggestion(transition: .immediate) + if refreshScrolling { if isEditing { if wasEditing { diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 2781cd9d00..e98c6094ac 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -2859,7 +2859,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { component: AnyComponent(EmojiSuggestionsComponent( context: context, userLocation: .other, - theme: theme, + theme: EmojiSuggestionsComponent.Theme(theme: theme), animationCache: presentationContext.animationCache, animationRenderer: presentationContext.animationRenderer, files: value,