mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-05 05:51:42 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
75129f6ecb
@ -979,6 +979,7 @@ private final class NotificationServiceHandler {
|
|||||||
case deleteMessage([MessageId])
|
case deleteMessage([MessageId])
|
||||||
case readReactions([MessageId])
|
case readReactions([MessageId])
|
||||||
case readMessage(MessageId)
|
case readMessage(MessageId)
|
||||||
|
case readStories(peerId: PeerId, maxId: Int32)
|
||||||
case call(CallData)
|
case call(CallData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1030,6 +1031,14 @@ private final class NotificationServiceHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "READ_STORIES":
|
||||||
|
if let peerId = peerId {
|
||||||
|
if let storyIdString = payloadJson["max_id"] as? String {
|
||||||
|
if let maxId = Int32(storyIdString) {
|
||||||
|
action = .readStories(peerId: peerId, maxId: maxId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -1787,6 +1796,10 @@ private final class NotificationServiceHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let wasDisplayed = stateManager.postbox.transaction { transaction -> Bool in
|
||||||
|
return _internal_getStoryNotificationWasDisplayed(transaction: transaction, id: StoryId(peerId: peerId, id: storyId))
|
||||||
|
}
|
||||||
|
|
||||||
Logger.shared.log("NotificationService \(episode)", "Will fetch media")
|
Logger.shared.log("NotificationService \(episode)", "Will fetch media")
|
||||||
let _ = (combineLatest(queue: queue,
|
let _ = (combineLatest(queue: queue,
|
||||||
fetchMediaSignal
|
fetchMediaSignal
|
||||||
@ -1794,10 +1807,11 @@ private final class NotificationServiceHandler {
|
|||||||
fetchNotificationSoundSignal
|
fetchNotificationSoundSignal
|
||||||
|> timeout(10.0, queue: queue, alternate: .single(nil)),
|
|> timeout(10.0, queue: queue, alternate: .single(nil)),
|
||||||
fetchStoriesSignal
|
fetchStoriesSignal
|
||||||
|> timeout(10.0, queue: queue, alternate: .single(Void()))
|
|> timeout(10.0, queue: queue, alternate: .single(Void())),
|
||||||
|
wasDisplayed
|
||||||
)
|
)
|
||||||
|> deliverOn(queue)).start(next: { mediaData, notificationSoundData, _ in
|
|> deliverOn(queue)).start(next: { mediaData, notificationSoundData, _, wasDisplayed in
|
||||||
guard let strongSelf = self, let _ = strongSelf.stateManager else {
|
guard let strongSelf = self, let stateManager = strongSelf.stateManager else {
|
||||||
completed()
|
completed()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1812,6 +1826,15 @@ private final class NotificationServiceHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var content = content
|
||||||
|
if wasDisplayed {
|
||||||
|
content = NotificationContent(isLockedMessage: nil)
|
||||||
|
} else {
|
||||||
|
let _ = (stateManager.postbox.transaction { transaction -> Void in
|
||||||
|
_internal_setStoryNotificationWasDisplayed(transaction: transaction, id: StoryId(peerId: peerId, id: storyId))
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
|
||||||
Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)")
|
Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)")
|
||||||
|
|
||||||
updateCurrentContent(content)
|
updateCurrentContent(content)
|
||||||
@ -2003,6 +2026,39 @@ private final class NotificationServiceHandler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !removeIdentifiers.isEmpty {
|
||||||
|
Logger.shared.log("NotificationService \(episode)", "Will try to remove \(removeIdentifiers.count) notifications")
|
||||||
|
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: removeIdentifiers)
|
||||||
|
queue.after(1.0, {
|
||||||
|
completeRemoval()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
completeRemoval()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
case let .readStories(peerId, maxId):
|
||||||
|
Logger.shared.log("NotificationService \(episode)", "Will read stories peerId: \(peerId) maxId: \(maxId)")
|
||||||
|
let _ = (stateManager.postbox.transaction { transaction -> Void in
|
||||||
|
}
|
||||||
|
|> deliverOn(strongSelf.queue)).start(completed: {
|
||||||
|
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in
|
||||||
|
var removeIdentifiers: [String] = []
|
||||||
|
for notification in notifications {
|
||||||
|
if let peerIdString = notification.request.content.userInfo["peerId"] as? String, let peerIdValue = Int64(peerIdString), let messageIdString = notification.request.content.userInfo["story_id"] as? String, let messageIdValue = Int32(messageIdString) {
|
||||||
|
if PeerId(peerIdValue) == peerId && messageIdValue <= maxId {
|
||||||
|
removeIdentifiers.append(notification.request.identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let completeRemoval: () -> Void = {
|
||||||
|
let content = NotificationContent(isLockedMessage: nil)
|
||||||
|
updateCurrentContent(content)
|
||||||
|
|
||||||
|
completed()
|
||||||
|
}
|
||||||
|
|
||||||
if !removeIdentifiers.isEmpty {
|
if !removeIdentifiers.isEmpty {
|
||||||
Logger.shared.log("NotificationService \(episode)", "Will try to remove \(removeIdentifiers.count) notifications")
|
Logger.shared.log("NotificationService \(episode)", "Will try to remove \(removeIdentifiers.count) notifications")
|
||||||
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: removeIdentifiers)
|
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: removeIdentifiers)
|
||||||
|
|||||||
@ -23,4 +23,5 @@ public protocol ChatListController: ViewController {
|
|||||||
func navigateToFolder(folderId: Int32, completion: @escaping () -> Void)
|
func navigateToFolder(folderId: Int32, completion: @escaping () -> Void)
|
||||||
|
|
||||||
func openStories(peerId: EnginePeer.Id)
|
func openStories(peerId: EnginePeer.Id)
|
||||||
|
func openStoriesFromNotification(peerId: EnginePeer.Id, storyId: Int32)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -953,6 +953,9 @@ public final class AvatarNode: ASDisplayNode {
|
|||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if let previousDisposable = self.loadingStatuses.copyItemsWithIndices().first(where: { $0.0 == index })?.1 {
|
||||||
|
previousDisposable.dispose()
|
||||||
|
}
|
||||||
self.loadingStatuses.remove(index)
|
self.loadingStatuses.remove(index)
|
||||||
if self.loadingStatuses.isEmpty {
|
if self.loadingStatuses.isEmpty {
|
||||||
self.updateStoryIndicator(transition: .immediate)
|
self.updateStoryIndicator(transition: .immediate)
|
||||||
@ -964,6 +967,9 @@ public final class AvatarNode: ASDisplayNode {
|
|||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if let previousDisposable = self.loadingStatuses.copyItemsWithIndices().first(where: { $0.0 == index })?.1 {
|
||||||
|
previousDisposable.dispose()
|
||||||
|
}
|
||||||
self.loadingStatuses.remove(index)
|
self.loadingStatuses.remove(index)
|
||||||
if self.loadingStatuses.isEmpty {
|
if self.loadingStatuses.isEmpty {
|
||||||
self.updateStoryIndicator(transition: .immediate)
|
self.updateStoryIndicator(transition: .immediate)
|
||||||
|
|||||||
@ -207,6 +207,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
private var preloadStorySubscriptionsDisposable: Disposable?
|
private var preloadStorySubscriptionsDisposable: Disposable?
|
||||||
private var preloadStoryResourceDisposables: [MediaId: Disposable] = [:]
|
private var preloadStoryResourceDisposables: [MediaId: Disposable] = [:]
|
||||||
|
|
||||||
|
private var sharedOpenStoryProgressDisposable = MetaDisposable()
|
||||||
|
|
||||||
private var fullScreenEffectView: RippleEffectView?
|
private var fullScreenEffectView: RippleEffectView?
|
||||||
|
|
||||||
public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
|
public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
@ -778,6 +780,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
self.preloadStorySubscriptionsDisposable?.dispose()
|
self.preloadStorySubscriptionsDisposable?.dispose()
|
||||||
self.storyProgressDisposable?.dispose()
|
self.storyProgressDisposable?.dispose()
|
||||||
self.storiesPostingAvailabilityDisposable?.dispose()
|
self.storiesPostingAvailabilityDisposable?.dispose()
|
||||||
|
self.sharedOpenStoryProgressDisposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateNavigationMetadata() {
|
private func updateNavigationMetadata() {
|
||||||
@ -1362,7 +1365,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
case .archive:
|
case .archive:
|
||||||
StoryContainerScreen.openArchivedStories(context: self.context, parentController: self, avatarNode: itemNode.avatarNode)
|
StoryContainerScreen.openArchivedStories(context: self.context, parentController: self, avatarNode: itemNode.avatarNode)
|
||||||
case let .peer(peerId):
|
case let .peer(peerId):
|
||||||
StoryContainerScreen.openPeerStories(context: self.context, peerId: peerId, parentController: self, avatarNode: itemNode.avatarNode)
|
StoryContainerScreen.openPeerStories(context: self.context, peerId: peerId, parentController: self, avatarNode: itemNode.avatarNode, sharedProgressDisposable: self.sharedOpenStoryProgressDisposable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3672,6 +3675,64 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func openStoriesFromNotification(peerId: EnginePeer.Id, storyId: Int32) {
|
||||||
|
let presentationData = self.presentationData
|
||||||
|
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
|
||||||
|
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
||||||
|
self?.present(controller, in: .window(.root))
|
||||||
|
return ActionDisposable { [weak controller] in
|
||||||
|
Queue.mainQueue().async() {
|
||||||
|
controller?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> runOn(Queue.mainQueue())
|
||||||
|
|> delay(0.8, queue: Queue.mainQueue())
|
||||||
|
let progressDisposable = progressSignal.start()
|
||||||
|
|
||||||
|
let signal: Signal<Never, NoError> = self.context.engine.messages.peerStoriesAreReady(
|
||||||
|
id: peerId,
|
||||||
|
minId: storyId
|
||||||
|
)
|
||||||
|
|> filter { $0 }
|
||||||
|
|> deliverOnMainQueue
|
||||||
|
|> timeout(5.0, queue: .mainQueue(), alternate: .single(false))
|
||||||
|
|> take(1)
|
||||||
|
|> ignoreValues
|
||||||
|
|> afterDisposed {
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
progressDisposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.sharedOpenStoryProgressDisposable.set((signal |> deliverOnMainQueue).start(completed: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
StoryContainerScreen.openPeerStoriesCustom(
|
||||||
|
context: self.context,
|
||||||
|
peerId: peerId,
|
||||||
|
isHidden: false,
|
||||||
|
singlePeer: true,
|
||||||
|
parentController: self,
|
||||||
|
transitionIn: {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
transitionOut: { _ in
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
setFocusedItem: { _ in
|
||||||
|
},
|
||||||
|
setProgress: { [weak self] signal in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.sharedOpenStoryProgressDisposable.set(signal.start())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
public func openStories(peerId: EnginePeer.Id) {
|
public func openStories(peerId: EnginePeer.Id) {
|
||||||
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
|
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
|
||||||
if navigationBarView.storiesUnlocked {
|
if navigationBarView.storiesUnlocked {
|
||||||
|
|||||||
@ -167,13 +167,22 @@ class MessageStatsOverviewItemNode: ListViewItemNode {
|
|||||||
|
|
||||||
rightValueLabelLayoutAndApply = makeRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, item.stats.forwards - Int($0))))" } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
rightValueLabelLayoutAndApply = makeRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, item.stats.forwards - Int($0))))" } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
leftTitleLabelLayoutAndApply = makeLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_Views, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
var remainingWidth: CGFloat = params.width - leftInset - rightInset - sideInset * 2.0
|
||||||
|
let maxItemWidth: CGFloat = floor(remainingWidth / 2.8)
|
||||||
|
|
||||||
centerTitleLabelLayoutAndApply = makeCenterTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_PublicShares, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
leftTitleLabelLayoutAndApply = makeLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_Views, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
remainingWidth -= leftTitleLabelLayoutAndApply!.0.size.width - 4.0
|
||||||
|
|
||||||
rightTitleLabelLayoutAndApply = makeRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_PrivateShares, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
centerTitleLabelLayoutAndApply = makeCenterTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_PublicShares, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
remainingWidth -= centerTitleLabelLayoutAndApply!.0.size.width - 4.0
|
||||||
|
|
||||||
height += rightValueLabelLayoutAndApply!.0.size.height + rightTitleLabelLayoutAndApply!.0.size.height
|
rightTitleLabelLayoutAndApply = makeRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_PrivateShares, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
|
var maxLabelHeight = rightTitleLabelLayoutAndApply!.0.size.height
|
||||||
|
maxLabelHeight = max(maxLabelHeight, centerTitleLabelLayoutAndApply!.0.size.height)
|
||||||
|
maxLabelHeight = max(maxLabelHeight, leftTitleLabelLayoutAndApply!.0.size.height)
|
||||||
|
|
||||||
|
height += rightValueLabelLayoutAndApply!.0.size.height + maxLabelHeight
|
||||||
|
|
||||||
let contentSize = CGSize(width: params.width, height: height)
|
let contentSize = CGSize(width: params.width, height: height)
|
||||||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||||
@ -255,7 +264,7 @@ class MessageStatsOverviewItemNode: ListViewItemNode {
|
|||||||
let maxCenterWidth = max(centerValueLabelLayoutAndApply?.0.size.width ?? 0.0, centerTitleLabelLayoutAndApply?.0.size.width ?? 0.0)
|
let maxCenterWidth = max(centerValueLabelLayoutAndApply?.0.size.width ?? 0.0, centerTitleLabelLayoutAndApply?.0.size.width ?? 0.0)
|
||||||
let maxRightWidth = max(rightValueLabelLayoutAndApply?.0.size.width ?? 0.0, rightTitleLabelLayoutAndApply?.0.size.width ?? 0.0)
|
let maxRightWidth = max(rightValueLabelLayoutAndApply?.0.size.width ?? 0.0, rightTitleLabelLayoutAndApply?.0.size.width ?? 0.0)
|
||||||
|
|
||||||
let horizontalSpacing = min(60, (params.width - leftInset - rightInset - sideInset * 2.0 - maxLeftWidth - maxCenterWidth - maxRightWidth) / 2.0)
|
let horizontalSpacing = max(1.0, min(60, (params.width - leftInset - rightInset - sideInset * 2.0 - maxLeftWidth - maxCenterWidth - maxRightWidth) / 2.0))
|
||||||
|
|
||||||
var x: CGFloat = leftInset + (params.width - leftInset - rightInset - maxLeftWidth - maxCenterWidth - maxRightWidth - horizontalSpacing * 2.0) / 2.0
|
var x: CGFloat = leftInset + (params.width - leftInset - rightInset - maxLeftWidth - maxCenterWidth - maxRightWidth - horizontalSpacing * 2.0) / 2.0
|
||||||
if let leftValueLabelLayout = leftValueLabelLayoutAndApply?.0, let leftTitleLabelLayout = leftTitleLabelLayoutAndApply?.0 {
|
if let leftValueLabelLayout = leftValueLabelLayoutAndApply?.0, let leftTitleLabelLayout = leftTitleLabelLayoutAndApply?.0 {
|
||||||
|
|||||||
@ -451,6 +451,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[695856818] = { return Api.LangPackString.parse_langPackStringDeleted($0) }
|
dict[695856818] = { return Api.LangPackString.parse_langPackStringDeleted($0) }
|
||||||
dict[1816636575] = { return Api.LangPackString.parse_langPackStringPluralized($0) }
|
dict[1816636575] = { return Api.LangPackString.parse_langPackStringPluralized($0) }
|
||||||
dict[-1361650766] = { return Api.MaskCoords.parse_maskCoords($0) }
|
dict[-1361650766] = { return Api.MaskCoords.parse_maskCoords($0) }
|
||||||
|
dict[-1300094593] = { return Api.MediaArea.parse_inputMediaAreaVenue($0) }
|
||||||
|
dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) }
|
||||||
|
dict[64088654] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) }
|
||||||
dict[940666592] = { return Api.Message.parse_message($0) }
|
dict[940666592] = { return Api.Message.parse_message($0) }
|
||||||
dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) }
|
dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) }
|
||||||
dict[721967202] = { return Api.Message.parse_messageService($0) }
|
dict[721967202] = { return Api.Message.parse_messageService($0) }
|
||||||
@ -797,7 +800,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[1445635639] = { return Api.StoryItem.parse_storyItem($0) }
|
dict[1445635639] = { return Api.StoryItem.parse_storyItem($0) }
|
||||||
dict[1374088783] = { return Api.StoryItem.parse_storyItemDeleted($0) }
|
dict[1374088783] = { return Api.StoryItem.parse_storyItemDeleted($0) }
|
||||||
dict[-5388013] = { return Api.StoryItem.parse_storyItemSkipped($0) }
|
dict[-5388013] = { return Api.StoryItem.parse_storyItemSkipped($0) }
|
||||||
dict[-1491424062] = { return Api.StoryView.parse_storyView($0) }
|
dict[-793729058] = { return Api.StoryView.parse_storyView($0) }
|
||||||
dict[-748199729] = { return Api.StoryViews.parse_storyViews($0) }
|
dict[-748199729] = { return Api.StoryViews.parse_storyViews($0) }
|
||||||
dict[1964978502] = { return Api.TextWithEntities.parse_textWithEntities($0) }
|
dict[1964978502] = { return Api.TextWithEntities.parse_textWithEntities($0) }
|
||||||
dict[-1609668650] = { return Api.Theme.parse_theme($0) }
|
dict[-1609668650] = { return Api.Theme.parse_theme($0) }
|
||||||
@ -884,7 +887,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[967122427] = { return Api.Update.parse_updateNewScheduledMessage($0) }
|
dict[967122427] = { return Api.Update.parse_updateNewScheduledMessage($0) }
|
||||||
dict[1753886890] = { return Api.Update.parse_updateNewStickerSet($0) }
|
dict[1753886890] = { return Api.Update.parse_updateNewStickerSet($0) }
|
||||||
dict[-1094555409] = { return Api.Update.parse_updateNotifySettings($0) }
|
dict[-1094555409] = { return Api.Update.parse_updateNotifySettings($0) }
|
||||||
dict[610945826] = { return Api.Update.parse_updatePeerBlocked($0) }
|
dict[-337610926] = { return Api.Update.parse_updatePeerBlocked($0) }
|
||||||
dict[-1147422299] = { return Api.Update.parse_updatePeerHistoryTTL($0) }
|
dict[-1147422299] = { return Api.Update.parse_updatePeerHistoryTTL($0) }
|
||||||
dict[-1263546448] = { return Api.Update.parse_updatePeerLocated($0) }
|
dict[-1263546448] = { return Api.Update.parse_updatePeerLocated($0) }
|
||||||
dict[1786671974] = { return Api.Update.parse_updatePeerSettings($0) }
|
dict[1786671974] = { return Api.Update.parse_updatePeerSettings($0) }
|
||||||
@ -914,7 +917,6 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[-337352679] = { return Api.Update.parse_updateServiceNotification($0) }
|
dict[-337352679] = { return Api.Update.parse_updateServiceNotification($0) }
|
||||||
dict[834816008] = { return Api.Update.parse_updateStickerSets($0) }
|
dict[834816008] = { return Api.Update.parse_updateStickerSets($0) }
|
||||||
dict[196268545] = { return Api.Update.parse_updateStickerSetsOrder($0) }
|
dict[196268545] = { return Api.Update.parse_updateStickerSetsOrder($0) }
|
||||||
dict[-719158423] = { return Api.Update.parse_updateStoriesStealth($0) }
|
|
||||||
dict[738741697] = { return Api.Update.parse_updateStoriesStealthMode($0) }
|
dict[738741697] = { return Api.Update.parse_updateStoriesStealthMode($0) }
|
||||||
dict[542785843] = { return Api.Update.parse_updateStory($0) }
|
dict[542785843] = { return Api.Update.parse_updateStory($0) }
|
||||||
dict[468923833] = { return Api.Update.parse_updateStoryID($0) }
|
dict[468923833] = { return Api.Update.parse_updateStoryID($0) }
|
||||||
@ -1542,6 +1544,10 @@ public extension Api {
|
|||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.MaskCoords:
|
case let _1 as Api.MaskCoords:
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
|
case let _1 as Api.MediaArea:
|
||||||
|
_1.serialize(buffer, boxed)
|
||||||
|
case let _1 as Api.MediaAreaCoordinates:
|
||||||
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.Message:
|
case let _1 as Api.Message:
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.MessageAction:
|
case let _1 as Api.MessageAction:
|
||||||
|
|||||||
@ -46,6 +46,152 @@ public extension Api {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public extension Api {
|
||||||
|
enum MediaArea: TypeConstructorDescription {
|
||||||
|
case inputMediaAreaVenue(coordinates: Api.MediaAreaCoordinates, queryId: Int64, resultId: String)
|
||||||
|
case mediaAreaVenue(coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String)
|
||||||
|
|
||||||
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
|
switch self {
|
||||||
|
case .inputMediaAreaVenue(let coordinates, let queryId, let resultId):
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(-1300094593)
|
||||||
|
}
|
||||||
|
coordinates.serialize(buffer, true)
|
||||||
|
serializeInt64(queryId, buffer: buffer, boxed: false)
|
||||||
|
serializeString(resultId, buffer: buffer, boxed: false)
|
||||||
|
break
|
||||||
|
case .mediaAreaVenue(let coordinates, let geo, let title, let address, let provider, let venueId, let venueType):
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(-1098720356)
|
||||||
|
}
|
||||||
|
coordinates.serialize(buffer, true)
|
||||||
|
geo.serialize(buffer, true)
|
||||||
|
serializeString(title, buffer: buffer, boxed: false)
|
||||||
|
serializeString(address, buffer: buffer, boxed: false)
|
||||||
|
serializeString(provider, buffer: buffer, boxed: false)
|
||||||
|
serializeString(venueId, buffer: buffer, boxed: false)
|
||||||
|
serializeString(venueType, buffer: buffer, boxed: false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||||
|
switch self {
|
||||||
|
case .inputMediaAreaVenue(let coordinates, let queryId, let resultId):
|
||||||
|
return ("inputMediaAreaVenue", [("coordinates", coordinates as Any), ("queryId", queryId as Any), ("resultId", resultId as Any)])
|
||||||
|
case .mediaAreaVenue(let coordinates, let geo, let title, let address, let provider, let venueId, let venueType):
|
||||||
|
return ("mediaAreaVenue", [("coordinates", coordinates as Any), ("geo", geo as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func parse_inputMediaAreaVenue(_ reader: BufferReader) -> MediaArea? {
|
||||||
|
var _1: Api.MediaAreaCoordinates?
|
||||||
|
if let signature = reader.readInt32() {
|
||||||
|
_1 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates
|
||||||
|
}
|
||||||
|
var _2: Int64?
|
||||||
|
_2 = reader.readInt64()
|
||||||
|
var _3: String?
|
||||||
|
_3 = parseString(reader)
|
||||||
|
let _c1 = _1 != nil
|
||||||
|
let _c2 = _2 != nil
|
||||||
|
let _c3 = _3 != nil
|
||||||
|
if _c1 && _c2 && _c3 {
|
||||||
|
return Api.MediaArea.inputMediaAreaVenue(coordinates: _1!, queryId: _2!, resultId: _3!)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static func parse_mediaAreaVenue(_ reader: BufferReader) -> MediaArea? {
|
||||||
|
var _1: Api.MediaAreaCoordinates?
|
||||||
|
if let signature = reader.readInt32() {
|
||||||
|
_1 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates
|
||||||
|
}
|
||||||
|
var _2: Api.GeoPoint?
|
||||||
|
if let signature = reader.readInt32() {
|
||||||
|
_2 = Api.parse(reader, signature: signature) as? Api.GeoPoint
|
||||||
|
}
|
||||||
|
var _3: String?
|
||||||
|
_3 = parseString(reader)
|
||||||
|
var _4: String?
|
||||||
|
_4 = parseString(reader)
|
||||||
|
var _5: String?
|
||||||
|
_5 = parseString(reader)
|
||||||
|
var _6: String?
|
||||||
|
_6 = parseString(reader)
|
||||||
|
var _7: String?
|
||||||
|
_7 = parseString(reader)
|
||||||
|
let _c1 = _1 != nil
|
||||||
|
let _c2 = _2 != nil
|
||||||
|
let _c3 = _3 != nil
|
||||||
|
let _c4 = _4 != nil
|
||||||
|
let _c5 = _5 != nil
|
||||||
|
let _c6 = _6 != nil
|
||||||
|
let _c7 = _7 != nil
|
||||||
|
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
|
||||||
|
return Api.MediaArea.mediaAreaVenue(coordinates: _1!, geo: _2!, title: _3!, address: _4!, provider: _5!, venueId: _6!, venueType: _7!)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public extension Api {
|
||||||
|
enum MediaAreaCoordinates: TypeConstructorDescription {
|
||||||
|
case mediaAreaCoordinates(x: Double, y: Double, w: Double, h: Double, rotation: Double)
|
||||||
|
|
||||||
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
|
switch self {
|
||||||
|
case .mediaAreaCoordinates(let x, let y, let w, let h, let rotation):
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(64088654)
|
||||||
|
}
|
||||||
|
serializeDouble(x, buffer: buffer, boxed: false)
|
||||||
|
serializeDouble(y, buffer: buffer, boxed: false)
|
||||||
|
serializeDouble(w, buffer: buffer, boxed: false)
|
||||||
|
serializeDouble(h, buffer: buffer, boxed: false)
|
||||||
|
serializeDouble(rotation, buffer: buffer, boxed: false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||||
|
switch self {
|
||||||
|
case .mediaAreaCoordinates(let x, let y, let w, let h, let rotation):
|
||||||
|
return ("mediaAreaCoordinates", [("x", x as Any), ("y", y as Any), ("w", w as Any), ("h", h as Any), ("rotation", rotation as Any)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func parse_mediaAreaCoordinates(_ reader: BufferReader) -> MediaAreaCoordinates? {
|
||||||
|
var _1: Double?
|
||||||
|
_1 = reader.readDouble()
|
||||||
|
var _2: Double?
|
||||||
|
_2 = reader.readDouble()
|
||||||
|
var _3: Double?
|
||||||
|
_3 = reader.readDouble()
|
||||||
|
var _4: Double?
|
||||||
|
_4 = reader.readDouble()
|
||||||
|
var _5: Double?
|
||||||
|
_5 = reader.readDouble()
|
||||||
|
let _c1 = _1 != nil
|
||||||
|
let _c2 = _2 != nil
|
||||||
|
let _c3 = _3 != nil
|
||||||
|
let _c4 = _4 != nil
|
||||||
|
let _c5 = _5 != nil
|
||||||
|
if _c1 && _c2 && _c3 && _c4 && _c5 {
|
||||||
|
return Api.MediaAreaCoordinates.mediaAreaCoordinates(x: _1!, y: _2!, w: _3!, h: _4!, rotation: _5!)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
public extension Api {
|
public extension Api {
|
||||||
indirect enum Message: TypeConstructorDescription {
|
indirect enum Message: TypeConstructorDescription {
|
||||||
case message(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?)
|
case message(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?)
|
||||||
|
|||||||
@ -544,14 +544,15 @@ public extension Api {
|
|||||||
}
|
}
|
||||||
public extension Api {
|
public extension Api {
|
||||||
enum StoryView: TypeConstructorDescription {
|
enum StoryView: TypeConstructorDescription {
|
||||||
case storyView(userId: Int64, date: Int32)
|
case storyView(flags: Int32, userId: Int64, date: Int32)
|
||||||
|
|
||||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
switch self {
|
switch self {
|
||||||
case .storyView(let userId, let date):
|
case .storyView(let flags, let userId, let date):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(-1491424062)
|
buffer.appendInt32(-793729058)
|
||||||
}
|
}
|
||||||
|
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||||
serializeInt64(userId, buffer: buffer, boxed: false)
|
serializeInt64(userId, buffer: buffer, boxed: false)
|
||||||
serializeInt32(date, buffer: buffer, boxed: false)
|
serializeInt32(date, buffer: buffer, boxed: false)
|
||||||
break
|
break
|
||||||
@ -560,20 +561,23 @@ public extension Api {
|
|||||||
|
|
||||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||||
switch self {
|
switch self {
|
||||||
case .storyView(let userId, let date):
|
case .storyView(let flags, let userId, let date):
|
||||||
return ("storyView", [("userId", userId as Any), ("date", date as Any)])
|
return ("storyView", [("flags", flags as Any), ("userId", userId as Any), ("date", date as Any)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func parse_storyView(_ reader: BufferReader) -> StoryView? {
|
public static func parse_storyView(_ reader: BufferReader) -> StoryView? {
|
||||||
var _1: Int64?
|
var _1: Int32?
|
||||||
_1 = reader.readInt64()
|
_1 = reader.readInt32()
|
||||||
var _2: Int32?
|
var _2: Int64?
|
||||||
_2 = reader.readInt32()
|
_2 = reader.readInt64()
|
||||||
|
var _3: Int32?
|
||||||
|
_3 = reader.readInt32()
|
||||||
let _c1 = _1 != nil
|
let _c1 = _1 != nil
|
||||||
let _c2 = _2 != nil
|
let _c2 = _2 != nil
|
||||||
if _c1 && _c2 {
|
let _c3 = _3 != nil
|
||||||
return Api.StoryView.storyView(userId: _1!, date: _2!)
|
if _c1 && _c2 && _c3 {
|
||||||
|
return Api.StoryView.storyView(flags: _1!, userId: _2!, date: _3!)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return nil
|
return nil
|
||||||
@ -1100,7 +1104,7 @@ public extension Api {
|
|||||||
case updateNewScheduledMessage(message: Api.Message)
|
case updateNewScheduledMessage(message: Api.Message)
|
||||||
case updateNewStickerSet(stickerset: Api.messages.StickerSet)
|
case updateNewStickerSet(stickerset: Api.messages.StickerSet)
|
||||||
case updateNotifySettings(peer: Api.NotifyPeer, notifySettings: Api.PeerNotifySettings)
|
case updateNotifySettings(peer: Api.NotifyPeer, notifySettings: Api.PeerNotifySettings)
|
||||||
case updatePeerBlocked(peerId: Api.Peer, blocked: Api.Bool)
|
case updatePeerBlocked(flags: Int32, peerId: Api.Peer)
|
||||||
case updatePeerHistoryTTL(flags: Int32, peer: Api.Peer, ttlPeriod: Int32?)
|
case updatePeerHistoryTTL(flags: Int32, peer: Api.Peer, ttlPeriod: Int32?)
|
||||||
case updatePeerLocated(peers: [Api.PeerLocated])
|
case updatePeerLocated(peers: [Api.PeerLocated])
|
||||||
case updatePeerSettings(peer: Api.Peer, settings: Api.PeerSettings)
|
case updatePeerSettings(peer: Api.Peer, settings: Api.PeerSettings)
|
||||||
@ -1130,7 +1134,6 @@ public extension Api {
|
|||||||
case updateServiceNotification(flags: Int32, inboxDate: Int32?, type: String, message: String, media: Api.MessageMedia, entities: [Api.MessageEntity])
|
case updateServiceNotification(flags: Int32, inboxDate: Int32?, type: String, message: String, media: Api.MessageMedia, entities: [Api.MessageEntity])
|
||||||
case updateStickerSets(flags: Int32)
|
case updateStickerSets(flags: Int32)
|
||||||
case updateStickerSetsOrder(flags: Int32, order: [Int64])
|
case updateStickerSetsOrder(flags: Int32, order: [Int64])
|
||||||
case updateStoriesStealth(expireDate: Int32)
|
|
||||||
case updateStoriesStealthMode(stealthMode: Api.StoriesStealthMode)
|
case updateStoriesStealthMode(stealthMode: Api.StoriesStealthMode)
|
||||||
case updateStory(userId: Int64, story: Api.StoryItem)
|
case updateStory(userId: Int64, story: Api.StoryItem)
|
||||||
case updateStoryID(id: Int32, randomId: Int64)
|
case updateStoryID(id: Int32, randomId: Int64)
|
||||||
@ -1768,12 +1771,12 @@ public extension Api {
|
|||||||
peer.serialize(buffer, true)
|
peer.serialize(buffer, true)
|
||||||
notifySettings.serialize(buffer, true)
|
notifySettings.serialize(buffer, true)
|
||||||
break
|
break
|
||||||
case .updatePeerBlocked(let peerId, let blocked):
|
case .updatePeerBlocked(let flags, let peerId):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(610945826)
|
buffer.appendInt32(-337610926)
|
||||||
}
|
}
|
||||||
|
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||||
peerId.serialize(buffer, true)
|
peerId.serialize(buffer, true)
|
||||||
blocked.serialize(buffer, true)
|
|
||||||
break
|
break
|
||||||
case .updatePeerHistoryTTL(let flags, let peer, let ttlPeriod):
|
case .updatePeerHistoryTTL(let flags, let peer, let ttlPeriod):
|
||||||
if boxed {
|
if boxed {
|
||||||
@ -2033,12 +2036,6 @@ public extension Api {
|
|||||||
serializeInt64(item, buffer: buffer, boxed: false)
|
serializeInt64(item, buffer: buffer, boxed: false)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case .updateStoriesStealth(let expireDate):
|
|
||||||
if boxed {
|
|
||||||
buffer.appendInt32(-719158423)
|
|
||||||
}
|
|
||||||
serializeInt32(expireDate, buffer: buffer, boxed: false)
|
|
||||||
break
|
|
||||||
case .updateStoriesStealthMode(let stealthMode):
|
case .updateStoriesStealthMode(let stealthMode):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(738741697)
|
buffer.appendInt32(738741697)
|
||||||
@ -2285,8 +2282,8 @@ public extension Api {
|
|||||||
return ("updateNewStickerSet", [("stickerset", stickerset as Any)])
|
return ("updateNewStickerSet", [("stickerset", stickerset as Any)])
|
||||||
case .updateNotifySettings(let peer, let notifySettings):
|
case .updateNotifySettings(let peer, let notifySettings):
|
||||||
return ("updateNotifySettings", [("peer", peer as Any), ("notifySettings", notifySettings as Any)])
|
return ("updateNotifySettings", [("peer", peer as Any), ("notifySettings", notifySettings as Any)])
|
||||||
case .updatePeerBlocked(let peerId, let blocked):
|
case .updatePeerBlocked(let flags, let peerId):
|
||||||
return ("updatePeerBlocked", [("peerId", peerId as Any), ("blocked", blocked as Any)])
|
return ("updatePeerBlocked", [("flags", flags as Any), ("peerId", peerId as Any)])
|
||||||
case .updatePeerHistoryTTL(let flags, let peer, let ttlPeriod):
|
case .updatePeerHistoryTTL(let flags, let peer, let ttlPeriod):
|
||||||
return ("updatePeerHistoryTTL", [("flags", flags as Any), ("peer", peer as Any), ("ttlPeriod", ttlPeriod as Any)])
|
return ("updatePeerHistoryTTL", [("flags", flags as Any), ("peer", peer as Any), ("ttlPeriod", ttlPeriod as Any)])
|
||||||
case .updatePeerLocated(let peers):
|
case .updatePeerLocated(let peers):
|
||||||
@ -2345,8 +2342,6 @@ public extension Api {
|
|||||||
return ("updateStickerSets", [("flags", flags as Any)])
|
return ("updateStickerSets", [("flags", flags as Any)])
|
||||||
case .updateStickerSetsOrder(let flags, let order):
|
case .updateStickerSetsOrder(let flags, let order):
|
||||||
return ("updateStickerSetsOrder", [("flags", flags as Any), ("order", order as Any)])
|
return ("updateStickerSetsOrder", [("flags", flags as Any), ("order", order as Any)])
|
||||||
case .updateStoriesStealth(let expireDate):
|
|
||||||
return ("updateStoriesStealth", [("expireDate", expireDate as Any)])
|
|
||||||
case .updateStoriesStealthMode(let stealthMode):
|
case .updateStoriesStealthMode(let stealthMode):
|
||||||
return ("updateStoriesStealthMode", [("stealthMode", stealthMode as Any)])
|
return ("updateStoriesStealthMode", [("stealthMode", stealthMode as Any)])
|
||||||
case .updateStory(let userId, let story):
|
case .updateStory(let userId, let story):
|
||||||
@ -3686,18 +3681,16 @@ public extension Api {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static func parse_updatePeerBlocked(_ reader: BufferReader) -> Update? {
|
public static func parse_updatePeerBlocked(_ reader: BufferReader) -> Update? {
|
||||||
var _1: Api.Peer?
|
var _1: Int32?
|
||||||
|
_1 = reader.readInt32()
|
||||||
|
var _2: Api.Peer?
|
||||||
if let signature = reader.readInt32() {
|
if let signature = reader.readInt32() {
|
||||||
_1 = Api.parse(reader, signature: signature) as? Api.Peer
|
_2 = Api.parse(reader, signature: signature) as? Api.Peer
|
||||||
}
|
|
||||||
var _2: Api.Bool?
|
|
||||||
if let signature = reader.readInt32() {
|
|
||||||
_2 = Api.parse(reader, signature: signature) as? Api.Bool
|
|
||||||
}
|
}
|
||||||
let _c1 = _1 != nil
|
let _c1 = _1 != nil
|
||||||
let _c2 = _2 != nil
|
let _c2 = _2 != nil
|
||||||
if _c1 && _c2 {
|
if _c1 && _c2 {
|
||||||
return Api.Update.updatePeerBlocked(peerId: _1!, blocked: _2!)
|
return Api.Update.updatePeerBlocked(flags: _1!, peerId: _2!)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return nil
|
return nil
|
||||||
@ -4140,17 +4133,6 @@ public extension Api {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static func parse_updateStoriesStealth(_ reader: BufferReader) -> Update? {
|
|
||||||
var _1: Int32?
|
|
||||||
_1 = reader.readInt32()
|
|
||||||
let _c1 = _1 != nil
|
|
||||||
if _c1 {
|
|
||||||
return Api.Update.updateStoriesStealth(expireDate: _1!)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public static func parse_updateStoriesStealthMode(_ reader: BufferReader) -> Update? {
|
public static func parse_updateStoriesStealthMode(_ reader: BufferReader) -> Update? {
|
||||||
var _1: Api.StoriesStealthMode?
|
var _1: Api.StoriesStealthMode?
|
||||||
if let signature = reader.readInt32() {
|
if let signature = reader.readInt32() {
|
||||||
|
|||||||
@ -3218,11 +3218,12 @@ public extension Api.functions.contacts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
public extension Api.functions.contacts {
|
public extension Api.functions.contacts {
|
||||||
static func block(id: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
static func block(flags: Int32, id: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||||
let buffer = Buffer()
|
let buffer = Buffer()
|
||||||
buffer.appendInt32(1758204945)
|
buffer.appendInt32(774801204)
|
||||||
|
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||||
id.serialize(buffer, true)
|
id.serialize(buffer, true)
|
||||||
return (FunctionDescription(name: "contacts.block", parameters: [("id", String(describing: id))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
return (FunctionDescription(name: "contacts.block", parameters: [("flags", String(describing: flags)), ("id", String(describing: id))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||||
let reader = BufferReader(buffer)
|
let reader = BufferReader(buffer)
|
||||||
var result: Api.Bool?
|
var result: Api.Bool?
|
||||||
if let signature = reader.readInt32() {
|
if let signature = reader.readInt32() {
|
||||||
@ -3321,12 +3322,13 @@ public extension Api.functions.contacts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
public extension Api.functions.contacts {
|
public extension Api.functions.contacts {
|
||||||
static func getBlocked(offset: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.contacts.Blocked>) {
|
static func getBlocked(flags: Int32, offset: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.contacts.Blocked>) {
|
||||||
let buffer = Buffer()
|
let buffer = Buffer()
|
||||||
buffer.appendInt32(-176409329)
|
buffer.appendInt32(-1702457472)
|
||||||
|
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||||
serializeInt32(offset, buffer: buffer, boxed: false)
|
serializeInt32(offset, buffer: buffer, boxed: false)
|
||||||
serializeInt32(limit, buffer: buffer, boxed: false)
|
serializeInt32(limit, buffer: buffer, boxed: false)
|
||||||
return (FunctionDescription(name: "contacts.getBlocked", parameters: [("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.contacts.Blocked? in
|
return (FunctionDescription(name: "contacts.getBlocked", parameters: [("flags", String(describing: flags)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.contacts.Blocked? in
|
||||||
let reader = BufferReader(buffer)
|
let reader = BufferReader(buffer)
|
||||||
var result: Api.contacts.Blocked?
|
var result: Api.contacts.Blocked?
|
||||||
if let signature = reader.readInt32() {
|
if let signature = reader.readInt32() {
|
||||||
@ -3542,6 +3544,27 @@ public extension Api.functions.contacts {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public extension Api.functions.contacts {
|
||||||
|
static func setBlocked(flags: Int32, id: [Api.InputPeer], limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||||
|
let buffer = Buffer()
|
||||||
|
buffer.appendInt32(-1798939530)
|
||||||
|
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||||
|
buffer.appendInt32(481674261)
|
||||||
|
buffer.appendInt32(Int32(id.count))
|
||||||
|
for item in id {
|
||||||
|
item.serialize(buffer, true)
|
||||||
|
}
|
||||||
|
serializeInt32(limit, buffer: buffer, boxed: false)
|
||||||
|
return (FunctionDescription(name: "contacts.setBlocked", parameters: [("flags", String(describing: flags)), ("id", String(describing: id)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||||
|
let reader = BufferReader(buffer)
|
||||||
|
var result: Api.Bool?
|
||||||
|
if let signature = reader.readInt32() {
|
||||||
|
result = Api.parse(reader, signature: signature) as? Api.Bool
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
public extension Api.functions.contacts {
|
public extension Api.functions.contacts {
|
||||||
static func toggleStoriesHidden(id: Api.InputUser, hidden: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
static func toggleStoriesHidden(id: Api.InputUser, hidden: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||||
let buffer = Buffer()
|
let buffer = Buffer()
|
||||||
@ -3574,11 +3597,12 @@ public extension Api.functions.contacts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
public extension Api.functions.contacts {
|
public extension Api.functions.contacts {
|
||||||
static func unblock(id: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
static func unblock(flags: Int32, id: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||||
let buffer = Buffer()
|
let buffer = Buffer()
|
||||||
buffer.appendInt32(-1096393392)
|
buffer.appendInt32(-1252994264)
|
||||||
|
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||||
id.serialize(buffer, true)
|
id.serialize(buffer, true)
|
||||||
return (FunctionDescription(name: "contacts.unblock", parameters: [("id", String(describing: id))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
return (FunctionDescription(name: "contacts.unblock", parameters: [("flags", String(describing: flags)), ("id", String(describing: id))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||||
let reader = BufferReader(buffer)
|
let reader = BufferReader(buffer)
|
||||||
var result: Api.Bool?
|
var result: Api.Bool?
|
||||||
if let signature = reader.readInt32() {
|
if let signature = reader.readInt32() {
|
||||||
|
|||||||
@ -295,7 +295,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
|
|||||||
|> retryRequest
|
|> retryRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
func request<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), expectedResponseSize: Int32? = nil) -> Signal<T, MTRpcError> {
|
func request<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), expectedResponseSize: Int32? = nil, automaticFloodWait: Bool = true) -> Signal<T, MTRpcError> {
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
let request = MTRequest()
|
let request = MTRequest()
|
||||||
request.expectedResponseSize = expectedResponseSize ?? 0
|
request.expectedResponseSize = expectedResponseSize ?? 0
|
||||||
@ -311,6 +311,12 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
|
|||||||
request.needsTimeoutTimer = self.useRequestTimeoutTimers
|
request.needsTimeoutTimer = self.useRequestTimeoutTimers
|
||||||
|
|
||||||
request.shouldContinueExecutionWithErrorContext = { errorContext in
|
request.shouldContinueExecutionWithErrorContext = { errorContext in
|
||||||
|
guard let errorContext = errorContext else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if errorContext.floodWaitSeconds > 0 && !automaticFloodWait {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1337,9 +1337,9 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
|
|||||||
}
|
}
|
||||||
var userFlags = previous.flags
|
var userFlags = previous.flags
|
||||||
if (flags & (1 << 1)) != 0 {
|
if (flags & (1 << 1)) != 0 {
|
||||||
userFlags.insert(.isBlockedFromMyStories)
|
userFlags.insert(.isBlockedFromStories)
|
||||||
} else {
|
} else {
|
||||||
userFlags.remove(.isBlockedFromMyStories)
|
userFlags.remove(.isBlockedFromStories)
|
||||||
}
|
}
|
||||||
return previous.withUpdatedIsBlocked((flags & (1 << 0)) != 0).withUpdatedFlags(userFlags)
|
return previous.withUpdatedIsBlocked((flags & (1 << 0)) != 0).withUpdatedFlags(userFlags)
|
||||||
})
|
})
|
||||||
@ -1679,8 +1679,6 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
|
|||||||
updatedState.readStories(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), maxId: id)
|
updatedState.readStories(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), maxId: id)
|
||||||
case let .updateStoriesStealthMode(stealthMode):
|
case let .updateStoriesStealthMode(stealthMode):
|
||||||
updatedState.updateStoryStealthMode(stealthMode)
|
updatedState.updateStoryStealthMode(stealthMode)
|
||||||
case let .updateStoriesStealth(expireDate):
|
|
||||||
updatedState.updateStoryStealth(expireDate: expireDate)
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -2106,25 +2104,29 @@ func resolveStories<T>(postbox: Postbox, source: FetchMessageHistoryHoleSource,
|
|||||||
while idOffset < allIds.count {
|
while idOffset < allIds.count {
|
||||||
let bucketLength = min(100, allIds.count - idOffset)
|
let bucketLength = min(100, allIds.count - idOffset)
|
||||||
let ids = Array(allIds[idOffset ..< (idOffset + bucketLength)])
|
let ids = Array(allIds[idOffset ..< (idOffset + bucketLength)])
|
||||||
signals.append(_internal_getStoriesById(accountPeerId: accountPeerId, postbox: postbox, source: source, peerId: peerId, peerReference: additionalPeers.get(peerId).flatMap(PeerReference.init), ids: ids)
|
signals.append(_internal_getStoriesById(accountPeerId: accountPeerId, postbox: postbox, source: source, peerId: peerId, peerReference: additionalPeers.get(peerId).flatMap(PeerReference.init), ids: ids, allowFloodWait: false)
|
||||||
|> mapToSignal { result -> Signal<Never, NoError> in
|
|> mapToSignal { result -> Signal<Never, NoError> in
|
||||||
return postbox.transaction { transaction -> Void in
|
if let result = result {
|
||||||
for id in ids {
|
return postbox.transaction { transaction -> Void in
|
||||||
let current = transaction.getStory(id: StoryId(peerId: peerId, id: id))
|
for id in ids {
|
||||||
var updated: CodableEntry?
|
let current = transaction.getStory(id: StoryId(peerId: peerId, id: id))
|
||||||
if let updatedItem = result.first(where: { $0.id == id }) {
|
var updated: CodableEntry?
|
||||||
if let entry = CodableEntry(updatedItem) {
|
if let updatedItem = result.first(where: { $0.id == id }) {
|
||||||
updated = entry
|
if let entry = CodableEntry(updatedItem) {
|
||||||
|
updated = entry
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updated = CodableEntry(data: Data())
|
||||||
|
}
|
||||||
|
if current != updated {
|
||||||
|
transaction.setStory(id: StoryId(peerId: peerId, id: id), value: updated ?? CodableEntry(data: Data()))
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
updated = CodableEntry(data: Data())
|
|
||||||
}
|
|
||||||
if current != updated {
|
|
||||||
transaction.setStory(id: StoryId(peerId: peerId, id: id), value: updated ?? CodableEntry(data: Data()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|> ignoreValues
|
||||||
|
} else {
|
||||||
|
return .complete()
|
||||||
}
|
}
|
||||||
|> ignoreValues
|
|
||||||
})
|
})
|
||||||
idOffset += bucketLength
|
idOffset += bucketLength
|
||||||
}
|
}
|
||||||
|
|||||||
@ -108,12 +108,12 @@ enum FetchMessageHistoryHoleSource {
|
|||||||
case network(Network)
|
case network(Network)
|
||||||
case download(Download)
|
case download(Download)
|
||||||
|
|
||||||
func request<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>)) -> Signal<T, MTRpcError> {
|
func request<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), automaticFloodWait: Bool = true) -> Signal<T, MTRpcError> {
|
||||||
switch self {
|
switch self {
|
||||||
case let .network(network):
|
case let .network(network):
|
||||||
return network.request(data)
|
return network.request(data, automaticFloodWait: automaticFloodWait)
|
||||||
case let .download(download):
|
case let .download(download):
|
||||||
return download.request(data)
|
return download.request(data, automaticFloodWait: automaticFloodWait)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -149,7 +149,7 @@ public struct CachedUserFlags: OptionSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static let translationHidden = CachedUserFlags(rawValue: 1 << 0)
|
public static let translationHidden = CachedUserFlags(rawValue: 1 << 0)
|
||||||
public static let isBlockedFromMyStories = CachedUserFlags(rawValue: 1 << 1)
|
public static let isBlockedFromStories = CachedUserFlags(rawValue: 1 << 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class EditableBotInfo: PostboxCoding, Equatable {
|
public final class EditableBotInfo: PostboxCoding, Equatable {
|
||||||
|
|||||||
@ -107,6 +107,7 @@ public struct Namespaces {
|
|||||||
public static let emojiSearchCategories: Int8 = 25
|
public static let emojiSearchCategories: Int8 = 25
|
||||||
public static let cachedEmojiQueryResults: Int8 = 26
|
public static let cachedEmojiQueryResults: Int8 = 26
|
||||||
public static let cachedPeerStoryListHeads: Int8 = 27
|
public static let cachedPeerStoryListHeads: Int8 = 27
|
||||||
|
public static let displayedStoryNotifications: Int8 = 28
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct UnorderedItemList {
|
public struct UnorderedItemList {
|
||||||
|
|||||||
@ -35,8 +35,8 @@ func _internal_addSynchronizeViewStoriesOperation(peerId: PeerId, storyId: Int32
|
|||||||
if let (topOperation, topLocalIndex) = topOperation {
|
if let (topOperation, topLocalIndex) = topOperation {
|
||||||
if topOperation.storyId < storyId {
|
if topOperation.storyId < storyId {
|
||||||
let _ = transaction.operationLogRemoveEntry(peerId: peerId, tag: tag, tagLocalIndex: topLocalIndex)
|
let _ = transaction.operationLogRemoveEntry(peerId: peerId, tag: tag, tagLocalIndex: topLocalIndex)
|
||||||
|
replace = true
|
||||||
}
|
}
|
||||||
replace = true
|
|
||||||
} else {
|
} else {
|
||||||
replace = true
|
replace = true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -915,7 +915,7 @@ public extension TelegramEngine.EngineData.Item {
|
|||||||
preconditionFailure()
|
preconditionFailure()
|
||||||
}
|
}
|
||||||
if let cachedData = view.cachedPeerData as? CachedUserData {
|
if let cachedData = view.cachedPeerData as? CachedUserData {
|
||||||
return .known(cachedData.flags.contains(.isBlockedFromMyStories))
|
return .known(cachedData.flags.contains(.isBlockedFromStories))
|
||||||
} else {
|
} else {
|
||||||
return .unknown
|
return .unknown
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1435,25 +1435,25 @@ func _internal_getStoriesById(accountPeerId: PeerId, postbox: Postbox, network:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func _internal_getStoriesById(accountPeerId: PeerId, postbox: Postbox, source: FetchMessageHistoryHoleSource, peerId: PeerId, peerReference: PeerReference?, ids: [Int32]) -> Signal<[Stories.StoredItem], NoError> {
|
func _internal_getStoriesById(accountPeerId: PeerId, postbox: Postbox, source: FetchMessageHistoryHoleSource, peerId: PeerId, peerReference: PeerReference?, ids: [Int32], allowFloodWait: Bool) -> Signal<[Stories.StoredItem]?, NoError> {
|
||||||
return postbox.transaction { transaction -> Api.InputUser? in
|
return postbox.transaction { transaction -> Api.InputUser? in
|
||||||
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||||
}
|
}
|
||||||
|> mapToSignal { inputUser -> Signal<[Stories.StoredItem], NoError> in
|
|> mapToSignal { inputUser -> Signal<[Stories.StoredItem]?, NoError> in
|
||||||
guard let inputUser = inputUser ?? peerReference?.inputUser else {
|
guard let inputUser = inputUser ?? peerReference?.inputUser else {
|
||||||
return .single([])
|
return .single([])
|
||||||
}
|
}
|
||||||
|
|
||||||
return source.request(Api.functions.stories.getStoriesByID(userId: inputUser, id: ids))
|
return source.request(Api.functions.stories.getStoriesByID(userId: inputUser, id: ids), automaticFloodWait: allowFloodWait)
|
||||||
|> map(Optional.init)
|
|> map(Optional.init)
|
||||||
|> `catch` { _ -> Signal<Api.stories.Stories?, NoError> in
|
|> `catch` { _ -> Signal<Api.stories.Stories?, NoError> in
|
||||||
return .single(nil)
|
return .single(nil)
|
||||||
}
|
}
|
||||||
|> mapToSignal { result -> Signal<[Stories.StoredItem], NoError> in
|
|> mapToSignal { result -> Signal<[Stories.StoredItem]?, NoError> in
|
||||||
guard let result = result else {
|
guard let result = result else {
|
||||||
return .single([])
|
return .single(nil)
|
||||||
}
|
}
|
||||||
return postbox.transaction { transaction -> [Stories.StoredItem] in
|
return postbox.transaction { transaction -> [Stories.StoredItem]? in
|
||||||
switch result {
|
switch result {
|
||||||
case let .stories(_, stories, users):
|
case let .stories(_, stories, users):
|
||||||
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users))
|
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users))
|
||||||
@ -1506,7 +1506,7 @@ func _internal_getStoryViewList(account: Account, id: Int32, offsetTimestamp: In
|
|||||||
var items: [StoryViewList.Item] = []
|
var items: [StoryViewList.Item] = []
|
||||||
for view in views {
|
for view in views {
|
||||||
switch view {
|
switch view {
|
||||||
case let .storyView(userId, date):
|
case let .storyView(_, userId, date):
|
||||||
if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))) {
|
if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))) {
|
||||||
items.append(StoryViewList.Item(peer: EnginePeer(peer), timestamp: date))
|
items.append(StoryViewList.Item(peer: EnginePeer(peer), timestamp: date))
|
||||||
}
|
}
|
||||||
@ -1678,8 +1678,26 @@ public final class EngineStoryViewListContext {
|
|||||||
var nextOffset: NextOffset?
|
var nextOffset: NextOffset?
|
||||||
for view in views {
|
for view in views {
|
||||||
switch view {
|
switch view {
|
||||||
case let .storyView(userId, date):
|
case let .storyView(flags, userId, date):
|
||||||
|
let isBlocked = (flags & (1 << 0)) != 0
|
||||||
|
let isBlockedFromStories = (flags & (1 << 1)) != 0
|
||||||
|
|
||||||
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
|
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
|
||||||
|
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData in
|
||||||
|
let previousData: CachedUserData
|
||||||
|
if let current = cachedData as? CachedUserData {
|
||||||
|
previousData = current
|
||||||
|
} else {
|
||||||
|
previousData = CachedUserData()
|
||||||
|
}
|
||||||
|
var updatedFlags = previousData.flags
|
||||||
|
if isBlockedFromStories {
|
||||||
|
updatedFlags.insert(.isBlockedFromStories)
|
||||||
|
} else {
|
||||||
|
updatedFlags.remove(.isBlockedFromStories)
|
||||||
|
}
|
||||||
|
return previousData.withUpdatedIsBlocked(isBlocked).withUpdatedFlags(updatedFlags)
|
||||||
|
})
|
||||||
if let peer = transaction.getPeer(peerId) {
|
if let peer = transaction.getPeer(peerId) {
|
||||||
items.append(Item(peer: EnginePeer(peer), timestamp: date, storyStats: transaction.getPeerStoryStats(peerId: peerId)))
|
items.append(Item(peer: EnginePeer(peer), timestamp: date, storyStats: transaction.getPeerStoryStats(peerId: peerId)))
|
||||||
|
|
||||||
@ -1904,8 +1922,11 @@ func _internal_exportStoryLink(account: Account, peerId: EnginePeer.Id, id: Int3
|
|||||||
}
|
}
|
||||||
|
|
||||||
func _internal_refreshStories(account: Account, peerId: PeerId, ids: [Int32]) -> Signal<Never, NoError> {
|
func _internal_refreshStories(account: Account, peerId: PeerId, ids: [Int32]) -> Signal<Never, NoError> {
|
||||||
return _internal_getStoriesById(accountPeerId: account.peerId, postbox: account.postbox, source: .network(account.network), peerId: peerId, peerReference: nil, ids: ids)
|
return _internal_getStoriesById(accountPeerId: account.peerId, postbox: account.postbox, source: .network(account.network), peerId: peerId, peerReference: nil, ids: ids, allowFloodWait: true)
|
||||||
|> mapToSignal { result -> Signal<Never, NoError> in
|
|> mapToSignal { result -> Signal<Never, NoError> in
|
||||||
|
guard let result = result else {
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
return account.postbox.transaction { transaction -> Void in
|
return account.postbox.transaction { transaction -> Void in
|
||||||
var currentItems = transaction.getStoryItems(peerId: peerId)
|
var currentItems = transaction.getStoryItems(peerId: peerId)
|
||||||
for i in 0 ..< currentItems.count {
|
for i in 0 ..< currentItems.count {
|
||||||
@ -2058,3 +2079,17 @@ func _internal_enableStoryStealthMode(account: Account) -> Signal<Never, NoError
|
|||||||
|> ignoreValues
|
|> ignoreValues
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func _internal_getStoryNotificationWasDisplayed(transaction: Transaction, id: StoryId) -> Bool {
|
||||||
|
let key = ValueBoxKey(length: 8 + 4)
|
||||||
|
key.setInt64(0, value: id.peerId.toInt64())
|
||||||
|
key.setInt32(8, value: id.id)
|
||||||
|
return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedStoryNotifications, key: key)) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public func _internal_setStoryNotificationWasDisplayed(transaction: Transaction, id: StoryId) {
|
||||||
|
let key = ValueBoxKey(length: 8 + 4)
|
||||||
|
key.setInt64(0, value: id.peerId.toInt64())
|
||||||
|
key.setInt32(8, value: id.id)
|
||||||
|
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedStoryNotifications, key: key), entry: CodableEntry(data: Data()))
|
||||||
|
}
|
||||||
|
|||||||
@ -629,6 +629,20 @@ public extension TelegramEngine {
|
|||||||
}).start()
|
}).start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func peerStoriesAreReady(id: EnginePeer.Id, minId: Int32) -> Signal<Bool, NoError> {
|
||||||
|
return self.account.postbox.combinedView(keys: [
|
||||||
|
PostboxViewKey.storyItems(peerId: id)
|
||||||
|
])
|
||||||
|
|> map { views -> Bool in
|
||||||
|
guard let view = views.views[PostboxViewKey.storyItems(peerId: id)] as? StoryItemsView else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return view.items.contains(where: { item in
|
||||||
|
return item.id >= minId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func storySubscriptions(isHidden: Bool, tempKeepNewlyArchived: Bool = false) -> Signal<EngineStorySubscriptions, NoError> {
|
public func storySubscriptions(isHidden: Bool, tempKeepNewlyArchived: Bool = false) -> Signal<EngineStorySubscriptions, NoError> {
|
||||||
return `deferred` { () -> Signal<EngineStorySubscriptions, NoError> in
|
return `deferred` { () -> Signal<EngineStorySubscriptions, NoError> in
|
||||||
let debugTimerSignal: Signal<Bool, NoError>
|
let debugTimerSignal: Signal<Bool, NoError>
|
||||||
|
|||||||
@ -7,12 +7,11 @@ import MtProtoKit
|
|||||||
func _internal_requestUpdatePeerIsBlocked(account: Account, peerId: PeerId, isBlocked: Bool) -> Signal<Void, NoError> {
|
func _internal_requestUpdatePeerIsBlocked(account: Account, peerId: PeerId, isBlocked: Bool) -> Signal<Void, NoError> {
|
||||||
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
||||||
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
|
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
|
||||||
let flags: Int32 = 0
|
|
||||||
let signal: Signal<Api.Bool, MTRpcError>
|
let signal: Signal<Api.Bool, MTRpcError>
|
||||||
if isBlocked {
|
if isBlocked {
|
||||||
signal = account.network.request(Api.functions.contacts.block(flags: flags, id: inputPeer))
|
signal = account.network.request(Api.functions.contacts.block(flags: 0, id: inputPeer))
|
||||||
} else {
|
} else {
|
||||||
signal = account.network.request(Api.functions.contacts.unblock(flags: flags, id: inputPeer))
|
signal = account.network.request(Api.functions.contacts.unblock(flags: 0, id: inputPeer))
|
||||||
}
|
}
|
||||||
return signal
|
return signal
|
||||||
|> map(Optional.init)
|
|> map(Optional.init)
|
||||||
@ -67,9 +66,9 @@ func _internal_requestUpdatePeerIsBlockedFromStories(account: Account, peerId: P
|
|||||||
}
|
}
|
||||||
var userFlags = previous.flags
|
var userFlags = previous.flags
|
||||||
if isBlocked {
|
if isBlocked {
|
||||||
userFlags.insert(.isBlockedFromMyStories)
|
userFlags.insert(.isBlockedFromStories)
|
||||||
} else {
|
} else {
|
||||||
userFlags.remove(.isBlockedFromMyStories)
|
userFlags.remove(.isBlockedFromStories)
|
||||||
}
|
}
|
||||||
return previous.withUpdatedFlags(userFlags)
|
return previous.withUpdatedFlags(userFlags)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -196,7 +196,7 @@ public final class BlockedPeersContext {
|
|||||||
}
|
}
|
||||||
if case .stories = subject {
|
if case .stories = subject {
|
||||||
var userFlags = previous.flags
|
var userFlags = previous.flags
|
||||||
userFlags.insert(.isBlockedFromMyStories)
|
userFlags.insert(.isBlockedFromStories)
|
||||||
return previous.withUpdatedFlags(userFlags)
|
return previous.withUpdatedFlags(userFlags)
|
||||||
} else {
|
} else {
|
||||||
return previous.withUpdatedIsBlocked(true)
|
return previous.withUpdatedIsBlocked(true)
|
||||||
@ -269,7 +269,7 @@ public final class BlockedPeersContext {
|
|||||||
}
|
}
|
||||||
if case .stories = subject {
|
if case .stories = subject {
|
||||||
var userFlags = previous.flags
|
var userFlags = previous.flags
|
||||||
userFlags.remove(.isBlockedFromMyStories)
|
userFlags.remove(.isBlockedFromStories)
|
||||||
return previous.withUpdatedFlags(userFlags)
|
return previous.withUpdatedFlags(userFlags)
|
||||||
} else {
|
} else {
|
||||||
return previous.withUpdatedIsBlocked(false)
|
return previous.withUpdatedIsBlocked(false)
|
||||||
|
|||||||
@ -191,7 +191,7 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti
|
|||||||
outgoingLinkTextColor = outgoingAccent
|
outgoingLinkTextColor = outgoingAccent
|
||||||
outgoingScamColor = UIColor(rgb: 0xff3b30)
|
outgoingScamColor = UIColor(rgb: 0xff3b30)
|
||||||
outgoingControlColor = outgoingAccent
|
outgoingControlColor = outgoingAccent
|
||||||
outgoingInactiveControlColor = outgoingAccent
|
outgoingInactiveControlColor = outgoingAccent.withMultipliedAlpha(0.5)
|
||||||
outgoingFileTitleColor = outgoingAccent
|
outgoingFileTitleColor = outgoingAccent
|
||||||
outgoingPollsProgressColor = outgoingControlColor
|
outgoingPollsProgressColor = outgoingControlColor
|
||||||
outgoingSelectionColor = outgoingAccent.withAlphaComponent(0.2)
|
outgoingSelectionColor = outgoingAccent.withAlphaComponent(0.2)
|
||||||
|
|||||||
@ -28,6 +28,10 @@ private final class LottieDirectContent: LottieComponent.Content {
|
|||||||
self.path = path
|
self.path = path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var frameRange: Range<Double> {
|
||||||
|
return 0.0 ..< 1.0
|
||||||
|
}
|
||||||
|
|
||||||
override func isEqual(to other: LottieComponent.Content) -> Bool {
|
override func isEqual(to other: LottieComponent.Content) -> Bool {
|
||||||
guard let other = other as? LottieDirectContent else {
|
guard let other = other as? LottieDirectContent else {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -12,6 +12,10 @@ public final class LottieComponent: Component {
|
|||||||
public typealias EnvironmentType = Empty
|
public typealias EnvironmentType = Empty
|
||||||
|
|
||||||
open class Content: Equatable {
|
open class Content: Equatable {
|
||||||
|
open var frameRange: Range<Double> {
|
||||||
|
preconditionFailure()
|
||||||
|
}
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,8 +38,14 @@ public final class LottieComponent: Component {
|
|||||||
public final class AppBundleContent: Content {
|
public final class AppBundleContent: Content {
|
||||||
public let name: String
|
public let name: String
|
||||||
|
|
||||||
public init(name: String) {
|
private let frameRangeValue: Range<Double>
|
||||||
|
override public var frameRange: Range<Double> {
|
||||||
|
return self.frameRangeValue
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(name: String, frameRange: Range<Double> = 0.0 ..< 1.0) {
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.frameRangeValue = frameRange
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func isEqual(to other: Content) -> Bool {
|
override public func isEqual(to other: Content) -> Bool {
|
||||||
@ -45,6 +55,9 @@ public final class LottieComponent: Component {
|
|||||||
if self.name != other.name {
|
if self.name != other.name {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if self.frameRangeValue != other.frameRangeValue {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,9 +72,10 @@ public final class LottieComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum StartingPosition {
|
public enum StartingPosition: Equatable {
|
||||||
case begin
|
case begin
|
||||||
case end
|
case end
|
||||||
|
case fraction(Double)
|
||||||
}
|
}
|
||||||
|
|
||||||
public let content: Content
|
public let content: Content
|
||||||
@ -104,6 +118,7 @@ public final class LottieComponent: Component {
|
|||||||
private var scheduledPlayOnce: Bool = false
|
private var scheduledPlayOnce: Bool = false
|
||||||
private var playOnceCompletion: (() -> Void)?
|
private var playOnceCompletion: (() -> Void)?
|
||||||
private var animationInstance: LottieInstance?
|
private var animationInstance: LottieInstance?
|
||||||
|
private var animationFrameRange: Range<Int>?
|
||||||
private var currentDisplaySize: CGSize?
|
private var currentDisplaySize: CGSize?
|
||||||
private var currentContentDisposable: Disposable?
|
private var currentContentDisposable: Disposable?
|
||||||
|
|
||||||
@ -171,7 +186,7 @@ public final class LottieComponent: Component {
|
|||||||
public func playOnce(delay: Double = 0.0, completion: (() -> Void)? = nil) {
|
public func playOnce(delay: Double = 0.0, completion: (() -> Void)? = nil) {
|
||||||
self.playOnceCompletion = completion
|
self.playOnceCompletion = completion
|
||||||
|
|
||||||
guard let _ = self.animationInstance else {
|
guard let _ = self.animationInstance, let animationFrameRange = self.animationFrameRange else {
|
||||||
self.scheduledPlayOnce = true
|
self.scheduledPlayOnce = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -182,8 +197,8 @@ public final class LottieComponent: Component {
|
|||||||
|
|
||||||
self.scheduledPlayOnce = false
|
self.scheduledPlayOnce = false
|
||||||
|
|
||||||
if self.currentFrame != 0 {
|
if self.currentFrame != animationFrameRange.lowerBound {
|
||||||
self.currentFrame = 0
|
self.currentFrame = animationFrameRange.lowerBound
|
||||||
self.updateImage()
|
self.updateImage()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,24 +233,35 @@ public final class LottieComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadAnimation(data: Data, cacheKey: String?, startingPosition: StartingPosition) {
|
private func loadAnimation(data: Data, cacheKey: String?, startingPosition: StartingPosition, frameRange: Range<Double>) {
|
||||||
self.animationInstance = LottieInstance(data: data, fitzModifier: .none, colorReplacements: nil, cacheKey: cacheKey ?? "")
|
self.animationInstance = LottieInstance(data: data, fitzModifier: .none, colorReplacements: nil, cacheKey: cacheKey ?? "")
|
||||||
|
if let animationInstance = self.animationInstance {
|
||||||
|
self.animationFrameRange = Int(floor(frameRange.lowerBound * Double(animationInstance.frameCount))) ..< Int(floor(frameRange.upperBound * Double(animationInstance.frameCount)))
|
||||||
|
} else {
|
||||||
|
self.animationFrameRange = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let _ = self.animationInstance, let animationFrameRange = self.animationFrameRange {
|
||||||
|
switch startingPosition {
|
||||||
|
case .begin:
|
||||||
|
self.currentFrame = animationFrameRange.lowerBound
|
||||||
|
case .end:
|
||||||
|
self.currentFrame = Int(max(animationFrameRange.lowerBound, animationFrameRange.upperBound - 1))
|
||||||
|
case let .fraction(fraction):
|
||||||
|
self.currentFrame = animationFrameRange.lowerBound + Int(floor(Double(animationFrameRange.upperBound - animationFrameRange.lowerBound) * fraction))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if self.scheduledPlayOnce {
|
if self.scheduledPlayOnce {
|
||||||
self.scheduledPlayOnce = false
|
self.scheduledPlayOnce = false
|
||||||
self.playOnce()
|
self.playOnce()
|
||||||
} else if let animationInstance = self.animationInstance {
|
} else {
|
||||||
switch startingPosition {
|
|
||||||
case .begin:
|
|
||||||
self.currentFrame = 0
|
|
||||||
case .end:
|
|
||||||
self.currentFrame = Int(animationInstance.frameCount - 1)
|
|
||||||
}
|
|
||||||
self.updateImage()
|
self.updateImage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func advanceIfNeeded() {
|
private func advanceIfNeeded() {
|
||||||
guard let animationInstance = self.animationInstance else {
|
guard let animationInstance = self.animationInstance, let animationFrameRange = self.animationFrameRange else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let currentFrameStartTime = self.currentFrameStartTime else {
|
guard let currentFrameStartTime = self.currentFrameStartTime else {
|
||||||
@ -258,8 +284,8 @@ public final class LottieComponent: Component {
|
|||||||
advanceFrameCount = 4
|
advanceFrameCount = 4
|
||||||
}
|
}
|
||||||
self.currentFrame += advanceFrameCount
|
self.currentFrame += advanceFrameCount
|
||||||
if self.currentFrame >= Int(animationInstance.frameCount) - 1 {
|
if self.currentFrame >= animationFrameRange.upperBound - 1 {
|
||||||
self.currentFrame = Int(animationInstance.frameCount) - 1
|
self.currentFrame = animationFrameRange.upperBound - 1
|
||||||
self.updateImage()
|
self.updateImage()
|
||||||
self.displayLink?.invalidate()
|
self.displayLink?.invalidate()
|
||||||
self.displayLink = nil
|
self.displayLink = nil
|
||||||
@ -276,14 +302,17 @@ public final class LottieComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateImage() {
|
private func updateImage() {
|
||||||
guard let animationInstance = self.animationInstance, let currentDisplaySize = self.currentDisplaySize else {
|
guard let animationInstance = self.animationInstance, let animationFrameRange = self.animationFrameRange, let currentDisplaySize = self.currentDisplaySize else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let context = DrawingContext(size: currentDisplaySize, scale: 1.0, opaque: false, clear: true) else {
|
guard let context = DrawingContext(size: currentDisplaySize, scale: 1.0, opaque: false, clear: true) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
animationInstance.renderFrame(with: Int32(self.currentFrame % Int(animationInstance.frameCount)), into: context.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(currentDisplaySize.width), height: Int32(currentDisplaySize.height), bytesPerRow: Int32(context.bytesPerRow))
|
var effectiveFrameIndex = self.currentFrame
|
||||||
|
effectiveFrameIndex = max(animationFrameRange.lowerBound, min(animationFrameRange.upperBound, effectiveFrameIndex))
|
||||||
|
|
||||||
|
animationInstance.renderFrame(with: Int32(effectiveFrameIndex), into: context.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(currentDisplaySize.width), height: Int32(currentDisplaySize.height), bytesPerRow: Int32(context.bytesPerRow))
|
||||||
|
|
||||||
var image = context.generateImage()
|
var image = context.generateImage()
|
||||||
if let _ = self.component?.color {
|
if let _ = self.component?.color {
|
||||||
@ -316,12 +345,13 @@ public final class LottieComponent: Component {
|
|||||||
if previousComponent?.content != component.content {
|
if previousComponent?.content != component.content {
|
||||||
self.currentContentDisposable?.dispose()
|
self.currentContentDisposable?.dispose()
|
||||||
let content = component.content
|
let content = component.content
|
||||||
|
let frameRange = content.frameRange
|
||||||
self.currentContentDisposable = component.content.load { [weak self, weak content] data, cacheKey in
|
self.currentContentDisposable = component.content.load { [weak self, weak content] data, cacheKey in
|
||||||
Queue.mainQueue().async {
|
Queue.mainQueue().async {
|
||||||
guard let self, let component = self.component, component.content == content else {
|
guard let self, let component = self.component, component.content == content else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.loadAnimation(data: data, cacheKey: cacheKey, startingPosition: component.startingPosition)
|
self.loadAnimation(data: data, cacheKey: cacheKey, startingPosition: component.startingPosition, frameRange: frameRange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if redrawImage {
|
} else if redrawImage {
|
||||||
|
|||||||
@ -10,6 +10,10 @@ public extension LottieComponent {
|
|||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let fileId: Int64
|
private let fileId: Int64
|
||||||
|
|
||||||
|
override public var frameRange: Range<Double> {
|
||||||
|
return 0.0 ..< 1.0
|
||||||
|
}
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
fileId: Int64
|
fileId: Int64
|
||||||
|
|||||||
@ -47,6 +47,10 @@ public final class PlainButtonComponent: Component {
|
|||||||
private let contentContainer = UIView()
|
private let contentContainer = UIView()
|
||||||
private let content = ComponentView<Empty>()
|
private let content = ComponentView<Empty>()
|
||||||
|
|
||||||
|
public var contentView: UIView? {
|
||||||
|
return self.content.view
|
||||||
|
}
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
|||||||
@ -78,6 +78,12 @@ swift_library(
|
|||||||
"//submodules/WebPBinding",
|
"//submodules/WebPBinding",
|
||||||
"//submodules/Utils/RangeSet",
|
"//submodules/Utils/RangeSet",
|
||||||
"//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen",
|
"//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen",
|
||||||
|
"//submodules/TelegramUI/Components/LottieComponent",
|
||||||
|
"//submodules/TextSelectionNode",
|
||||||
|
"//submodules/Pasteboard",
|
||||||
|
"//submodules/Speak",
|
||||||
|
"//submodules/TranslateUI",
|
||||||
|
"//submodules/TelegramNotices",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -85,7 +85,7 @@ public extension StoryContainerScreen {
|
|||||||
let _ = avatarNode.pushLoadingStatus(signal: signal)
|
let _ = avatarNode.pushLoadingStatus(signal: signal)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func openPeerStories(context: AccountContext, peerId: EnginePeer.Id, parentController: ViewController, avatarNode: AvatarNode) {
|
static func openPeerStories(context: AccountContext, peerId: EnginePeer.Id, parentController: ViewController, avatarNode: AvatarNode?, sharedProgressDisposable: MetaDisposable? = nil) {
|
||||||
return openPeerStoriesCustom(
|
return openPeerStoriesCustom(
|
||||||
context: context,
|
context: context,
|
||||||
peerId: peerId,
|
peerId: peerId,
|
||||||
@ -149,7 +149,7 @@ public extension StoryContainerScreen {
|
|||||||
guard let avatarNode else {
|
guard let avatarNode else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let _ = avatarNode.pushLoadingStatus(signal: signal)
|
sharedProgressDisposable?.set(avatarNode.pushLoadingStatus(signal: signal))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1169,7 +1169,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
|
|||||||
if let current = self.focusedId {
|
if let current = self.focusedId {
|
||||||
if let index = state.items.firstIndex(where: { $0.id == current }) {
|
if let index = state.items.firstIndex(where: { $0.id == current }) {
|
||||||
focusedIndex = index
|
focusedIndex = index
|
||||||
} else if let index = state.items.firstIndex(where: { $0.id >= current }) {
|
} else if let index = state.items.firstIndex(where: { $0.id <= current }) {
|
||||||
focusedIndex = index
|
focusedIndex = index
|
||||||
} else if !state.items.isEmpty {
|
} else if !state.items.isEmpty {
|
||||||
focusedIndex = 0
|
focusedIndex = 0
|
||||||
@ -1179,7 +1179,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
|
|||||||
} else if let initialId = initialId {
|
} else if let initialId = initialId {
|
||||||
if let index = state.items.firstIndex(where: { $0.id == initialId }) {
|
if let index = state.items.firstIndex(where: { $0.id == initialId }) {
|
||||||
focusedIndex = index
|
focusedIndex = index
|
||||||
} else if let index = state.items.firstIndex(where: { $0.id >= initialId }) {
|
} else if let index = state.items.firstIndex(where: { $0.id <= initialId }) {
|
||||||
focusedIndex = index
|
focusedIndex = index
|
||||||
} else {
|
} else {
|
||||||
focusedIndex = nil
|
focusedIndex = nil
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import TextFormat
|
|||||||
import InvisibleInkDustNode
|
import InvisibleInkDustNode
|
||||||
import UrlEscaping
|
import UrlEscaping
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
import TextSelectionNode
|
||||||
|
|
||||||
final class StoryContentCaptionComponent: Component {
|
final class StoryContentCaptionComponent: Component {
|
||||||
enum Action {
|
enum Action {
|
||||||
@ -23,6 +24,7 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
|
|
||||||
final class ExternalState {
|
final class ExternalState {
|
||||||
fileprivate(set) var isExpanded: Bool = false
|
fileprivate(set) var isExpanded: Bool = false
|
||||||
|
fileprivate(set) var isSelectingText: Bool = false
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
}
|
}
|
||||||
@ -51,30 +53,39 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
let externalState: ExternalState
|
let externalState: ExternalState
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let strings: PresentationStrings
|
let strings: PresentationStrings
|
||||||
|
let theme: PresentationTheme
|
||||||
let text: String
|
let text: String
|
||||||
let entities: [MessageTextEntity]
|
let entities: [MessageTextEntity]
|
||||||
let entityFiles: [EngineMedia.Id: TelegramMediaFile]
|
let entityFiles: [EngineMedia.Id: TelegramMediaFile]
|
||||||
let action: (Action) -> Void
|
let action: (Action) -> Void
|
||||||
let longTapAction: (Action) -> Void
|
let longTapAction: (Action) -> Void
|
||||||
|
let textSelectionAction: (NSAttributedString, TextSelectionAction) -> Void
|
||||||
|
let controller: () -> ViewController?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
externalState: ExternalState,
|
externalState: ExternalState,
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
strings: PresentationStrings,
|
strings: PresentationStrings,
|
||||||
|
theme: PresentationTheme,
|
||||||
text: String,
|
text: String,
|
||||||
entities: [MessageTextEntity],
|
entities: [MessageTextEntity],
|
||||||
entityFiles: [EngineMedia.Id: TelegramMediaFile],
|
entityFiles: [EngineMedia.Id: TelegramMediaFile],
|
||||||
action: @escaping (Action) -> Void,
|
action: @escaping (Action) -> Void,
|
||||||
longTapAction: @escaping (Action) -> Void
|
longTapAction: @escaping (Action) -> Void,
|
||||||
|
textSelectionAction: @escaping (NSAttributedString, TextSelectionAction) -> Void,
|
||||||
|
controller: @escaping () -> ViewController?
|
||||||
) {
|
) {
|
||||||
self.externalState = externalState
|
self.externalState = externalState
|
||||||
self.context = context
|
self.context = context
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
|
self.theme = theme
|
||||||
self.text = text
|
self.text = text
|
||||||
self.entities = entities
|
self.entities = entities
|
||||||
self.entityFiles = entityFiles
|
self.entityFiles = entityFiles
|
||||||
self.action = action
|
self.action = action
|
||||||
self.longTapAction = longTapAction
|
self.longTapAction = longTapAction
|
||||||
|
self.textSelectionAction = textSelectionAction
|
||||||
|
self.controller = controller
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: StoryContentCaptionComponent, rhs: StoryContentCaptionComponent) -> Bool {
|
static func ==(lhs: StoryContentCaptionComponent, rhs: StoryContentCaptionComponent) -> Bool {
|
||||||
@ -87,6 +98,9 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
if lhs.strings !== rhs.strings {
|
if lhs.strings !== rhs.strings {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.text != rhs.text {
|
if lhs.text != rhs.text {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -136,6 +150,7 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
|
|
||||||
private let collapsedText: ContentItem
|
private let collapsedText: ContentItem
|
||||||
private let expandedText: ContentItem
|
private let expandedText: ContentItem
|
||||||
|
private var textSelectionNode: TextSelectionNode?
|
||||||
|
|
||||||
private let scrollMaskContainer: UIView
|
private let scrollMaskContainer: UIView
|
||||||
private let scrollFullMaskView: UIView
|
private let scrollFullMaskView: UIView
|
||||||
@ -217,9 +232,6 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
self.scrollView.addSubview(self.expandedText)
|
self.scrollView.addSubview(self.expandedText)
|
||||||
|
|
||||||
self.scrollViewContainer.mask = self.scrollMaskContainer
|
self.scrollViewContainer.mask = self.scrollMaskContainer
|
||||||
|
|
||||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
|
||||||
self.addGestureRecognizer(tapRecognizer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -231,10 +243,12 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if let textView = self.collapsedText.textNode?.textNode.view {
|
let contentItem = self.isExpanded ? self.expandedText : self.collapsedText
|
||||||
|
|
||||||
|
if let textView = contentItem.textNode?.textNode.view {
|
||||||
let textLocalPoint = self.convert(point, to: textView)
|
let textLocalPoint = self.convert(point, to: textView)
|
||||||
if textLocalPoint.y >= -7.0 {
|
if textLocalPoint.y >= -7.0 {
|
||||||
return textView
|
return self.textSelectionNode?.view ?? textView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,6 +265,15 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if component.externalState.isSelectingText {
|
||||||
|
self.cancelTextSelection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
if !self.ignoreScrolling {
|
if !self.ignoreScrolling {
|
||||||
self.updateScrolling(transition: .immediate)
|
self.updateScrolling(transition: .immediate)
|
||||||
@ -292,6 +315,10 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
self.updateScrolling(transition: transition.withUserData(InternalTransitionHint(bounceScrolling: true)))
|
self.updateScrolling(transition: transition.withUserData(InternalTransitionHint(bounceScrolling: true)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cancelTextSelection() {
|
||||||
|
self.textSelectionNode?.cancelSelection()
|
||||||
|
}
|
||||||
|
|
||||||
private func updateScrolling(transition: Transition) {
|
private func updateScrolling(transition: Transition) {
|
||||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||||
return
|
return
|
||||||
@ -340,6 +367,12 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||||
|
if let textSelectionNode = self.textSelectionNode {
|
||||||
|
if textSelectionNode.didRecognizeTap {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let contentItem = self.isExpanded ? self.expandedText : self.collapsedText
|
let contentItem = self.isExpanded ? self.expandedText : self.collapsedText
|
||||||
let otherContentItem = !self.isExpanded ? self.expandedText : self.collapsedText
|
let otherContentItem = !self.isExpanded ? self.expandedText : self.collapsedText
|
||||||
|
|
||||||
@ -386,7 +419,9 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if case .tap = gesture {
|
if case .tap = gesture {
|
||||||
if self.isExpanded {
|
if component.externalState.isSelectingText {
|
||||||
|
self.cancelTextSelection()
|
||||||
|
} else if self.isExpanded {
|
||||||
self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
} else {
|
} else {
|
||||||
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
@ -395,7 +430,9 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if case .tap = gesture {
|
if case .tap = gesture {
|
||||||
if self.isExpanded {
|
if component.externalState.isSelectingText {
|
||||||
|
self.cancelTextSelection()
|
||||||
|
} else if self.isExpanded {
|
||||||
self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
} else {
|
} else {
|
||||||
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
@ -545,18 +582,6 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
self.collapsedText.textNode = collapsedTextNode
|
self.collapsedText.textNode = collapsedTextNode
|
||||||
if collapsedTextNode.textNode.view.superview == nil {
|
if collapsedTextNode.textNode.view.superview == nil {
|
||||||
self.collapsedText.addSubview(collapsedTextNode.textNode.view)
|
self.collapsedText.addSubview(collapsedTextNode.textNode.view)
|
||||||
|
|
||||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
|
||||||
recognizer.tapActionAtPoint = { point in
|
|
||||||
return .waitForSingleTap
|
|
||||||
}
|
|
||||||
recognizer.highlight = { [weak self] point in
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.updateTouchesAtPoint(point)
|
|
||||||
}
|
|
||||||
collapsedTextNode.textNode.view.addGestureRecognizer(recognizer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
collapsedTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
|
collapsedTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
|
||||||
@ -623,18 +648,6 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
self.expandedText.textNode = expandedTextNode
|
self.expandedText.textNode = expandedTextNode
|
||||||
if expandedTextNode.textNode.view.superview == nil {
|
if expandedTextNode.textNode.view.superview == nil {
|
||||||
self.expandedText.addSubview(expandedTextNode.textNode.view)
|
self.expandedText.addSubview(expandedTextNode.textNode.view)
|
||||||
|
|
||||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
|
||||||
recognizer.tapActionAtPoint = { point in
|
|
||||||
return .waitForSingleTap
|
|
||||||
}
|
|
||||||
recognizer.highlight = { [weak self] point in
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.updateTouchesAtPoint(point)
|
|
||||||
}
|
|
||||||
expandedTextNode.textNode.view.addGestureRecognizer(recognizer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
expandedTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
|
expandedTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
|
||||||
@ -687,6 +700,114 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.textSelectionNode == nil, let controller = component.controller(), let textNode = self.expandedText.textNode?.textNode {
|
||||||
|
let selectionColor = UIColor(white: 1.0, alpha: 0.5)
|
||||||
|
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: component.theme.list.itemAccentColor), strings: component.strings, textNode: textNode, updateIsActive: { [weak self] value in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if component.externalState.isSelectingText != value {
|
||||||
|
component.externalState.isSelectingText = value
|
||||||
|
|
||||||
|
if !self.ignoreExternalState {
|
||||||
|
self.state?.updated(transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, present: { [weak self] c, a in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.controller()?.presentInGlobalOverlay(c, with: a)
|
||||||
|
}, rootNode: controller.displayNode, performAction: { [weak self] text, action in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.textSelectionAction(text, action)
|
||||||
|
})
|
||||||
|
/*textSelectionNode.updateRange = { [weak self] selectionRange in
|
||||||
|
if let strongSelf = self, let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.textNode.cachedLayout, !textLayout.spoilers.isEmpty, let selectionRange = selectionRange {
|
||||||
|
for (spoilerRange, _) in textLayout.spoilers {
|
||||||
|
if let intersection = selectionRange.intersection(spoilerRange), intersection.length > 0 {
|
||||||
|
dustNode.update(revealed: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
textSelectionNode.enableLookup = true
|
||||||
|
self.textSelectionNode = textSelectionNode
|
||||||
|
self.scrollView.addSubview(textSelectionNode.view)
|
||||||
|
self.scrollView.insertSubview(textSelectionNode.highlightAreaNode.view, at: 0)
|
||||||
|
|
||||||
|
textSelectionNode.canBeginSelection = { [weak self] location in
|
||||||
|
guard let self else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentItem = self.expandedText
|
||||||
|
guard let textNode = contentItem.textNode else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleFrame = textNode.textNode.view.bounds
|
||||||
|
|
||||||
|
if let (index, attributes) = textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
|
||||||
|
let action: Action?
|
||||||
|
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(contentItem.dustNode?.isRevealed ?? true) {
|
||||||
|
return false
|
||||||
|
} else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||||
|
var concealed = true
|
||||||
|
if let (attributeText, fullText) = textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
||||||
|
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
|
||||||
|
}
|
||||||
|
action = .url(url: url, concealed: concealed)
|
||||||
|
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
|
||||||
|
action = .peerMention(peerId: peerMention.peerId, mention: peerMention.mention)
|
||||||
|
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
|
||||||
|
action = .textMention(peerName)
|
||||||
|
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
||||||
|
action = .hashtag(hashtag.peerName, hashtag.hashtag)
|
||||||
|
} else if let bankCard = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard)] as? String {
|
||||||
|
action = .bankCard(bankCard)
|
||||||
|
} else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file {
|
||||||
|
action = .customEmoji(file)
|
||||||
|
} else {
|
||||||
|
action = nil
|
||||||
|
}
|
||||||
|
if action != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
//let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||||
|
//textSelectionNode.view.addGestureRecognizer(tapRecognizer)
|
||||||
|
|
||||||
|
let _ = textSelectionNode.view
|
||||||
|
|
||||||
|
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
||||||
|
/*if let selectionRecognizer = textSelectionNode.recognizer {
|
||||||
|
recognizer.require(toFail: selectionRecognizer)
|
||||||
|
}*/
|
||||||
|
recognizer.tapActionAtPoint = { point in
|
||||||
|
return .waitForSingleTap
|
||||||
|
}
|
||||||
|
recognizer.highlight = { [weak self] point in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.updateTouchesAtPoint(point)
|
||||||
|
}
|
||||||
|
textSelectionNode.view.addGestureRecognizer(recognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let textSelectionNode = self.textSelectionNode, let textNode = self.expandedText.textNode?.textNode {
|
||||||
|
textSelectionNode.frame = textNode.frame.offsetBy(dx: self.expandedText.frame.minX, dy: self.expandedText.frame.minY)
|
||||||
|
textSelectionNode.highlightAreaNode.frame = textSelectionNode.frame
|
||||||
|
}
|
||||||
|
|
||||||
self.itemLayout = ItemLayout(
|
self.itemLayout = ItemLayout(
|
||||||
containerSize: availableSize,
|
containerSize: availableSize,
|
||||||
visibleTextHeight: visibleTextHeight,
|
visibleTextHeight: visibleTextHeight,
|
||||||
|
|||||||
@ -15,7 +15,8 @@ import MultilineTextComponent
|
|||||||
|
|
||||||
final class StoryItemImageView: UIView {
|
final class StoryItemImageView: UIView {
|
||||||
private let contentView: UIImageView
|
private let contentView: UIImageView
|
||||||
private var captureProtectedContentLayer: CaptureProtectedContentLayer?
|
private var captureProtectedView: UITextField?
|
||||||
|
|
||||||
private var captureProtectedInfo: ComponentView<Empty>?
|
private var captureProtectedInfo: ComponentView<Empty>?
|
||||||
|
|
||||||
private var currentMedia: EngineMedia?
|
private var currentMedia: EngineMedia?
|
||||||
@ -30,8 +31,6 @@ final class StoryItemImageView: UIView {
|
|||||||
self.contentView.contentMode = .scaleAspectFill
|
self.contentView.contentMode = .scaleAspectFill
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
self.addSubview(self.contentView)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -43,28 +42,27 @@ final class StoryItemImageView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateImage(image: UIImage, isCaptureProtected: Bool) {
|
private func updateImage(image: UIImage, isCaptureProtected: Bool) {
|
||||||
|
self.contentView.image = image
|
||||||
|
|
||||||
if isCaptureProtected {
|
if isCaptureProtected {
|
||||||
let captureProtectedContentLayer: CaptureProtectedContentLayer
|
let captureProtectedView: UITextField
|
||||||
if let current = self.captureProtectedContentLayer {
|
if let current = self.captureProtectedView {
|
||||||
captureProtectedContentLayer = current
|
captureProtectedView = current
|
||||||
} else {
|
} else {
|
||||||
captureProtectedContentLayer = CaptureProtectedContentLayer()
|
captureProtectedView = UITextField(frame: self.contentView.frame)
|
||||||
|
captureProtectedView.isSecureTextEntry = true
|
||||||
captureProtectedContentLayer.videoGravity = .resizeAspectFill
|
self.captureProtectedView = captureProtectedView
|
||||||
if #available(iOS 13.0, *) {
|
self.layer.addSublayer(captureProtectedView.layer)
|
||||||
captureProtectedContentLayer.preventsCapture = true
|
captureProtectedView.layer.sublayers?.first?.addSublayer(self.contentView.layer)
|
||||||
captureProtectedContentLayer.preventsDisplaySleepDuringVideoPlayback = false
|
|
||||||
}
|
|
||||||
|
|
||||||
captureProtectedContentLayer.frame = self.contentView.frame
|
|
||||||
self.captureProtectedContentLayer = captureProtectedContentLayer
|
|
||||||
self.layer.addSublayer(captureProtectedContentLayer)
|
|
||||||
}
|
|
||||||
if let cmSampleBuffer = image.cmSampleBuffer {
|
|
||||||
captureProtectedContentLayer.enqueue(cmSampleBuffer)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.contentView.image = image
|
if self.contentView.layer.superlayer !== self.layer {
|
||||||
|
self.layer.addSublayer(self.contentView.layer)
|
||||||
|
}
|
||||||
|
if let captureProtectedView = self.captureProtectedView {
|
||||||
|
self.captureProtectedView = nil
|
||||||
|
captureProtectedView.layer.removeFromSuperlayer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,20 +84,6 @@ final class StoryItemImageView: UIView {
|
|||||||
dimensions = representation.dimensions.cgSize
|
dimensions = representation.dimensions.cgSize
|
||||||
|
|
||||||
if isMediaUpdated {
|
if isMediaUpdated {
|
||||||
if isCaptureProtected {
|
|
||||||
if let thumbnailData = image.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) {
|
|
||||||
if let image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3) {
|
|
||||||
self.updateImage(image: image, isCaptureProtected: false)
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.contentView.image = nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if attemptSynchronous, let path = context.account.postbox.mediaBox.completedResourcePath(id: representation.resource.id, pathExtension: nil) {
|
if attemptSynchronous, let path = context.account.postbox.mediaBox.completedResourcePath(id: representation.resource.id, pathExtension: nil) {
|
||||||
if #available(iOS 15.0, *) {
|
if #available(iOS 15.0, *) {
|
||||||
if let image = UIImage(contentsOfFile: path)?.preparingForDisplay() {
|
if let image = UIImage(contentsOfFile: path)?.preparingForDisplay() {
|
||||||
@ -159,20 +143,6 @@ final class StoryItemImageView: UIView {
|
|||||||
dimensions = file.dimensions?.cgSize
|
dimensions = file.dimensions?.cgSize
|
||||||
|
|
||||||
if isMediaUpdated {
|
if isMediaUpdated {
|
||||||
if isCaptureProtected {
|
|
||||||
if let thumbnailData = file.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) {
|
|
||||||
if let image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3) {
|
|
||||||
self.updateImage(image: image, isCaptureProtected: false)
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.contentView.image = nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let cachedPath = context.account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedVideoFirstFrameRepresentation())
|
let cachedPath = context.account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedVideoFirstFrameRepresentation())
|
||||||
|
|
||||||
if attemptSynchronous, FileManager.default.fileExists(atPath: cachedPath) {
|
if attemptSynchronous, FileManager.default.fileExists(atPath: cachedPath) {
|
||||||
@ -234,10 +204,12 @@ final class StoryItemImageView: UIView {
|
|||||||
if let dimensions {
|
if let dimensions {
|
||||||
let filledSize = dimensions.aspectFilled(size)
|
let filledSize = dimensions.aspectFilled(size)
|
||||||
let contentFrame = CGRect(origin: CGPoint(x: floor((size.width - filledSize.width) * 0.5), y: floor((size.height - filledSize.height) * 0.5)), size: filledSize)
|
let contentFrame = CGRect(origin: CGPoint(x: floor((size.width - filledSize.width) * 0.5), y: floor((size.height - filledSize.height) * 0.5)), size: filledSize)
|
||||||
transition.setFrame(view: self.contentView, frame: contentFrame)
|
|
||||||
|
|
||||||
if let captureProtectedContentLayer = self.captureProtectedContentLayer {
|
if let captureProtectedView = self.captureProtectedView {
|
||||||
transition.setFrame(layer: captureProtectedContentLayer, frame: contentFrame)
|
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: contentFrame.size))
|
||||||
|
transition.setFrame(view: captureProtectedView, frame: contentFrame)
|
||||||
|
} else {
|
||||||
|
transition.setFrame(view: self.contentView, frame: contentFrame)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -35,6 +35,9 @@ import AttachmentUI
|
|||||||
import StickerPackPreviewUI
|
import StickerPackPreviewUI
|
||||||
import TextNodeWithEntities
|
import TextNodeWithEntities
|
||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
|
import LottieComponent
|
||||||
|
import Pasteboard
|
||||||
|
import Speak
|
||||||
|
|
||||||
public final class StoryAvailableReactions: Equatable {
|
public final class StoryAvailableReactions: Equatable {
|
||||||
let reactionItems: [ReactionItem]
|
let reactionItems: [ReactionItem]
|
||||||
@ -372,6 +375,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
var leftInfoItem: InfoItem?
|
var leftInfoItem: InfoItem?
|
||||||
|
|
||||||
let moreButton = ComponentView<Empty>()
|
let moreButton = ComponentView<Empty>()
|
||||||
|
var currentSoundButtonState: Bool?
|
||||||
let soundButton = ComponentView<Empty>()
|
let soundButton = ComponentView<Empty>()
|
||||||
var privacyIcon: ComponentView<Empty>?
|
var privacyIcon: ComponentView<Empty>?
|
||||||
|
|
||||||
@ -763,9 +767,13 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let captionItem = self.captionItem, captionItem.externalState.isExpanded {
|
} else if let captionItem = self.captionItem, (captionItem.externalState.isExpanded || captionItem.externalState.isSelectingText) {
|
||||||
if let captionItemView = captionItem.view.view as? StoryContentCaptionComponent.View {
|
if let captionItemView = captionItem.view.view as? StoryContentCaptionComponent.View {
|
||||||
captionItemView.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
if captionItem.externalState.isSelectingText {
|
||||||
|
captionItemView.cancelTextSelection()
|
||||||
|
} else {
|
||||||
|
captionItemView.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let point = recognizer.location(in: self)
|
let point = recognizer.location(in: self)
|
||||||
@ -965,13 +973,16 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
if self.sendMessageContext.statusController != nil {
|
if self.sendMessageContext.statusController != nil {
|
||||||
return .pause
|
return .pause
|
||||||
}
|
}
|
||||||
|
if self.sendMessageContext.lookupController != nil {
|
||||||
|
return .pause
|
||||||
|
}
|
||||||
if let navigationController = component.controller()?.navigationController as? NavigationController {
|
if let navigationController = component.controller()?.navigationController as? NavigationController {
|
||||||
let topViewController = navigationController.topViewController
|
let topViewController = navigationController.topViewController
|
||||||
if !(topViewController is StoryContainerScreen) && !(topViewController is MediaEditorScreen) && !(topViewController is ShareWithPeersScreen) && !(topViewController is AttachmentController) {
|
if !(topViewController is StoryContainerScreen) && !(topViewController is MediaEditorScreen) && !(topViewController is ShareWithPeersScreen) && !(topViewController is AttachmentController) {
|
||||||
return .pause
|
return .pause
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let captionItem = self.captionItem, captionItem.externalState.isExpanded {
|
if let captionItem = self.captionItem, captionItem.externalState.isExpanded || captionItem.externalState.isSelectingText {
|
||||||
return .blurred
|
return .blurred
|
||||||
}
|
}
|
||||||
return .play
|
return .play
|
||||||
@ -2213,26 +2224,24 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
guard let self, let component = self.component else {
|
guard let self, let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = (component.context.engine.data.get(
|
let _ = (component.context.engine.data.get(
|
||||||
|
TelegramEngine.EngineData.Item.Peer.IsContact(id: peer.id),
|
||||||
TelegramEngine.EngineData.Item.Peer.IsBlocked(id: peer.id),
|
TelegramEngine.EngineData.Item.Peer.IsBlocked(id: peer.id),
|
||||||
TelegramEngine.EngineData.Item.Peer.IsBlockedFromStories(id: peer.id),
|
TelegramEngine.EngineData.Item.Peer.IsBlockedFromStories(id: peer.id)
|
||||||
TelegramEngine.EngineData.Item.Peer.IsContact(id: peer.id)
|
) |> deliverOnMainQueue).start(next: { [weak self] isContact, maybeIsBlocked, maybeIsBlockedFromStories in
|
||||||
) |> deliverOnMainQueue).start(next: { [weak self] isBlocked, isBlockedFromStories, isContact in
|
|
||||||
var isBlockedValue = false
|
|
||||||
var isBlockedFromStoriesValue = false
|
|
||||||
|
|
||||||
if case let .known(value) = isBlocked {
|
|
||||||
isBlockedValue = value
|
|
||||||
}
|
|
||||||
if case let .known(value) = isBlockedFromStories {
|
|
||||||
isBlockedFromStoriesValue = value
|
|
||||||
}
|
|
||||||
|
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||||
var itemList: [ContextMenuItem] = []
|
var itemList: [ContextMenuItem] = []
|
||||||
|
|
||||||
if isBlockedFromStoriesValue {
|
var isBlocked = false
|
||||||
|
if case let .known(value) = maybeIsBlocked {
|
||||||
|
isBlocked = value
|
||||||
|
}
|
||||||
|
var isBlockedFromStories = false
|
||||||
|
if case let .known(value) = maybeIsBlockedFromStories {
|
||||||
|
isBlockedFromStories = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if isBlockedFromStories {
|
||||||
itemList.append(.action(ContextMenuActionItem(text: "Show My Stories To \(peer.compactDisplayTitle)", icon: { theme in
|
itemList.append(.action(ContextMenuActionItem(text: "Show My Stories To \(peer.compactDisplayTitle)", icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Stories"), color: theme.contextMenu.primaryColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Stories"), color: theme.contextMenu.primaryColor)
|
||||||
}, action: { [weak self] _, f in
|
}, action: { [weak self] _, f in
|
||||||
@ -2278,20 +2287,47 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
if isContact {
|
if isContact {
|
||||||
itemList.append(.action(ContextMenuActionItem(text: "Delete Contact", textColor: .destructive, icon: { theme in
|
itemList.append(.action(ContextMenuActionItem(text: "Delete Contact", textColor: .destructive, icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
||||||
}, action: { _, f in
|
}, action: { [weak self] _, f in
|
||||||
f(.default)
|
f(.default)
|
||||||
|
|
||||||
|
let _ = component.context.engine.contacts.deleteContactPeerInteractively(peerId: peer.id)
|
||||||
|
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||||
|
self.component?.presentController(UndoOverlayController(
|
||||||
|
presentationData: presentationData,
|
||||||
|
content: .info(title: nil, text: "**\(peer.compactDisplayTitle)** has been removed from your contacts.", timeout: nil),
|
||||||
|
elevatedLayout: false,
|
||||||
|
position: .top,
|
||||||
|
animateInAsReplacement: false,
|
||||||
|
action: { _ in return false }
|
||||||
|
), nil)
|
||||||
})))
|
})))
|
||||||
} else {
|
} else {
|
||||||
if isBlockedValue {
|
if isBlocked {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuBlock, textColor: .destructive, icon: { theme in
|
itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuBlock, textColor: .destructive, icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.contextMenu.destructiveColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.contextMenu.destructiveColor)
|
||||||
}, action: { _, f in
|
}, action: { [weak self] _, f in
|
||||||
f(.default)
|
f(.default)
|
||||||
|
|
||||||
let _ = component.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: true).start()
|
let _ = component.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: true).start()
|
||||||
|
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||||
|
self.component?.presentController(UndoOverlayController(
|
||||||
|
presentationData: presentationData,
|
||||||
|
content: .info(title: nil, text: "**\(peer.compactDisplayTitle)** has been blocked.", timeout: nil),
|
||||||
|
elevatedLayout: false,
|
||||||
|
position: .top,
|
||||||
|
animateInAsReplacement: false,
|
||||||
|
action: { _ in return false }
|
||||||
|
), nil)
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2470,36 +2506,29 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
|
|
||||||
let moreButtonSize = self.moreButton.update(
|
let moreButtonSize = self.moreButton.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(MessageInputActionButtonComponent(
|
component: AnyComponent(PlainButtonComponent(
|
||||||
mode: .more,
|
content: AnyComponent(LottieComponent(
|
||||||
action: { _, _, _ in
|
content: LottieComponent.AppBundleContent(
|
||||||
},
|
name: "anim_story_more"
|
||||||
longPressAction: nil,
|
),
|
||||||
switchMediaInputMode: {
|
color: .white,
|
||||||
},
|
startingPosition: .end,
|
||||||
updateMediaCancelFraction: { _ in
|
size: CGSize(width: 30.0, height: 30.0)
|
||||||
},
|
)),
|
||||||
lockMediaRecording: {
|
effectAlignment: .center,
|
||||||
},
|
minSize: CGSize(width: 33.0, height: 64.0),
|
||||||
stopAndPreviewMediaRecording: {
|
action: { [weak self] in
|
||||||
},
|
|
||||||
moreAction: { [weak self] view, gesture in
|
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.performMoreAction(sourceView: view, gesture: gesture)
|
guard let moreButtonView = self.moreButton.view else {
|
||||||
},
|
|
||||||
context: component.context,
|
|
||||||
theme: component.theme,
|
|
||||||
strings: component.strings,
|
|
||||||
presentController: { [weak self] c in
|
|
||||||
guard let self, let component = self.component else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
component.presentController(c, nil)
|
if let animationView = (moreButtonView as? PlainButtonComponent.View)?.contentView as? LottieComponent.View {
|
||||||
},
|
animationView.playOnce()
|
||||||
audioRecorder: nil,
|
}
|
||||||
videoRecordingStatus: nil
|
self.performMoreAction(sourceView: moreButtonView, gesture: nil)
|
||||||
|
}
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: 33.0, height: 64.0)
|
containerSize: CGSize(width: 33.0, height: 64.0)
|
||||||
@ -2511,7 +2540,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
moreButtonView.isUserInteractionEnabled = !component.slice.item.storyItem.isPending
|
moreButtonView.isUserInteractionEnabled = !component.slice.item.storyItem.isPending
|
||||||
transition.setFrame(view: moreButtonView, frame: CGRect(origin: CGPoint(x: headerRightOffset - moreButtonSize.width, y: 2.0), size: moreButtonSize))
|
transition.setFrame(view: moreButtonView, frame: CGRect(origin: CGPoint(x: headerRightOffset - moreButtonSize.width, y: 2.0), size: moreButtonSize))
|
||||||
transition.setAlpha(view: moreButtonView, alpha: component.slice.item.storyItem.isPending ? 0.5 : 1.0)
|
transition.setAlpha(view: moreButtonView, alpha: component.slice.item.storyItem.isPending ? 0.5 : 1.0)
|
||||||
headerRightOffset -= moreButtonSize.width + 15.0
|
headerRightOffset -= moreButtonSize.width + 12.0
|
||||||
}
|
}
|
||||||
|
|
||||||
var isSilentVideo = false
|
var isSilentVideo = false
|
||||||
@ -2530,20 +2559,18 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let soundImage: String
|
let soundButtonState = isSilentVideo || component.isAudioMuted
|
||||||
if isSilentVideo || component.isAudioMuted {
|
|
||||||
soundImage = "Stories/SoundOff"
|
|
||||||
} else {
|
|
||||||
soundImage = "Stories/SoundOn"
|
|
||||||
}
|
|
||||||
|
|
||||||
let soundButtonSize = self.soundButton.update(
|
let soundButtonSize = self.soundButton.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(PlainButtonComponent(
|
component: AnyComponent(PlainButtonComponent(
|
||||||
content: AnyComponent(BundleIconComponent(
|
content: AnyComponent(LottieComponent(
|
||||||
name: soundImage,
|
content: LottieComponent.AppBundleContent(
|
||||||
tintColor: .white,
|
name: "anim_storymute",
|
||||||
maxSize: nil
|
frameRange: soundButtonState ? (0.0 ..< 0.5) : (0.5 ..< 1.0)
|
||||||
|
),
|
||||||
|
color: .white,
|
||||||
|
startingPosition: .end,
|
||||||
|
size: CGSize(width: 30.0, height: 30.0)
|
||||||
)),
|
)),
|
||||||
effectAlignment: .center,
|
effectAlignment: .center,
|
||||||
minSize: CGSize(width: 33.0, height: 64.0),
|
minSize: CGSize(width: 33.0, height: 64.0),
|
||||||
@ -2583,6 +2610,13 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
if isVideo {
|
if isVideo {
|
||||||
headerRightOffset -= soundButtonSize.width + 13.0
|
headerRightOffset -= soundButtonSize.width + 13.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let currentSoundButtonState = self.currentSoundButtonState, currentSoundButtonState != soundButtonState {
|
||||||
|
if let lottieView = (soundButtonView as? PlainButtonComponent.View)?.contentView as? LottieComponent.View {
|
||||||
|
lottieView.playOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.currentSoundButtonState = soundButtonState
|
||||||
}
|
}
|
||||||
|
|
||||||
let storyPrivacyIcon: StoryPrivacyIconComponent.Privacy?
|
let storyPrivacyIcon: StoryPrivacyIconComponent.Privacy?
|
||||||
@ -2858,6 +2892,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
externalState: captionItem.externalState,
|
externalState: captionItem.externalState,
|
||||||
context: component.context,
|
context: component.context,
|
||||||
strings: component.strings,
|
strings: component.strings,
|
||||||
|
theme: component.theme,
|
||||||
text: component.slice.item.storyItem.text,
|
text: component.slice.item.storyItem.text,
|
||||||
entities: component.slice.item.storyItem.entities,
|
entities: component.slice.item.storyItem.entities,
|
||||||
entityFiles: component.slice.item.entityFiles,
|
entityFiles: component.slice.item.entityFiles,
|
||||||
@ -2907,6 +2942,39 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.sendMessageContext.openResolved(view: self, result: resolved, forceExternal: false, concealed: concealed)
|
self.sendMessageContext.openResolved(view: self, result: resolved, forceExternal: false, concealed: concealed)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
textSelectionAction: { [weak self] text, action in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch action {
|
||||||
|
case .copy:
|
||||||
|
storeAttributedTextInPasteboard(text)
|
||||||
|
case .share:
|
||||||
|
self.sendMessageContext.performShareTextAction(view: self, text: text.string)
|
||||||
|
case .lookup:
|
||||||
|
self.sendMessageContext.performLookupTextAction(view: self, text: text.string)
|
||||||
|
case .speak:
|
||||||
|
if let speechHolder = speakText(context: component.context, text: text.string) {
|
||||||
|
speechHolder.completion = { [weak self, weak speechHolder] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.sendMessageContext.currentSpeechHolder === speechHolder {
|
||||||
|
self.sendMessageContext.currentSpeechHolder = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.sendMessageContext.currentSpeechHolder = speechHolder
|
||||||
|
}
|
||||||
|
case .translate:
|
||||||
|
self.sendMessageContext.performTranslateTextAction(view: self, text: text.string)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
controller: { [weak self] in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return component.controller()
|
||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
|
|||||||
@ -43,6 +43,12 @@ import WebPBinding
|
|||||||
import ContextUI
|
import ContextUI
|
||||||
import ChatScheduleTimeController
|
import ChatScheduleTimeController
|
||||||
import StoryStealthModeSheetScreen
|
import StoryStealthModeSheetScreen
|
||||||
|
import Speak
|
||||||
|
import TranslateUI
|
||||||
|
import TelegramNotices
|
||||||
|
import ObjectiveC
|
||||||
|
|
||||||
|
private var ObjCKey_DeinitWatcher: Int?
|
||||||
|
|
||||||
final class StoryItemSetContainerSendMessage {
|
final class StoryItemSetContainerSendMessage {
|
||||||
enum InputMode {
|
enum InputMode {
|
||||||
@ -59,6 +65,7 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
weak var tooltipScreen: ViewController?
|
weak var tooltipScreen: ViewController?
|
||||||
weak var actionSheet: ViewController?
|
weak var actionSheet: ViewController?
|
||||||
weak var statusController: ViewController?
|
weak var statusController: ViewController?
|
||||||
|
weak var lookupController: UIViewController?
|
||||||
var isViewingAttachedStickers = false
|
var isViewingAttachedStickers = false
|
||||||
|
|
||||||
var currentTooltipUpdateTimer: Foundation.Timer?
|
var currentTooltipUpdateTimer: Foundation.Timer?
|
||||||
@ -86,6 +93,8 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
let navigationActionDisposable = MetaDisposable()
|
let navigationActionDisposable = MetaDisposable()
|
||||||
let resolvePeerByNameDisposable = MetaDisposable()
|
let resolvePeerByNameDisposable = MetaDisposable()
|
||||||
|
|
||||||
|
var currentSpeechHolder: SpeechSynthesizerHolder?
|
||||||
|
|
||||||
private(set) var isMediaRecordingLocked: Bool = false
|
private(set) var isMediaRecordingLocked: Bool = false
|
||||||
var wasRecordingDismissed: Bool = false
|
var wasRecordingDismissed: Bool = false
|
||||||
|
|
||||||
@ -1016,6 +1025,129 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func performShareTextAction(view: StoryItemSetContainerComponent.View, text: String) {
|
||||||
|
guard let component = view.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let controller = component.controller() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let theme = component.theme
|
||||||
|
let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) })
|
||||||
|
|
||||||
|
let shareController = ShareController(context: component.context, subject: .text(text), externalShare: true, immediateExternalShare: false, updatedPresentationData: updatedPresentationData)
|
||||||
|
|
||||||
|
self.shareController = shareController
|
||||||
|
view.updateIsProgressPaused()
|
||||||
|
|
||||||
|
shareController.dismissed = { [weak self, weak view] _ in
|
||||||
|
guard let self, let view else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.shareController = nil
|
||||||
|
view.updateIsProgressPaused()
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.present(shareController, in: .window(.root))
|
||||||
|
}
|
||||||
|
|
||||||
|
func performTranslateTextAction(view: StoryItemSetContainerComponent.View, text: String) {
|
||||||
|
guard let component = view.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = (component.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings])
|
||||||
|
|> take(1)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self, weak view] sharedData in
|
||||||
|
guard let self, let view else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let peer = component.slice.peer
|
||||||
|
|
||||||
|
let _ = self
|
||||||
|
|
||||||
|
let translationSettings: TranslationSettings
|
||||||
|
if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) {
|
||||||
|
translationSettings = current
|
||||||
|
} else {
|
||||||
|
translationSettings = TranslationSettings.defaultSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
var showTranslateIfTopical = false
|
||||||
|
if case let .channel(channel) = peer, !(channel.addressName ?? "").isEmpty {
|
||||||
|
showTranslateIfTopical = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_, language) = canTranslateText(context: component.context, text: text, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: showTranslateIfTopical, ignoredLanguages: translationSettings.ignoredLanguages)
|
||||||
|
|
||||||
|
let _ = ApplicationSpecificNotice.incrementTranslationSuggestion(accountManager: component.context.sharedContext.accountManager, timestamp: Int32(Date().timeIntervalSince1970)).start()
|
||||||
|
|
||||||
|
let translateController = TranslateScreen(context: component.context, text: text, canCopy: true, fromLanguage: language)
|
||||||
|
translateController.pushController = { [weak view] c in
|
||||||
|
guard let view, let component = view.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.controller()?.push(c)
|
||||||
|
}
|
||||||
|
translateController.presentController = { [weak view] c in
|
||||||
|
guard let view, let component = view.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.controller()?.present(c, in: .window(.root))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.actionSheet = translateController
|
||||||
|
view.updateIsProgressPaused()
|
||||||
|
|
||||||
|
translateController.wasDismissed = { [weak self, weak view] in
|
||||||
|
guard let self, let view else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.actionSheet = nil
|
||||||
|
view.updateIsProgressPaused()
|
||||||
|
}
|
||||||
|
|
||||||
|
component.controller()?.present(translateController, in: .window(.root))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func performLookupTextAction(view: StoryItemSetContainerComponent.View, text: String) {
|
||||||
|
guard let component = view.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let controller = UIReferenceLibraryViewController(term: text)
|
||||||
|
if let window = component.controller()?.view.window {
|
||||||
|
controller.popoverPresentationController?.sourceView = window
|
||||||
|
controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
|
||||||
|
window.rootViewController?.present(controller, animated: true)
|
||||||
|
|
||||||
|
final class DeinitWatcher: NSObject {
|
||||||
|
let f: () -> Void
|
||||||
|
|
||||||
|
init(_ f: @escaping () -> Void) {
|
||||||
|
self.f = f
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.lookupController = controller
|
||||||
|
view.updateIsProgressPaused()
|
||||||
|
|
||||||
|
objc_setAssociatedObject(controller, &ObjCKey_DeinitWatcher, DeinitWatcher { [weak self, weak view] in
|
||||||
|
guard let self, let view else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.lookupController = nil
|
||||||
|
view.updateIsProgressPaused()
|
||||||
|
}, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func performCopyLinkAction(view: StoryItemSetContainerComponent.View) {
|
func performCopyLinkAction(view: StoryItemSetContainerComponent.View) {
|
||||||
guard let component = view.component else {
|
guard let component = view.component else {
|
||||||
return
|
return
|
||||||
|
|||||||
@ -18,6 +18,19 @@ import AvatarNode
|
|||||||
import Markdown
|
import Markdown
|
||||||
import ButtonComponent
|
import ButtonComponent
|
||||||
|
|
||||||
|
private func cancelContextGestures(view: UIView) {
|
||||||
|
if let gestureRecognizers = view.gestureRecognizers {
|
||||||
|
for gesture in gestureRecognizers {
|
||||||
|
if let gesture = gesture as? ContextGesture {
|
||||||
|
gesture.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for subview in view.subviews {
|
||||||
|
cancelContextGestures(view: subview)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class StoryItemSetViewListComponent: Component {
|
final class StoryItemSetViewListComponent: Component {
|
||||||
final class AnimationHint {
|
final class AnimationHint {
|
||||||
let synchronous: Bool
|
let synchronous: Bool
|
||||||
@ -414,6 +427,10 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
cancelContextGestures(view: scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
private func updateScrolling(transition: Transition) {
|
private func updateScrolling(transition: Transition) {
|
||||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||||
return
|
return
|
||||||
|
|||||||
@ -886,7 +886,8 @@ public final class StoryPeerListComponent: Component {
|
|||||||
} else {
|
} else {
|
||||||
overscrollScaleFactor = 0.0
|
overscrollScaleFactor = 0.0
|
||||||
}
|
}
|
||||||
let maximizedItemScale: CGFloat = 1.0 + overscrollStage1 * 0.1 + overscrollScaleFactor * overscrollStage2 * 0.5
|
var maximizedItemScale: CGFloat = 1.0 + overscrollStage1 * 0.1 + overscrollScaleFactor * overscrollStage2 * 0.5
|
||||||
|
maximizedItemScale = min(1.6, maximizedItemScale)
|
||||||
|
|
||||||
let minItemScale: CGFloat = minimizedItemScale.interpolate(to: minimizedMaxItemScale, amount: collapsedState.minFraction) * (1.0 - collapsedState.activityFraction) + 0.1 * collapsedState.activityFraction
|
let minItemScale: CGFloat = minimizedItemScale.interpolate(to: minimizedMaxItemScale, amount: collapsedState.minFraction) * (1.0 - collapsedState.activityFraction) + 0.1 * collapsedState.activityFraction
|
||||||
|
|
||||||
|
|||||||
@ -194,11 +194,23 @@ private final class StoryProgressLayer: HierarchyTrackingLayer {
|
|||||||
|
|
||||||
switch params.value {
|
switch params.value {
|
||||||
case let .progress(progress):
|
case let .progress(progress):
|
||||||
|
var animateIn = false
|
||||||
if self.indefiniteReplicatorLayer.superlayer != nil {
|
if self.indefiniteReplicatorLayer.superlayer != nil {
|
||||||
self.indefiniteReplicatorLayer.removeFromSuperlayer()
|
self.indefiniteReplicatorLayer.opacity = 0.0
|
||||||
|
self.indefiniteReplicatorLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] finished in
|
||||||
|
guard let self, finished else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.indefiniteReplicatorLayer.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
animateIn = true
|
||||||
}
|
}
|
||||||
if self.uploadProgressLayer.superlayer == nil {
|
if self.uploadProgressLayer.superlayer == nil {
|
||||||
self.addSublayer(self.uploadProgressLayer)
|
self.addSublayer(self.uploadProgressLayer)
|
||||||
|
if animateIn {
|
||||||
|
self.uploadProgressLayer.opacity = 1.0
|
||||||
|
self.uploadProgressLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
transition.setShapeLayerStrokeEnd(layer: self.uploadProgressLayer, strokeEnd: CGFloat(progress))
|
transition.setShapeLayerStrokeEnd(layer: self.uploadProgressLayer, strokeEnd: CGFloat(progress))
|
||||||
if self.uploadProgressLayer.animation(forKey: "rotation") == nil {
|
if self.uploadProgressLayer.animation(forKey: "rotation") == nil {
|
||||||
@ -211,11 +223,23 @@ private final class StoryProgressLayer: HierarchyTrackingLayer {
|
|||||||
self.uploadProgressLayer.add(rotationAnimation, forKey: "rotation")
|
self.uploadProgressLayer.add(rotationAnimation, forKey: "rotation")
|
||||||
}
|
}
|
||||||
case .indefinite:
|
case .indefinite:
|
||||||
if self.uploadProgressLayer.superlayer == nil {
|
var animateIn = false
|
||||||
self.uploadProgressLayer.removeFromSuperlayer()
|
if self.uploadProgressLayer.superlayer != nil {
|
||||||
|
self.uploadProgressLayer.opacity = 0.0
|
||||||
|
self.uploadProgressLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] finished in
|
||||||
|
guard let self, finished else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.uploadProgressLayer.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
animateIn = true
|
||||||
}
|
}
|
||||||
if self.indefiniteReplicatorLayer.superlayer == nil {
|
if self.indefiniteReplicatorLayer.superlayer == nil {
|
||||||
self.addSublayer(self.indefiniteReplicatorLayer)
|
self.addSublayer(self.indefiniteReplicatorLayer)
|
||||||
|
if animateIn {
|
||||||
|
self.indefiniteReplicatorLayer.opacity = 1.0
|
||||||
|
self.indefiniteReplicatorLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if self.indefiniteReplicatorLayer.animation(forKey: "rotation") == nil {
|
if self.indefiniteReplicatorLayer.animation(forKey: "rotation") == nil {
|
||||||
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||||
@ -882,19 +906,32 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
case .loading:
|
case .loading:
|
||||||
progressLayer.update(size: progressFrame.size, lineWidth: indicatorLineUnseenWidth, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, value: .indefinite, transition: transition)
|
progressLayer.update(size: progressFrame.size, lineWidth: indicatorLineUnseenWidth, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, value: .indefinite, transition: transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.indicatorShapeSeenLayer.opacity = 0.0
|
self.indicatorShapeSeenLayer.opacity = 0.0
|
||||||
self.indicatorShapeUnseenLayer.opacity = 0.0
|
self.indicatorShapeUnseenLayer.opacity = 0.0
|
||||||
|
|
||||||
|
if let previousComponent = previousComponent, previousComponent.ringAnimation == nil {
|
||||||
|
progressLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
self.indicatorShapeSeenLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||||
|
self.indicatorShapeUnseenLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.indicatorShapeSeenLayer.opacity = 1.0
|
self.indicatorShapeSeenLayer.opacity = 1.0
|
||||||
self.indicatorShapeUnseenLayer.opacity = 1.0
|
self.indicatorShapeUnseenLayer.opacity = 1.0
|
||||||
|
|
||||||
if let progressLayer = self.progressLayer {
|
if let progressLayer = self.progressLayer {
|
||||||
|
self.indicatorShapeSeenLayer.opacity = 1.0
|
||||||
|
self.indicatorShapeUnseenLayer.opacity = 1.0
|
||||||
|
|
||||||
|
self.indicatorShapeSeenLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
self.indicatorShapeUnseenLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
|
||||||
self.progressLayer = nil
|
self.progressLayer = nil
|
||||||
if transition.animation.isImmediate {
|
if transition.animation.isImmediate {
|
||||||
progressLayer.reset()
|
progressLayer.reset()
|
||||||
progressLayer.removeFromSuperlayer()
|
progressLayer.removeFromSuperlayer()
|
||||||
} else {
|
} else {
|
||||||
progressLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak progressLayer] _ in
|
progressLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak progressLayer] _ in
|
||||||
progressLayer?.reset()
|
progressLayer?.reset()
|
||||||
progressLayer?.removeFromSuperlayer()
|
progressLayer?.removeFromSuperlayer()
|
||||||
})
|
})
|
||||||
|
|||||||
BIN
submodules/TelegramUI/Resources/Animations/anim_story_more.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/anim_story_more.tgs
Normal file
Binary file not shown.
@ -27,6 +27,7 @@ import ContextUI
|
|||||||
import TelegramCallsUI
|
import TelegramCallsUI
|
||||||
import AuthorizationUI
|
import AuthorizationUI
|
||||||
import ChatListUI
|
import ChatListUI
|
||||||
|
import StoryContainerScreen
|
||||||
|
|
||||||
final class UnauthorizedApplicationContext {
|
final class UnauthorizedApplicationContext {
|
||||||
let sharedContext: SharedAccountContextImpl
|
let sharedContext: SharedAccountContextImpl
|
||||||
@ -854,7 +855,18 @@ final class AuthorizedApplicationContext {
|
|||||||
|
|
||||||
func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?) {
|
func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?) {
|
||||||
if let storyId {
|
if let storyId {
|
||||||
if let chatListController = self.rootController.chatListController as? ChatListControllerImpl {
|
var controllers = self.rootController.viewControllers
|
||||||
|
controllers = controllers.filter { c in
|
||||||
|
if c is StoryContainerScreen {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
self.rootController.setViewControllers(controllers, animated: false)
|
||||||
|
|
||||||
|
self.rootController.chatListController?.openStoriesFromNotification(peerId: storyId.peerId, storyId: storyId.id)
|
||||||
|
|
||||||
|
/*if let chatListController = self.rootController.chatListController as? ChatListControllerImpl {
|
||||||
let _ = (chatListController.context.account.postbox.transaction { transaction -> Bool in
|
let _ = (chatListController.context.account.postbox.transaction { transaction -> Bool in
|
||||||
if let peer = transaction.getPeer(storyId.peerId) as? TelegramUser, let storiesHidden = peer.storiesHidden, storiesHidden {
|
if let peer = transaction.getPeer(storyId.peerId) as? TelegramUser, let storiesHidden = peer.storiesHidden, storiesHidden {
|
||||||
return true
|
return true
|
||||||
@ -902,7 +914,7 @@ final class AuthorizedApplicationContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}*/
|
||||||
} else {
|
} else {
|
||||||
var visiblePeerId: PeerId?
|
var visiblePeerId: PeerId?
|
||||||
if let controller = self.rootController.topViewController as? ChatControllerImpl, controller.chatLocation.peerId == peerId, controller.chatLocation.threadId == threadId {
|
if let controller = self.rootController.topViewController as? ChatControllerImpl, controller.chatLocation.peerId == peerId, controller.chatLocation.threadId == threadId {
|
||||||
|
|||||||
@ -1564,11 +1564,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isSending {
|
if isSending {
|
||||||
if case .progress = streamingState {
|
streamingState = .none
|
||||||
} else {
|
|
||||||
let adjustedProgress: CGFloat = 0.027
|
|
||||||
streamingState = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0))
|
|
||||||
}
|
|
||||||
|
|
||||||
if case .progress = state {
|
if case .progress = state {
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -419,7 +419,7 @@ public func fetchVideoLibraryMediaResource(account: Account, resource: VideoLibr
|
|||||||
var value = stat()
|
var value = stat()
|
||||||
if stat(result.fileURL.path, &value) == 0 {
|
if stat(result.fileURL.path, &value) == 0 {
|
||||||
let remuxedTempFile = TempBox.shared.tempFile(fileName: "video.mp4")
|
let remuxedTempFile = TempBox.shared.tempFile(fileName: "video.mp4")
|
||||||
if let size = fileSize(result.fileURL.path), size <= 32 * 1024 * 1024, FFMpegRemuxer.remux(result.fileURL.path, to: remuxedTempFile.path) {
|
if !"".isEmpty, let size = fileSize(result.fileURL.path), size <= 32 * 1024 * 1024, FFMpegRemuxer.remux(result.fileURL.path, to: remuxedTempFile.path) {
|
||||||
TempBox.shared.dispose(tempFile)
|
TempBox.shared.dispose(tempFile)
|
||||||
subscriber.putNext(.moveTempFile(file: remuxedTempFile))
|
subscriber.putNext(.moveTempFile(file: remuxedTempFile))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -74,18 +74,21 @@ private enum Knob {
|
|||||||
case right
|
case right
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class TextSelectionGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate {
|
public final class TextSelectionGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate {
|
||||||
private var longTapTimer: Timer?
|
private var longTapTimer: Timer?
|
||||||
private var movingKnob: (Knob, CGPoint, CGPoint)?
|
private var movingKnob: (Knob, CGPoint, CGPoint)?
|
||||||
private var currentLocation: CGPoint?
|
private var currentLocation: CGPoint?
|
||||||
|
|
||||||
var beginSelection: ((CGPoint) -> Void)?
|
public var canBeginSelection: ((CGPoint) -> Bool)?
|
||||||
var knobAtPoint: ((CGPoint) -> (Knob, CGPoint)?)?
|
public var beginSelection: ((CGPoint) -> Void)?
|
||||||
var moveKnob: ((Knob, CGPoint) -> Void)?
|
fileprivate var knobAtPoint: ((CGPoint) -> (Knob, CGPoint)?)?
|
||||||
var finishedMovingKnob: (() -> Void)?
|
fileprivate var moveKnob: ((Knob, CGPoint) -> Void)?
|
||||||
var clearSelection: (() -> Void)?
|
public var finishedMovingKnob: (() -> Void)?
|
||||||
|
public var clearSelection: (() -> Void)?
|
||||||
|
public private(set) var didRecognizeTap: Bool = false
|
||||||
|
fileprivate var isSelecting: Bool = false
|
||||||
|
|
||||||
override init(target: Any?, action: Selector?) {
|
override public init(target: Any?, action: Selector?) {
|
||||||
super.init(target: nil, action: nil)
|
super.init(target: nil, action: nil)
|
||||||
|
|
||||||
self.delegate = self
|
self.delegate = self
|
||||||
@ -101,7 +104,7 @@ private final class TextSelectionGestureRecognizer: UIGestureRecognizer, UIGestu
|
|||||||
self.currentLocation = nil
|
self.currentLocation = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
super.touchesBegan(touches, with: event)
|
super.touchesBegan(touches, with: event)
|
||||||
|
|
||||||
let currentLocation = touches.first?.location(in: self.view)
|
let currentLocation = touches.first?.location(in: self.view)
|
||||||
@ -112,28 +115,32 @@ private final class TextSelectionGestureRecognizer: UIGestureRecognizer, UIGestu
|
|||||||
self.movingKnob = (knob, knobPosition, currentLocation)
|
self.movingKnob = (knob, knobPosition, currentLocation)
|
||||||
cancelScrollViewGestures(view: self.view?.superview)
|
cancelScrollViewGestures(view: self.view?.superview)
|
||||||
self.state = .began
|
self.state = .began
|
||||||
} else if self.longTapTimer == nil {
|
} else if self.canBeginSelection?(currentLocation) ?? true {
|
||||||
final class TimerTarget: NSObject {
|
if self.longTapTimer == nil {
|
||||||
let f: () -> Void
|
final class TimerTarget: NSObject {
|
||||||
|
let f: () -> Void
|
||||||
|
|
||||||
init(_ f: @escaping () -> Void) {
|
init(_ f: @escaping () -> Void) {
|
||||||
self.f = f
|
self.f = f
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func event() {
|
@objc func event() {
|
||||||
self.f()
|
self.f()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
let longTapTimer = Timer(timeInterval: 0.3, target: TimerTarget({ [weak self] in
|
||||||
|
self?.longTapEvent()
|
||||||
|
}), selector: #selector(TimerTarget.event), userInfo: nil, repeats: false)
|
||||||
|
self.longTapTimer = longTapTimer
|
||||||
|
RunLoop.main.add(longTapTimer, forMode: .common)
|
||||||
}
|
}
|
||||||
let longTapTimer = Timer(timeInterval: 0.3, target: TimerTarget({ [weak self] in
|
} else {
|
||||||
self?.longTapEvent()
|
self.state = .failed
|
||||||
}), selector: #selector(TimerTarget.event), userInfo: nil, repeats: false)
|
|
||||||
self.longTapTimer = longTapTimer
|
|
||||||
RunLoop.main.add(longTapTimer, forMode: .common)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
super.touchesMoved(touches, with: event)
|
super.touchesMoved(touches, with: event)
|
||||||
|
|
||||||
let currentLocation = touches.first?.location(in: self.view)
|
let currentLocation = touches.first?.location(in: self.view)
|
||||||
@ -144,12 +151,20 @@ private final class TextSelectionGestureRecognizer: UIGestureRecognizer, UIGestu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
super.touchesEnded(touches, with: event)
|
super.touchesEnded(touches, with: event)
|
||||||
|
|
||||||
if let longTapTimer = self.longTapTimer {
|
if let longTapTimer = self.longTapTimer {
|
||||||
self.longTapTimer = nil
|
self.longTapTimer = nil
|
||||||
longTapTimer.invalidate()
|
longTapTimer.invalidate()
|
||||||
|
|
||||||
|
if self.isSelecting {
|
||||||
|
self.didRecognizeTap = true
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.didRecognizeTap = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.clearSelection?()
|
self.clearSelection?()
|
||||||
} else {
|
} else {
|
||||||
if let _ = self.currentLocation, let _ = self.movingKnob {
|
if let _ = self.currentLocation, let _ = self.movingKnob {
|
||||||
@ -159,7 +174,7 @@ private final class TextSelectionGestureRecognizer: UIGestureRecognizer, UIGestu
|
|||||||
self.state = .ended
|
self.state = .ended
|
||||||
}
|
}
|
||||||
|
|
||||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
super.touchesCancelled(touches, with: event)
|
super.touchesCancelled(touches, with: event)
|
||||||
|
|
||||||
self.state = .cancelled
|
self.state = .cancelled
|
||||||
@ -172,12 +187,11 @@ private final class TextSelectionGestureRecognizer: UIGestureRecognizer, UIGestu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 9.0, *)
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive press: UIPress) -> Bool {
|
||||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive press: UIPress) -> Bool {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,6 +217,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||||||
private let strings: PresentationStrings
|
private let strings: PresentationStrings
|
||||||
private let textNode: TextNode
|
private let textNode: TextNode
|
||||||
private let updateIsActive: (Bool) -> Void
|
private let updateIsActive: (Bool) -> Void
|
||||||
|
public var canBeginSelection: (CGPoint) -> Bool = { _ in true }
|
||||||
public var updateRange: ((NSRange?) -> Void)?
|
public var updateRange: ((NSRange?) -> Void)?
|
||||||
private let present: (ViewController, Any?) -> Void
|
private let present: (ViewController, Any?) -> Void
|
||||||
private weak var rootNode: ASDisplayNode?
|
private weak var rootNode: ASDisplayNode?
|
||||||
@ -216,9 +231,15 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||||||
|
|
||||||
public let highlightAreaNode: ASDisplayNode
|
public let highlightAreaNode: ASDisplayNode
|
||||||
|
|
||||||
private var recognizer: TextSelectionGestureRecognizer?
|
public private(set) var recognizer: TextSelectionGestureRecognizer?
|
||||||
private var displayLinkAnimator: DisplayLinkAnimator?
|
private var displayLinkAnimator: DisplayLinkAnimator?
|
||||||
|
|
||||||
|
public var enableLookup: Bool = true
|
||||||
|
|
||||||
|
public var didRecognizeTap: Bool {
|
||||||
|
return self.recognizer?.didRecognizeTap ?? false
|
||||||
|
}
|
||||||
|
|
||||||
public init(theme: TextSelectionTheme, strings: PresentationStrings, textNode: TextNode, updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootNode: ASDisplayNode, performAction: @escaping (NSAttributedString, TextSelectionAction) -> Void) {
|
public init(theme: TextSelectionTheme, strings: PresentationStrings, textNode: TextNode, updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootNode: ASDisplayNode, performAction: @escaping (NSAttributedString, TextSelectionAction) -> Void) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
@ -332,12 +353,19 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
strongSelf.updateSelection(range: resultRange, animateIn: true)
|
strongSelf.updateSelection(range: resultRange, animateIn: true)
|
||||||
strongSelf.displayMenu()
|
strongSelf.displayMenu()
|
||||||
|
strongSelf.recognizer?.isSelecting = true
|
||||||
strongSelf.updateIsActive(true)
|
strongSelf.updateIsActive(true)
|
||||||
}
|
}
|
||||||
recognizer.clearSelection = { [weak self] in
|
recognizer.clearSelection = { [weak self] in
|
||||||
self?.dismissSelection()
|
self?.dismissSelection()
|
||||||
self?.updateIsActive(false)
|
self?.updateIsActive(false)
|
||||||
}
|
}
|
||||||
|
recognizer.canBeginSelection = { [weak self] point in
|
||||||
|
guard let self else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return self.canBeginSelection(point)
|
||||||
|
}
|
||||||
self.recognizer = recognizer
|
self.recognizer = recognizer
|
||||||
self.view.addGestureRecognizer(recognizer)
|
self.view.addGestureRecognizer(recognizer)
|
||||||
}
|
}
|
||||||
@ -487,9 +515,15 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||||||
|
|
||||||
private func dismissSelection() {
|
private func dismissSelection() {
|
||||||
self.currentRange = nil
|
self.currentRange = nil
|
||||||
|
self.recognizer?.isSelecting = false
|
||||||
self.updateSelection(range: nil, animateIn: false)
|
self.updateSelection(range: nil, animateIn: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func cancelSelection() {
|
||||||
|
self.dismissSelection()
|
||||||
|
self.updateIsActive(false)
|
||||||
|
}
|
||||||
|
|
||||||
private func displayMenu() {
|
private func displayMenu() {
|
||||||
guard let currentRects = self.currentRects, !currentRects.isEmpty, let currentRange = self.currentRange, let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else {
|
guard let currentRects = self.currentRects, !currentRects.isEmpty, let currentRange = self.currentRange, let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else {
|
||||||
return
|
return
|
||||||
@ -529,16 +563,18 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||||||
var actions: [ContextMenuAction] = []
|
var actions: [ContextMenuAction] = []
|
||||||
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in
|
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in
|
||||||
self?.performAction(string, .copy)
|
self?.performAction(string, .copy)
|
||||||
self?.dismissSelection()
|
self?.cancelSelection()
|
||||||
}))
|
|
||||||
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuLookUp, accessibilityLabel: self.strings.Conversation_ContextMenuLookUp), action: { [weak self] in
|
|
||||||
self?.performAction(string, .lookup)
|
|
||||||
self?.dismissSelection()
|
|
||||||
}))
|
}))
|
||||||
|
if self.enableLookup {
|
||||||
|
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuLookUp, accessibilityLabel: self.strings.Conversation_ContextMenuLookUp), action: { [weak self] in
|
||||||
|
self?.performAction(string, .lookup)
|
||||||
|
self?.cancelSelection()
|
||||||
|
}))
|
||||||
|
}
|
||||||
if #available(iOS 15.0, *) {
|
if #available(iOS 15.0, *) {
|
||||||
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in
|
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in
|
||||||
self?.performAction(string, .translate)
|
self?.performAction(string, .translate)
|
||||||
self?.dismissSelection()
|
self?.cancelSelection()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
// if isSpeakSelectionEnabled() {
|
// if isSpeakSelectionEnabled() {
|
||||||
@ -549,7 +585,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||||||
// }
|
// }
|
||||||
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in
|
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in
|
||||||
self?.performAction(string, .share)
|
self?.performAction(string, .share)
|
||||||
self?.dismissSelection()
|
self?.cancelSelection()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
self.present(ContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false), ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
self.present(ContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false), ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Subproject commit ebc8452d3a0d0bc18822b4eb6ca61ec8fa7a70a5
|
Subproject commit 8ca3a90988bba9e139efe0e9ba24f344f5a91da5
|
||||||
@ -990,6 +990,8 @@ public class TranslateScreen: ViewController {
|
|||||||
public var pushController: (ViewController) -> Void = { _ in }
|
public var pushController: (ViewController) -> Void = { _ in }
|
||||||
public var presentController: (ViewController) -> Void = { _ in }
|
public var presentController: (ViewController) -> Void = { _ in }
|
||||||
|
|
||||||
|
public var wasDismissed: (() -> Void)?
|
||||||
|
|
||||||
public convenience init(context: AccountContext, text: String, canCopy: Bool, fromLanguage: String?, toLanguage: String? = nil, isExpanded: Bool = false) {
|
public convenience init(context: AccountContext, text: String, canCopy: Bool, fromLanguage: String?, toLanguage: String? = nil, isExpanded: Bool = false) {
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
@ -1086,13 +1088,16 @@ public class TranslateScreen: ViewController {
|
|||||||
|
|
||||||
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||||
self.view.endEditing(true)
|
self.view.endEditing(true)
|
||||||
|
let wasDismissed = self.wasDismissed
|
||||||
if flag {
|
if flag {
|
||||||
self.node.animateOut(completion: {
|
self.node.animateOut(completion: {
|
||||||
super.dismiss(animated: false, completion: {})
|
super.dismiss(animated: false, completion: {})
|
||||||
|
wasDismissed?()
|
||||||
completion?()
|
completion?()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
super.dismiss(animated: false, completion: {})
|
super.dismiss(animated: false, completion: {})
|
||||||
|
wasDismissed?()
|
||||||
completion?()
|
completion?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"app": "9.6.6",
|
"app": "9.6.7",
|
||||||
"bazel": "6.1.1",
|
"bazel": "6.1.1",
|
||||||
"xcode": "14.2"
|
"xcode": "14.2"
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user