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/DeviceAccess/Sources/DeviceAccess.swift b/submodules/DeviceAccess/Sources/DeviceAccess.swift index 4751691d08..eeab79c9d2 100644 --- a/submodules/DeviceAccess/Sources/DeviceAccess.swift +++ b/submodules/DeviceAccess/Sources/DeviceAccess.swift @@ -11,7 +11,6 @@ import AddressBook import UserNotifications import CoreTelephony import TelegramPresentationData -import LegacyComponents import AccountContext public enum DeviceAccessCameraSubject { @@ -88,7 +87,7 @@ public final class DeviceAccess { } public static func isCameraAccessAuthorized() -> Bool { - return PGCamera.cameraAuthorizationStatus() == PGCameraAuthorizationStatusAuthorized + return AVCaptureDevice.authorizationStatus(for: .video) == .authorized } public static func authorizationStatus(applicationInForeground: Signal? = nil, siriAuthorization: (() -> AccessType)? = nil, subject: DeviceAccessSubject) -> Signal { @@ -257,8 +256,8 @@ public final class DeviceAccess { public static func authorizeAccess(to subject: DeviceAccessSubject, onlyCheck: Bool = false, registerForNotifications: ((@escaping (Bool) -> Void) -> Void)? = nil, requestSiriAuthorization: ((@escaping (Bool) -> Void) -> Void)? = nil, locationManager: LocationManager? = nil, presentationData: PresentationData? = nil, present: @escaping (ViewController, Any?) -> Void = { _, _ in }, openSettings: @escaping () -> Void = { }, displayNotificationFromBackground: @escaping (String) -> Void = { _ in }, _ completion: @escaping (Bool) -> Void = { _ in }) { switch subject { case let .camera(cameraSubject): - let status = PGCamera.cameraAuthorizationStatus() - if status == PGCameraAuthorizationStatusNotDetermined { + let status = AVCaptureDevice.authorizationStatus(for: .video) + if case .notDetermined = status { if !onlyCheck { AVCaptureDevice.requestAccess(for: AVMediaType.video) { response in Queue.mainQueue().async { @@ -282,9 +281,9 @@ public final class DeviceAccess { } else { completion(true) } - } else if status == PGCameraAuthorizationStatusRestricted || status == PGCameraAuthorizationStatusDenied, let presentationData = presentationData { + } else if [.restricted, .denied].contains(status), let presentationData = presentationData { let text: String - if status == PGCameraAuthorizationStatusRestricted { + if case .restricted = status { text = presentationData.strings.AccessDenied_CameraRestricted } else { switch cameraSubject { @@ -300,7 +299,7 @@ public final class DeviceAccess { present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) - } else if status == PGCameraAuthorizationStatusAuthorized { + } else if case .authorized = status { completion(true) } else { assertionFailure() 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/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index 8e828bccf0..179d73c650 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -611,3 +611,26 @@ extension Api.EncryptedMessage { } } } + +extension Api.InputMedia { + func withUpdatedStickers(_ stickers: [Api.InputDocument]?) -> Api.InputMedia { + switch self { + case let .inputMediaUploadedDocument(flags, file, thumb, mimeType, attributes, _, ttlSeconds): + var flags = flags + var attributes = attributes + if let _ = stickers { + flags |= (1 << 0) + attributes.append(.documentAttributeHasStickers) + } + return .inputMediaUploadedDocument(flags: flags, file: file, thumb: thumb, mimeType: mimeType, attributes: attributes, stickers: stickers, ttlSeconds: ttlSeconds) + case let .inputMediaUploadedPhoto(flags, file, _, ttlSeconds): + var flags = flags + if let _ = stickers { + flags |= (1 << 0) + } + return .inputMediaUploadedPhoto(flags: flags, file: file, stickers: stickers, ttlSeconds: ttlSeconds) + default: + return self + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift index c7cf01878f..445370d041 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift @@ -11,6 +11,7 @@ public extension Stories { case media case text case entities + case embeddedStickers case pin case privacy case isForwardingDisabled @@ -23,6 +24,7 @@ public extension Stories { public let media: Media public let text: String public let entities: [MessageTextEntity] + public let embeddedStickers: [TelegramMediaFile] public let pin: Bool public let privacy: EngineStoryPrivacy public let isForwardingDisabled: Bool @@ -35,6 +37,7 @@ public extension Stories { media: Media, text: String, entities: [MessageTextEntity], + embeddedStickers: [TelegramMediaFile], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, @@ -46,6 +49,7 @@ public extension Stories { self.media = media self.text = text self.entities = entities + self.embeddedStickers = embeddedStickers self.pin = pin self.privacy = privacy self.isForwardingDisabled = isForwardingDisabled @@ -64,6 +68,11 @@ public extension Stories { self.text = try container.decode(String.self, forKey: .text) self.entities = try container.decode([MessageTextEntity].self, forKey: .entities) + + let stickersData = try container.decode(Data.self, forKey: .embeddedStickers) + let stickersDecoder = PostboxDecoder(buffer: MemoryBuffer(data: stickersData)) + self.embeddedStickers = (try? stickersDecoder.decodeObjectArrayWithCustomDecoderForKey("stickers", decoder: { TelegramMediaFile(decoder: $0) })) ?? [] + self.pin = try container.decode(Bool.self, forKey: .pin) self.privacy = try container.decode(EngineStoryPrivacy.self, forKey: .privacy) self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false @@ -83,6 +92,11 @@ public extension Stories { try container.encode(self.text, forKey: .text) try container.encode(self.entities, forKey: .entities) + + let stickersEncoder = PostboxEncoder() + stickersEncoder.encodeObjectArray(self.embeddedStickers, forKey: "stickers") + try container.encode(stickersEncoder.makeData(), forKey: .embeddedStickers) + try container.encode(self.pin, forKey: .pin) try container.encode(self.privacy, forKey: .privacy) try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled) @@ -270,7 +284,7 @@ final class PendingStoryManager { self.currentPendingItemContext = pendingItemContext let stableId = firstItem.stableId - pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, stableId: stableId, media: firstItem.media, text: firstItem.text, entities: firstItem.entities, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), randomId: firstItem.randomId) + pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, stableId: stableId, media: firstItem.media, text: firstItem.text, entities: firstItem.entities, embeddedStickers: firstItem.embeddedStickers, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), randomId: firstItem.randomId) |> deliverOn(self.queue)).start(next: { [weak self] event in guard let `self` = self else { return diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index f303ec28ce..b7b11cdd4e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -4,8 +4,15 @@ import Postbox import TelegramApi public enum EngineStoryInputMedia { - case image(dimensions: PixelDimensions, data: Data) - case video(dimensions: PixelDimensions, duration: Double, resource: TelegramMediaResource, firstFrameImageData: Data?) + case image(dimensions: PixelDimensions, data: Data, stickers: [TelegramMediaFile]) + case video(dimensions: PixelDimensions, duration: Double, resource: TelegramMediaResource, firstFrameImageData: Data?, stickers: [TelegramMediaFile]) + + var embeddedStickers: [TelegramMediaFile] { + switch self { + case let .image(_, _, stickers), let .video(_, _, _, _, stickers): + return stickers + } + } } public struct EngineStoryPrivacy: Codable, Equatable { @@ -576,7 +583,7 @@ public enum StoryUploadResult { private func prepareUploadStoryContent(account: Account, media: EngineStoryInputMedia) -> Media { switch media { - case let .image(dimensions, data): + case let .image(dimensions, data, _): let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) account.postbox.mediaBox.storeResourceData(resource.id, data: data) @@ -589,7 +596,7 @@ private func prepareUploadStoryContent(account: Account, media: EngineStoryInput flags: [] ) return imageMedia - case let .video(dimensions, duration, resource, firstFrameImageData): + case let .video(dimensions, duration, resource, firstFrameImageData, _): var previewRepresentations: [TelegramMediaImageRepresentation] = [] if let firstFrameImageData = firstFrameImageData { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) @@ -616,7 +623,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 @@ -630,7 +637,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: [], @@ -718,6 +725,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: media: inputMedia, text: text, entities: entities, + embeddedStickers: media.embeddedStickers, pin: pin, privacy: privacy, isForwardingDisabled: isForwardingDisabled, @@ -766,8 +774,9 @@ 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) +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], embeddedStickers: [TelegramMediaFile], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64) -> Signal { + 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 { @@ -810,6 +819,17 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId flags |= 1 << 4 } + var inputMedia = inputMedia + if !embeddedStickers.isEmpty { + var stickersValue: [Api.InputDocument] = [] + for file in embeddedStickers { + if let resource = file.resource as? CloudDocumentMediaResource, let fileReference = resource.fileReference { + stickersValue.append(Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference))) + } + } + inputMedia = inputMedia.withUpdatedStickers(stickersValue) + } + return network.request(Api.functions.stories.sendStory( flags: flags, media: inputMedia, @@ -906,7 +926,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/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 4a1ec258b8..4861eac853 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -1088,6 +1088,16 @@ public final class PeerExpiringStoryListContext { return self.items.contains(where: { $0.id > self.maxReadId }) } + public var unseenCount: Int { + var count: Int = 0 + for item in items { + if item.id > maxReadId { + count += 1 + } + } + return count + } + public var hasUnseenCloseFriends: Bool { return self.items.contains(where: { $0.id > self.maxReadId && $0.isCloseFriends }) } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 892b747940..17aeaab15e 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -98,7 +98,7 @@ private final class CameraScreenComponent: CombinedComponent { let hasAppeared: Bool let isVisible: Bool let panelWidth: CGFloat - let flipAnimationAction: ActionSlot + let animateFlipAction: ActionSlot let animateShutter: () -> Void let present: (ViewController) -> Void let push: (ViewController) -> Void @@ -112,7 +112,7 @@ private final class CameraScreenComponent: CombinedComponent { hasAppeared: Bool, isVisible: Bool, panelWidth: CGFloat, - flipAnimationAction: ActionSlot, + animateFlipAction: ActionSlot, animateShutter: @escaping () -> Void, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, @@ -125,7 +125,7 @@ private final class CameraScreenComponent: CombinedComponent { self.hasAppeared = hasAppeared self.isVisible = isVisible self.panelWidth = panelWidth - self.flipAnimationAction = flipAnimationAction + self.animateFlipAction = animateFlipAction self.animateShutter = animateShutter self.present = present self.push = push @@ -170,6 +170,10 @@ private final class CameraScreenComponent: CombinedComponent { } } + private var cameraAuthorizationStatus: AVAuthorizationStatus = .notDetermined + private var microphoneAuthorizationStatus: AVAuthorizationStatus = .notDetermined + private var galleryAuthorizationStatus: PHAuthorizationStatus = .notDetermined + private let context: AccountContext fileprivate let camera: Camera private let present: (ViewController) -> Void @@ -359,11 +363,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) { @@ -643,7 +656,7 @@ private final class CameraScreenComponent: CombinedComponent { } } - let flipAnimationAction = component.flipAnimationAction + let animateFlipAction = component.animateFlipAction let captureControlsAvailableSize: CGSize if isTablet { captureControlsAvailableSize = CGSize(width: panelWidth, height: availableSize.height) @@ -697,7 +710,7 @@ private final class CameraScreenComponent: CombinedComponent { guard let state else { return } - state.togglePosition(flipAnimationAction) + state.togglePosition(animateFlipAction) }, galleryTapped: { guard let controller = environment.controller() as? CameraScreen else { @@ -711,7 +724,7 @@ private final class CameraScreenComponent: CombinedComponent { zoomUpdated: { fraction in state.updateZoom(fraction: fraction) }, - flipAnimationAction: flipAnimationAction + flipAnimationAction: animateFlipAction ), availableSize: captureControlsAvailableSize, transition: context.transition @@ -734,14 +747,14 @@ private final class CameraScreenComponent: CombinedComponent { id: "flip", component: AnyComponent( FlipButtonContentComponent( - action: flipAnimationAction, + action: animateFlipAction, maskFrame: .zero ) ) ), minSize: CGSize(width: 44.0, height: 44.0), action: { - state.togglePosition(flipAnimationAction) + state.togglePosition(animateFlipAction) } ), availableSize: availableSize, @@ -1027,7 +1040,7 @@ public class CameraScreen: ViewController { private var pipPosition: PIPPosition = .bottomRight fileprivate var previewBlurPromise = ValuePromise(false) - private let flipAnimationAction = ActionSlot() + private let animateFlipAction = ActionSlot() fileprivate var cameraIsActive = true fileprivate var hasGallery = false @@ -1600,13 +1613,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 @@ -1731,6 +1742,7 @@ public class CameraScreen: ViewController { self.hasAppeared = hasAppeared transition = transition.withUserData(CameraScreenTransition.finishedAnimateIn) + // self.presentCameraTooltip() // self.presentDualCameraTooltip() } @@ -1746,7 +1758,7 @@ public class CameraScreen: ViewController { hasAppeared: self.hasAppeared, isVisible: self.cameraIsActive && !self.hasGallery, panelWidth: panelWidth, - flipAnimationAction: self.flipAnimationAction, + animateFlipAction: self.animateFlipAction, animateShutter: { [weak self] in self?.mainPreviewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) }, diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift index 92be146087..e2ab97ef27 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift @@ -8,6 +8,7 @@ import LocalMediaResources import CameraButtonComponent enum ShutterButtonState: Equatable { + case disabled case generic case video case stopRecording @@ -162,7 +163,7 @@ private final class ShutterButtonContentComponent: Component { let ringWidth: CGFloat = 3.0 var recordingProgress: Float? switch component.shutterState { - case .generic: + case .generic, .disabled: innerColor = .white innerSize = CGSize(width: 60.0, height: 60.0) ringSize = CGSize(width: 68.0, height: 68.0) @@ -772,6 +773,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) @@ -983,7 +987,7 @@ final class CaptureControlsComponent: Component { var blobState: ShutterBlobView.BlobState switch component.shutterState { - case .generic: + case .generic, .disabled: blobState = .generic case .video, .transition: blobState = .video 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/MediaEditorComposer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift index 0e84964a99..cc6e774792 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift @@ -134,9 +134,9 @@ final class MediaEditorComposer { } private var filteredImage: CIImage? - func processImage(inputImage: UIImage, pool: CVPixelBufferPool?, time: CMTime, completion: @escaping (CVPixelBuffer?, CMTime) -> Void) { + func processImage(inputImage: UIImage, pool: CVPixelBufferPool?, time: CMTime, completion: @escaping (CVPixelBuffer?) -> Void) { guard let pool else { - completion(nil, time) + completion(nil) return } if self.filteredImage == nil, let device = self.device { @@ -161,15 +161,15 @@ final class MediaEditorComposer { compositedImage = compositedImage.samplingLinear().transformed(by: CGAffineTransform(scaleX: scale, y: scale)) self.ciContext?.render(compositedImage, to: pixelBuffer) - completion(pixelBuffer, time) + completion(pixelBuffer) } else { - completion(nil, time) + completion(nil) } }) return } } - completion(nil, time) + completion(nil) } func processImage(inputImage: CIImage, time: CMTime, completion: @escaping (CIImage?) -> Void) { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index 4d8160470d..0b0ef2fc7d 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -569,18 +569,30 @@ public final class MediaEditorVideoExport { let progress = (position - .zero).seconds / duration self.statusValue = .progress(Float(progress)) - composer.processImage(inputImage: image, pool: writer.pixelBufferPool, time: position, completion: { pixelBuffer, timestamp in + composer.processImage(inputImage: image, pool: writer.pixelBufferPool, time: position, completion: { pixelBuffer in if let pixelBuffer { - if !writer.appendPixelBuffer(pixelBuffer, at: timestamp) { - Logger.shared.log("VideoExport", "Failed to append pixelbuffer") - writer.markVideoAsFinished() - appendFailed = true + if !writer.appendPixelBuffer(pixelBuffer, at: position) { + Logger.shared.log("VideoExport", "Failed to append pixelbuffer at \(position.seconds), trying to wait") + Queue.concurrentDefaultQueue().after(1.0, { + if !writer.appendPixelBuffer(pixelBuffer, at: position) { + Logger.shared.log("VideoExport", "Failed to append pixelbuffer at \(position.seconds), complete failure") + writer.markVideoAsFinished() + appendFailed = true + self.semaphore.signal() + } + }) + } else { + Logger.shared.log("VideoExport", "Appended pixelbuffer at \(position.seconds)") + + Thread.sleep(forTimeInterval: 0.01) + self.semaphore.signal() } } else { Logger.shared.log("VideoExport", "No pixelbuffer from composer") + + Thread.sleep(forTimeInterval: 0.01) + self.semaphore.signal() } - Thread.sleep(forTimeInterval: 0.001) - self.semaphore.signal() }) self.semaphore.wait() diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift index fb017e0585..cd9c61ebf0 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..6c8024f4d9 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 { @@ -3010,7 +3016,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate fileprivate let transitionOut: (Bool, Bool?) -> TransitionOut? public var cancelled: (Bool) -> Void = { _ in } - public var completion: (Int64, MediaEditorScreen.Result?, NSAttributedString, MediaEditorResultPrivacy , @escaping (@escaping () -> Void) -> Void) -> Void = { _, _, _, _, _ in } + public var completion: (Int64, MediaEditorScreen.Result?, NSAttributedString, MediaEditorResultPrivacy, [TelegramMediaFile], @escaping (@escaping () -> Void) -> Void) -> Void = { _, _, _, _, _, _ in } public var dismissed: () -> Void = { } public var willDismiss: () -> Void = { } @@ -3025,7 +3031,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate initialVideoPosition: Double? = nil, transitionIn: TransitionIn?, transitionOut: @escaping (Bool, Bool?) -> TransitionOut?, - completion: @escaping (Int64, MediaEditorScreen.Result?, NSAttributedString, MediaEditorResultPrivacy, @escaping (@escaping () -> Void) -> Void) -> Void + completion: @escaping (Int64, MediaEditorScreen.Result?, NSAttributedString, MediaEditorResultPrivacy, [TelegramMediaFile], @escaping (@escaping () -> Void) -> Void) -> Void ) { self.context = context self.subject = subject @@ -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() @@ -3470,8 +3484,22 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate randomId = Int64.random(in: .min ... .max) } + var stickers: [TelegramMediaFile] = [] + for entity in codableEntities { + if case let .sticker(stickerEntity) = entity, case let .file(file) = stickerEntity.content { + stickers.append(file) + if let subEntities = stickerEntity.renderSubEntities { + for entity in subEntities { + if let stickerEntity = entity as? DrawingStickerEntity, case let .file(file) = stickerEntity.content { + stickers.append(file) + } + } + } + } + } + if self.isEditingStory && !self.node.hasAnyChanges { - self.completion(randomId, nil, caption, self.state.privacy, { [weak self] finished in + self.completion(randomId, nil, caption, self.state.privacy, stickers, { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -3602,7 +3630,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let self { makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: image ?? UIImage(), dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] coverImage in if let self { - self.completion(randomId, .video(video: videoResult, coverImage: coverImage, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions), caption, self.state.privacy, { [weak self] finished in + self.completion(randomId, .video(video: videoResult, coverImage: coverImage, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions), caption, self.state.privacy, stickers, { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -3624,7 +3652,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] resultImage in if let self, let resultImage { - self.completion(randomId, .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), caption, self.state.privacy, { [weak self] finished in + self.completion(randomId, .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), caption, self.state.privacy, stickers, { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -3973,8 +4001,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 +4016,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/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 33d39ac87d..cefd8b52eb 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2843,7 +2843,7 @@ public final class StoryItemSetContainerComponent: Component { initialVideoPosition: videoPlaybackPosition, transitionIn: nil, transitionOut: { _, _ in return nil }, - completion: { [weak self] _, mediaResult, caption, privacy, commit in + completion: { [weak self] _, mediaResult, caption, privacy, stickers, commit in guard let self else { return } @@ -2865,7 +2865,7 @@ public final class StoryItemSetContainerComponent: Component { updateProgressImpl?(0.0) if let imageData = compressImageToJPEG(image, quality: 0.7) { - let _ = (context.engine.messages.editStory(media: .image(dimensions: dimensions, data: imageData), id: id, text: updatedText, entities: updatedEntities, privacy: updatedPrivacy) + let _ = (context.engine.messages.editStory(media: .image(dimensions: dimensions, data: imageData, stickers: stickers), id: id, text: updatedText, entities: updatedEntities, privacy: updatedPrivacy) |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return @@ -2904,7 +2904,7 @@ public final class StoryItemSetContainerComponent: Component { } let firstFrameImageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) } - let _ = (context.engine.messages.editStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: firstFrameImageData), id: id, text: updatedText, entities: updatedEntities, privacy: updatedPrivacy) + let _ = (context.engine.messages.editStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: firstFrameImageData, stickers: stickers), id: id, text: updatedText, entities: updatedEntities, privacy: updatedPrivacy) |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return @@ -3024,6 +3024,14 @@ public final class StoryItemSetContainerComponent: Component { let additionalCount = component.slice.item.storyItem.privacy?.additionallyIncludePeers.count ?? 0 + var hasLinkedStickers = false + let media = component.slice.item.storyItem.media._asMedia() + if let image = media as? TelegramMediaImage { + hasLinkedStickers = image.flags.contains(.hasStickers) + } else if let file = media as? TelegramMediaFile { + hasLinkedStickers = file.hasLinkedStickers + } + let privacyText: String switch component.slice.item.storyItem.privacy?.base { case .closeFriends: @@ -3160,6 +3168,8 @@ public final class StoryItemSetContainerComponent: Component { self.sendMessageContext.performShareAction(view: self) }))) } + + let _ = hasLinkedStickers let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) 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/Images.xcassets/Media Editor/Next.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/Contents.json index ffc2f05f85..89b3ac9ee6 100644 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "arrow_left.pdf", + "filename" : "ic_next.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/arrow_left.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/arrow_left.pdf deleted file mode 100644 index 7b20434672..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/arrow_left.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/ic_next.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/ic_next.pdf new file mode 100644 index 0000000000..58537192a4 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/ic_next.pdf @@ -0,0 +1,92 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +0.000000 -1.000000 1.000000 0.000000 -6.195312 14.000000 cm +1.000000 1.000000 1.000000 scn +-0.707107 8.902419 m +-1.097631 8.511895 -1.097631 7.878730 -0.707107 7.488206 c +-0.316583 7.097682 0.316583 7.097682 0.707107 7.488206 c +-0.707107 8.902419 l +h +6.000000 14.195312 m +6.707107 14.902419 l +6.316583 15.292944 5.683417 15.292944 5.292893 14.902419 c +6.000000 14.195312 l +h +11.292893 7.488206 m +11.683417 7.097682 12.316583 7.097682 12.707107 7.488206 c +13.097631 7.878730 13.097631 8.511895 12.707107 8.902419 c +11.292893 7.488206 l +h +0.707107 7.488206 m +6.707107 13.488206 l +5.292893 14.902419 l +-0.707107 8.902419 l +0.707107 7.488206 l +h +5.292893 13.488206 m +11.292893 7.488206 l +12.707107 8.902419 l +6.707107 14.902419 l +5.292893 13.488206 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 785 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 10.000000 16.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000875 00000 n +0000000897 00000 n +0000001070 00000 n +0000001144 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1203 +%%EOF \ No newline at end of file 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, diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index b5d2010422..4e10eb5517 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -354,7 +354,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } else { return nil } - }, completion: { [weak self] randomId, mediaResult, caption, privacy, commit in + }, completion: { [weak self] randomId, mediaResult, caption, privacy, stickers, commit in guard let self, let mediaResult else { dismissCameraImpl?() commit({}) @@ -373,7 +373,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon case let .image(image, dimensions): if let imageData = compressImageToJPEG(image, quality: 0.7) { let entities = generateChatInputTextEntities(caption) - self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption.string, entities: entities, pin: privacy.pin, privacy: privacy.privacy, isForwardingDisabled: privacy.isForwardingDisabled, period: privacy.timeout, randomId: randomId) + self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData, stickers: stickers), text: caption.string, entities: entities, pin: privacy.pin, privacy: privacy.privacy, isForwardingDisabled: privacy.isForwardingDisabled, period: privacy.timeout, randomId: randomId) Queue.mainQueue().justDispatch { commit({}) } @@ -396,7 +396,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) } let entities = generateChatInputTextEntities(caption) - self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: imageData), text: caption.string, entities: entities, pin: privacy.pin, privacy: privacy.privacy, isForwardingDisabled: privacy.isForwardingDisabled, period: privacy.timeout, randomId: randomId) + self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: imageData, stickers: stickers), text: caption.string, entities: entities, pin: privacy.pin, privacy: privacy.privacy, isForwardingDisabled: privacy.isForwardingDisabled, period: privacy.timeout, randomId: randomId) Queue.mainQueue().justDispatch { commit({}) }