Scheduled Messages Improvements

This commit is contained in:
Ilya Laktyushin 2019-08-09 18:58:58 +03:00
parent 19c1ee8c44
commit 559cc51e00
47 changed files with 3986 additions and 3573 deletions

View File

@ -4594,12 +4594,27 @@ Any member of this group will be able to see messages in the channel.";
"Conversation.ScheduleMessage.Title" = "Schedule Message";
"Conversation.ScheduleMessage.SendToday" = "Send today at %@";
"Conversation.ScheduleMessage.SendTomorrow" = "Send tomorrow at %@";
"Conversation.ScheduleMessage.SendOn" = "Send on %@ at %@";
"Conversation.SetReminder.Title" = "Set a Reminder";
"Conversation.SetReminder.RemindToday" = "Remind today at %@";
"Conversation.SetReminder.RemindTomorrow" = "Remind tomorrow at %@";
"Conversation.SetReminder.RemindOn" = "Remind on %@ at %@";
"ScheduledMessages.Title" = "Scheduled Messages";
"ScheduledMessages.RemindersTitle" = "Reminders";
"ScheduledMessages.ScheduledDate" = "Scheduled for %@";
"ScheduledMessages.SendNow" = "Send Now";
"ScheduledMessages.EditTime" = "Edit Time";
"ScheduledMessages.EditTime" = "Reschedule";
"ScheduledMessages.ClearAll" = "Clear All";
"ScheduledMessages.Delete" = "Delete";
"Conversation.SendMessage.SetReminder" = "Set a Reminder";
"Conversation.SelectedMessages_1" = "%@ Message Selected";
"Conversation.SelectedMessages_2" = "%@ Messages Selected";
"Conversation.SelectedMessages_3_10" = "%@ Messages Selected";
"Conversation.SelectedMessages_any" = "%@ Messages Selected";
"Conversation.SelectedMessages_many" = "%@ Messages Selected";
"Conversation.SelectedMessages_0" = "%@ Messages Selected";

View File

@ -604,7 +604,7 @@ final class MessageHistoryTable: Table {
func topMessage(_ peerId: PeerId) -> IntermediateMessage? {
var topIndex: MessageIndex?
for namespace in self.messageHistoryIndexTable.existingNamespaces(peerId: peerId) {
for namespace in self.messageHistoryIndexTable.existingNamespaces(peerId: peerId) where self.seedConfiguration.chatMessagesNamespaces.contains(namespace) {
self.valueBox.range(self.table, start: self.upperBound(peerId: peerId, namespace: namespace), end: self.lowerBound(peerId: peerId, namespace: namespace), keys: { key in
let index = extractKey(key)
if let topIndexValue = topIndex {

View File

@ -1788,13 +1788,14 @@ public final class Postbox {
fileprivate func getTopPeerMessageIndex(peerId: PeerId) -> MessageIndex? {
var indices: [MessageIndex] = []
for namespace in self.messageHistoryIndexTable.existingNamespaces(peerId: peerId) {
for namespace in self.messageHistoryIndexTable.existingNamespaces(peerId: peerId) where self.seedConfiguration.chatMessagesNamespaces.contains(namespace) {
if let index = self.messageHistoryTable.topIndexEntry(peerId: peerId, namespace: namespace) {
indices.append(index)
}
}
return indices.max()
}
fileprivate func getPeerChatListInclusion(_ id: PeerId) -> PeerChatListInclusion {
if let inclusion = self.currentUpdatedChatListInclusions[id] {
return inclusion

View File

@ -21,8 +21,9 @@ public final class SeedConfiguration {
public let additionalChatListIndexNamespace: MessageId.Namespace?
public let messageNamespacesRequiringGroupStatsValidation: Set<MessageId.Namespace>
public let defaultMessageNamespaceReadStates: [MessageId.Namespace: PeerReadState]
public let chatMessagesNamespaces: Set<MessageId.Namespace>
public init(globalMessageIdsPeerIdNamespaces: Set<GlobalMessageIdsNamespace>, initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?), messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]], existingMessageTags: MessageTags, messageTagsWithSummary: MessageTags, existingGlobalMessageTags: GlobalMessageTags, peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace], peerSummaryCounterTags: @escaping (Peer) -> PeerSummaryCounterTags, additionalChatListIndexNamespace: MessageId.Namespace?, messageNamespacesRequiringGroupStatsValidation: Set<MessageId.Namespace>, defaultMessageNamespaceReadStates: [MessageId.Namespace: PeerReadState]) {
public init(globalMessageIdsPeerIdNamespaces: Set<GlobalMessageIdsNamespace>, initializeChatListWithHole: (topLevel: ChatListHole?, groups: ChatListHole?), messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set<MessageTags>]], existingMessageTags: MessageTags, messageTagsWithSummary: MessageTags, existingGlobalMessageTags: GlobalMessageTags, peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace], peerSummaryCounterTags: @escaping (Peer) -> PeerSummaryCounterTags, additionalChatListIndexNamespace: MessageId.Namespace?, messageNamespacesRequiringGroupStatsValidation: Set<MessageId.Namespace>, defaultMessageNamespaceReadStates: [MessageId.Namespace: PeerReadState], chatMessagesNamespaces: Set<MessageId.Namespace>) {
self.globalMessageIdsPeerIdNamespaces = globalMessageIdsPeerIdNamespaces
self.initializeChatListWithHole = initializeChatListWithHole
self.messageHoles = messageHoles
@ -33,5 +34,6 @@ public final class SeedConfiguration {
self.additionalChatListIndexNamespace = additionalChatListIndexNamespace
self.messageNamespacesRequiringGroupStatsValidation = messageNamespacesRequiringGroupStatsValidation
self.defaultMessageNamespaceReadStates = defaultMessageNamespaceReadStates
self.chatMessagesNamespaces = chatMessagesNamespaces
}
}

View File

@ -98,7 +98,7 @@ class ChatListTableTests: XCTestCase {
path = NSTemporaryDirectory() + "\(randomId)"
self.valueBox = SqliteValueBox(basePath: path!, queue: Queue.mainQueue())
let seedConfiguration = SeedConfiguration(initializeChatListWithHole: (topLevel: nil, groups: nil), initializeMessageNamespacesWithHoles: [], existingMessageTags: [], messageTagsWithSummary: [], existingGlobalMessageTags: [], peerNamespacesRequiringMessageTextIndex: [], peerSummaryCounterTags: { _ in PeerSummaryCounterTags(rawValue: 0) }, additionalChatListIndexNamespace: nil)
let seedConfiguration = SeedConfiguration(initializeChatListWithHole: (topLevel: nil, groups: nil), initializeMessageNamespacesWithHoles: [], existingMessageTags: [], messageTagsWithSummary: [], existingGlobalMessageTags: [], peerNamespacesRequiringMessageTextIndex: [], peerSummaryCounterTags: { _ in PeerSummaryCounterTags(rawValue: 0) }, additionalChatListIndexNamespace: nil, chatMessagesNamespaces: Set())
self.globalMessageIdsTable = GlobalMessageIdsTable(valueBox: self.valueBox!, table: GlobalMessageIdsTable.tableSpec(7), namespace: namespace)
self.historyMetadataTable = MessageHistoryMetadataTable(valueBox: self.valueBox!, table: MessageHistoryMetadataTable.tableSpec(8))

View File

@ -53,7 +53,7 @@ class MessageHistoryIndexTableTests: XCTestCase {
]
]
let seedConfiguration = SeedConfiguration(globalMessageIdsPeerIdNamespaces: Set(), initializeChatListWithHole: (topLevel: nil, groups: nil), messageHoles: messageHoles, existingMessageTags: [.media], messageTagsWithSummary: [], existingGlobalMessageTags: [], peerNamespacesRequiringMessageTextIndex: [], peerSummaryCounterTags: { _ in PeerSummaryCounterTags(rawValue: 0) }, additionalChatListIndexNamespace: nil, messageNamespacesRequiringGroupStatsValidation: Set())
let seedConfiguration = SeedConfiguration(globalMessageIdsPeerIdNamespaces: Set(), initializeChatListWithHole: (topLevel: nil, groups: nil), messageHoles: messageHoles, existingMessageTags: [.media], messageTagsWithSummary: [], existingGlobalMessageTags: [], peerNamespacesRequiringMessageTextIndex: [], peerSummaryCounterTags: { _ in PeerSummaryCounterTags(rawValue: 0) }, additionalChatListIndexNamespace: nil, messageNamespacesRequiringGroupStatsValidation: Set(), chatMessagesNamespaces: Set())
self.postbox = Postbox(queue: Queue.mainQueue(), basePath: path!, seedConfiguration: seedConfiguration, valueBox: self.valueBox!)
}

View File

@ -302,7 +302,7 @@ class MessageHistoryTableTests: XCTestCase {
path = NSTemporaryDirectory() + "\(randomId)"
self.valueBox = SqliteValueBox(basePath: path!, queue: Queue.mainQueue())
let seedConfiguration = SeedConfiguration(initializeChatListWithHole: (topLevel: nil, groups: nil), initializeMessageNamespacesWithHoles: [], existingMessageTags: [.First, .Second, .Summary], messageTagsWithSummary: [.Summary], existingGlobalMessageTags: [], peerNamespacesRequiringMessageTextIndex: [], additionalChatListIndexNamespace: nil)
let seedConfiguration = SeedConfiguration(initializeChatListWithHole: (topLevel: nil, groups: nil), initializeMessageNamespacesWithHoles: [], existingMessageTags: [.First, .Second, .Summary], messageTagsWithSummary: [.Summary], existingGlobalMessageTags: [], peerNamespacesRequiringMessageTextIndex: [], additionalChatListIndexNamespace: nil, chatMessagesNamespaces: Set())
self.globalMessageIdsTable = GlobalMessageIdsTable(valueBox: self.valueBox!, table: GlobalMessageIdsTable.tableSpec(5), namespace: namespace)
self.historyMetadataTable = MessageHistoryMetadataTable(valueBox: self.valueBox!, table: MessageHistoryMetadataTable.tableSpec(7))

View File

@ -67,7 +67,7 @@ class MessageHistoryViewTests: XCTestCase {
]
]
let seedConfiguration = SeedConfiguration(globalMessageIdsPeerIdNamespaces: Set(), initializeChatListWithHole: (topLevel: nil, groups: nil), messageHoles: messageHoles, existingMessageTags: [], messageTagsWithSummary: [], existingGlobalMessageTags: [], peerNamespacesRequiringMessageTextIndex: [], peerSummaryCounterTags: { _ in PeerSummaryCounterTags(rawValue: 0) }, additionalChatListIndexNamespace: nil, messageNamespacesRequiringGroupStatsValidation: Set())
let seedConfiguration = SeedConfiguration(globalMessageIdsPeerIdNamespaces: Set(), initializeChatListWithHole: (topLevel: nil, groups: nil), messageHoles: messageHoles, existingMessageTags: [], messageTagsWithSummary: [], existingGlobalMessageTags: [], peerNamespacesRequiringMessageTextIndex: [], peerSummaryCounterTags: { _ in PeerSummaryCounterTags(rawValue: 0) }, additionalChatListIndexNamespace: nil, messageNamespacesRequiringGroupStatsValidation: Set(), chatMessagesNamespaces: Set())
self.postbox = Postbox(queue: Queue.mainQueue(), basePath: path!, seedConfiguration: seedConfiguration, valueBox: self.valueBox!)
}

View File

@ -94,7 +94,7 @@ class OrderStatisticTreeTests: XCTestCase {
path = NSTemporaryDirectory() + "\(randomId)"
self.valueBox = SqliteValueBox(basePath: path!, queue: Queue.mainQueue())
let seedConfiguration = SeedConfiguration(initializeChatListWithHole: (topLevel: nil, groups: nil), initializeMessageNamespacesWithHoles: [], existingMessageTags: [.First, .Second], messageTagsWithSummary: [], existingGlobalMessageTags: [], peerNamespacesRequiringMessageTextIndex: [], peerSummaryCounterTags: { _ in PeerSummaryCounterTags(rawValue: 0) }, additionalChatListIndexNamespace: nil)
let seedConfiguration = SeedConfiguration(initializeChatListWithHole: (topLevel: nil, groups: nil), initializeMessageNamespacesWithHoles: [], existingMessageTags: [.First, .Second], messageTagsWithSummary: [], existingGlobalMessageTags: [], peerNamespacesRequiringMessageTextIndex: [], peerSummaryCounterTags: { _ in PeerSummaryCounterTags(rawValue: 0) }, additionalChatListIndexNamespace: nil, chatMessagesNamespaces: Set())
self.globalMessageIdsTable = GlobalMessageIdsTable(valueBox: self.valueBox!, table: GlobalMessageIdsTable.tableSpec(5), namespace: namespace)
self.historyMetadataTable = MessageHistoryMetadataTable(valueBox: self.valueBox!, table: MessageHistoryMetadataTable.tableSpec(7))

View File

@ -93,7 +93,7 @@ class ReadStateTableTests: XCTestCase {
path = NSTemporaryDirectory() + "\(randomId)"
self.valueBox = SqliteValueBox(basePath: path!, queue: Queue.mainQueue())
let seedConfiguration = SeedConfiguration(initializeChatListWithHole: (topLevel: nil, groups: nil), initializeMessageNamespacesWithHoles: [], existingMessageTags: [.First, .Second], messageTagsWithSummary: [], existingGlobalMessageTags: [], peerNamespacesRequiringMessageTextIndex: [], peerSummaryCounterTags: { _ in PeerSummaryCounterTags(rawValue: 0) }, additionalChatListIndexNamespace: nil)
let seedConfiguration = SeedConfiguration(initializeChatListWithHole: (topLevel: nil, groups: nil), initializeMessageNamespacesWithHoles: [], existingMessageTags: [.First, .Second], messageTagsWithSummary: [], existingGlobalMessageTags: [], peerNamespacesRequiringMessageTextIndex: [], peerSummaryCounterTags: { _ in PeerSummaryCounterTags(rawValue: 0) }, additionalChatListIndexNamespace: nil, chatMessagesNamespaces: Set())
self.globalMessageIdsTable = GlobalMessageIdsTable(valueBox: self.valueBox!, table: GlobalMessageIdsTable.tableSpec(5), namespace: namespace)
self.historyMetadataTable = MessageHistoryMetadataTable(valueBox: self.valueBox!, table: MessageHistoryMetadataTable.tableSpec(7))

View File

@ -494,7 +494,7 @@ public func chatMessageAnimatedSticker(postbox: Postbox, file: TelegramMediaFile
}
var thumbnailImage: (UIImage, UIImage)?
if fullSizeImage == nil, let thumbnailData = thumbnailData {
if fullSizeImage == nil, let thumbnailData = thumbnailData, fitzModifier == nil {
if let image = imageFromAJpeg(data: thumbnailData) {
thumbnailImage = image
}

View File

@ -272,7 +272,7 @@ let telegramPostboxSeedConfiguration: SeedConfiguration = {
} else {
return [.regularChatsAndPrivateGroups]
}
}, additionalChatListIndexNamespace: Namespaces.Message.Cloud, messageNamespacesRequiringGroupStatsValidation: [Namespaces.Message.Cloud], defaultMessageNamespaceReadStates: [Namespaces.Message.Local: .idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 0, markedUnread: false)])
}, additionalChatListIndexNamespace: Namespaces.Message.Cloud, messageNamespacesRequiringGroupStatsValidation: [Namespaces.Message.Cloud], defaultMessageNamespaceReadStates: [Namespaces.Message.Local: .idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 0, markedUnread: false)], chatMessagesNamespaces: Set([Namespaces.Message.Cloud, Namespaces.Message.Local, Namespaces.Message.SecretIncoming]))
}()
public enum AccountPreferenceEntriesResult {

View File

@ -148,6 +148,7 @@ private var declaredEncodables: Void = {
declareEncodable(CloudStickerPackThumbnailMediaResource.self, f: { CloudStickerPackThumbnailMediaResource(decoder: $0) })
declareEncodable(AccountBackupDataAttribute.self, f: { AccountBackupDataAttribute(decoder: $0) })
declareEncodable(ContentRequiresValidationMessageAttribute.self, f: { ContentRequiresValidationMessageAttribute(decoder: $0) })
declareEncodable(OutgoingScheduleInfoMessageAttribute.self, f: { OutgoingScheduleInfoMessageAttribute(decoder: $0) })
return
}()

View File

@ -924,8 +924,11 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo
}
updatedState.addMessages([message], location: .UpperHistoryBlock)
}
case let .updateServiceNotification(_, date, type, text, media, entities):
if let date = date {
case let .updateServiceNotification(flags, date, type, text, media, entities):
let popup = (flags & (1 << 0)) != 0
if popup {
updatedState.addDisplayAlert(text, isDropAuth: type.hasPrefix("AUTH_KEY_DROP_"))
} else if let date = date {
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 777000)
if updatedState.peers[peerId] == nil {
@ -969,8 +972,6 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo
let message = StoreMessage(peerId: peerId, namespace: Namespaces.Message.Local, globallyUniqueId: nil, groupingKey: nil, timestamp: date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: peerId, text: messageText, attributes: attributes, media: medias)
updatedState.addMessages([message], location: .UpperHistoryBlock)
}
} else {
updatedState.addDisplayAlert(text, isDropAuth: type.hasPrefix("AUTH_KEY_DROP_"))
}
case let .updateReadChannelInbox(_, folderId, channelId, maxId, stillUnreadCount, pts):
updatedState.resetIncomingReadState(groupId: PeerGroupId(rawValue: folderId ?? 0), peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), namespace: Namespaces.Message.Cloud, maxIncomingReadId: maxId, count: stillUnreadCount, pts: pts)
@ -1299,6 +1300,10 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo
peersNearby.append(PeerNearby(id: peer.peerId, expires: expires, distance: distance))
}
updatedState.updatePeersNearby(peersNearby)
case let .updateNewScheduledMessage(apiMessage):
if let message = StoreMessage(apiMessage: apiMessage, namespace: Namespaces.Message.CloudScheduled) {
updatedState.addMessages([message], location: .Random)
}
case let .updateDeleteScheduledMessages(peer, messages):
var messageIds: [MessageId] = []
for message in messages {

View File

@ -24,11 +24,11 @@ func managedAppChangelog(postbox: Postbox, network: Network, stateManager: Accou
}
|> mapToSignal { appChangelogState -> Signal<Void, NoError> in
let appChangelogState = appChangelogState
// if appChangelogState.checkedVersion == appVersion {
// return .complete()
// }
if appChangelogState.checkedVersion == appVersion {
return .complete()
}
let previousVersion = appChangelogState.previousVersion
return network.request(Api.functions.help.getAppChangelog(prevAppVersion: "5.9")) //previousVersion))
return network.request(Api.functions.help.getAppChangelog(prevAppVersion: previousVersion))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)

View File

@ -13,6 +13,13 @@ public struct HistoryPreloadIndex: Comparable {
public let isMuted: Bool
public let isPriority: Bool
public init(index: ChatListIndex?, hasUnread: Bool, isMuted: Bool, isPriority: Bool) {
self.index = index
self.hasUnread = hasUnread
self.isMuted = isMuted
self.isPriority = isPriority
}
public static func <(lhs: HistoryPreloadIndex, rhs: HistoryPreloadIndex) -> Bool {
if lhs.isPriority != rhs.isPriority {
if lhs.isPriority {

View File

@ -84,6 +84,8 @@ private func filterMessageAttributesForOutgoingMessage(_ attributes: [MessageAtt
return true
case _ as NotificationInfoMessageAttribute:
return true
case _ as OutgoingScheduleInfoMessageAttribute:
return true
default:
return false
}

View File

@ -21,43 +21,20 @@ public struct OutgoingContentInfoFlags: OptionSet {
public class OutgoingContentInfoMessageAttribute: MessageAttribute {
public let flags: OutgoingContentInfoFlags
public let scheduleTime: Int32?
public init(flags: OutgoingContentInfoFlags, scheduleTime: Int32?) {
public init(flags: OutgoingContentInfoFlags) {
self.flags = flags
self.scheduleTime = scheduleTime
}
required public init(decoder: PostboxDecoder) {
self.flags = OutgoingContentInfoFlags(rawValue: decoder.decodeInt32ForKey("f", orElse: 0))
self.scheduleTime = decoder.decodeOptionalInt32ForKey("t")
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.flags.rawValue, forKey: "f")
if let scheduleTime = self.scheduleTime {
encoder.encodeInt32(scheduleTime, forKey: "t")
} else {
encoder.encodeNil(forKey: "t")
}
}
public func withUpdatedFlags(_ flags: OutgoingContentInfoFlags) -> OutgoingContentInfoMessageAttribute {
return OutgoingContentInfoMessageAttribute(flags: flags, scheduleTime: self.scheduleTime)
}
public func withUpdatedScheduleTime(_ scheduleTime: Int32?) -> OutgoingContentInfoMessageAttribute {
return OutgoingContentInfoMessageAttribute(flags: self.flags, scheduleTime: scheduleTime)
}
}
public extension Message {
var scheduleTime: Int32? {
for attribute in self.attributes {
if let attribute = attribute as? OutgoingContentInfoMessageAttribute, let scheduleTime = attribute.scheduleTime {
return scheduleTime
}
}
return nil
return OutgoingContentInfoMessageAttribute(flags: flags)
}
}

View File

@ -0,0 +1,37 @@
import Foundation
#if os(macOS)
import PostboxMac
#else
import Postbox
#endif
public class OutgoingScheduleInfoMessageAttribute: MessageAttribute {
public let scheduleTime: Int32
public init(scheduleTime: Int32) {
self.scheduleTime = scheduleTime
}
required public init(decoder: PostboxDecoder) {
self.scheduleTime = decoder.decodeInt32ForKey("t", orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(scheduleTime, forKey: "t")
}
public func withUpdatedScheduleTime(_ scheduleTime: Int32) -> OutgoingScheduleInfoMessageAttribute {
return OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime)
}
}
public extension Message {
var scheduleTime: Int32? {
for attribute in self.attributes {
if let attribute = attribute as? OutgoingScheduleInfoMessageAttribute {
return attribute.scheduleTime
}
}
return nil
}
}

View File

@ -894,14 +894,13 @@ public final class PendingMessageManager {
if attribute.flags.contains(.disableLinkPreviews) {
flags |= Int32(1 << 1)
}
if let time = attribute.scheduleTime {
flags |= Int32(1 << 10)
scheduleTime = time
}
} else if let attribute = attribute as? NotificationInfoMessageAttribute {
if attribute.flags.contains(.muted) {
flags |= Int32(1 << 5)
}
} else if let attribute = attribute as? OutgoingScheduleInfoMessageAttribute {
flags |= Int32(1 << 10)
scheduleTime = attribute.scheduleTime
}
}

View File

@ -35,11 +35,11 @@ public enum RequestEditMessageError {
case restricted
}
public func requestEditMessage(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute? = nil, disableUrlPreview: Bool = false) -> Signal<RequestEditMessageResult, RequestEditMessageError> {
return requestEditMessageInternal(account: account, messageId: messageId, text: text, media: media, entities: entities, disableUrlPreview: disableUrlPreview, forceReupload: false)
public func requestEditMessage(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute? = nil, disableUrlPreview: Bool = false, scheduleTime: Int32? = nil) -> Signal<RequestEditMessageResult, RequestEditMessageError> {
return requestEditMessageInternal(account: account, messageId: messageId, text: text, media: media, entities: entities, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: false)
|> `catch` { error -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> in
if case .invalidReference = error {
return requestEditMessageInternal(account: account, messageId: messageId, text: text, media: media, entities: entities, disableUrlPreview: disableUrlPreview, forceReupload: true)
return requestEditMessageInternal(account: account, messageId: messageId, text: text, media: media, entities: entities, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: true)
} else {
return .fail(error)
}
@ -54,7 +54,7 @@ public func requestEditMessage(account: Account, messageId: MessageId, text: Str
}
}
private func requestEditMessageInternal(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool, forceReupload: Bool) -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> {
private func requestEditMessageInternal(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool, scheduleTime: Int32?, forceReupload: Bool) -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> {
let uploadedMedia: Signal<PendingMessageUploadedContentResult?, NoError>
switch media {
case .keep:
@ -95,9 +95,9 @@ private func requestEditMessageInternal(account: Account, messageId: MessageId,
pendingMediaContent = content.content
}
}
return account.postbox.transaction { transaction -> (Peer?, SimpleDictionary<PeerId, Peer>) in
return account.postbox.transaction { transaction -> (Peer?, Message?, SimpleDictionary<PeerId, Peer>) in
guard let message = transaction.getMessage(messageId) else {
return (nil, SimpleDictionary())
return (nil, nil, SimpleDictionary())
}
if text.isEmpty {
@ -106,7 +106,7 @@ private func requestEditMessageInternal(account: Account, messageId: MessageId,
case _ as TelegramMediaImage, _ as TelegramMediaFile:
break
default:
return (nil, SimpleDictionary())
return (nil, nil, SimpleDictionary())
}
}
}
@ -120,11 +120,11 @@ private func requestEditMessageInternal(account: Account, messageId: MessageId,
}
}
}
return (transaction.getPeer(messageId.peerId), peers)
return (transaction.getPeer(messageId.peerId), message, peers)
}
|> mapError { _ -> RequestEditMessageInternalError in return .error(.generic) }
|> mapToSignal { peer, associatedPeers -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> in
if let peer = peer, let inputPeer = apiInputPeer(peer) {
|> mapToSignal { peer, message, associatedPeers -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> in
if let peer = peer, let message = message, let inputPeer = apiInputPeer(peer) {
var flags: Int32 = 1 << 11
var apiEntities: [Api.MessageEntity]?
@ -150,7 +150,17 @@ private func requestEditMessageInternal(account: Account, messageId: MessageId,
flags |= Int32(1 << 14)
}
return account.network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: text, media: inputMedia, replyMarkup: nil, entities: apiEntities, scheduleDate: nil))
var effectiveScheduleTime: Int32?
if messageId.namespace == Namespaces.Message.CloudScheduled {
if let scheduleTime = scheduleTime {
effectiveScheduleTime = scheduleTime
} else {
effectiveScheduleTime = message.timestamp
}
flags |= Int32(1 << 15)
}
return account.network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: text, media: inputMedia, replyMarkup: nil, entities: apiEntities, scheduleDate: effectiveScheduleTime))
|> map { result -> Api.Updates? in
return result
}

View File

@ -0,0 +1,29 @@
import Foundation
#if os(macOS)
import PostboxMac
import SwiftSignalKitMac
import TelegramApiMac
#else
import Postbox
import SwiftSignalKit
import TelegramApi
#endif
public func sendScheduledMessageNow(account: Account, messageId: MessageId) -> Signal<Void, NoError> {
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
if let _ = transaction.getMessage(messageId), let peer = transaction.getPeer(messageId.peerId), let inputPeer = apiInputPeer(peer) {
return account.network.request(Api.functions.messages.sendScheduledMessages(peer: inputPeer, id: [messageId.id]))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
}
return .complete()
} |> switchToLatest
}

View File

@ -106,10 +106,9 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M
if attribute.flags.contains(.disableLinkPreviews) {
flags |= Int32(1 << 1)
}
if let time = attribute.scheduleTime {
flags |= Int32(1 << 10)
scheduleTime = time
}
} else if let attribute = attribute as? OutgoingScheduleInfoMessageAttribute {
flags |= Int32(1 << 10)
scheduleTime = attribute.scheduleTime
}
}

View File

@ -25,6 +25,8 @@
09EC0DE922C6825D00E7185B /* AppUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09EC0DE822C6825D00E7185B /* AppUpdate.swift */; };
09EDAD382213120C0012A50B /* AutodownloadSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09EDAD372213120C0012A50B /* AutodownloadSettings.swift */; };
09EDAD3A22131D010012A50B /* ManagedAutodownloadSettingsUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09EDAD3922131D010012A50B /* ManagedAutodownloadSettingsUpdates.swift */; };
09FC986B22FD882200915E37 /* OutgoingScheduleInfoMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09FC986A22FD882200915E37 /* OutgoingScheduleInfoMessageAttribute.swift */; };
09FC986D22FD99D400915E37 /* ScheduledMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09FC986C22FD99D400915E37 /* ScheduledMessages.swift */; };
9F06831021A40DEC001D8EDB /* NotificationExceptionsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06830F21A40DEC001D8EDB /* NotificationExceptionsList.swift */; };
9F06831121A40DEC001D8EDB /* NotificationExceptionsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06830F21A40DEC001D8EDB /* NotificationExceptionsList.swift */; };
9F10CE8C20613CDB002DD61A /* TelegramDeviceContactImportInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B2F7732052DEF700D3BFB9 /* TelegramDeviceContactImportInfo.swift */; };
@ -844,6 +846,8 @@
09EC0DE822C6825D00E7185B /* AppUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdate.swift; sourceTree = "<group>"; };
09EDAD372213120C0012A50B /* AutodownloadSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutodownloadSettings.swift; sourceTree = "<group>"; };
09EDAD3922131D010012A50B /* ManagedAutodownloadSettingsUpdates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedAutodownloadSettingsUpdates.swift; sourceTree = "<group>"; };
09FC986A22FD882200915E37 /* OutgoingScheduleInfoMessageAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingScheduleInfoMessageAttribute.swift; sourceTree = "<group>"; };
09FC986C22FD99D400915E37 /* ScheduledMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduledMessages.swift; sourceTree = "<group>"; };
9F06830F21A40DEC001D8EDB /* NotificationExceptionsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationExceptionsList.swift; sourceTree = "<group>"; };
9FC8ADAA206BBFF10094F7B4 /* RecentWebSessions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentWebSessions.swift; sourceTree = "<group>"; };
C205FEA71EB3B75900455808 /* ExportMessageLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExportMessageLink.swift; sourceTree = "<group>"; };
@ -1498,6 +1502,7 @@
C28725411EF967E700613564 /* NotificationInfoMessageAttribute.swift */,
C210DD611FBDB90800F673D8 /* SourceReferenceMessageAttribute.swift */,
D0439B5F228EDE430067E026 /* ContentRequiresValidationMessageAttribute.swift */,
09FC986A22FD882200915E37 /* OutgoingScheduleInfoMessageAttribute.swift */,
);
name = Attributes;
sourceTree = "<group>";
@ -1699,6 +1704,7 @@
D0E8B8B22044706300605593 /* ForwardGame.swift */,
D0119CAF20CA9EA800895300 /* MarkAllChatsAsRead.swift */,
D023E67721540624008C27D1 /* UpdateMessageMedia.swift */,
09FC986C22FD99D400915E37 /* ScheduledMessages.swift */,
);
name = Messages;
sourceTree = "<group>";
@ -2223,6 +2229,7 @@
D00DBBD71E64E41100DB5485 /* CreateSecretChat.swift in Sources */,
C2FD33EB1E696C78008D13D4 /* GroupsInCommon.swift in Sources */,
D0FA8BB01E1FEC7E001E855B /* SecretChatEncryptionConfig.swift in Sources */,
09FC986B22FD882200915E37 /* OutgoingScheduleInfoMessageAttribute.swift in Sources */,
D021E0DF1DB539FC00C6B04F /* StickerPack.swift in Sources */,
D03B0D091D62255C00955575 /* EnqueueMessage.swift in Sources */,
D0CA8E4B227209C4008A74C3 /* ManagedSynchronizeGroupMessageStats.swift in Sources */,
@ -2231,6 +2238,7 @@
D00D343C1E6EC9770057B307 /* TelegramMediaGame.swift in Sources */,
D033FEB01E61EB0200644997 /* PeerContactSettings.swift in Sources */,
D050F2511E4A59C200988324 /* JoinLink.swift in Sources */,
09FC986D22FD99D400915E37 /* ScheduledMessages.swift in Sources */,
D07827C91E02F59C00071108 /* InstantPage.swift in Sources */,
D0458C881E69B4AB00FB34C1 /* OutgoingContentInfoMessageAttribute.swift in Sources */,
D07827CB1E02F5B200071108 /* RichText.swift in Sources */,

View File

@ -11,38 +11,28 @@ import RLottie
import MediaResources
import MobileCoreServices
let colorKeyRegex = try? NSRegularExpression(pattern: "\"k\":\\[[\\d\\.]+\\,[\\d\\.]+\\,[\\d\\.]+\\,[\\d\\.]+\\]")
private func transformedWithFitzModifier(data: Data, fitzModifier: EmojiFitzModifier?) -> Data {
if let fitzModifier = fitzModifier, var string = String(data: data, encoding: .utf8) {
let color1: UIColor
let color2: UIColor
let color3: UIColor
let color4: UIColor
var colors: [UIColor] = [0xf77e41, 0xffb139, 0xffd140, 0xffdf79].map { UIColor(rgb: $0) }
let replacementColors: [UIColor]
switch fitzModifier {
case .type12:
color1 = UIColor(rgb: 0xca907a)
color2 = UIColor(rgb: 0xedc5a5)
color3 = UIColor(rgb: 0xf7e3c3)
color4 = UIColor(rgb: 0xfbefd6)
replacementColors = [0xca907a, 0xedc5a5, 0xf7e3c3, 0xfbefd6].map { UIColor(rgb: $0) }
case .type3:
color1 = UIColor(rgb: 0xaa7c60)
color2 = UIColor(rgb: 0xc8a987)
color3 = UIColor(rgb: 0xddc89f)
color4 = UIColor(rgb: 0xe6d6b2)
replacementColors = [0xaa7c60, 0xc8a987, 0xddc89f, 0xe6d6b2].map { UIColor(rgb: $0) }
case .type4:
color1 = UIColor(rgb: 0x8c6148)
color2 = UIColor(rgb: 0xad8562)
color3 = UIColor(rgb: 0xc49e76)
color4 = UIColor(rgb: 0xd4b188)
replacementColors = [0x8c6148, 0xad8562, 0xc49e76, 0xd4b188].map { UIColor(rgb: $0) }
case .type5:
color1 = UIColor(rgb: 0x6e3c2c)
color2 = UIColor(rgb: 0x925a34)
color3 = UIColor(rgb: 0xa16e46)
color4 = UIColor(rgb: 0xac7a52)
replacementColors = [0x6e3c2c, 0x925a34, 0xa16e46, 0xac7a52].map { UIColor(rgb: $0) }
case .type6:
color1 = UIColor(rgb: 0x291c12)
color2 = UIColor(rgb: 0x472a22)
color3 = UIColor(rgb: 0x573b30)
color4 = UIColor(rgb: 0x68493c)
replacementColors = [0x291c12, 0x472a22, 0x573b30, 0x68493c].map { UIColor(rgb: $0) }
}
func colorToString(_ color: UIColor) -> String {
@ -50,15 +40,48 @@ private func transformedWithFitzModifier(data: Data, fitzModifier: EmojiFitzModi
var g: CGFloat = 0.0
var b: CGFloat = 0.0
if color.getRed(&r, green: &g, blue: &b, alpha: nil) {
return "\(r),\(g),\(b)"
return "\"k\":[\(r),\(g),\(b),1]"
}
return ""
}
string = string.replacingOccurrences(of: "0.96862745285,0.494117647409,0.254901975393", with: colorToString(color1))
string = string.replacingOccurrences(of: "1,0.694117665291,0.223529413342", with: colorToString(color2))
string = string.replacingOccurrences(of: "1,0.819607853889,0.250980407", with: colorToString(color3))
string = string.replacingOccurrences(of: "1,0.874509811401,0.474509805441", with: colorToString(color4))
func match(_ a: Double, _ b: Double, eps: Double) -> Bool {
return abs(a - b) < eps
}
var replacements: [(NSTextCheckingResult, String)] = []
if let colorKeyRegex = colorKeyRegex {
let results = colorKeyRegex.matches(in: string, range: NSRange(string.startIndex..., in: string))
for result in results.reversed() {
if let range = Range(result.range, in: string) {
let substring = String(string[range])
let color = substring[substring.index(string.startIndex, offsetBy: "\"k\":[".count) ..< substring.index(before: substring.endIndex)]
let components = color.split(separator: ",")
if components.count == 4, let r = Double(components[0]), let g = Double(components[1]), let b = Double(components[2]), let a = Double(components[3]) {
if match(a, 1.0, eps: 0.01) {
for i in 0 ..< colors.count {
let color = colors[i]
var cr: CGFloat = 0.0
var cg: CGFloat = 0.0
var cb: CGFloat = 0.0
if color.getRed(&cr, green: &cg, blue: &cb, alpha: nil) {
if match(r, Double(cr), eps: 0.01) && match(g, Double(cg), eps: 0.01) && match(b, Double(cb), eps: 0.01) {
replacements.append((result, colorToString(replacementColors[i])))
}
}
}
}
}
}
}
}
for (result, text) in replacements {
if let range = Range(result.range, in: string) {
string = string.replacingCharacters(in: range, with: text)
}
}
return string.data(using: .utf8) ?? data
} else {

View File

@ -108,7 +108,7 @@ private func calculateSlowmodeActiveUntilTimestamp(account: Account, untilTimest
let ChatControllerCount = Atomic<Int32>(value: 0)
public enum ChatControllerSubject {
public enum ChatControllerSubject: Equatable {
case message(MessageId)
case scheduledMessages
}
@ -123,7 +123,7 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar
private let context: AccountContext
public let chatLocation: ChatLocation
private let subject: ChatControllerSubject?
public let subject: ChatControllerSubject?
private let botStart: ChatControllerInitialBotStart?
private let peerDisposable = MetaDisposable()
@ -1287,8 +1287,6 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar
switch strongSelf.chatLocation {
case let .peer(peerId):
strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, animated: true, completion: nil)
default:
break
}
}, requestRedeliveryOfFailedMessages: { [weak self] id in
guard let strongSelf = self else {
@ -1437,9 +1435,15 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar
}
}, scheduleCurrentMessage: { [weak self] in
if let strongSelf = self {
let controller = ChatScheduleTimeController(context: strongSelf.context, completion: { [weak self] time in
let mode: ChatScheduleTimeControllerMode
if case let .peer(peerId) = strongSelf.presentationInterfaceState.chatLocation, peerId == strongSelf.context.account.peerId {
mode = .reminders
} else {
mode = .scheduledMessages
}
let controller = ChatScheduleTimeController(context: strongSelf.context, mode: mode, completion: { [weak self] scheduleTime in
if let strongSelf = self {
strongSelf.chatDisplayNode.sendCurrentMessage(scheduleTime: time)
strongSelf.chatDisplayNode.sendCurrentMessage(scheduleTime: scheduleTime)
if !strongSelf.presentationInterfaceState.isScheduledMessages {
let controller = ChatController(context: strongSelf.context, chatLocation: strongSelf.chatLocation, subject: .scheduledMessages)
(strongSelf.navigationController as? NavigationController)?.pushViewController(controller)
@ -1449,6 +1453,50 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar
strongSelf.chatDisplayNode.dismissInput()
strongSelf.present(controller, in: .window(.root))
}
}, sendScheduledMessagesNow: { [weak self] messageIds in
if let strongSelf = self {
strongSelf.sendScheduledMessagesNow(messageIds)
}
}, editScheduledMessagesTime: { [weak self] messageIds in
if let strongSelf = self {
let mode: ChatScheduleTimeControllerMode
if case let .peer(peerId) = strongSelf.presentationInterfaceState.chatLocation, peerId == strongSelf.context.account.peerId {
mode = .reminders
} else {
mode = .scheduledMessages
}
let controller = ChatScheduleTimeController(context: strongSelf.context, mode: mode, completion: { [weak self] scheduleTime in
if let strongSelf = self, let messageId = messageIds.first {
let signal = (strongSelf.context.account.postbox.transaction { transaction -> Message? in
return transaction.getMessage(messageId)
}
|> introduceError(RequestEditMessageError.self)
|> mapToSignal({ message -> Signal<RequestEditMessageResult, RequestEditMessageError> in
if let message = message {
var entities: TextEntitiesMessageAttribute?
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
entities = attribute
break
}
}
return requestEditMessage(account: strongSelf.context.account, messageId: messageId, text: message.text, media: .keep
, entities: entities, disableUrlPreview: false, scheduleTime: scheduleTime)
} else {
return .complete()
}
}))
strongSelf.editMessageDisposable.set((signal |> deliverOnMainQueue).start(next: { result in
}, error: { error in
}))
}
})
strongSelf.chatDisplayNode.dismissInput()
strongSelf.present(controller, in: .window(.root))
}
}, requestMessageUpdate: { [weak self] id in
if let strongSelf = self {
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
@ -2442,7 +2490,7 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar
}
}
self.chatDisplayNode.sendMessages = { [weak self] messages, silentPosting, isAnyMessageTextPartitioned in
self.chatDisplayNode.sendMessages = { [weak self] messages, silentPosting, scheduleTime, isAnyMessageTextPartitioned in
if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation {
strongSelf.commitPurposefulAction()
@ -2471,6 +2519,8 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar
let transformedMessages: [EnqueueMessage]
if let silentPosting = silentPosting {
transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting)
} else if let scheduleTime = scheduleTime {
transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime)
} else {
transformedMessages = strongSelf.transformEnqueueMessages(messages)
}
@ -5175,18 +5225,25 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar
return transformEnqueueMessages(messages, silentPosting: silentPosting)
}
private func transformEnqueueMessages(_ messages: [EnqueueMessage], silentPosting: Bool) -> [EnqueueMessage] {
private func transformEnqueueMessages(_ messages: [EnqueueMessage], silentPosting: Bool, scheduleTime: Int32? = nil) -> [EnqueueMessage] {
return messages.map { message in
if silentPosting {
if silentPosting || scheduleTime != nil {
return message.withUpdatedAttributes { attributes in
var attributes = attributes
for i in 0 ..< attributes.count {
for i in (0 ..< attributes.count).reversed() {
if attributes[i] is NotificationInfoMessageAttribute {
attributes.remove(at: i)
break
}
if let _ = scheduleTime, attributes[i] is OutgoingScheduleInfoMessageAttribute {
attributes.remove(at: i)
}
}
attributes.append(NotificationInfoMessageAttribute(flags: .muted))
if silentPosting {
attributes.append(NotificationInfoMessageAttribute(flags: .muted))
}
if let scheduleTime = scheduleTime {
attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime))
}
return attributes
}
} else {
@ -5195,8 +5252,16 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar
}
}
private func sendMessages(_ messages: [EnqueueMessage]) {
if case let .peer(peerId) = self.chatLocation {
private func sendScheduledMessagesNow(_ messageId: [MessageId]) {
let _ = sendScheduledMessageNow(account: self.context.account, messageId: messageId.first!).start()
}
private func sendMessages(_ messages: [EnqueueMessage], commit: Bool = false) {
guard case let .peer(peerId) = self.chatLocation else {
return
}
if commit || !self.presentationInterfaceState.isScheduledMessages {
self.commitPurposefulAction()
let _ = (enqueueMessages(account: self.context.account, peerId: peerId, messages: self.transformEnqueueMessages(messages))
@ -5205,6 +5270,20 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar
})
self.donateIntent()
} else {
let mode: ChatScheduleTimeControllerMode
if peerId == self.context.account.peerId {
mode = .reminders
} else {
mode = .scheduledMessages
}
let controller = ChatScheduleTimeController(context: self.context, mode: mode, completion: { [weak self] time in
if let strongSelf = self {
strongSelf.sendMessages(strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: time), commit: true)
}
})
self.chatDisplayNode.dismissInput()
self.present(controller, in: .window(.root))
}
}
@ -5719,7 +5798,7 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar
}
}
if case let .peer(peerId) = self.chatLocation, let messageId = messageLocation.messageId, (messageId.peerId != peerId && !forceInCurrentChat) {
if case let .peer(peerId) = self.chatLocation, let messageId = messageLocation.messageId, (messageId.peerId != peerId && !forceInCurrentChat) || self.presentationInterfaceState.isScheduledMessages {
if let navigationController = self.navigationController as? NavigationController {
navigateToChatController(navigationController: navigationController, context: self.context, chatLocation: .peer(messageId.peerId), messageId: messageId, keepStack: .always)
}
@ -6866,13 +6945,17 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar
}
if options.contains(.deleteLocally) {
var localOptionText = self.presentationData.strings.Conversation_DeleteMessagesForMe
if options.contains(.unsendPersonal) {
localOptionText = self.presentationData.strings.Chat_DeleteMessagesConfirmation(Int32(messageIds.count))
} else if case .peer(self.context.account.peerId) = self.chatLocation {
if messageIds.count == 1 {
localOptionText = self.presentationData.strings.Conversation_Moderate_Delete
} else {
localOptionText = self.presentationData.strings.Conversation_DeleteManyMessages
if self.presentationInterfaceState.isScheduledMessages {
localOptionText = self.presentationData.strings.ScheduledMessages_Delete
} else {
if options.contains(.unsendPersonal) {
localOptionText = self.presentationData.strings.Chat_DeleteMessagesConfirmation(Int32(messageIds.count))
} else if case .peer(self.context.account.peerId) = self.chatLocation {
if messageIds.count == 1 {
localOptionText = self.presentationData.strings.Conversation_Moderate_Delete
} else {
localOptionText = self.presentationData.strings.Conversation_DeleteManyMessages
}
}
}
items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak self, weak actionSheet] in

View File

@ -87,6 +87,8 @@ public final class ChatControllerInteraction {
let displayMessageTooltip: (MessageId, String, ASDisplayNode?, CGRect?) -> Void
let seekToTimecode: (Message, Double, Bool) -> Void
let scheduleCurrentMessage: () -> Void
let sendScheduledMessagesNow: ([MessageId]) -> Void
let editScheduledMessagesTime: ([MessageId]) -> Void
let requestMessageUpdate: (MessageId) -> Void
let cancelInteractiveKeyboardGestures: () -> Void
@ -101,7 +103,7 @@ public final class ChatControllerInteraction {
var searchTextHighightState: String?
var seenOneTimeAnimatedMedia = Set<MessageId>()
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
self.openMessage = openMessage
self.openPeer = openPeer
self.openPeerMention = openPeerMention
@ -144,7 +146,9 @@ public final class ChatControllerInteraction {
self.displayMessageTooltip = displayMessageTooltip
self.seekToTimecode = seekToTimecode
self.scheduleCurrentMessage = scheduleCurrentMessage
self.sendScheduledMessagesNow = sendScheduledMessagesNow
self.editScheduledMessagesTime = editScheduledMessagesTime
self.requestMessageUpdate = requestMessageUpdate
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures
@ -171,6 +175,8 @@ public final class ChatControllerInteraction {
}, displayMessageTooltip: { _, _, _, _ in
}, seekToTimecode: { _, _, _ in
}, scheduleCurrentMessage: {
}, sendScheduledMessagesNow: { _ in
}, editScheduledMessagesTime: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -129,7 +129,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
var requestUpdateChatInterfaceState: (Bool, Bool, (ChatInterfaceState) -> ChatInterfaceState) -> Void = { _, _, _ in }
var requestUpdateInterfaceState: (ContainedViewLayoutTransition, Bool, (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void = { _, _, _ in }
var sendMessages: ([EnqueueMessage], Bool?, Bool) -> Void = { _, _, _ in }
var sendMessages: ([EnqueueMessage], Bool?, Int32?, Bool) -> Void = { _, _, _, _ in }
var displayAttachmentMenu: () -> Void = { }
var paste: (ChatTextInputPanelPasteData) -> Void = { _ in }
var updateTypingActivity: (Bool) -> Void = { _ in }
@ -284,7 +284,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.textInputPanelNode?.sendMessage = { [weak self] in
if let strongSelf = self {
if strongSelf.chatPresentationInterfaceState.isScheduledMessages {
if strongSelf.chatPresentationInterfaceState.isScheduledMessages && strongSelf.chatPresentationInterfaceState.editMessageState == nil {
strongSelf.controllerInteraction.scheduleCurrentMessage()
} else {
strongSelf.sendCurrentMessage()
@ -2049,11 +2049,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
}
var webpage: TelegramMediaWebpage?
if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != nil {
attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews], scheduleTime: scheduleTime))
attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews]))
} else {
if let scheduleTime = scheduleTime {
attributes.append(OutgoingContentInfoMessageAttribute(flags: [], scheduleTime: scheduleTime))
}
webpage = self.chatPresentationInterfaceState.urlPreview?.1
}
messages.append(.message(text: text.string, attributes: attributes, mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil))
@ -2077,7 +2074,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
}
if case .peer = self.chatLocation {
self.sendMessages(messages, silentPosting, messages.count > 1)
self.sendMessages(messages, silentPosting, scheduleTime, messages.count > 1)
}
}
}

View File

@ -456,7 +456,7 @@ final class ChatEmptyNode: ASDisplayNode {
let contentType: ChatEmptyNodeContentType
if let peer = interfaceState.renderedPeer?.peer {
if peer.id == self.accountPeerId {
if peer.id == self.accountPeerId && !interfaceState.isScheduledMessages {
contentType = .cloud
} else if let _ = peer as? TelegramSecretChat {
contentType = .secret

View File

@ -24,8 +24,9 @@ func canEditMessage(context: AccountContext, limitsConfiguration: LimitsConfigur
var hasEditRights = false
var unlimitedInterval = false
if message.id.peerId.namespace == Namespaces.Peer.SecretChat || message.id.namespace != Namespaces.Message.Cloud {
if message.id.namespace == Namespaces.Message.CloudScheduled {
hasEditRights = true
} else if message.id.peerId.namespace == Namespaces.Peer.SecretChat || message.id.namespace != Namespaces.Message.Cloud {
hasEditRights = false
} else if let author = message.author, author.id == context.account.peerId {
hasEditRights = true
@ -102,6 +103,9 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS
guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else {
return false
}
guard !chatPresentationInterfaceState.isScheduledMessages else {
return false
}
var canReply = false
switch chatPresentationInterfaceState.chatLocation {
@ -277,7 +281,11 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
canDeleteMessage = context.account.peerId == message.author?.id
}
if messages[0].flags.intersection([.Failed, .Unsent]).isEmpty {
if message.id.namespace == Namespaces.Message.CloudScheduled {
canReply = false
canPin = false
}
else if messages[0].flags.intersection([.Failed, .Unsent]).isEmpty {
switch chatPresentationInterfaceState.chatLocation {
case .peer:
if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel {
@ -363,8 +371,20 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
})))
}
if data.messageActions.options.contains(.sendScheduledNow) {
actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.ScheduledMessages_SendNow, action: {
controllerInteraction.sendScheduledMessagesNow(selectAll ? messages.map { $0.id } : [message.id])
})))
}
if data.messageActions.options.contains(.editScheduledTime) {
actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.ScheduledMessages_EditTime, action: {
controllerInteraction.editScheduledMessagesTime(selectAll ? messages.map { $0.id } : [message.id])
})))
}
if data.canEdit {
actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_Edit, action: {
actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, action: {
interfaceInteraction.setupEditMessage(messages[0].id)
})))
}
@ -613,6 +633,8 @@ struct ChatAvailableMessageActionOptions: OptionSet {
static let rateCall = ChatAvailableMessageActionOptions(rawValue: 1 << 5)
static let cancelSending = ChatAvailableMessageActionOptions(rawValue: 1 << 6)
static let unsendPersonal = ChatAvailableMessageActionOptions(rawValue: 1 << 7)
static let sendScheduledNow = ChatAvailableMessageActionOptions(rawValue: 1 << 8)
static let editScheduledTime = ChatAvailableMessageActionOptions(rawValue: 1 << 9)
}
struct ChatAvailableMessageActions {
@ -669,6 +691,7 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag
var hadPersonalIncoming = false
var hadBanPeerId = false
for id in messageIds {
let isScheduled = id.namespace == Namespaces.Message.CloudScheduled
if optionsMap[id] == nil {
optionsMap[id] = []
}
@ -685,7 +708,11 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag
optionsMap[id]!.insert(.rateCall)
}
}
if id.peerId == accountPeerId {
if id.namespace == Namespaces.Message.CloudScheduled {
optionsMap[id]!.insert(.sendScheduledNow)
optionsMap[id]!.insert(.editScheduledTime)
optionsMap[id]!.insert(.deleteLocally)
} else if id.peerId == accountPeerId {
if !(message.flags.isSending || message.flags.contains(.Failed)) {
optionsMap[id]!.insert(.forward)
}
@ -726,7 +753,7 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag
}
}
}
if !message.flags.contains(.Incoming) {
optionsMap[id]!.insert(.deleteGlobally)
} else {
@ -770,7 +797,7 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag
}
}
} else if let user = peer as? TelegramUser {
if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia && !isAction {
if !isScheduled && message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia && !isAction {
if !(message.flags.isSending || message.flags.contains(.Failed)) {
optionsMap[id]!.insert(.forward)
}

View File

@ -26,15 +26,22 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha
return currentButton
} else if let peer = presentationInterfaceState.renderedPeer?.peer {
let canClear: Bool
if peer is TelegramUser || peer is TelegramGroup || peer is TelegramSecretChat {
canClear = true
} else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.addressName == nil && presentationInterfaceState.peerGeoLocation == nil {
var title = strings.Conversation_ClearAll
if presentationInterfaceState.isScheduledMessages {
canClear = true
title = strings.ScheduledMessages_ClearAll
} else {
canClear = false
if peer is TelegramUser || peer is TelegramGroup || peer is TelegramSecretChat {
canClear = true
} else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.addressName == nil && presentationInterfaceState.peerGeoLocation == nil {
canClear = true
} else {
canClear = false
}
}
if canClear {
return ChatNavigationButton(action: .clearHistory, buttonItem: UIBarButtonItem(title: strings.Conversation_ClearAll, style: .plain, target: target, action: selector))
return ChatNavigationButton(action: .clearHistory, buttonItem: UIBarButtonItem(title: title, style: .plain, target: target, action: selector))
}
}
}
@ -53,9 +60,13 @@ func rightNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Ch
if case .standard(true) = presentationInterfaceState.mode {
} else if let peer = presentationInterfaceState.renderedPeer?.peer {
if presentationInterfaceState.accountPeerId == peer.id {
let buttonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector)
buttonItem.accessibilityLabel = strings.Conversation_Info
return ChatNavigationButton(action: .search, buttonItem: buttonItem)
if presentationInterfaceState.isScheduledMessages {
return nil
} else {
let buttonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector)
buttonItem.accessibilityLabel = strings.Conversation_Info
return ChatNavigationButton(action: .search, buttonItem: buttonItem)
}
}
}

View File

@ -395,6 +395,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, displayMessageTooltip: { _, _, _, _ in
}, seekToTimecode: { _, _, _ in
}, scheduleCurrentMessage: {
}, sendScheduledMessagesNow: { _ in
}, editScheduledMessagesTime: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,

View File

@ -7,6 +7,11 @@ import TelegramCore
import SwiftSignalKit
import AccountContext
enum ChatScheduleTimeControllerMode {
case scheduledMessages
case reminders
}
final class ChatScheduleTimeController: ViewController {
private var controllerNode: ChatScheduleTimeControllerNode {
return self.displayNode as! ChatScheduleTimeControllerNode
@ -15,10 +20,12 @@ final class ChatScheduleTimeController: ViewController {
private var animatedIn = false
private let context: AccountContext
private let mode: ChatScheduleTimeControllerMode
private let completion: (Int32) -> Void
init(context: AccountContext, completion: @escaping (Int32) -> Void) {
init(context: AccountContext, mode: ChatScheduleTimeControllerMode, completion: @escaping (Int32) -> Void) {
self.context = context
self.mode = mode
self.completion = completion
super.init(navigationBarPresentationData: nil)
@ -31,7 +38,7 @@ final class ChatScheduleTimeController: ViewController {
}
override public func loadDisplayNode() {
self.displayNode = ChatScheduleTimeControllerNode(context: self.context)
self.displayNode = ChatScheduleTimeControllerNode(context: self.context, mode: self.mode)
self.controllerNode.completion = { [weak self] time in
self?.completion(time + 5)
self?.dismiss()

View File

@ -11,6 +11,7 @@ import ShareController
class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDelegate {
private let context: AccountContext
private let mode: ChatScheduleTimeControllerMode
private var presentationData: PresentationData
private let dimNode: ASDisplayNode
@ -31,8 +32,9 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel
var dismiss: (() -> Void)?
var cancel: (() -> Void)?
init(context: AccountContext) {
init(context: AccountContext, mode: ChatScheduleTimeControllerMode) {
self.context = context
self.mode = mode
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.wrappingScrollNode = ASScrollNode()
@ -54,8 +56,16 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel
self.contentBackgroundNode.displayWithoutProcessing = true
self.contentBackgroundNode.image = roundedBackground
let title: String
switch mode {
case .scheduledMessages:
title = self.presentationData.strings.Conversation_ScheduleMessage_Title
case .reminders:
title = self.presentationData.strings.Conversation_SetReminder_Title
}
self.titleNode = ASTextNode()
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.Conversation_ScheduleMessage_Title, font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
self.separatorNode = ASDisplayNode()
//self.separatorNode.backgroundColor = self.theme.controlColor
@ -68,7 +78,6 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel
self.pickerView = UIDatePicker()
self.pickerView.timeZone = TimeZone(secondsFromGMT: 0)
self.pickerView.datePickerMode = .dateAndTime
self.pickerView.date = Date() //Date(timeIntervalSince1970: Double(roundDateToDays(currentValue)))
self.pickerView.locale = localeWithStrings(self.presentationData.strings)
self.dateFormatter = DateFormatter()
@ -96,7 +105,6 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel
self.pickerView.timeZone = TimeZone.current
self.pickerView.minuteInterval = 5
self.pickerView.minimumDate = Date()
self.pickerView.maximumDate = Date(timeIntervalSince1970: Double(Int32.max - 1))
self.pickerView.setValue(self.presentationData.theme.actionSheet.primaryTextColor, forKey: "textColor")
@ -107,13 +115,34 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel
self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
self.doneButton.pressed = { [weak self] in
if let strongSelf = self {
strongSelf.completion?(Int32(strongSelf.pickerView.date.timeIntervalSince1970))
if strongSelf.pickerView.date < Date() {
strongSelf.updateMinimumDate()
strongSelf.pickerView.layer.addShakeAnimation()
} else {
strongSelf.completion?(Int32(strongSelf.pickerView.date.timeIntervalSince1970))
}
}
}
self.updateMinimumDate()
self.updateButtonTitle()
}
private func updateMinimumDate() {
let timeZone = TimeZone(secondsFromGMT: 0)!
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = timeZone
let currentDate = Date()
var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: currentDate)
components.second = 0
let minute = (components.minute ?? 0) % 5
if let date = calendar.date(byAdding: .minute, value: 5 - minute, to: calendar.date(from: components)!) {
self.pickerView.minimumDate = date
self.pickerView.date = date
}
}
override func didLoad() {
super.didLoad()
@ -123,19 +152,39 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel
}
private func updateButtonTitle() {
let calendar = Calendar.current
let calendar = Calendar(identifier: .gregorian)
let date = self.pickerView.date
let time = stringForMessageTimestamp(timestamp: Int32(date.timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat)
if calendar.isDateInToday(date) {
self.doneButton.title = self.presentationData.strings.Conversation_ScheduleMessage_SendToday(time).0
} else {
self.doneButton.title = self.presentationData.strings.Conversation_ScheduleMessage_SendOn(self.dateFormatter.string(from: date), time).0
switch mode {
case .scheduledMessages:
if calendar.isDateInToday(date) {
self.doneButton.title = self.presentationData.strings.Conversation_ScheduleMessage_SendToday(time).0
} else if calendar.isDateInTomorrow(date) {
self.doneButton.title = self.presentationData.strings.Conversation_ScheduleMessage_SendTomorrow(time).0
} else {
self.doneButton.title = self.presentationData.strings.Conversation_ScheduleMessage_SendOn(self.dateFormatter.string(from: date), time).0
}
case .reminders:
if calendar.isDateInToday(date) {
self.doneButton.title = self.presentationData.strings.Conversation_SetReminder_RemindToday(time).0
} else if calendar.isDateInTomorrow(date) {
self.doneButton.title = self.presentationData.strings.Conversation_SetReminder_RemindTomorrow(time).0
} else {
self.doneButton.title = self.presentationData.strings.Conversation_SetReminder_RemindOn(self.dateFormatter.string(from: date), time).0
}
}
}
@objc private func datePickerUpdated() {
self.updateButtonTitle()
if self.pickerView.date < Date() {
self.doneButton.alpha = 0.4
self.doneButton.isUserInteractionEnabled = false
} else {
self.doneButton.alpha = 1.0
self.doneButton.isUserInteractionEnabled = true
}
}
@objc func cancelButtonPressed() {

View File

@ -61,7 +61,12 @@ final class ChatSendMessageActionSheetController: ViewController {
forwardedCount = forwardMessageIds.count
}
self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, sendButtonFrame: self.sendButtonFrame, textInputNode: self.textInputNode, forwardedCount: forwardedCount, send: { [weak self] in
var reminders = false
if case let .peer(peerId) = self.interfaceState.chatLocation, peerId == context.account.peerId {
reminders = true
}
self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, reminders: reminders, sendButtonFrame: self.sendButtonFrame, textInputNode: self.textInputNode, forwardedCount: forwardedCount, send: { [weak self] in
self?.controllerInteraction?.sendCurrentMessage(false)
self?.dismiss(cancel: false)
}, sendSilently: { [weak self] in

View File

@ -151,7 +151,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
private var validLayout: ContainerViewLayout?
init(context: AccountContext, sendButtonFrame: CGRect, textInputNode: EditableTextNode, forwardedCount: Int?, send: (() -> Void)?, sendSilently: (() -> Void)?, schedule: (() -> Void)?, cancel: (() -> Void)?) {
init(context: AccountContext, reminders: Bool, sendButtonFrame: CGRect, textInputNode: EditableTextNode, forwardedCount: Int?, send: (() -> Void)?, sendSilently: (() -> Void)?, schedule: (() -> Void)?, cancel: (() -> Void)?) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.sendButtonFrame = sendButtonFrame
@ -209,10 +209,12 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
self.contentContainerNode.clipsToBounds = true
var contentNodes: [ActionSheetItemNode] = []
contentNodes.append(ActionSheetItemNode(theme: self.presentationData.theme, title: self.presentationData.strings.Conversation_SendMessage_SendSilently, icon: .sendWithoutSound, hasSeparator: true, action: {
sendSilently?()
}))
contentNodes.append(ActionSheetItemNode(theme: self.presentationData.theme, title: self.presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: .schedule, hasSeparator: false, action: {
if !reminders {
contentNodes.append(ActionSheetItemNode(theme: self.presentationData.theme, title: self.presentationData.strings.Conversation_SendMessage_SendSilently, icon: .sendWithoutSound, hasSeparator: true, action: {
sendSilently?()
}))
}
contentNodes.append(ActionSheetItemNode(theme: self.presentationData.theme, title: reminders ? self.presentationData.strings.Conversation_SendMessage_SetReminder: self.presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: .schedule, hasSeparator: false, action: {
schedule?()
}))
self.contentNodes = contentNodes

View File

@ -775,7 +775,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize)
}
self.actionButtons.sendButtonLongPressEnabled = peer.id != interfaceState.accountPeerId && peer.id.namespace != Namespaces.Peer.SecretChat
self.actionButtons.sendButtonLongPressEnabled = peer.id.namespace != Namespaces.Peer.SecretChat && !interfaceState.isScheduledMessages
}
let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil

View File

@ -255,9 +255,9 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
var inputActivitiesAllowed = true
if let titleContent = self.titleContent {
switch titleContent {
case let .peer(peerView, _, _):
case let .peer(peerView, _, isScheduledMessages):
if let peer = peerViewMainPeer(peerView) {
if peer.id == self.account.peerId {
if peer.id == self.account.peerId || isScheduledMessages {
inputActivitiesAllowed = false
}
}

View File

@ -10,6 +10,12 @@ public func freeMediaFileInteractiveFetched(account: Account, fileReference: Fil
return fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(fileReference.media.resource))
}
func freeMediaFileInteractiveFetched(fetchManager: FetchManager, fileReference: FileMediaReference, priority: FetchManagerPriority) -> Signal<Void, NoError> {
let file = fileReference.media
let mediaReference = AnyMediaReference.standalone(media: fileReference.media)
return fetchManager.interactivelyFetched(category: fetchCategoryForFile(file), location: .chat(PeerId(namespace: 0, id: 0)), locationKey: .free, mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(file.resource), ranges: IndexSet(integersIn: 0 ..< Int(Int32.max) as Range<Int>), statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil)
}
func freeMediaFileResourceInteractiveFetched(account: Account, fileReference: FileMediaReference, resource: MediaResource) -> Signal<FetchResourceSourceType, FetchResourceError> {
return fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(resource))
}

View File

@ -365,7 +365,14 @@ class GalleryController: ViewController {
switch source {
case .peerMessagesAtId:
if let tags = tagsForMessage(message!) {
let view = context.account.postbox.aroundMessageHistoryViewForLocation(.peer(message!.id.peerId), anchor: .index(message!.index), count: 50, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tags, excludeNamespaces: [Namespaces.Message.CloudScheduled], orderStatistics: [.combinedLocation])
var excludeNamespaces: [MessageId.Namespace]
if message!.id.namespace == Namespaces.Message.CloudScheduled {
excludeNamespaces = [Namespaces.Message.Cloud, Namespaces.Message.Local, Namespaces.Message.SecretIncoming]
} else {
excludeNamespaces = [Namespaces.Message.CloudScheduled]
}
let view = context.account.postbox.aroundMessageHistoryViewForLocation(.peer(message!.id.peerId), anchor: .index(message!.index), count: 50, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tags, excludeNamespaces: excludeNamespaces, orderStatistics: [.combinedLocation])
return view
|> mapToSignal { (view, _, _) -> Signal<GalleryMessageHistoryView?, NoError> in

View File

@ -15,7 +15,7 @@ public func navigateToChatController(navigationController: NavigationController,
var found = false
var isFirst = true
for controller in navigationController.viewControllers.reversed() {
if let controller = controller as? ChatController, controller.chatLocation == chatLocation {
if let controller = controller as? ChatController, controller.chatLocation == chatLocation && controller.subject != .scheduledMessages {
if let updateTextInputState = updateTextInputState {
controller.updateTextInputState(updateTextInputState)
}

View File

@ -104,6 +104,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, displayMessageTooltip: { _, _, _, _ in
}, seekToTimecode: { _, _, _ in
}, scheduleCurrentMessage: {
}, sendScheduledMessagesNow: { _ in
}, editScheduledMessagesTime: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false))

View File

@ -277,6 +277,8 @@ public class PeerMediaCollectionController: TelegramBaseController {
}, displayMessageTooltip: { _, _, _, _ in
}, seekToTimecode: { _, _, _ in
}, scheduleCurrentMessage: {
}, sendScheduledMessagesNow: { _ in
}, editScheduledMessagesTime: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -12,6 +12,11 @@ private final class PrefetchMediaContext {
}
}
public enum PrefetchMediaItem {
case chatHistory(ChatHistoryPreloadMediaItem)
case animatedEmojiSticker(TelegramMediaFile)
}
private final class PrefetchManagerImpl {
private let queue: Queue
private let account: Account
@ -37,7 +42,37 @@ private final class PrefetchManagerImpl {
}
|> distinctUntilChanged
self.listDisposable = (combineLatest(account.viewTracker.orderedPreloadMedia, sharedContext.automaticMediaDownloadSettings, networkType)
let orderedPreloadMedia = account.viewTracker.orderedPreloadMedia
|> mapToSignal { orderedPreloadMedia in
return loadedStickerPack(postbox: account.postbox, network: account.network, reference: .animatedEmoji, forceActualized: false)
|> map { result -> [PrefetchMediaItem] in
let chatHistoryMediaItems = orderedPreloadMedia.map { PrefetchMediaItem.chatHistory($0) }
switch result {
case let .result(_, items, _):
var animatedEmojiStickers: [String: StickerPackItem] = [:]
for case let item as StickerPackItem in items {
if let emoji = item.getStringRepresentationsOfIndexKeys().first {
animatedEmojiStickers[emoji.basicEmoji.0] = item
}
}
var stickerItems: [PrefetchMediaItem] = []
let popularEmoji = ["\u{2764}", "👍", "😳", "😒", "🥳"]
for emoji in popularEmoji {
if let sticker = animatedEmojiStickers[emoji] {
if let _ = account.postbox.mediaBox.completedResourcePath(sticker.file.resource) {
} else {
stickerItems.append(.animatedEmojiSticker(sticker.file))
}
}
}
return stickerItems + chatHistoryMediaItems
default:
return chatHistoryMediaItems
}
}
}
self.listDisposable = (combineLatest(orderedPreloadMedia, sharedContext.automaticMediaDownloadSettings, networkType)
|> deliverOn(self.queue)).start(next: { [weak self] orderedPreloadMedia, automaticDownloadSettings, networkType in
self?.updateOrderedPreloadMedia(orderedPreloadMedia, automaticDownloadSettings: automaticDownloadSettings, networkType: networkType)
})
@ -48,80 +83,120 @@ private final class PrefetchManagerImpl {
self.listDisposable?.dispose()
}
private func updateOrderedPreloadMedia(_ orderedPreloadMedia: [ChatHistoryPreloadMediaItem], automaticDownloadSettings: MediaAutoDownloadSettings, networkType: MediaAutoDownloadNetworkType) {
private func updateOrderedPreloadMedia(_ items: [PrefetchMediaItem], automaticDownloadSettings: MediaAutoDownloadSettings, networkType: MediaAutoDownloadNetworkType) {
var validIds = Set<MediaId>()
for mediaItem in orderedPreloadMedia {
guard let id = mediaItem.media.media.id else {
continue
}
if validIds.contains(id) {
continue
}
var automaticDownload: InteractiveMediaNodeAutodownloadMode = .none
let peerType: MediaAutoDownloadPeerType
if mediaItem.media.authorIsContact {
peerType = .contact
} else if let channel = mediaItem.media.peer as? TelegramChannel {
if case .group = channel.info {
peerType = .group
} else {
peerType = .channel
}
} else if mediaItem.media.peer is TelegramGroup {
peerType = .group
} else {
peerType = .otherPrivate
}
var mediaResource: MediaResource?
if let telegramImage = mediaItem.media.media as? TelegramMediaImage {
mediaResource = largestRepresentationForPhoto(telegramImage)?.resource
if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: peerType, networkType: networkType, authorPeerId: nil, contactsPeerIds: [], media: telegramImage) {
automaticDownload = .full
}
} else if let telegramFile = mediaItem.media.media as? TelegramMediaFile {
mediaResource = telegramFile.resource
if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: peerType, networkType: networkType, authorPeerId: nil, contactsPeerIds: [], media: telegramFile) {
automaticDownload = .full
} else if shouldPredownloadMedia(settings: automaticDownloadSettings, peerType: peerType, networkType: networkType, media: telegramFile) {
automaticDownload = .prefetch
}
}
if case .none = automaticDownload {
continue
}
guard let resource = mediaResource else {
continue
}
validIds.insert(id)
let context: PrefetchMediaContext
if let current = self.contexts[id] {
context = current
} else {
context = PrefetchMediaContext()
self.contexts[id] = context
let media = mediaItem.media.media
let priority: FetchManagerPriority = .backgroundPrefetch(locationOrder: mediaItem.preloadIndex, localOrder: mediaItem.media.index)
if case .full = automaticDownload {
if let image = media as? TelegramMediaImage {
context.fetchDisposable.set(messageMediaImageInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), image: image, resource: resource, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil).start())
} else if let _ = media as? TelegramMediaWebFile {
//strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: context.account, image: image).start())
} else if let file = media as? TelegramMediaFile {
let fetchSignal = messageMediaFileInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), file: file, userInitiated: false, priority: priority)
context.fetchDisposable.set(fetchSignal.start())
var order: Int32 = 0
for mediaItem in items {
switch mediaItem {
case let .chatHistory(mediaItem):
guard let id = mediaItem.media.media.id else {
continue
}
} else if case .prefetch = automaticDownload, mediaItem.media.peer.id.namespace != Namespaces.Peer.SecretChat {
if let file = media as? TelegramMediaFile, let _ = file.size {
context.fetchDisposable.set(preloadVideoResource(postbox: self.account.postbox, resourceReference: FileMediaReference.message(message: MessageReference(peer: mediaItem.media.peer, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), media: file).resourceReference(file.resource), duration: 4.0).start())
if validIds.contains(id) {
continue
}
var automaticDownload: InteractiveMediaNodeAutodownloadMode = .none
let peerType: MediaAutoDownloadPeerType
if mediaItem.media.authorIsContact {
peerType = .contact
} else if let channel = mediaItem.media.peer as? TelegramChannel {
if case .group = channel.info {
peerType = .group
} else {
peerType = .channel
}
} else if mediaItem.media.peer is TelegramGroup {
peerType = .group
} else {
peerType = .otherPrivate
}
var mediaResource: MediaResource?
if let telegramImage = mediaItem.media.media as? TelegramMediaImage {
mediaResource = largestRepresentationForPhoto(telegramImage)?.resource
if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: peerType, networkType: networkType, authorPeerId: nil, contactsPeerIds: [], media: telegramImage) {
automaticDownload = .full
}
} else if let telegramFile = mediaItem.media.media as? TelegramMediaFile {
mediaResource = telegramFile.resource
if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: peerType, networkType: networkType, authorPeerId: nil, contactsPeerIds: [], media: telegramFile) {
automaticDownload = .full
} else if shouldPredownloadMedia(settings: automaticDownloadSettings, peerType: peerType, networkType: networkType, media: telegramFile) {
automaticDownload = .prefetch
}
}
if case .none = automaticDownload {
continue
}
guard let resource = mediaResource else {
continue
}
validIds.insert(id)
let context: PrefetchMediaContext
if let current = self.contexts[id] {
context = current
} else {
context = PrefetchMediaContext()
self.contexts[id] = context
let media = mediaItem.media.media
let priority: FetchManagerPriority = .backgroundPrefetch(locationOrder: mediaItem.preloadIndex, localOrder: mediaItem.media.index)
if case .full = automaticDownload {
if let image = media as? TelegramMediaImage {
context.fetchDisposable.set(messageMediaImageInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), image: image, resource: resource, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil).start())
} else if let _ = media as? TelegramMediaWebFile {
//strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: context.account, image: image).start())
} else if let file = media as? TelegramMediaFile {
let fetchSignal = messageMediaFileInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), file: file, userInitiated: false, priority: priority)
context.fetchDisposable.set(fetchSignal.start())
}
} else if case .prefetch = automaticDownload, mediaItem.media.peer.id.namespace != Namespaces.Peer.SecretChat {
if let file = media as? TelegramMediaFile, let _ = file.size {
context.fetchDisposable.set(preloadVideoResource(postbox: self.account.postbox, resourceReference: FileMediaReference.message(message: MessageReference(peer: mediaItem.media.peer, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), media: file).resourceReference(file.resource), duration: 4.0).start())
}
}
}
case let .animatedEmojiSticker(media):
guard let id = media.id else {
continue
}
if validIds.contains(id) {
continue
}
var automaticDownload: InteractiveMediaNodeAutodownloadMode = .none
let peerType = MediaAutoDownloadPeerType.contact
if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: peerType, networkType: networkType, authorPeerId: nil, contactsPeerIds: [], media: media) {
automaticDownload = .full
}
if case .none = automaticDownload {
continue
}
validIds.insert(id)
let context: PrefetchMediaContext
if let current = self.contexts[id] {
context = current
} else {
context = PrefetchMediaContext()
self.contexts[id] = context
let priority: FetchManagerPriority = .backgroundPrefetch(locationOrder: HistoryPreloadIndex(index: nil, hasUnread: false, isMuted: false, isPriority: true), localOrder: MessageIndex(id: MessageId(peerId: PeerId(namespace: 0, id: 0), namespace: 0, id: order), timestamp: 0))
if case .full = automaticDownload {
let fetchSignal = freeMediaFileInteractiveFetched(fetchManager: self.fetchManager, fileReference: .standalone(media: media), priority: priority)
context.fetchDisposable.set(fetchSignal.start())
}
order += 1
}
}
}
}
var removeIds: [MediaId] = []

View File

@ -294,7 +294,9 @@ public class ShareRootControllerImpl {
}
cancelImpl = { [weak shareController] in
shareController?.dismiss()
shareController?.dismiss(completion: { [weak self] in
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
})
}
if let strongSelf = self {