mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Merge commit 'cb415abfccadff11e210e797ace8c1b59d74e4f7'
This commit is contained in:
commit
0940d52b88
@ -94,7 +94,7 @@ func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, con
|
|||||||
items.append(.action(ContextMenuActionItem(text: "Move to Chats", icon: { theme in
|
items.append(.action(ContextMenuActionItem(text: "Move to Chats", icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MoveToChats"), color: theme.contextMenu.primaryColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MoveToChats"), color: theme.contextMenu.primaryColor)
|
||||||
}, action: { _, f in
|
}, action: { _, f in
|
||||||
f(.default)
|
f(.dismissWithoutContent)
|
||||||
|
|
||||||
context.engine.peers.updatePeerStoriesHidden(id: peerId, isHidden: false)
|
context.engine.peers.updatePeerStoriesHidden(id: peerId, isHidden: false)
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ import AddressBook
|
|||||||
import UserNotifications
|
import UserNotifications
|
||||||
import CoreTelephony
|
import CoreTelephony
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import LegacyComponents
|
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
|
||||||
public enum DeviceAccessCameraSubject {
|
public enum DeviceAccessCameraSubject {
|
||||||
@ -88,7 +87,7 @@ public final class DeviceAccess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static func isCameraAccessAuthorized() -> Bool {
|
public static func isCameraAccessAuthorized() -> Bool {
|
||||||
return PGCamera.cameraAuthorizationStatus() == PGCameraAuthorizationStatusAuthorized
|
return AVCaptureDevice.authorizationStatus(for: .video) == .authorized
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func authorizationStatus(applicationInForeground: Signal<Bool, NoError>? = nil, siriAuthorization: (() -> AccessType)? = nil, subject: DeviceAccessSubject) -> Signal<AccessType, NoError> {
|
public static func authorizationStatus(applicationInForeground: Signal<Bool, NoError>? = nil, siriAuthorization: (() -> AccessType)? = nil, subject: DeviceAccessSubject) -> Signal<AccessType, NoError> {
|
||||||
@ -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 }) {
|
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 {
|
switch subject {
|
||||||
case let .camera(cameraSubject):
|
case let .camera(cameraSubject):
|
||||||
let status = PGCamera.cameraAuthorizationStatus()
|
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
||||||
if status == PGCameraAuthorizationStatusNotDetermined {
|
if case .notDetermined = status {
|
||||||
if !onlyCheck {
|
if !onlyCheck {
|
||||||
AVCaptureDevice.requestAccess(for: AVMediaType.video) { response in
|
AVCaptureDevice.requestAccess(for: AVMediaType.video) { response in
|
||||||
Queue.mainQueue().async {
|
Queue.mainQueue().async {
|
||||||
@ -282,9 +281,9 @@ public final class DeviceAccess {
|
|||||||
} else {
|
} else {
|
||||||
completion(true)
|
completion(true)
|
||||||
}
|
}
|
||||||
} else if status == PGCameraAuthorizationStatusRestricted || status == PGCameraAuthorizationStatusDenied, let presentationData = presentationData {
|
} else if [.restricted, .denied].contains(status), let presentationData = presentationData {
|
||||||
let text: String
|
let text: String
|
||||||
if status == PGCameraAuthorizationStatusRestricted {
|
if case .restricted = status {
|
||||||
text = presentationData.strings.AccessDenied_CameraRestricted
|
text = presentationData.strings.AccessDenied_CameraRestricted
|
||||||
} else {
|
} else {
|
||||||
switch cameraSubject {
|
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: {
|
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()
|
openSettings()
|
||||||
})]), nil)
|
})]), nil)
|
||||||
} else if status == PGCameraAuthorizationStatusAuthorized {
|
} else if case .authorized = status {
|
||||||
completion(true)
|
completion(true)
|
||||||
} else {
|
} else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
|
@ -96,7 +96,7 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po
|
|||||||
return .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaStory(userId: inputUser, id: media.storyId.id), ""), reuploadInfo: nil, cacheReferenceKey: nil))
|
return .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaStory(userId: inputUser, id: media.storyId.id), ""), reuploadInfo: nil, cacheReferenceKey: nil))
|
||||||
}
|
}
|
||||||
|> castError(PendingMessageUploadError.self), .text)
|
|> castError(PendingMessageUploadError.self), .text)
|
||||||
} else if let media = media.first, let mediaResult = mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: false, peerId: peerId, media: media, text: text, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, messageId: messageId, attributes: attributes) {
|
} else if let media = media.first, let mediaResult = mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, peerId: peerId, media: media, text: text, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, messageId: messageId, attributes: attributes) {
|
||||||
return .signal(mediaResult, .media)
|
return .signal(mediaResult, .media)
|
||||||
} else {
|
} else {
|
||||||
return .signal(.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .text(text), reuploadInfo: nil, cacheReferenceKey: nil))), .text)
|
return .signal(.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .text(text), reuploadInfo: nil, cacheReferenceKey: nil))), .text)
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -11,6 +11,7 @@ public extension Stories {
|
|||||||
case media
|
case media
|
||||||
case text
|
case text
|
||||||
case entities
|
case entities
|
||||||
|
case embeddedStickers
|
||||||
case pin
|
case pin
|
||||||
case privacy
|
case privacy
|
||||||
case isForwardingDisabled
|
case isForwardingDisabled
|
||||||
@ -23,6 +24,7 @@ public extension Stories {
|
|||||||
public let media: Media
|
public let media: Media
|
||||||
public let text: String
|
public let text: String
|
||||||
public let entities: [MessageTextEntity]
|
public let entities: [MessageTextEntity]
|
||||||
|
public let embeddedStickers: [TelegramMediaFile]
|
||||||
public let pin: Bool
|
public let pin: Bool
|
||||||
public let privacy: EngineStoryPrivacy
|
public let privacy: EngineStoryPrivacy
|
||||||
public let isForwardingDisabled: Bool
|
public let isForwardingDisabled: Bool
|
||||||
@ -35,6 +37,7 @@ public extension Stories {
|
|||||||
media: Media,
|
media: Media,
|
||||||
text: String,
|
text: String,
|
||||||
entities: [MessageTextEntity],
|
entities: [MessageTextEntity],
|
||||||
|
embeddedStickers: [TelegramMediaFile],
|
||||||
pin: Bool,
|
pin: Bool,
|
||||||
privacy: EngineStoryPrivacy,
|
privacy: EngineStoryPrivacy,
|
||||||
isForwardingDisabled: Bool,
|
isForwardingDisabled: Bool,
|
||||||
@ -46,6 +49,7 @@ public extension Stories {
|
|||||||
self.media = media
|
self.media = media
|
||||||
self.text = text
|
self.text = text
|
||||||
self.entities = entities
|
self.entities = entities
|
||||||
|
self.embeddedStickers = embeddedStickers
|
||||||
self.pin = pin
|
self.pin = pin
|
||||||
self.privacy = privacy
|
self.privacy = privacy
|
||||||
self.isForwardingDisabled = isForwardingDisabled
|
self.isForwardingDisabled = isForwardingDisabled
|
||||||
@ -64,6 +68,11 @@ public extension Stories {
|
|||||||
|
|
||||||
self.text = try container.decode(String.self, forKey: .text)
|
self.text = try container.decode(String.self, forKey: .text)
|
||||||
self.entities = try container.decode([MessageTextEntity].self, forKey: .entities)
|
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.pin = try container.decode(Bool.self, forKey: .pin)
|
||||||
self.privacy = try container.decode(EngineStoryPrivacy.self, forKey: .privacy)
|
self.privacy = try container.decode(EngineStoryPrivacy.self, forKey: .privacy)
|
||||||
self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false
|
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.text, forKey: .text)
|
||||||
try container.encode(self.entities, forKey: .entities)
|
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.pin, forKey: .pin)
|
||||||
try container.encode(self.privacy, forKey: .privacy)
|
try container.encode(self.privacy, forKey: .privacy)
|
||||||
try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled)
|
try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled)
|
||||||
@ -270,7 +284,7 @@ final class PendingStoryManager {
|
|||||||
self.currentPendingItemContext = pendingItemContext
|
self.currentPendingItemContext = pendingItemContext
|
||||||
|
|
||||||
let stableId = firstItem.stableId
|
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
|
|> deliverOn(self.queue)).start(next: { [weak self] event in
|
||||||
guard let `self` = self else {
|
guard let `self` = self else {
|
||||||
return
|
return
|
||||||
|
@ -4,8 +4,15 @@ import Postbox
|
|||||||
import TelegramApi
|
import TelegramApi
|
||||||
|
|
||||||
public enum EngineStoryInputMedia {
|
public enum EngineStoryInputMedia {
|
||||||
case image(dimensions: PixelDimensions, data: Data)
|
case image(dimensions: PixelDimensions, data: Data, stickers: [TelegramMediaFile])
|
||||||
case video(dimensions: PixelDimensions, duration: Double, resource: TelegramMediaResource, firstFrameImageData: Data?)
|
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 {
|
public struct EngineStoryPrivacy: Codable, Equatable {
|
||||||
@ -576,7 +583,7 @@ public enum StoryUploadResult {
|
|||||||
|
|
||||||
private func prepareUploadStoryContent(account: Account, media: EngineStoryInputMedia) -> Media {
|
private func prepareUploadStoryContent(account: Account, media: EngineStoryInputMedia) -> Media {
|
||||||
switch media {
|
switch media {
|
||||||
case let .image(dimensions, data):
|
case let .image(dimensions, data, _):
|
||||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||||
account.postbox.mediaBox.storeResourceData(resource.id, data: data)
|
account.postbox.mediaBox.storeResourceData(resource.id, data: data)
|
||||||
|
|
||||||
@ -589,7 +596,7 @@ private func prepareUploadStoryContent(account: Account, media: EngineStoryInput
|
|||||||
flags: []
|
flags: []
|
||||||
)
|
)
|
||||||
return imageMedia
|
return imageMedia
|
||||||
case let .video(dimensions, duration, resource, firstFrameImageData):
|
case let .video(dimensions, duration, resource, firstFrameImageData, _):
|
||||||
var previewRepresentations: [TelegramMediaImageRepresentation] = []
|
var previewRepresentations: [TelegramMediaImageRepresentation] = []
|
||||||
if let firstFrameImageData = firstFrameImageData {
|
if let firstFrameImageData = firstFrameImageData {
|
||||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
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<PendingMessageUploadedContentResult?, NoError>, media: Media) {
|
private func uploadedStoryContent(postbox: Postbox, network: Network, media: Media, accountPeerId: PeerId, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods, passFetchProgress: Bool) -> (signal: Signal<PendingMessageUploadedContentResult?, NoError>, media: Media) {
|
||||||
let originalMedia: Media = media
|
let originalMedia: Media = media
|
||||||
let contentToUpload: MessageContentToUpload
|
let contentToUpload: MessageContentToUpload
|
||||||
|
|
||||||
@ -630,7 +637,7 @@ private func uploadedStoryContent(postbox: Postbox, network: Network, media: Med
|
|||||||
revalidationContext: revalidationContext,
|
revalidationContext: revalidationContext,
|
||||||
forceReupload: true,
|
forceReupload: true,
|
||||||
isGrouped: false,
|
isGrouped: false,
|
||||||
passFetchProgress: false,
|
passFetchProgress: passFetchProgress,
|
||||||
peerId: accountPeerId,
|
peerId: accountPeerId,
|
||||||
messageId: nil,
|
messageId: nil,
|
||||||
attributes: [],
|
attributes: [],
|
||||||
@ -718,6 +725,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
|||||||
media: inputMedia,
|
media: inputMedia,
|
||||||
text: text,
|
text: text,
|
||||||
entities: entities,
|
entities: entities,
|
||||||
|
embeddedStickers: media.embeddedStickers,
|
||||||
pin: pin,
|
pin: pin,
|
||||||
privacy: privacy,
|
privacy: privacy,
|
||||||
isForwardingDisabled: isForwardingDisabled,
|
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<StoryUploadResult, NoError> {
|
func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods, stableId: Int32, media: Media, text: String, entities: [MessageTextEntity], embeddedStickers: [TelegramMediaFile], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> {
|
||||||
let (contentSignal, originalMedia) = uploadedStoryContent(postbox: postbox, network: network, media: media, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods)
|
let passFetchProgress = media is TelegramMediaFile
|
||||||
|
let (contentSignal, originalMedia) = uploadedStoryContent(postbox: postbox, network: network, media: media, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods, passFetchProgress: passFetchProgress)
|
||||||
return contentSignal
|
return contentSignal
|
||||||
|> mapToSignal { result -> Signal<StoryUploadResult, NoError> in
|
|> mapToSignal { result -> Signal<StoryUploadResult, NoError> in
|
||||||
switch result {
|
switch result {
|
||||||
@ -810,6 +819,17 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
|
|||||||
flags |= 1 << 4
|
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(
|
return network.request(Api.functions.stories.sendStory(
|
||||||
flags: flags,
|
flags: flags,
|
||||||
media: inputMedia,
|
media: inputMedia,
|
||||||
@ -906,7 +926,11 @@ func _internal_editStory(account: Account, media: EngineStoryInputMedia?, id: In
|
|||||||
let contentSignal: Signal<PendingMessageUploadedContentResult?, NoError>
|
let contentSignal: Signal<PendingMessageUploadedContentResult?, NoError>
|
||||||
let originalMedia: Media?
|
let originalMedia: Media?
|
||||||
if let media = media {
|
if let media = media {
|
||||||
(contentSignal, originalMedia) = uploadedStoryContent(postbox: account.postbox, network: account.network, media: prepareUploadStoryContent(account: account, media: media), accountPeerId: account.peerId, messageMediaPreuploadManager: account.messageMediaPreuploadManager, revalidationContext: account.mediaReferenceRevalidationContext, auxiliaryMethods: account.auxiliaryMethods)
|
var passFetchProgress = false
|
||||||
|
if case .video = media {
|
||||||
|
passFetchProgress = true
|
||||||
|
}
|
||||||
|
(contentSignal, originalMedia) = uploadedStoryContent(postbox: account.postbox, network: account.network, media: prepareUploadStoryContent(account: account, media: media), accountPeerId: account.peerId, messageMediaPreuploadManager: account.messageMediaPreuploadManager, revalidationContext: account.mediaReferenceRevalidationContext, auxiliaryMethods: account.auxiliaryMethods, passFetchProgress: passFetchProgress)
|
||||||
} else {
|
} else {
|
||||||
contentSignal = .single(nil)
|
contentSignal = .single(nil)
|
||||||
originalMedia = nil
|
originalMedia = nil
|
||||||
|
@ -1088,6 +1088,16 @@ public final class PeerExpiringStoryListContext {
|
|||||||
return self.items.contains(where: { $0.id > self.maxReadId })
|
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 {
|
public var hasUnseenCloseFriends: Bool {
|
||||||
return self.items.contains(where: { $0.id > self.maxReadId && $0.isCloseFriends })
|
return self.items.contains(where: { $0.id > self.maxReadId && $0.isCloseFriends })
|
||||||
}
|
}
|
||||||
|
@ -98,7 +98,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
|||||||
let hasAppeared: Bool
|
let hasAppeared: Bool
|
||||||
let isVisible: Bool
|
let isVisible: Bool
|
||||||
let panelWidth: CGFloat
|
let panelWidth: CGFloat
|
||||||
let flipAnimationAction: ActionSlot<Void>
|
let animateFlipAction: ActionSlot<Void>
|
||||||
let animateShutter: () -> Void
|
let animateShutter: () -> Void
|
||||||
let present: (ViewController) -> Void
|
let present: (ViewController) -> Void
|
||||||
let push: (ViewController) -> Void
|
let push: (ViewController) -> Void
|
||||||
@ -112,7 +112,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
|||||||
hasAppeared: Bool,
|
hasAppeared: Bool,
|
||||||
isVisible: Bool,
|
isVisible: Bool,
|
||||||
panelWidth: CGFloat,
|
panelWidth: CGFloat,
|
||||||
flipAnimationAction: ActionSlot<Void>,
|
animateFlipAction: ActionSlot<Void>,
|
||||||
animateShutter: @escaping () -> Void,
|
animateShutter: @escaping () -> Void,
|
||||||
present: @escaping (ViewController) -> Void,
|
present: @escaping (ViewController) -> Void,
|
||||||
push: @escaping (ViewController) -> Void,
|
push: @escaping (ViewController) -> Void,
|
||||||
@ -125,7 +125,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
|||||||
self.hasAppeared = hasAppeared
|
self.hasAppeared = hasAppeared
|
||||||
self.isVisible = isVisible
|
self.isVisible = isVisible
|
||||||
self.panelWidth = panelWidth
|
self.panelWidth = panelWidth
|
||||||
self.flipAnimationAction = flipAnimationAction
|
self.animateFlipAction = animateFlipAction
|
||||||
self.animateShutter = animateShutter
|
self.animateShutter = animateShutter
|
||||||
self.present = present
|
self.present = present
|
||||||
self.push = push
|
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
|
private let context: AccountContext
|
||||||
fileprivate let camera: Camera
|
fileprivate let camera: Camera
|
||||||
private let present: (ViewController) -> Void
|
private let present: (ViewController) -> Void
|
||||||
@ -359,11 +363,20 @@ private final class CameraScreenComponent: CombinedComponent {
|
|||||||
action.invoke(Void())
|
action.invoke(Void())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var lastDualCameraTimestamp: Double?
|
||||||
func toggleDualCamera() {
|
func toggleDualCamera() {
|
||||||
|
let currentTimestamp = CACurrentMediaTime()
|
||||||
|
if let lastDualCameraTimestamp = self.lastDualCameraTimestamp, currentTimestamp - lastDualCameraTimestamp < 1.5 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.lastDualCameraTimestamp = currentTimestamp
|
||||||
|
|
||||||
let isEnabled = !self.cameraState.isDualCamEnabled
|
let isEnabled = !self.cameraState.isDualCamEnabled
|
||||||
self.camera.setDualCamEnabled(isEnabled)
|
self.camera.setDualCamEnabled(isEnabled)
|
||||||
self.cameraState = self.cameraState.updatedIsDualCamEnabled(isEnabled)
|
self.cameraState = self.cameraState.updatedIsDualCamEnabled(isEnabled)
|
||||||
self.updated(transition: .easeInOut(duration: 0.1))
|
self.updated(transition: .easeInOut(duration: 0.1))
|
||||||
|
|
||||||
|
self.hapticFeedback.impact(.light)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSwipeHint(_ hint: CaptureControlsComponent.SwipeHint) {
|
func updateSwipeHint(_ hint: CaptureControlsComponent.SwipeHint) {
|
||||||
@ -643,7 +656,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let flipAnimationAction = component.flipAnimationAction
|
let animateFlipAction = component.animateFlipAction
|
||||||
let captureControlsAvailableSize: CGSize
|
let captureControlsAvailableSize: CGSize
|
||||||
if isTablet {
|
if isTablet {
|
||||||
captureControlsAvailableSize = CGSize(width: panelWidth, height: availableSize.height)
|
captureControlsAvailableSize = CGSize(width: panelWidth, height: availableSize.height)
|
||||||
@ -697,7 +710,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
|||||||
guard let state else {
|
guard let state else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state.togglePosition(flipAnimationAction)
|
state.togglePosition(animateFlipAction)
|
||||||
},
|
},
|
||||||
galleryTapped: {
|
galleryTapped: {
|
||||||
guard let controller = environment.controller() as? CameraScreen else {
|
guard let controller = environment.controller() as? CameraScreen else {
|
||||||
@ -711,7 +724,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
|||||||
zoomUpdated: { fraction in
|
zoomUpdated: { fraction in
|
||||||
state.updateZoom(fraction: fraction)
|
state.updateZoom(fraction: fraction)
|
||||||
},
|
},
|
||||||
flipAnimationAction: flipAnimationAction
|
flipAnimationAction: animateFlipAction
|
||||||
),
|
),
|
||||||
availableSize: captureControlsAvailableSize,
|
availableSize: captureControlsAvailableSize,
|
||||||
transition: context.transition
|
transition: context.transition
|
||||||
@ -734,14 +747,14 @@ private final class CameraScreenComponent: CombinedComponent {
|
|||||||
id: "flip",
|
id: "flip",
|
||||||
component: AnyComponent(
|
component: AnyComponent(
|
||||||
FlipButtonContentComponent(
|
FlipButtonContentComponent(
|
||||||
action: flipAnimationAction,
|
action: animateFlipAction,
|
||||||
maskFrame: .zero
|
maskFrame: .zero
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
minSize: CGSize(width: 44.0, height: 44.0),
|
minSize: CGSize(width: 44.0, height: 44.0),
|
||||||
action: {
|
action: {
|
||||||
state.togglePosition(flipAnimationAction)
|
state.togglePosition(animateFlipAction)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
availableSize: availableSize,
|
availableSize: availableSize,
|
||||||
@ -1027,7 +1040,7 @@ public class CameraScreen: ViewController {
|
|||||||
private var pipPosition: PIPPosition = .bottomRight
|
private var pipPosition: PIPPosition = .bottomRight
|
||||||
|
|
||||||
fileprivate var previewBlurPromise = ValuePromise<Bool>(false)
|
fileprivate var previewBlurPromise = ValuePromise<Bool>(false)
|
||||||
private let flipAnimationAction = ActionSlot<Void>()
|
private let animateFlipAction = ActionSlot<Void>()
|
||||||
|
|
||||||
fileprivate var cameraIsActive = true
|
fileprivate var cameraIsActive = true
|
||||||
fileprivate var hasGallery = false
|
fileprivate var hasGallery = false
|
||||||
@ -1600,13 +1613,11 @@ public class CameraScreen: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func presentDraftTooltip() {
|
func presentDraftTooltip() {
|
||||||
guard let sourceView = self.componentHost.findTaggedView(tag: galleryButtonTag) else {
|
guard let sourceView = self.componentHost.findTaggedView(tag: galleryButtonTag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: self.view) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let parentFrame = self.view.convert(self.bounds, to: nil)
|
let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 29.0), size: CGSize())
|
||||||
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
|
|
||||||
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 4.0), size: CGSize())
|
|
||||||
|
|
||||||
let controller = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: "Draft Saved"), location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _ in
|
let controller = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: "Draft Saved"), location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _ in
|
||||||
return .ignore
|
return .ignore
|
||||||
@ -1731,6 +1742,7 @@ public class CameraScreen: ViewController {
|
|||||||
self.hasAppeared = hasAppeared
|
self.hasAppeared = hasAppeared
|
||||||
transition = transition.withUserData(CameraScreenTransition.finishedAnimateIn)
|
transition = transition.withUserData(CameraScreenTransition.finishedAnimateIn)
|
||||||
|
|
||||||
|
|
||||||
// self.presentCameraTooltip()
|
// self.presentCameraTooltip()
|
||||||
// self.presentDualCameraTooltip()
|
// self.presentDualCameraTooltip()
|
||||||
}
|
}
|
||||||
@ -1746,7 +1758,7 @@ public class CameraScreen: ViewController {
|
|||||||
hasAppeared: self.hasAppeared,
|
hasAppeared: self.hasAppeared,
|
||||||
isVisible: self.cameraIsActive && !self.hasGallery,
|
isVisible: self.cameraIsActive && !self.hasGallery,
|
||||||
panelWidth: panelWidth,
|
panelWidth: panelWidth,
|
||||||
flipAnimationAction: self.flipAnimationAction,
|
animateFlipAction: self.animateFlipAction,
|
||||||
animateShutter: { [weak self] in
|
animateShutter: { [weak self] in
|
||||||
self?.mainPreviewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
self?.mainPreviewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||||
},
|
},
|
||||||
|
@ -8,6 +8,7 @@ import LocalMediaResources
|
|||||||
import CameraButtonComponent
|
import CameraButtonComponent
|
||||||
|
|
||||||
enum ShutterButtonState: Equatable {
|
enum ShutterButtonState: Equatable {
|
||||||
|
case disabled
|
||||||
case generic
|
case generic
|
||||||
case video
|
case video
|
||||||
case stopRecording
|
case stopRecording
|
||||||
@ -162,7 +163,7 @@ private final class ShutterButtonContentComponent: Component {
|
|||||||
let ringWidth: CGFloat = 3.0
|
let ringWidth: CGFloat = 3.0
|
||||||
var recordingProgress: Float?
|
var recordingProgress: Float?
|
||||||
switch component.shutterState {
|
switch component.shutterState {
|
||||||
case .generic:
|
case .generic, .disabled:
|
||||||
innerColor = .white
|
innerColor = .white
|
||||||
innerSize = CGSize(width: 60.0, height: 60.0)
|
innerSize = CGSize(width: 60.0, height: 60.0)
|
||||||
ringSize = CGSize(width: 68.0, height: 68.0)
|
ringSize = CGSize(width: 68.0, height: 68.0)
|
||||||
@ -772,6 +773,9 @@ final class CaptureControlsComponent: Component {
|
|||||||
self.component?.swipeHintUpdated(.flip)
|
self.component?.swipeHintUpdated(.flip)
|
||||||
if location.x > self.frame.width / 2.0 + 60.0 {
|
if location.x > self.frame.width / 2.0 + 60.0 {
|
||||||
self.panBlobState = .transientToFlip
|
self.panBlobState = .transientToFlip
|
||||||
|
if self.didFlip && location.x < self.frame.width - 100.0 {
|
||||||
|
self.didFlip = false
|
||||||
|
}
|
||||||
if !self.didFlip && location.x > self.frame.width - 70.0 {
|
if !self.didFlip && location.x > self.frame.width - 70.0 {
|
||||||
self.didFlip = true
|
self.didFlip = true
|
||||||
self.hapticFeedback.impact(.light)
|
self.hapticFeedback.impact(.light)
|
||||||
@ -983,7 +987,7 @@ final class CaptureControlsComponent: Component {
|
|||||||
|
|
||||||
var blobState: ShutterBlobView.BlobState
|
var blobState: ShutterBlobView.BlobState
|
||||||
switch component.shutterState {
|
switch component.shutterState {
|
||||||
case .generic:
|
case .generic, .disabled:
|
||||||
blobState = .generic
|
blobState = .generic
|
||||||
case .video, .transition:
|
case .video, .transition:
|
||||||
blobState = .video
|
blobState = .video
|
||||||
|
@ -15,6 +15,22 @@ import TelegramUIPreferences
|
|||||||
public final class EmojiSuggestionsComponent: Component {
|
public final class EmojiSuggestionsComponent: Component {
|
||||||
public typealias EnvironmentType = Empty
|
public typealias EnvironmentType = Empty
|
||||||
|
|
||||||
|
public struct Theme: Equatable {
|
||||||
|
let backgroundColor: UIColor
|
||||||
|
let textColor: UIColor
|
||||||
|
let placeholderColor: UIColor
|
||||||
|
|
||||||
|
public init(
|
||||||
|
backgroundColor: UIColor,
|
||||||
|
textColor: UIColor,
|
||||||
|
placeholderColor: UIColor
|
||||||
|
) {
|
||||||
|
self.backgroundColor = backgroundColor
|
||||||
|
self.textColor = textColor
|
||||||
|
self.placeholderColor = placeholderColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static func suggestionData(context: AccountContext, isSavedMessages: Bool, query: String) -> Signal<[TelegramMediaFile], NoError> {
|
public static func suggestionData(context: AccountContext, isSavedMessages: Bool, query: String) -> Signal<[TelegramMediaFile], NoError> {
|
||||||
let hasPremium: Signal<Bool, NoError>
|
let hasPremium: Signal<Bool, NoError>
|
||||||
if isSavedMessages {
|
if isSavedMessages {
|
||||||
@ -98,7 +114,7 @@ public final class EmojiSuggestionsComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public let context: AccountContext
|
public let context: AccountContext
|
||||||
public let theme: PresentationTheme
|
public let theme: Theme
|
||||||
public let animationCache: AnimationCache
|
public let animationCache: AnimationCache
|
||||||
public let animationRenderer: MultiAnimationRenderer
|
public let animationRenderer: MultiAnimationRenderer
|
||||||
public let files: [TelegramMediaFile]
|
public let files: [TelegramMediaFile]
|
||||||
@ -107,7 +123,7 @@ public final class EmojiSuggestionsComponent: Component {
|
|||||||
public init(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
userLocation: MediaResourceUserLocation,
|
userLocation: MediaResourceUserLocation,
|
||||||
theme: PresentationTheme,
|
theme: Theme,
|
||||||
animationCache: AnimationCache,
|
animationCache: AnimationCache,
|
||||||
animationRenderer: MultiAnimationRenderer,
|
animationRenderer: MultiAnimationRenderer,
|
||||||
files: [TelegramMediaFile],
|
files: [TelegramMediaFile],
|
||||||
@ -125,7 +141,7 @@ public final class EmojiSuggestionsComponent: Component {
|
|||||||
if lhs.context !== rhs.context {
|
if lhs.context !== rhs.context {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if lhs.theme !== rhs.theme {
|
if lhs.theme != rhs.theme {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if lhs.animationCache !== rhs.animationCache {
|
if lhs.animationCache !== rhs.animationCache {
|
||||||
@ -305,7 +321,7 @@ public final class EmojiSuggestionsComponent: Component {
|
|||||||
let itemLayer: InlineStickerItemLayer
|
let itemLayer: InlineStickerItemLayer
|
||||||
if let current = self.visibleLayers[item.fileId] {
|
if let current = self.visibleLayers[item.fileId] {
|
||||||
itemLayer = current
|
itemLayer = current
|
||||||
itemLayer.dynamicColor = component.theme.list.itemPrimaryTextColor
|
itemLayer.dynamicColor = component.theme.textColor
|
||||||
} else {
|
} else {
|
||||||
itemLayer = InlineStickerItemLayer(
|
itemLayer = InlineStickerItemLayer(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
@ -315,9 +331,9 @@ public final class EmojiSuggestionsComponent: Component {
|
|||||||
file: item,
|
file: item,
|
||||||
cache: component.animationCache,
|
cache: component.animationCache,
|
||||||
renderer: component.animationRenderer,
|
renderer: component.animationRenderer,
|
||||||
placeholderColor: component.theme.list.mediaPlaceholderColor,
|
placeholderColor: component.theme.placeholderColor,
|
||||||
pointSize: itemFrame.size,
|
pointSize: itemFrame.size,
|
||||||
dynamicColor: component.theme.list.itemPrimaryTextColor
|
dynamicColor: component.theme.textColor
|
||||||
)
|
)
|
||||||
self.visibleLayers[item.fileId] = itemLayer
|
self.visibleLayers[item.fileId] = itemLayer
|
||||||
self.scrollView.layer.addSublayer(itemLayer)
|
self.scrollView.layer.addSublayer(itemLayer)
|
||||||
@ -382,10 +398,10 @@ public final class EmojiSuggestionsComponent: Component {
|
|||||||
func update(component: EmojiSuggestionsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
func update(component: EmojiSuggestionsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||||
let height: CGFloat = 54.0
|
let height: CGFloat = 54.0
|
||||||
|
|
||||||
if self.component?.theme !== component.theme {
|
if self.component?.theme.backgroundColor != component.theme.backgroundColor {
|
||||||
//self.backgroundLayer.fillColor = component.theme.list.plainBackgroundColor.cgColor
|
//self.backgroundLayer.fillColor = component.theme.list.plainBackgroundColor.cgColor
|
||||||
self.backgroundLayer.fillColor = UIColor.black.cgColor
|
self.backgroundLayer.fillColor = UIColor.black.cgColor
|
||||||
self.blurView.updateColor(color: component.theme.list.plainBackgroundColor.withMultipliedAlpha(0.88), transition: .immediate)
|
self.blurView.updateColor(color: component.theme.backgroundColor, transition: .immediate)
|
||||||
}
|
}
|
||||||
var resetScrollingPosition = false
|
var resetScrollingPosition = false
|
||||||
if self.component?.files != component.files {
|
if self.component?.files != component.files {
|
||||||
@ -427,3 +443,11 @@ public final class EmojiSuggestionsComponent: Component {
|
|||||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension EmojiSuggestionsComponent.Theme {
|
||||||
|
init(theme: PresentationTheme) {
|
||||||
|
self.backgroundColor = theme.list.plainBackgroundColor.withMultipliedAlpha(0.88)
|
||||||
|
self.textColor = theme.list.itemPrimaryTextColor
|
||||||
|
self.placeholderColor = theme.list.mediaPlaceholderColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -134,9 +134,9 @@ final class MediaEditorComposer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var filteredImage: CIImage?
|
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 {
|
guard let pool else {
|
||||||
completion(nil, time)
|
completion(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if self.filteredImage == nil, let device = self.device {
|
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))
|
compositedImage = compositedImage.samplingLinear().transformed(by: CGAffineTransform(scaleX: scale, y: scale))
|
||||||
|
|
||||||
self.ciContext?.render(compositedImage, to: pixelBuffer)
|
self.ciContext?.render(compositedImage, to: pixelBuffer)
|
||||||
completion(pixelBuffer, time)
|
completion(pixelBuffer)
|
||||||
} else {
|
} else {
|
||||||
completion(nil, time)
|
completion(nil)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
completion(nil, time)
|
completion(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func processImage(inputImage: CIImage, time: CMTime, completion: @escaping (CIImage?) -> Void) {
|
func processImage(inputImage: CIImage, time: CMTime, completion: @escaping (CIImage?) -> Void) {
|
||||||
|
@ -569,18 +569,30 @@ public final class MediaEditorVideoExport {
|
|||||||
|
|
||||||
let progress = (position - .zero).seconds / duration
|
let progress = (position - .zero).seconds / duration
|
||||||
self.statusValue = .progress(Float(progress))
|
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 let pixelBuffer {
|
||||||
if !writer.appendPixelBuffer(pixelBuffer, at: timestamp) {
|
if !writer.appendPixelBuffer(pixelBuffer, at: position) {
|
||||||
Logger.shared.log("VideoExport", "Failed to append pixelbuffer")
|
Logger.shared.log("VideoExport", "Failed to append pixelbuffer at \(position.seconds), trying to wait")
|
||||||
writer.markVideoAsFinished()
|
Queue.concurrentDefaultQueue().after(1.0, {
|
||||||
appendFailed = true
|
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 {
|
} else {
|
||||||
Logger.shared.log("VideoExport", "No pixelbuffer from composer")
|
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()
|
self.semaphore.wait()
|
||||||
|
|
||||||
|
@ -641,9 +641,13 @@ final class VideoInputScalePass: RenderPass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func process(input: MTLTexture, secondInput: MTLTexture?, timestamp: CMTime, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
func process(input: MTLTexture, secondInput: MTLTexture?, timestamp: CMTime, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||||
|
//#if targetEnvironment(simulator)
|
||||||
|
//
|
||||||
|
//#else
|
||||||
guard max(input.width, input.height) > 1920 || secondInput != nil else {
|
guard max(input.width, input.height) > 1920 || secondInput != nil else {
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
|
//#endif
|
||||||
|
|
||||||
let scaledSize = CGSize(width: input.width, height: input.height).fitted(CGSize(width: 1920.0, height: 1920.0))
|
let scaledSize = CGSize(width: input.width, height: input.height).fitted(CGSize(width: 1920.0, height: 1920.0))
|
||||||
let width: Int
|
let width: Int
|
||||||
@ -691,8 +695,11 @@ final class VideoInputScalePass: RenderPass {
|
|||||||
|
|
||||||
renderCommandEncoder.setRenderPipelineState(self.mainPipelineState!)
|
renderCommandEncoder.setRenderPipelineState(self.mainPipelineState!)
|
||||||
|
|
||||||
let (mainVideoState, additionalVideoState, transitionVideoState) = self.transitionState(for: timestamp, mainInput: input, additionalInput: secondInput)
|
//#if targetEnvironment(simulator)
|
||||||
|
// let secondInput = input
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
let (mainVideoState, additionalVideoState, transitionVideoState) = self.transitionState(for: timestamp, mainInput: input, additionalInput: secondInput)
|
||||||
|
|
||||||
if let transitionVideoState {
|
if let transitionVideoState {
|
||||||
self.encodeVideo(
|
self.encodeVideo(
|
||||||
|
@ -28,6 +28,7 @@ import CameraButtonComponent
|
|||||||
import UndoUI
|
import UndoUI
|
||||||
import ChatEntityKeyboardInputNode
|
import ChatEntityKeyboardInputNode
|
||||||
import ChatPresentationInterfaceState
|
import ChatPresentationInterfaceState
|
||||||
|
import TextFormat
|
||||||
|
|
||||||
enum DrawingScreenType {
|
enum DrawingScreenType {
|
||||||
case drawing
|
case drawing
|
||||||
@ -694,13 +695,18 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
transition.setAlpha(view: cancelButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0)
|
transition.setAlpha(view: cancelButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var doneButtonTitle = "NEXT"
|
||||||
|
if let controller = environment.controller() as? MediaEditorScreen, controller.isEditingStory {
|
||||||
|
doneButtonTitle = "DONE"
|
||||||
|
}
|
||||||
|
|
||||||
let doneButtonSize = self.doneButton.update(
|
let doneButtonSize = self.doneButton.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(Button(
|
component: AnyComponent(Button(
|
||||||
content: AnyComponent(DoneButtonComponent(
|
content: AnyComponent(DoneButtonComponent(
|
||||||
backgroundColor: UIColor(rgb: 0x007aff),
|
backgroundColor: UIColor(rgb: 0x007aff),
|
||||||
icon: UIImage(bundleImageName: "Media Editor/Next")!,
|
icon: UIImage(bundleImageName: "Media Editor/Next")!,
|
||||||
title: "NEXT")),
|
title: doneButtonTitle)),
|
||||||
action: {
|
action: {
|
||||||
guard let controller = environment.controller() as? MediaEditorScreen else {
|
guard let controller = environment.controller() as? MediaEditorScreen else {
|
||||||
return
|
return
|
||||||
@ -1857,25 +1863,25 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//#if DEBUG
|
#if targetEnvironment(simulator)
|
||||||
// if case let .asset(asset) = subject, asset.mediaType == .video {
|
if case let .asset(asset) = subject, asset.mediaType == .video {
|
||||||
// let videoEntity = DrawingStickerEntity(content: .dualVideoReference)
|
let videoEntity = DrawingStickerEntity(content: .dualVideoReference)
|
||||||
// videoEntity.referenceDrawingSize = storyDimensions
|
videoEntity.referenceDrawingSize = storyDimensions
|
||||||
// videoEntity.scale = 1.49
|
videoEntity.scale = 1.49
|
||||||
// videoEntity.position = PIPPosition.bottomRight.getPosition(storyDimensions)
|
videoEntity.position = PIPPosition.bottomRight.getPosition(storyDimensions)
|
||||||
// self.entitiesView.add(videoEntity, announce: false)
|
self.entitiesView.add(videoEntity, announce: false)
|
||||||
//
|
|
||||||
// mediaEditor.setAdditionalVideo("", positionChanges: [VideoPositionChange(additional: false, timestamp: 0.0), VideoPositionChange(additional: true, timestamp: 3.0)])
|
mediaEditor.setAdditionalVideo("", positionChanges: [VideoPositionChange(additional: false, timestamp: 0.0), VideoPositionChange(additional: true, timestamp: 3.0)])
|
||||||
// mediaEditor.setAdditionalVideoPosition(videoEntity.position, scale: videoEntity.scale, rotation: videoEntity.rotation)
|
mediaEditor.setAdditionalVideoPosition(videoEntity.position, scale: videoEntity.scale, rotation: videoEntity.rotation)
|
||||||
// if let entityView = self.entitiesView.getView(for: videoEntity.uuid) as? DrawingStickerEntityView {
|
if let entityView = self.entitiesView.getView(for: videoEntity.uuid) as? DrawingStickerEntityView {
|
||||||
// entityView.updated = { [weak videoEntity, weak self] in
|
entityView.updated = { [weak videoEntity, weak self] in
|
||||||
// if let self, let videoEntity {
|
if let self, let videoEntity {
|
||||||
// self.mediaEditor?.setAdditionalVideoPosition(videoEntity.position, scale: videoEntity.scale, rotation: videoEntity.rotation)
|
self.mediaEditor?.setAdditionalVideoPosition(videoEntity.position, scale: videoEntity.scale, rotation: videoEntity.rotation)
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
//#endif
|
#endif
|
||||||
|
|
||||||
self.gradientColorsDisposable = mediaEditor.gradientColors.start(next: { [weak self] colors in
|
self.gradientColorsDisposable = mediaEditor.gradientColors.start(next: { [weak self] colors in
|
||||||
if let self, let colors {
|
if let self, let colors {
|
||||||
@ -3010,7 +3016,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
fileprivate let transitionOut: (Bool, Bool?) -> TransitionOut?
|
fileprivate let transitionOut: (Bool, Bool?) -> TransitionOut?
|
||||||
|
|
||||||
public var cancelled: (Bool) -> Void = { _ in }
|
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 dismissed: () -> Void = { }
|
||||||
public var willDismiss: () -> Void = { }
|
public var willDismiss: () -> Void = { }
|
||||||
|
|
||||||
@ -3025,7 +3031,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
initialVideoPosition: Double? = nil,
|
initialVideoPosition: Double? = nil,
|
||||||
transitionIn: TransitionIn?,
|
transitionIn: TransitionIn?,
|
||||||
transitionOut: @escaping (Bool, Bool?) -> TransitionOut?,
|
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.context = context
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
@ -3098,6 +3104,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
|
|
||||||
let privacy = privacy ?? self.state.privacy
|
let privacy = privacy ?? self.state.privacy
|
||||||
|
|
||||||
|
let text = self.getCaption().string
|
||||||
|
let mentions = generateTextEntities(text, enabledTypes: [.mention], currentEntities: []).map { (text as NSString).substring(with: NSRange(location: $0.range.lowerBound + 1, length: $0.range.upperBound - $0.range.lowerBound - 1)) }
|
||||||
|
|
||||||
let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .stories(editing: false), initialPeerIds: Set(privacy.privacy.additionallyIncludePeers))
|
let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .stories(editing: false), initialPeerIds: Set(privacy.privacy.additionallyIncludePeers))
|
||||||
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
@ -3112,6 +3121,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
allowScreenshots: !privacy.isForwardingDisabled,
|
allowScreenshots: !privacy.isForwardingDisabled,
|
||||||
pin: privacy.pin,
|
pin: privacy.pin,
|
||||||
timeout: privacy.timeout,
|
timeout: privacy.timeout,
|
||||||
|
mentions: mentions,
|
||||||
stateContext: stateContext,
|
stateContext: stateContext,
|
||||||
completion: { [weak self] privacy, allowScreenshots, pin in
|
completion: { [weak self] privacy, allowScreenshots, pin in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
@ -3333,6 +3343,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
func requestDismiss(saveDraft: Bool, animated: Bool) {
|
func requestDismiss(saveDraft: Bool, animated: Bool) {
|
||||||
self.dismissAllTooltips()
|
self.dismissAllTooltips()
|
||||||
|
|
||||||
|
var showDraftTooltip = saveDraft
|
||||||
|
if let subject = self.node.subject, case .draft = subject {
|
||||||
|
showDraftTooltip = false
|
||||||
|
}
|
||||||
if saveDraft {
|
if saveDraft {
|
||||||
self.saveDraft(id: nil)
|
self.saveDraft(id: nil)
|
||||||
} else {
|
} else {
|
||||||
@ -3346,7 +3360,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
}
|
}
|
||||||
self.node.entitiesView.invalidate()
|
self.node.entitiesView.invalidate()
|
||||||
|
|
||||||
self.cancelled(saveDraft)
|
self.cancelled(showDraftTooltip)
|
||||||
|
|
||||||
self.willDismiss()
|
self.willDismiss()
|
||||||
|
|
||||||
@ -3470,8 +3484,22 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
randomId = Int64.random(in: .min ... .max)
|
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 {
|
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?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||||
self?.dismiss()
|
self?.dismiss()
|
||||||
Queue.mainQueue().justDispatch {
|
Queue.mainQueue().justDispatch {
|
||||||
@ -3602,7 +3630,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
if let self {
|
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
|
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 {
|
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?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||||
self?.dismiss()
|
self?.dismiss()
|
||||||
Queue.mainQueue().justDispatch {
|
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
|
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 {
|
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?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||||
self?.dismiss()
|
self?.dismiss()
|
||||||
Queue.mainQueue().justDispatch {
|
Queue.mainQueue().justDispatch {
|
||||||
@ -3973,8 +4001,10 @@ final class DoneButtonComponent: CombinedComponent {
|
|||||||
)
|
)
|
||||||
|
|
||||||
let backgroundHeight: CGFloat = 33.0
|
let backgroundHeight: CGFloat = 33.0
|
||||||
|
var backgroundSize = CGSize(width: backgroundHeight, height: backgroundHeight)
|
||||||
|
|
||||||
|
let textSpacing: CGFloat = 7.0
|
||||||
|
|
||||||
var textWidth: CGFloat = 0.0
|
|
||||||
var title: _UpdatedChildComponent?
|
var title: _UpdatedChildComponent?
|
||||||
if let titleText = context.component.title {
|
if let titleText = context.component.title {
|
||||||
title = text.update(
|
title = text.update(
|
||||||
@ -3986,12 +4016,13 @@ final class DoneButtonComponent: CombinedComponent {
|
|||||||
availableSize: CGSize(width: 180.0, height: 100.0),
|
availableSize: CGSize(width: 180.0, height: 100.0),
|
||||||
transition: .immediate
|
transition: .immediate
|
||||||
)
|
)
|
||||||
textWidth = title!.size.width
|
|
||||||
}
|
|
||||||
|
|
||||||
var backgroundSize = CGSize(width: 33.0, height: backgroundHeight)
|
let updatedBackgroundWidth = backgroundSize.width + textSpacing + title!.size.width
|
||||||
if !textWidth.isZero {
|
if updatedBackgroundWidth < 126.0 {
|
||||||
backgroundSize.width += textWidth + 7.0
|
backgroundSize.width = updatedBackgroundWidth
|
||||||
|
} else {
|
||||||
|
title = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let background = background.update(
|
let background = background.update(
|
||||||
|
@ -13,6 +13,7 @@ swift_library(
|
|||||||
"//submodules/Display",
|
"//submodules/Display",
|
||||||
"//submodules/ComponentFlow",
|
"//submodules/ComponentFlow",
|
||||||
"//submodules/AppBundle",
|
"//submodules/AppBundle",
|
||||||
|
"//submodules/TelegramCore",
|
||||||
"//submodules/TelegramUI/Components/TextFieldComponent",
|
"//submodules/TelegramUI/Components/TextFieldComponent",
|
||||||
"//submodules/Components/BundleIconComponent",
|
"//submodules/Components/BundleIconComponent",
|
||||||
"//submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton",
|
"//submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton",
|
||||||
@ -28,6 +29,7 @@ swift_library(
|
|||||||
"//submodules/TextFormat",
|
"//submodules/TextFormat",
|
||||||
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
|
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
|
||||||
"//submodules/TelegramUI/Components/MoreHeaderButton",
|
"//submodules/TelegramUI/Components/MoreHeaderButton",
|
||||||
|
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
import TelegramCore
|
||||||
import TextFieldComponent
|
import TextFieldComponent
|
||||||
import ChatContextQuery
|
import ChatContextQuery
|
||||||
import AccountContext
|
import AccountContext
|
||||||
@ -129,6 +130,110 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, inp
|
|||||||
|> castError(ChatContextQueryError.self)
|
|> castError(ChatContextQueryError.self)
|
||||||
|
|
||||||
return signal |> then(peers)
|
return signal |> then(peers)
|
||||||
|
case let .emojiSearch(query, languageCode, range):
|
||||||
|
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||||
|
|> map { peer -> Bool in
|
||||||
|
guard case let .user(user) = peer else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return user.isPremium
|
||||||
|
}
|
||||||
|
|> distinctUntilChanged
|
||||||
|
|
||||||
|
if query.isSingleEmoji {
|
||||||
|
return combineLatest(
|
||||||
|
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
|
||||||
|
hasPremium
|
||||||
|
)
|
||||||
|
|> map { view, hasPremium -> [(String, TelegramMediaFile?, String)] in
|
||||||
|
var result: [(String, TelegramMediaFile?, String)] = []
|
||||||
|
|
||||||
|
for entry in view.entries {
|
||||||
|
guard let item = entry.item as? StickerPackItem else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for attribute in item.file.attributes {
|
||||||
|
switch attribute {
|
||||||
|
case let .CustomEmoji(_, _, alt, _):
|
||||||
|
if alt == query {
|
||||||
|
if !item.file.isPremiumEmoji || hasPremium {
|
||||||
|
result.append((alt, item.file, alt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|> map { result -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||||
|
return { _ in return .emojis(result, range) }
|
||||||
|
}
|
||||||
|
|> castError(ChatContextQueryError.self)
|
||||||
|
} else {
|
||||||
|
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: query.count < 2)
|
||||||
|
if !languageCode.lowercased().hasPrefix("en") {
|
||||||
|
signal = signal
|
||||||
|
|> mapToSignal { keywords in
|
||||||
|
return .single(keywords)
|
||||||
|
|> then(
|
||||||
|
context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3)
|
||||||
|
|> map { englishKeywords in
|
||||||
|
return keywords + englishKeywords
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return signal
|
||||||
|
|> castError(ChatContextQueryError.self)
|
||||||
|
|> mapToSignal { keywords -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> in
|
||||||
|
return combineLatest(
|
||||||
|
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
|
||||||
|
hasPremium
|
||||||
|
)
|
||||||
|
|> map { view, hasPremium -> [(String, TelegramMediaFile?, String)] in
|
||||||
|
var result: [(String, TelegramMediaFile?, String)] = []
|
||||||
|
|
||||||
|
var allEmoticons: [String: String] = [:]
|
||||||
|
for keyword in keywords {
|
||||||
|
for emoticon in keyword.emoticons {
|
||||||
|
allEmoticons[emoticon] = keyword.keyword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in view.entries {
|
||||||
|
guard let item = entry.item as? StickerPackItem else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for attribute in item.file.attributes {
|
||||||
|
switch attribute {
|
||||||
|
case let .CustomEmoji(_, _, alt, _):
|
||||||
|
if !alt.isEmpty, let keyword = allEmoticons[alt] {
|
||||||
|
if !item.file.isPremiumEmoji || hasPremium {
|
||||||
|
result.append((alt, item.file, keyword))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for keyword in keywords {
|
||||||
|
for emoticon in keyword.emoticons {
|
||||||
|
result.append((emoticon, nil, keyword.keyword))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|> map { result -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||||
|
return { _ in return .emojis(result, range) }
|
||||||
|
}
|
||||||
|
|> castError(ChatContextQueryError.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return .complete()
|
return .complete()
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import UIKit
|
|||||||
import Display
|
import Display
|
||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
import TelegramCore
|
||||||
import AppBundle
|
import AppBundle
|
||||||
import TextFieldComponent
|
import TextFieldComponent
|
||||||
import BundleIconComponent
|
import BundleIconComponent
|
||||||
@ -12,6 +13,8 @@ import ChatPresentationInterfaceState
|
|||||||
import LottieComponent
|
import LottieComponent
|
||||||
import ChatContextQuery
|
import ChatContextQuery
|
||||||
import TextFormat
|
import TextFormat
|
||||||
|
import EmojiSuggestionsComponent
|
||||||
|
import AudioToolbox
|
||||||
|
|
||||||
public final class MessageInputPanelComponent: Component {
|
public final class MessageInputPanelComponent: Component {
|
||||||
public enum Style {
|
public enum Style {
|
||||||
@ -240,13 +243,15 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
private var currentMediaInputIsVoice: Bool = true
|
private var currentMediaInputIsVoice: Bool = true
|
||||||
private var mediaCancelFraction: CGFloat = 0.0
|
private var mediaCancelFraction: CGFloat = 0.0
|
||||||
|
|
||||||
|
private var currentInputMode: InputMode?
|
||||||
|
|
||||||
private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:]
|
private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:]
|
||||||
private var contextQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] = [:]
|
private var contextQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] = [:]
|
||||||
|
|
||||||
private var contextQueryResultPanel: ComponentView<Empty>?
|
private var contextQueryResultPanel: ComponentView<Empty>?
|
||||||
private var contextQueryResultPanelExternalState: ContextResultPanelComponent.ExternalState?
|
private var contextQueryResultPanelExternalState: ContextResultPanelComponent.ExternalState?
|
||||||
|
|
||||||
private var currentInputMode: InputMode?
|
private var viewForOverlayContent: ViewForOverlayContent?
|
||||||
|
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
|
||||||
|
|
||||||
private var component: MessageInputPanelComponent?
|
private var component: MessageInputPanelComponent?
|
||||||
private weak var state: EmptyComponentState?
|
private weak var state: EmptyComponentState?
|
||||||
@ -272,6 +277,28 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
self.addSubview(self.gradientView)
|
self.addSubview(self.gradientView)
|
||||||
self.fieldBackgroundView.addSubview(self.vibrancyEffectView)
|
self.fieldBackgroundView.addSubview(self.vibrancyEffectView)
|
||||||
self.addSubview(self.fieldBackgroundView)
|
self.addSubview(self.fieldBackgroundView)
|
||||||
|
|
||||||
|
self.viewForOverlayContent = ViewForOverlayContent(
|
||||||
|
ignoreHit: { [weak self] view, point in
|
||||||
|
guard let self else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if self.hitTest(view.convert(point, to: self), with: nil) != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if view.convert(point, to: self).y > self.bounds.maxY {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
dismissSuggestions: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.textFieldExternalState.dismissedEmojiSuggestionPosition = self.textFieldExternalState.currentEmojiSuggestion?.position
|
||||||
|
self.state?.updated()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -351,6 +378,17 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
let result = super.hitTest(point, with: event)
|
let result = super.hitTest(point, with: event)
|
||||||
|
|
||||||
|
if let _ = self.textField.view, let currentEmojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion, let currentEmojiSuggestionView = self.currentEmojiSuggestionView {
|
||||||
|
if let result = currentEmojiSuggestionView.hitTest(self.convert(point, to: currentEmojiSuggestionView), with: event) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
self.textFieldExternalState.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position
|
||||||
|
if let textFieldView = self.textField.view as? TextFieldComponent.View {
|
||||||
|
textFieldView.updateEmojiSuggestion(transition: .immediate)
|
||||||
|
}
|
||||||
|
self.state?.updated()
|
||||||
|
}
|
||||||
|
|
||||||
if result == nil, let contextQueryResultPanel = self.contextQueryResultPanel?.view, let panelResult = contextQueryResultPanel.hitTest(self.convert(point, to: contextQueryResultPanel), with: event), panelResult !== contextQueryResultPanel {
|
if result == nil, let contextQueryResultPanel = self.contextQueryResultPanel?.view, let panelResult = contextQueryResultPanel.hitTest(self.convert(point, to: contextQueryResultPanel), with: event), panelResult !== contextQueryResultPanel {
|
||||||
return panelResult
|
return panelResult
|
||||||
}
|
}
|
||||||
@ -513,9 +551,18 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
if let textFieldView = self.textField.view {
|
if let textFieldView = self.textField.view {
|
||||||
if textFieldView.superview == nil {
|
if textFieldView.superview == nil {
|
||||||
self.addSubview(textFieldView)
|
self.addSubview(textFieldView)
|
||||||
|
|
||||||
|
if let viewForOverlayContent = self.viewForOverlayContent {
|
||||||
|
self.addSubview(viewForOverlayContent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
transition.setFrame(view: textFieldView, frame: CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX, y: fieldBackgroundFrame.maxY - textFieldSize.height), size: textFieldSize))
|
let textFieldFrame = CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX, y: fieldBackgroundFrame.maxY - textFieldSize.height), size: textFieldSize)
|
||||||
|
transition.setFrame(view: textFieldView, frame: textFieldFrame)
|
||||||
transition.setAlpha(view: textFieldView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil) ? 0.0 : 1.0)
|
transition.setAlpha(view: textFieldView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil) ? 0.0 : 1.0)
|
||||||
|
|
||||||
|
if let viewForOverlayContent = self.viewForOverlayContent {
|
||||||
|
transition.setFrame(view: viewForOverlayContent, frame: textFieldFrame)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let disabledPlaceholderText = component.disabledPlaceholder {
|
if let disabledPlaceholderText = component.disabledPlaceholder {
|
||||||
@ -1124,6 +1171,9 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
|
|
||||||
self.updateContextQueries()
|
self.updateContextQueries()
|
||||||
|
|
||||||
|
let panelLeftInset: CGFloat = max(insets.left, 7.0)
|
||||||
|
let panelRightInset: CGFloat = max(insets.right, 41.0)
|
||||||
|
|
||||||
if let result = self.contextQueryResults[.mention], result.count > 0 && self.textFieldExternalState.isEditing {
|
if let result = self.contextQueryResults[.mention], result.count > 0 && self.textFieldExternalState.isEditing {
|
||||||
let availablePanelHeight: CGFloat = 413.0
|
let availablePanelHeight: CGFloat = 413.0
|
||||||
|
|
||||||
@ -1142,8 +1192,6 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
animateIn = true
|
animateIn = true
|
||||||
transition = .immediate
|
transition = .immediate
|
||||||
}
|
}
|
||||||
let panelLeftInset: CGFloat = max(insets.left, 7.0)
|
|
||||||
let panelRightInset: CGFloat = max(insets.right, 41.0)
|
|
||||||
let panelSize = panel.update(
|
let panelSize = panel.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(ContextResultPanelComponent(
|
component: AnyComponent(ContextResultPanelComponent(
|
||||||
@ -1209,6 +1257,143 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let emojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion, emojiSuggestion.disposable == nil {
|
||||||
|
emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self, weak emojiSuggestion] result in
|
||||||
|
guard let self, let emojiSuggestion, self.textFieldExternalState.currentEmojiSuggestion === emojiSuggestion else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emojiSuggestion.value = result
|
||||||
|
self.state?.updated()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasTrackingView = self.textFieldExternalState.hasTrackingView
|
||||||
|
if let currentEmojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion, let value = currentEmojiSuggestion.value as? [TelegramMediaFile], value.isEmpty {
|
||||||
|
hasTrackingView = false
|
||||||
|
}
|
||||||
|
if !self.textFieldExternalState.isEditing {
|
||||||
|
hasTrackingView = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasTrackingView {
|
||||||
|
if let currentEmojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion {
|
||||||
|
self.textFieldExternalState.currentEmojiSuggestion = nil
|
||||||
|
currentEmojiSuggestion.disposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let currentEmojiSuggestionView = self.currentEmojiSuggestionView {
|
||||||
|
self.currentEmojiSuggestionView = nil
|
||||||
|
|
||||||
|
currentEmojiSuggestionView.alpha = 0.0
|
||||||
|
currentEmojiSuggestionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak currentEmojiSuggestionView] _ in
|
||||||
|
currentEmojiSuggestionView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let currentEmojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion, let value = currentEmojiSuggestion.value as? [TelegramMediaFile] {
|
||||||
|
let currentEmojiSuggestionView: ComponentHostView<Empty>
|
||||||
|
if let current = self.currentEmojiSuggestionView {
|
||||||
|
currentEmojiSuggestionView = current
|
||||||
|
} else {
|
||||||
|
currentEmojiSuggestionView = ComponentHostView<Empty>()
|
||||||
|
self.currentEmojiSuggestionView = currentEmojiSuggestionView
|
||||||
|
self.addSubview(currentEmojiSuggestionView)
|
||||||
|
|
||||||
|
currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||||
|
|
||||||
|
//self.installEmojiSuggestionPreviewGesture(hostView: currentEmojiSuggestionView)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let globalPosition: CGPoint
|
||||||
|
if let textView = self.textField.view {
|
||||||
|
globalPosition = textView.convert(currentEmojiSuggestion.localPosition, to: self)
|
||||||
|
} else {
|
||||||
|
globalPosition = .zero
|
||||||
|
}
|
||||||
|
|
||||||
|
let sideInset: CGFloat = 7.0
|
||||||
|
|
||||||
|
let viewSize = currentEmojiSuggestionView.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(EmojiSuggestionsComponent(
|
||||||
|
context: component.context,
|
||||||
|
userLocation: .other,
|
||||||
|
theme: EmojiSuggestionsComponent.Theme(
|
||||||
|
backgroundColor: UIColor(white: 0.0, alpha: 0.5),
|
||||||
|
textColor: .white,
|
||||||
|
placeholderColor: UIColor(rgb: 0xffffff).mixedWith(UIColor(rgb: 0x1c1c1d), alpha: 0.9)
|
||||||
|
),
|
||||||
|
animationCache: component.context.animationCache,
|
||||||
|
animationRenderer: component.context.animationRenderer,
|
||||||
|
files: value,
|
||||||
|
action: { [weak self] file in
|
||||||
|
guard let self, let textView = self.textField.view as? TextFieldComponent.View, let currentEmojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioServicesPlaySystemSound(0x450)
|
||||||
|
|
||||||
|
let inputState = textView.getInputState()
|
||||||
|
let inputText = NSMutableAttributedString(attributedString: inputState.inputText)
|
||||||
|
|
||||||
|
var text: String?
|
||||||
|
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
|
||||||
|
loop: for attribute in file.attributes {
|
||||||
|
switch attribute {
|
||||||
|
case let .CustomEmoji(_, _, displayText, _):
|
||||||
|
text = displayText
|
||||||
|
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file)
|
||||||
|
break loop
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let emojiAttribute = emojiAttribute, let text = text {
|
||||||
|
let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])
|
||||||
|
|
||||||
|
let range = currentEmojiSuggestion.position.range
|
||||||
|
let previousText = inputText.attributedSubstring(from: range)
|
||||||
|
inputText.replaceCharacters(in: range, with: replacementText)
|
||||||
|
|
||||||
|
var replacedUpperBound = range.lowerBound
|
||||||
|
while true {
|
||||||
|
if inputText.attributedSubstring(from: NSRange(location: 0, length: replacedUpperBound)).string.hasSuffix(previousText.string) {
|
||||||
|
let replaceRange = NSRange(location: replacedUpperBound - previousText.length, length: previousText.length)
|
||||||
|
if replaceRange.location < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let adjacentString = inputText.attributedSubstring(from: replaceRange)
|
||||||
|
if adjacentString.string != previousText.string || adjacentString.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
inputText.replaceCharacters(in: replaceRange, with: NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: emojiAttribute.interactivelySelectedFromPackId, fileId: emojiAttribute.fileId, file: emojiAttribute.file)]))
|
||||||
|
replacedUpperBound = replaceRange.lowerBound
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectionPosition = range.lowerBound + (replacementText.string as NSString).length
|
||||||
|
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: self.bounds.width - panelLeftInset - panelRightInset, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let viewFrame = CGRect(origin: CGPoint(x: min(self.bounds.width - sideInset - viewSize.width, max(panelLeftInset, floor(globalPosition.x - viewSize.width / 2.0))), y: globalPosition.y - 4.0 - viewSize.height), size: viewSize)
|
||||||
|
currentEmojiSuggestionView.frame = viewFrame
|
||||||
|
if let componentView = currentEmojiSuggestionView.componentView as? EmojiSuggestionsComponent.View {
|
||||||
|
componentView.adjustBackground(relativePositionX: floor(globalPosition.x - viewFrame.minX))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1221,3 +1406,44 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class ViewForOverlayContent: UIView {
|
||||||
|
let ignoreHit: (UIView, CGPoint) -> Bool
|
||||||
|
let dismissSuggestions: () -> Void
|
||||||
|
|
||||||
|
init(ignoreHit: @escaping (UIView, CGPoint) -> Bool, dismissSuggestions: @escaping () -> Void) {
|
||||||
|
self.ignoreHit = ignoreHit
|
||||||
|
self.dismissSuggestions = dismissSuggestions
|
||||||
|
|
||||||
|
super.init(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(coder: NSCoder) {
|
||||||
|
preconditionFailure()
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeDismissContent(point: CGPoint) {
|
||||||
|
for subview in self.subviews.reversed() {
|
||||||
|
if let _ = subview.hitTest(self.convert(point, to: subview), with: nil) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dismissSuggestions()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
for subview in self.subviews.reversed() {
|
||||||
|
if let result = subview.hitTest(self.convert(point, to: subview), with: event) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if event == nil || self.ignoreHit(self, point) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dismissSuggestions()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -239,7 +239,7 @@ final class CategoryListItemComponent: Component {
|
|||||||
text: .plain(NSAttributedString(string: labelData.0, font: Font.regular(15.0), textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor))
|
text: .plain(NSAttributedString(string: labelData.0, font: Font.regular(15.0), textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor))
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
containerSize: CGSize(width: availableSize.width - leftInset - rightInset - 14.0, height: 100.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
let labelArrowSize = self.labelArrow.update(
|
let labelArrowSize = self.labelArrow.update(
|
||||||
|
@ -9,6 +9,7 @@ import ComponentDisplayAdapters
|
|||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
import Postbox
|
||||||
import MultilineTextComponent
|
import MultilineTextComponent
|
||||||
import SolidRoundedButtonComponent
|
import SolidRoundedButtonComponent
|
||||||
import PresentationDataUtils
|
import PresentationDataUtils
|
||||||
@ -31,6 +32,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
let screenshot: Bool
|
let screenshot: Bool
|
||||||
let pin: Bool
|
let pin: Bool
|
||||||
let timeout: Int
|
let timeout: Int
|
||||||
|
let mentions: [String]
|
||||||
let categoryItems: [CategoryItem]
|
let categoryItems: [CategoryItem]
|
||||||
let optionItems: [OptionItem]
|
let optionItems: [OptionItem]
|
||||||
let completion: (EngineStoryPrivacy, Bool, Bool) -> Void
|
let completion: (EngineStoryPrivacy, Bool, Bool) -> Void
|
||||||
@ -43,6 +45,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
screenshot: Bool,
|
screenshot: Bool,
|
||||||
pin: Bool,
|
pin: Bool,
|
||||||
timeout: Int,
|
timeout: Int,
|
||||||
|
mentions: [String],
|
||||||
categoryItems: [CategoryItem],
|
categoryItems: [CategoryItem],
|
||||||
optionItems: [OptionItem],
|
optionItems: [OptionItem],
|
||||||
completion: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void,
|
completion: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void,
|
||||||
@ -54,6 +57,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
self.screenshot = screenshot
|
self.screenshot = screenshot
|
||||||
self.pin = pin
|
self.pin = pin
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
self.mentions = mentions
|
||||||
self.categoryItems = categoryItems
|
self.categoryItems = categoryItems
|
||||||
self.optionItems = optionItems
|
self.optionItems = optionItems
|
||||||
self.completion = completion
|
self.completion = completion
|
||||||
@ -79,6 +83,9 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
if lhs.timeout != rhs.timeout {
|
if lhs.timeout != rhs.timeout {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.mentions != rhs.mentions {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.categoryItems != rhs.categoryItems {
|
if lhs.categoryItems != rhs.categoryItems {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -1522,17 +1529,126 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
base = .nobody
|
base = .nobody
|
||||||
}
|
}
|
||||||
|
|
||||||
component.completion(
|
let proceed = {
|
||||||
EngineStoryPrivacy(
|
component.completion(
|
||||||
base: base,
|
EngineStoryPrivacy(
|
||||||
additionallyIncludePeers: self.selectedPeers
|
base: base,
|
||||||
),
|
additionallyIncludePeers: self.selectedPeers
|
||||||
self.selectedOptions.contains(.screenshot),
|
),
|
||||||
self.selectedOptions.contains(.pin)
|
self.selectedOptions.contains(.screenshot),
|
||||||
)
|
self.selectedOptions.contains(.pin)
|
||||||
|
)
|
||||||
|
|
||||||
controller.dismissAllTooltips()
|
controller.dismissAllTooltips()
|
||||||
controller.dismiss()
|
controller.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentAlert: ([String]) -> Void = { usernames in
|
||||||
|
let usernamesString = String(usernames.map { "@\($0)" }.joined(separator: ", "))
|
||||||
|
let alertController = textAlertController(
|
||||||
|
context: component.context,
|
||||||
|
forceTheme: defaultDarkColorPresentationTheme,
|
||||||
|
title: "Privacy Restrictions",
|
||||||
|
text: "The privacy settings of your story will prevent some users you tagged (\( usernamesString )) from viewing it.",
|
||||||
|
actions: [
|
||||||
|
TextAlertAction(type: .defaultAction, title: "Proceed Anyway", action: {
|
||||||
|
proceed()
|
||||||
|
}),
|
||||||
|
TextAlertAction(type: .genericAction, title: "Cancel", action: {})
|
||||||
|
],
|
||||||
|
actionLayout: .vertical
|
||||||
|
)
|
||||||
|
controller.present(alertController, in: .window(.root))
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchingUsername(user: TelegramUser, usernames: Set<String>) -> String? {
|
||||||
|
for username in user.usernames {
|
||||||
|
if usernames.contains(username.username) {
|
||||||
|
return username.username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let username = user.username {
|
||||||
|
if usernames.contains(username) {
|
||||||
|
return username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = component.context
|
||||||
|
let selectedPeerIds = self.selectedPeers
|
||||||
|
|
||||||
|
if case .stories = component.stateContext.subject {
|
||||||
|
if component.mentions.isEmpty {
|
||||||
|
proceed()
|
||||||
|
} else if case .nobody = base {
|
||||||
|
if selectedPeerIds.isEmpty {
|
||||||
|
presentAlert(component.mentions)
|
||||||
|
} else {
|
||||||
|
let _ = (context.account.postbox.transaction { transaction in
|
||||||
|
var filteredMentions = Set(component.mentions)
|
||||||
|
for peerId in selectedPeerIds {
|
||||||
|
if let user = transaction.getPeer(peerId) as? TelegramUser, let username = matchingUsername(user: user, usernames: filteredMentions) {
|
||||||
|
filteredMentions.remove(username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array(filteredMentions)
|
||||||
|
}
|
||||||
|
|> deliverOnMainQueue).start(next: { mentions in
|
||||||
|
if mentions.isEmpty {
|
||||||
|
proceed()
|
||||||
|
} else {
|
||||||
|
presentAlert(mentions)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if case .contacts = base {
|
||||||
|
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false))
|
||||||
|
|> map { contacts -> [String] in
|
||||||
|
var filteredMentions = Set(component.mentions)
|
||||||
|
let peers = contacts.peers
|
||||||
|
for peer in peers {
|
||||||
|
if selectedPeerIds.contains(peer.id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if case let .user(user) = peer, let username = matchingUsername(user: user, usernames: filteredMentions) {
|
||||||
|
filteredMentions.remove(username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array(filteredMentions)
|
||||||
|
}
|
||||||
|
|> deliverOnMainQueue).start(next: { mentions in
|
||||||
|
if mentions.isEmpty {
|
||||||
|
proceed()
|
||||||
|
} else {
|
||||||
|
presentAlert(mentions)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if case .closeFriends = base {
|
||||||
|
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false))
|
||||||
|
|> map { contacts -> [String] in
|
||||||
|
var filteredMentions = Set(component.mentions)
|
||||||
|
let peers = contacts.peers
|
||||||
|
for peer in peers {
|
||||||
|
if case let .user(user) = peer, user.flags.contains(.isCloseFriend), let username = matchingUsername(user: user, usernames: filteredMentions) {
|
||||||
|
filteredMentions.remove(username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array(filteredMentions)
|
||||||
|
}
|
||||||
|
|> deliverOnMainQueue).start(next: { mentions in
|
||||||
|
if mentions.isEmpty {
|
||||||
|
proceed()
|
||||||
|
} else {
|
||||||
|
presentAlert(mentions)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
proceed()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
proceed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
@ -1665,11 +1781,27 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
|
|
||||||
switch subject {
|
switch subject {
|
||||||
case .stories:
|
case .stories:
|
||||||
let state = State(peers: [], presences: [:])
|
var signals: [Signal<EnginePeer?, NoError>] = []
|
||||||
self.stateValue = state
|
if initialPeerIds.count < 3 {
|
||||||
self.stateSubject.set(.single(state))
|
for peerId in initialPeerIds {
|
||||||
self.readySubject.set(true)
|
signals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)))
|
||||||
self.initialPeerIds = initialPeerIds
|
}
|
||||||
|
}
|
||||||
|
self.stateDisposable = (combineLatest(signals)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] peers in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = State(
|
||||||
|
peers: peers.compactMap { $0 },
|
||||||
|
presences: [:]
|
||||||
|
)
|
||||||
|
self.stateValue = state
|
||||||
|
self.stateSubject.set(.single(state))
|
||||||
|
|
||||||
|
self.readySubject.set(true)
|
||||||
|
})
|
||||||
case .chats:
|
case .chats:
|
||||||
self.stateDisposable = (context.engine.messages.chatList(group: .root, count: 200)
|
self.stateDisposable = (context.engine.messages.chatList(group: .root, count: 200)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] chatList in
|
|> deliverOnMainQueue).start(next: { [weak self] chatList in
|
||||||
@ -1805,12 +1937,15 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
allowScreenshots: Bool = true,
|
allowScreenshots: Bool = true,
|
||||||
pin: Bool = false,
|
pin: Bool = false,
|
||||||
timeout: Int = 0,
|
timeout: Int = 0,
|
||||||
|
mentions: [String] = [],
|
||||||
stateContext: StateContext,
|
stateContext: StateContext,
|
||||||
completion: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void,
|
completion: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void,
|
||||||
editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void
|
editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = []
|
var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = []
|
||||||
var optionItems: [ShareWithPeersScreenComponent.OptionItem] = []
|
var optionItems: [ShareWithPeersScreenComponent.OptionItem] = []
|
||||||
if case let .stories(editing) = stateContext.subject {
|
if case let .stories(editing) = stateContext.subject {
|
||||||
@ -1822,12 +1957,25 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
actionTitle: nil
|
actionTitle: nil
|
||||||
))
|
))
|
||||||
|
|
||||||
|
var peerNames = ""
|
||||||
|
if let peers = stateContext.stateValue?.peers, !peers.isEmpty {
|
||||||
|
peerNames = String(peers.map { $0.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) }.joined(separator: ", "))
|
||||||
|
}
|
||||||
|
|
||||||
var contactsSubtitle = "exclude people"
|
var contactsSubtitle = "exclude people"
|
||||||
if initialPrivacy.base == .contacts, initialPrivacy.additionallyIncludePeers.count > 0 {
|
if initialPrivacy.base == .contacts, initialPrivacy.additionallyIncludePeers.count > 0 {
|
||||||
if initialPrivacy.additionallyIncludePeers.count == 1 {
|
if initialPrivacy.additionallyIncludePeers.count == 1 {
|
||||||
contactsSubtitle = "except 1 person"
|
if !peerNames.isEmpty {
|
||||||
|
contactsSubtitle = "except \(peerNames)"
|
||||||
|
} else {
|
||||||
|
contactsSubtitle = "except 1 person"
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
contactsSubtitle = "except \(initialPrivacy.additionallyIncludePeers.count) people"
|
if !peerNames.isEmpty {
|
||||||
|
contactsSubtitle = "except \(peerNames)"
|
||||||
|
} else {
|
||||||
|
contactsSubtitle = "except \(initialPrivacy.additionallyIncludePeers.count) people"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||||
@ -1849,9 +1997,17 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
var selectedContactsSubtitle = "choose"
|
var selectedContactsSubtitle = "choose"
|
||||||
if initialPrivacy.base == .nobody, initialPrivacy.additionallyIncludePeers.count > 0 {
|
if initialPrivacy.base == .nobody, initialPrivacy.additionallyIncludePeers.count > 0 {
|
||||||
if initialPrivacy.additionallyIncludePeers.count == 1 {
|
if initialPrivacy.additionallyIncludePeers.count == 1 {
|
||||||
selectedContactsSubtitle = "1 person"
|
if !peerNames.isEmpty {
|
||||||
|
selectedContactsSubtitle = peerNames
|
||||||
|
} else {
|
||||||
|
selectedContactsSubtitle = "1 person"
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedContactsSubtitle = "\(initialPrivacy.additionallyIncludePeers.count) people"
|
if !peerNames.isEmpty {
|
||||||
|
selectedContactsSubtitle = peerNames
|
||||||
|
} else {
|
||||||
|
selectedContactsSubtitle = "\(initialPrivacy.additionallyIncludePeers.count) people"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||||
@ -1882,6 +2038,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
screenshot: allowScreenshots,
|
screenshot: allowScreenshots,
|
||||||
pin: pin,
|
pin: pin,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
|
mentions: mentions,
|
||||||
categoryItems: categoryItems,
|
categoryItems: categoryItems,
|
||||||
optionItems: optionItems,
|
optionItems: optionItems,
|
||||||
completion: completion,
|
completion: completion,
|
||||||
|
@ -2843,7 +2843,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
initialVideoPosition: videoPlaybackPosition,
|
initialVideoPosition: videoPlaybackPosition,
|
||||||
transitionIn: nil,
|
transitionIn: nil,
|
||||||
transitionOut: { _, _ in return 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 {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -2865,7 +2865,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
updateProgressImpl?(0.0)
|
updateProgressImpl?(0.0)
|
||||||
|
|
||||||
if let imageData = compressImageToJPEG(image, quality: 0.7) {
|
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
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -2904,7 +2904,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let firstFrameImageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) }
|
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
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -3024,6 +3024,14 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
|
|
||||||
let additionalCount = component.slice.item.storyItem.privacy?.additionallyIncludePeers.count ?? 0
|
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
|
let privacyText: String
|
||||||
switch component.slice.item.storyItem.privacy?.base {
|
switch component.slice.item.storyItem.privacy?.base {
|
||||||
case .closeFriends:
|
case .closeFriends:
|
||||||
@ -3161,6 +3169,8 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _ = hasLinkedStickers
|
||||||
|
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
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)
|
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)
|
||||||
contextController.dismissed = { [weak self] in
|
contextController.dismissed = { [weak self] in
|
||||||
|
@ -23,10 +23,34 @@ public final class TextFieldComponent: Component {
|
|||||||
public fileprivate(set) var hasText: Bool = false
|
public fileprivate(set) var hasText: Bool = false
|
||||||
public var initialText: NSAttributedString?
|
public var initialText: NSAttributedString?
|
||||||
|
|
||||||
|
public var hasTrackingView = false
|
||||||
|
|
||||||
|
public var currentEmojiSuggestion: EmojiSuggestion?
|
||||||
|
public var dismissedEmojiSuggestionPosition: EmojiSuggestion.Position?
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class EmojiSuggestion {
|
||||||
|
public struct Position: Equatable {
|
||||||
|
public var range: NSRange
|
||||||
|
public var value: String
|
||||||
|
}
|
||||||
|
|
||||||
|
public var localPosition: CGPoint
|
||||||
|
public var position: Position
|
||||||
|
public var disposable: Disposable?
|
||||||
|
public var value: Any?
|
||||||
|
|
||||||
|
init(localPosition: CGPoint, position: Position) {
|
||||||
|
self.localPosition = localPosition
|
||||||
|
self.position = position
|
||||||
|
self.disposable = nil
|
||||||
|
self.value = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public final class AnimationHint {
|
public final class AnimationHint {
|
||||||
public enum Kind {
|
public enum Kind {
|
||||||
case textChanged
|
case textChanged
|
||||||
@ -223,6 +247,7 @@ public final class TextFieldComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.updateSpoilersRevealed()
|
self.updateSpoilersRevealed()
|
||||||
|
self.updateEmojiSuggestion(transition: .immediate)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func textViewDidBeginEditing(_ textView: UITextView) {
|
public func textViewDidBeginEditing(_ textView: UITextView) {
|
||||||
@ -335,11 +360,6 @@ public final class TextFieldComponent: Component {
|
|||||||
}
|
}
|
||||||
self.textView.becomeFirstResponder()
|
self.textView.becomeFirstResponder()
|
||||||
}
|
}
|
||||||
// strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, {
|
|
||||||
// return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({
|
|
||||||
// $0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex))
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
component.present(controller)
|
component.present(controller)
|
||||||
@ -547,6 +567,60 @@ public final class TextFieldComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func updateEmojiSuggestion(transition: Transition) {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasTracking = false
|
||||||
|
var hasTrackingView = false
|
||||||
|
if self.textView.selectedRange.length == 0 && self.textView.selectedRange.location > 0 {
|
||||||
|
let selectedSubstring = self.textView.attributedText.attributedSubstring(from: NSRange(location: 0, length: self.textView.selectedRange.location))
|
||||||
|
if let lastCharacter = selectedSubstring.string.last, String(lastCharacter).isSingleEmoji {
|
||||||
|
let queryLength = (String(lastCharacter) as NSString).length
|
||||||
|
if selectedSubstring.attribute(ChatTextInputAttributes.customEmoji, at: selectedSubstring.length - queryLength, effectiveRange: nil) == nil {
|
||||||
|
let beginning = self.textView.beginningOfDocument
|
||||||
|
|
||||||
|
let characterRange = NSRange(location: selectedSubstring.length - queryLength, length: queryLength)
|
||||||
|
|
||||||
|
let start = self.textView.position(from: beginning, offset: selectedSubstring.length - queryLength)
|
||||||
|
let end = self.textView.position(from: beginning, offset: selectedSubstring.length)
|
||||||
|
|
||||||
|
if let start = start, let end = end, let textRange = self.textView.textRange(from: start, to: end) {
|
||||||
|
let selectionRects = self.textView.selectionRects(for: textRange)
|
||||||
|
let emojiSuggestionPosition = EmojiSuggestion.Position(range: characterRange, value: String(lastCharacter))
|
||||||
|
|
||||||
|
hasTracking = true
|
||||||
|
|
||||||
|
if let trackingRect = selectionRects.first?.rect {
|
||||||
|
let trackingPosition = CGPoint(x: trackingRect.midX, y: trackingRect.minY)
|
||||||
|
if component.externalState.dismissedEmojiSuggestionPosition == emojiSuggestionPosition {
|
||||||
|
} else {
|
||||||
|
hasTrackingView = true
|
||||||
|
|
||||||
|
let emojiSuggestion: EmojiSuggestion
|
||||||
|
if let current = component.externalState.currentEmojiSuggestion, current.position.value == emojiSuggestionPosition.value {
|
||||||
|
emojiSuggestion = current
|
||||||
|
} else {
|
||||||
|
|
||||||
|
emojiSuggestion = EmojiSuggestion(localPosition: trackingPosition, position: emojiSuggestionPosition)
|
||||||
|
component.externalState.currentEmojiSuggestion = emojiSuggestion
|
||||||
|
}
|
||||||
|
emojiSuggestion.localPosition = trackingPosition
|
||||||
|
emojiSuggestion.position = emojiSuggestionPosition
|
||||||
|
component.externalState.dismissedEmojiSuggestionPosition = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasTracking {
|
||||||
|
component.externalState.dismissedEmojiSuggestionPosition = nil
|
||||||
|
}
|
||||||
|
component.externalState.hasTrackingView = hasTrackingView
|
||||||
|
}
|
||||||
|
|
||||||
func update(component: TextFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
func update(component: TextFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
self.component = component
|
self.component = component
|
||||||
self.state = state
|
self.state = state
|
||||||
@ -584,6 +658,8 @@ public final class TextFieldComponent: Component {
|
|||||||
self.textView.frame = CGRect(origin: CGPoint(), size: size)
|
self.textView.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
self.textView.panGestureRecognizer.isEnabled = isEditing
|
self.textView.panGestureRecognizer.isEnabled = isEditing
|
||||||
|
|
||||||
|
self.updateEmojiSuggestion(transition: .immediate)
|
||||||
|
|
||||||
if refreshScrolling {
|
if refreshScrolling {
|
||||||
if isEditing {
|
if isEditing {
|
||||||
if wasEditing {
|
if wasEditing {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "arrow_left.pdf",
|
"filename" : "ic_next.pdf",
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
Binary file not shown.
92
submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/ic_next.pdf
vendored
Normal file
92
submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/ic_next.pdf
vendored
Normal file
@ -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
|
@ -2859,7 +2859,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
component: AnyComponent(EmojiSuggestionsComponent(
|
component: AnyComponent(EmojiSuggestionsComponent(
|
||||||
context: context,
|
context: context,
|
||||||
userLocation: .other,
|
userLocation: .other,
|
||||||
theme: theme,
|
theme: EmojiSuggestionsComponent.Theme(theme: theme),
|
||||||
animationCache: presentationContext.animationCache,
|
animationCache: presentationContext.animationCache,
|
||||||
animationRenderer: presentationContext.animationRenderer,
|
animationRenderer: presentationContext.animationRenderer,
|
||||||
files: value,
|
files: value,
|
||||||
|
@ -354,7 +354,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
|||||||
} else {
|
} else {
|
||||||
return nil
|
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 {
|
guard let self, let mediaResult else {
|
||||||
dismissCameraImpl?()
|
dismissCameraImpl?()
|
||||||
commit({})
|
commit({})
|
||||||
@ -373,7 +373,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
|||||||
case let .image(image, dimensions):
|
case let .image(image, dimensions):
|
||||||
if let imageData = compressImageToJPEG(image, quality: 0.7) {
|
if let imageData = compressImageToJPEG(image, quality: 0.7) {
|
||||||
let entities = generateChatInputTextEntities(caption)
|
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 {
|
Queue.mainQueue().justDispatch {
|
||||||
commit({})
|
commit({})
|
||||||
}
|
}
|
||||||
@ -396,7 +396,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
|||||||
}
|
}
|
||||||
let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) }
|
let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) }
|
||||||
let entities = generateChatInputTextEntities(caption)
|
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 {
|
Queue.mainQueue().justDispatch {
|
||||||
commit({})
|
commit({})
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user