diff --git a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTProto.h b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTProto.h index ab8301236f..66bbb90df0 100644 --- a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTProto.h +++ b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTProto.h @@ -71,7 +71,7 @@ - (MTQueue *)messageServiceQueue; - (void)requestTransportTransaction; - (void)requestSecureTransportReset; -- (void)resetSessionInfo; +- (void)resetSessionInfo:(bool)ifActive; - (void)requestTimeResync; - (void)_messageResendRequestFailed:(int64_t)messageId; diff --git a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequest.h b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequest.h index 28dba10b2d..d6ffc8ad64 100644 --- a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequest.h +++ b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequest.h @@ -33,6 +33,7 @@ @property (nonatomic) bool dependsOnPasswordEntry; @property (nonatomic) bool passthroughPasswordEntryError; @property (nonatomic) bool needsTimeoutTimer; +@property (nonatomic) int32_t expectedResponseSize; @property (nonatomic, copy) void (^completed)(id result, MTRequestResponseInfo *info, MTRpcError *error); @property (nonatomic, copy) void (^progressUpdated)(float progress, NSUInteger packetLength); diff --git a/submodules/MtProtoKit/Sources/MTProto.m b/submodules/MtProtoKit/Sources/MTProto.m index 7e71be1b65..e83566a145 100644 --- a/submodules/MtProtoKit/Sources/MTProto.m +++ b/submodules/MtProtoKit/Sources/MTProto.m @@ -354,13 +354,21 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; }]; } -- (void)resetSessionInfo +- (void)resetSessionInfo:(bool)ifActive { [[MTProto managerQueue] dispatchOnQueue:^ { if (MTLogEnabled()) { MTLogWithPrefix(_getLogPrefix, @"[MTProto#%p@%p resetting session]", self, _context); } + + if (ifActive) { + if (![self canAskForTransactions]) { + MTShortLog(@"[MTProto#%p@%p not resetting session: !canAskForTransactions]", self, _context); + return; + } + } + MTShortLog(@"[MTProto#%p@%p resetting session]", self, _context); _sessionInfo = [[MTSessionInfo alloc] initWithRandomSessionIdAndContext:_context]; @@ -1143,7 +1151,7 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; } MTShortLog(@"[MTProto#%p@%p client message id monotonity violated]", self, _context); - [self resetSessionInfo]; + [self resetSessionInfo:false]; } else if (saltSetEmpty) [self initiateTimeSync]; @@ -2090,7 +2098,7 @@ static NSString *dumpHexString(NSData *data, int maxLength) { } [self transportTransactionsMayHaveFailed:transport transactionIds:@[transactionId]]; - [self resetSessionInfo]; + [self resetSessionInfo:false]; } else { [_context reportTransportSchemeSuccessForDatacenterId:_datacenterId transportScheme:scheme]; [self transportTransactionsSucceeded:@[transactionId]]; @@ -2496,7 +2504,7 @@ static bool isDataEqualToDataConstTime(NSData *data1, NSData *data2) { case 32: case 33: { - [self resetSessionInfo]; + [self resetSessionInfo:false]; [self initiateTimeSync]; break; diff --git a/submodules/MtProtoKit/Sources/MTRequestMessageService.m b/submodules/MtProtoKit/Sources/MTRequestMessageService.m index d8c170f8ca..6cbed5d6a4 100644 --- a/submodules/MtProtoKit/Sources/MTRequestMessageService.m +++ b/submodules/MtProtoKit/Sources/MTRequestMessageService.m @@ -110,6 +110,7 @@ { bool anyNewDropRequests = false; bool removedAnyRequest = false; + bool mergedAskForReconnectionOnDrop = askForReconnectionOnDrop; int index = -1; for (MTRequest *request in _requests) @@ -122,6 +123,9 @@ { [_dropReponseContexts addObject:[[MTDropResponseContext alloc] initWithDropMessageId:request.requestContext.messageId]]; anyNewDropRequests = true; + if (request.expectedResponseSize >= 512 * 1024) { + mergedAskForReconnectionOnDrop = true; + } } if (request.requestContext.messageId != 0) { @@ -142,8 +146,9 @@ { MTProto *mtProto = _mtProto; - if (askForReconnectionOnDrop) - [mtProto requestSecureTransportReset]; + if (mergedAskForReconnectionOnDrop) { + [mtProto resetSessionInfo:true]; + } [mtProto requestTransportTransaction]; } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 016077496c..b0f4d2c3ec 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -793,6 +793,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1087454222] = { return Api.StickerSetCovered.parse_stickerSetFullCovered($0) } dict[872932635] = { return Api.StickerSetCovered.parse_stickerSetMultiCovered($0) } dict[2008112412] = { return Api.StickerSetCovered.parse_stickerSetNoCovered($0) } + dict[1898850301] = { return Api.StoriesStealthMode.parse_storiesStealthMode($0) } dict[1445635639] = { return Api.StoryItem.parse_storyItem($0) } dict[1374088783] = { return Api.StoryItem.parse_storyItemDeleted($0) } dict[-5388013] = { return Api.StoryItem.parse_storyItemSkipped($0) } @@ -913,6 +914,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-337352679] = { return Api.Update.parse_updateServiceNotification($0) } dict[834816008] = { return Api.Update.parse_updateStickerSets($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[542785843] = { return Api.Update.parse_updateStory($0) } dict[468923833] = { return Api.Update.parse_updateStoryID($0) } dict[-2112423005] = { return Api.Update.parse_updateTheme($0) } @@ -1164,8 +1167,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[172975040] = { return Api.storage.FileType.parse_filePng($0) } dict[-1432995067] = { return Api.storage.FileType.parse_fileUnknown($0) } dict[276907596] = { return Api.storage.FileType.parse_fileWebp($0) } - dict[-2086796248] = { return Api.stories.AllStories.parse_allStories($0) } - dict[1205903486] = { return Api.stories.AllStories.parse_allStoriesNotModified($0) } + dict[1369278878] = { return Api.stories.AllStories.parse_allStories($0) } + dict[291044926] = { return Api.stories.AllStories.parse_allStoriesNotModified($0) } dict[1340440049] = { return Api.stories.Stories.parse_stories($0) } dict[-560009955] = { return Api.stories.StoryViews.parse_storyViews($0) } dict[-79726676] = { return Api.stories.StoryViewsList.parse_storyViewsList($0) } @@ -1727,6 +1730,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.StickerSetCovered: _1.serialize(buffer, boxed) + case let _1 as Api.StoriesStealthMode: + _1.serialize(buffer, boxed) case let _1 as Api.StoryItem: _1.serialize(buffer, boxed) case let _1 as Api.StoryView: diff --git a/submodules/TelegramApi/Sources/Api21.swift b/submodules/TelegramApi/Sources/Api21.swift index 14a69394a4..217c1eabbb 100644 --- a/submodules/TelegramApi/Sources/Api21.swift +++ b/submodules/TelegramApi/Sources/Api21.swift @@ -362,6 +362,50 @@ public extension Api { } } +public extension Api { + enum StoriesStealthMode: TypeConstructorDescription { + case storiesStealthMode(flags: Int32, activeUntilDate: Int32?, cooldownUntilDate: Int32?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .storiesStealthMode(let flags, let activeUntilDate, let cooldownUntilDate): + if boxed { + buffer.appendInt32(1898850301) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(activeUntilDate!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(cooldownUntilDate!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .storiesStealthMode(let flags, let activeUntilDate, let cooldownUntilDate): + return ("storiesStealthMode", [("flags", flags as Any), ("activeUntilDate", activeUntilDate as Any), ("cooldownUntilDate", cooldownUntilDate as Any)]) + } + } + + public static func parse_storiesStealthMode(_ reader: BufferReader) -> StoriesStealthMode? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt32() } + var _3: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_3 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.StoriesStealthMode.storiesStealthMode(flags: _1!, activeUntilDate: _2, cooldownUntilDate: _3) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum StoryItem: TypeConstructorDescription { case storyItem(flags: Int32, id: Int32, date: Int32, expireDate: Int32, caption: String?, entities: [Api.MessageEntity]?, media: Api.MessageMedia, privacy: [Api.PrivacyRule]?, views: Api.StoryViews?) @@ -1086,6 +1130,8 @@ public extension Api { case updateServiceNotification(flags: Int32, inboxDate: Int32?, type: String, message: String, media: Api.MessageMedia, entities: [Api.MessageEntity]) case updateStickerSets(flags: Int32) case updateStickerSetsOrder(flags: Int32, order: [Int64]) + case updateStoriesStealth(expireDate: Int32) + case updateStoriesStealthMode(stealthMode: Api.StoriesStealthMode) case updateStory(userId: Int64, story: Api.StoryItem) case updateStoryID(id: Int32, randomId: Int64) case updateTheme(theme: Api.Theme) @@ -1987,6 +2033,18 @@ public extension Api { serializeInt64(item, buffer: buffer, boxed: false) } break + case .updateStoriesStealth(let expireDate): + if boxed { + buffer.appendInt32(-719158423) + } + serializeInt32(expireDate, buffer: buffer, boxed: false) + break + case .updateStoriesStealthMode(let stealthMode): + if boxed { + buffer.appendInt32(738741697) + } + stealthMode.serialize(buffer, true) + break case .updateStory(let userId, let story): if boxed { buffer.appendInt32(542785843) @@ -2287,6 +2345,10 @@ public extension Api { return ("updateStickerSets", [("flags", flags as Any)]) case .updateStickerSetsOrder(let flags, let order): return ("updateStickerSetsOrder", [("flags", flags as Any), ("order", order as Any)]) + case .updateStoriesStealth(let expireDate): + return ("updateStoriesStealth", [("expireDate", expireDate as Any)]) + case .updateStoriesStealthMode(let stealthMode): + return ("updateStoriesStealthMode", [("stealthMode", stealthMode as Any)]) case .updateStory(let userId, let story): return ("updateStory", [("userId", userId as Any), ("story", story as Any)]) case .updateStoryID(let id, let randomId): @@ -4078,6 +4140,30 @@ public extension Api { 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? { + var _1: Api.StoriesStealthMode? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StoriesStealthMode + } + let _c1 = _1 != nil + if _c1 { + return Api.Update.updateStoriesStealthMode(stealthMode: _1!) + } + else { + return nil + } + } public static func parse_updateStory(_ reader: BufferReader) -> Update? { var _1: Int64? _1 = reader.readInt64() diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index 644cc2f251..f7dd597db4 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -360,14 +360,14 @@ public extension Api.storage { } public extension Api.stories { enum AllStories: TypeConstructorDescription { - case allStories(flags: Int32, count: Int32, state: String, userStories: [Api.UserStories], users: [Api.User]) - case allStoriesNotModified(state: String) + case allStories(flags: Int32, count: Int32, state: String, userStories: [Api.UserStories], users: [Api.User], stealthMode: Api.StoriesStealthMode) + case allStoriesNotModified(flags: Int32, state: String, stealthMode: Api.StoriesStealthMode) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .allStories(let flags, let count, let state, let userStories, let users): + case .allStories(let flags, let count, let state, let userStories, let users, let stealthMode): if boxed { - buffer.appendInt32(-2086796248) + buffer.appendInt32(1369278878) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(count, buffer: buffer, boxed: false) @@ -382,22 +382,25 @@ public extension Api.stories { for item in users { item.serialize(buffer, true) } + stealthMode.serialize(buffer, true) break - case .allStoriesNotModified(let state): + case .allStoriesNotModified(let flags, let state, let stealthMode): if boxed { - buffer.appendInt32(1205903486) + buffer.appendInt32(291044926) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeString(state, buffer: buffer, boxed: false) + stealthMode.serialize(buffer, true) break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .allStories(let flags, let count, let state, let userStories, let users): - return ("allStories", [("flags", flags as Any), ("count", count as Any), ("state", state as Any), ("userStories", userStories as Any), ("users", users as Any)]) - case .allStoriesNotModified(let state): - return ("allStoriesNotModified", [("state", state as Any)]) + case .allStories(let flags, let count, let state, let userStories, let users, let stealthMode): + return ("allStories", [("flags", flags as Any), ("count", count as Any), ("state", state as Any), ("userStories", userStories as Any), ("users", users as Any), ("stealthMode", stealthMode as Any)]) + case .allStoriesNotModified(let flags, let state, let stealthMode): + return ("allStoriesNotModified", [("flags", flags as Any), ("state", state as Any), ("stealthMode", stealthMode as Any)]) } } @@ -416,24 +419,37 @@ public extension Api.stories { if let _ = reader.readInt32() { _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } + var _6: Api.StoriesStealthMode? + if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.StoriesStealthMode + } 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.stories.AllStories.allStories(flags: _1!, count: _2!, state: _3!, userStories: _4!, users: _5!) + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.stories.AllStories.allStories(flags: _1!, count: _2!, state: _3!, userStories: _4!, users: _5!, stealthMode: _6!) } else { return nil } } public static func parse_allStoriesNotModified(_ reader: BufferReader) -> AllStories? { - var _1: String? - _1 = parseString(reader) + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Api.StoriesStealthMode? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.StoriesStealthMode + } let _c1 = _1 != nil - if _c1 { - return Api.stories.AllStories.allStoriesNotModified(state: _1!) + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.stories.AllStories.allStoriesNotModified(flags: _1!, state: _2!, stealthMode: _3!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index f09db42721..16c3566bd7 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -8447,6 +8447,21 @@ public extension Api.functions.stickers { }) } } +public extension Api.functions.stories { + static func activateStealthMode(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(299359662) + serializeInt32(flags, buffer: buffer, boxed: false) + return (FunctionDescription(name: "stories.activateStealthMode", parameters: [("flags", String(describing: flags))]), 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.stories { static func deleteStories(id: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Int32]>) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 741fa2a1fc..b7de343f4b 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -122,6 +122,8 @@ enum AccountStateMutationOperation { case ResetForumTopic(topicId: MessageId, data: StoreMessageHistoryThreadData, pts: Int32) case UpdateStory(peerId: PeerId, story: Api.StoryItem) case UpdateReadStories(peerId: PeerId, maxId: Int32) + case UpdateStoryStealthMode(data: Api.StoriesStealthMode) + case UpdateStoryStealth(expireDate: Int32) } struct HoleFromPreviousState { @@ -643,9 +645,17 @@ struct AccountMutableState { self.addOperation(.UpdateReadStories(peerId: peerId, maxId: maxId)) } + mutating func updateStoryStealthMode(_ data: Api.StoriesStealthMode) { + self.addOperation(.UpdateStoryStealthMode(data: data)) + } + + mutating func updateStoryStealth(expireDate: Int32) { + self.addOperation(.UpdateStoryStealth(expireDate: expireDate)) + } + mutating func addOperation(_ operation: AccountStateMutationOperation) { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStoryStealth: break case let .AddMessages(messages, location): for message in messages { diff --git a/submodules/TelegramCore/Sources/Network/Download.swift b/submodules/TelegramCore/Sources/Network/Download.swift index d695966a5b..ede8908135 100644 --- a/submodules/TelegramCore/Sources/Network/Download.swift +++ b/submodules/TelegramCore/Sources/Network/Download.swift @@ -117,7 +117,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate { saveFilePart = Api.functions.upload.saveFilePart(fileId: fileId, filePart: Int32(index), bytes: Buffer(data: data)) } - return multiplexedManager.request(to: .main(datacenterId), consumerId: consumerId, resourceId: nil, data: wrapMethodBody(saveFilePart, useCompression: useCompression), tag: tag, continueInBackground: true) + return multiplexedManager.request(to: .main(datacenterId), consumerId: consumerId, resourceId: nil, data: wrapMethodBody(saveFilePart, useCompression: useCompression), tag: tag, continueInBackground: true, expectedResponseSize: nil) |> mapError { error -> UploadPartError in if error.errorCode == 400 { return .invalidMedia @@ -189,6 +189,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate { func webFilePart(location: Api.InputWebFileLocation, offset: Int, length: Int) -> Signal { return Signal { subscriber in let request = MTRequest() + request.expectedResponseSize = Int32(length) var updatedLength = roundUp(length, to: 4096) while updatedLength % 4096 != 0 || 1048576 % updatedLength != 0 { @@ -241,6 +242,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate { func part(location: Api.InputFileLocation, offset: Int64, length: Int) -> Signal { return Signal { subscriber in let request = MTRequest() + request.expectedResponseSize = Int32(length) var updatedLength = roundUp(length, to: 4096) while updatedLength % 4096 != 0 || 1048576 % updatedLength != 0 { @@ -293,9 +295,10 @@ class Download: NSObject, MTRequestMessageServiceDelegate { |> retryRequest } - func request(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse)) -> Signal { + func request(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), expectedResponseSize: Int32? = nil) -> Signal { return Signal { subscriber in let request = MTRequest() + request.expectedResponseSize = expectedResponseSize ?? 0 request.setPayload(data.1.makeData() as Data, metadata: WrappedRequestMetadata(metadata: WrappedFunctionDescription(data.0), tag: nil), shortMetadata: WrappedRequestShortMetadata(shortMetadata: WrappedShortFunctionDescription(data.0)), responseParser: { response in if let result = data.2.parse(Buffer(data: response)) { @@ -335,9 +338,10 @@ class Download: NSObject, MTRequestMessageServiceDelegate { } } - func requestWithAdditionalData(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), automaticFloodWait: Bool = true, failOnServerErrors: Bool = false) -> Signal<(T, Double), (MTRpcError, Double)> { + func requestWithAdditionalData(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), automaticFloodWait: Bool = true, failOnServerErrors: Bool = false, expectedResponseSize: Int32? = nil) -> Signal<(T, Double), (MTRpcError, Double)> { return Signal { subscriber in let request = MTRequest() + request.expectedResponseSize = expectedResponseSize ?? 0 request.setPayload(data.1.makeData() as Data, metadata: WrappedRequestMetadata(metadata: WrappedFunctionDescription(data.0), tag: nil), shortMetadata: WrappedRequestShortMetadata(shortMetadata: WrappedShortFunctionDescription(data.0)), responseParser: { response in if let result = data.2.parse(Buffer(data: response)) { @@ -386,10 +390,11 @@ class Download: NSObject, MTRequestMessageServiceDelegate { } } - func rawRequest(_ data: (FunctionDescription, Buffer, (Buffer) -> Any?), automaticFloodWait: Bool = true, failOnServerErrors: Bool = false, logPrefix: String = "") -> Signal<(Any, NetworkResponseInfo), (MTRpcError, Double)> { + func rawRequest(_ data: (FunctionDescription, Buffer, (Buffer) -> Any?), automaticFloodWait: Bool = true, failOnServerErrors: Bool = false, logPrefix: String = "", expectedResponseSize: Int32? = nil) -> Signal<(Any, NetworkResponseInfo), (MTRpcError, Double)> { let requestService = self.requestService return Signal { subscriber in let request = MTRequest() + request.expectedResponseSize = expectedResponseSize ?? 0 request.setPayload(data.1.makeData() as Data, metadata: WrappedRequestMetadata(metadata: WrappedFunctionDescription(data.0), tag: nil), shortMetadata: WrappedRequestShortMetadata(shortMetadata: WrappedShortFunctionDescription(data.0)), responseParser: { response in if let result = data.2(Buffer(data: response)) { diff --git a/submodules/TelegramCore/Sources/Network/FetchV2.swift b/submodules/TelegramCore/Sources/Network/FetchV2.swift index cd56b068fe..b454a2a770 100644 --- a/submodules/TelegramCore/Sources/Network/FetchV2.swift +++ b/submodules/TelegramCore/Sources/Network/FetchV2.swift @@ -459,7 +459,8 @@ private final class FetchImpl { requestToken: Buffer(data: state.refreshToken) ), tag: nil, - continueInBackground: self.continueInBackground + continueInBackground: self.continueInBackground, + expectedResponseSize: nil ) let cdnData = state.cdnData @@ -573,7 +574,8 @@ private final class FetchImpl { limit: Int32(requestedLength) ), tag: self.parameters?.tag, - continueInBackground: self.continueInBackground + continueInBackground: self.continueInBackground, + expectedResponseSize: Int32(requestedLength) ) |> map { result -> FilePartResult in switch result { @@ -617,7 +619,8 @@ private final class FetchImpl { offset: part.fetchRange.lowerBound, limit: Int32(requestedLength)), tag: self.parameters?.tag, - continueInBackground: self.continueInBackground + continueInBackground: self.continueInBackground, + expectedResponseSize: Int32(requestedLength) ) |> map { result -> FilePartResult in switch result { diff --git a/submodules/TelegramCore/Sources/Network/MultipartFetch.swift b/submodules/TelegramCore/Sources/Network/MultipartFetch.swift index c201c679f1..be2a615e61 100644 --- a/submodules/TelegramCore/Sources/Network/MultipartFetch.swift +++ b/submodules/TelegramCore/Sources/Network/MultipartFetch.swift @@ -103,14 +103,14 @@ private struct DownloadWrapper { self.useMainConnection = useMainConnection } - func request(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool) -> Signal<(T, NetworkResponseInfo), MTRpcError> { + func request(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool, expectedResponseSize: Int32?) -> Signal<(T, NetworkResponseInfo), MTRpcError> { let target: MultiplexedRequestTarget if self.isCdn { target = .cdn(Int(self.datacenterId)) } else { target = .main(Int(self.datacenterId)) } - return network.multiplexedRequestManager.requestWithAdditionalInfo(to: target, consumerId: self.consumerId, resourceId: self.resourceId, data: data, tag: tag, continueInBackground: continueInBackground) + return network.multiplexedRequestManager.requestWithAdditionalInfo(to: target, consumerId: self.consumerId, resourceId: self.resourceId, data: data, tag: tag, continueInBackground: continueInBackground, expectedResponseSize: expectedResponseSize) |> mapError { error, _ -> MTRpcError in return error } @@ -191,7 +191,7 @@ private final class MultipartCdnHashSource { clusterContext = ClusterContext(disposable: disposable) self.clusterContexts[offset] = clusterContext - disposable.set((self.masterDownload.request(Api.functions.upload.getCdnFileHashes(fileToken: Buffer(data: self.fileToken), offset: offset), tag: nil, continueInBackground: self.continueInBackground) + disposable.set((self.masterDownload.request(Api.functions.upload.getCdnFileHashes(fileToken: Buffer(data: self.fileToken), offset: offset), tag: nil, continueInBackground: self.continueInBackground, expectedResponseSize: nil) |> map { partHashes, _ -> [Int64: Data] in var parsedPartHashes: [Int64: Data] = [:] for part in partHashes { @@ -347,7 +347,7 @@ private enum MultipartFetchSource { case .revalidate: return .fail(.revalidateMediaReference) case let .location(parsedLocation): - return download.request(Api.functions.upload.getFile(flags: 0, location: parsedLocation, offset: offset, limit: Int32(limit)), tag: tag, continueInBackground: continueInBackground) + return download.request(Api.functions.upload.getFile(flags: 0, location: parsedLocation, offset: offset, limit: Int32(limit)), tag: tag, continueInBackground: continueInBackground, expectedResponseSize: Int32(limit)) |> mapError { error -> MultipartFetchDownloadError in if error.errorDescription.hasPrefix("FILEREF_INVALID") || error.errorDescription.hasPrefix("FILE_REFERENCE_") { return .revalidateMediaReference @@ -377,7 +377,7 @@ private enum MultipartFetchSource { } } case let .web(_, location): - return download.request(Api.functions.upload.getWebFile(location: location, offset: Int32(offset), limit: Int32(limit)), tag: tag, continueInBackground: continueInBackground) + return download.request(Api.functions.upload.getWebFile(location: location, offset: Int32(offset), limit: Int32(limit)), tag: tag, continueInBackground: continueInBackground, expectedResponseSize: Int32(limit)) |> mapError { error -> MultipartFetchDownloadError in return .fatal } @@ -398,7 +398,7 @@ private enum MultipartFetchSource { updatedLength += 1 } - let part = download.request(Api.functions.upload.getCdnFile(fileToken: Buffer(data: fileToken), offset: offset, limit: Int32(updatedLength)), tag: nil, continueInBackground: continueInBackground) + let part = download.request(Api.functions.upload.getCdnFile(fileToken: Buffer(data: fileToken), offset: offset, limit: Int32(updatedLength)), tag: nil, continueInBackground: continueInBackground, expectedResponseSize: Int32(updatedLength)) |> mapError { _ -> MultipartFetchDownloadError in return .generic } @@ -913,7 +913,7 @@ private final class MultipartFetchManager { case let .cdn(_, _, fileToken, _, _, _, masterDownload, _): if !strongSelf.reuploadingToCdn { strongSelf.reuploadingToCdn = true - let reupload: Signal<[Api.FileHash], NoError> = masterDownload.request(Api.functions.upload.reuploadCdnFile(fileToken: Buffer(data: fileToken), requestToken: Buffer(data: token)), tag: nil, continueInBackground: strongSelf.continueInBackground) + let reupload: Signal<[Api.FileHash], NoError> = masterDownload.request(Api.functions.upload.reuploadCdnFile(fileToken: Buffer(data: fileToken), requestToken: Buffer(data: token)), tag: nil, continueInBackground: strongSelf.continueInBackground, expectedResponseSize: nil) |> map { result, _ -> [Api.FileHash] in return result } diff --git a/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift b/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift index e0929a3d47..c8c3b54d50 100644 --- a/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift +++ b/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift @@ -33,11 +33,12 @@ private final class RequestData { let tag: MediaResourceFetchTag? let continueInBackground: Bool let automaticFloodWait: Bool + let expectedResponseSize: Int32? let deserializeResponse: (Buffer) -> Any? let completed: (Any, NetworkResponseInfo) -> Void let error: (MTRpcError, Double) -> Void - init(id: Int32, consumerId: Int64, resourceId: String?, target: MultiplexedRequestTarget, functionDescription: FunctionDescription, payload: Buffer, tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, deserializeResponse: @escaping (Buffer) -> Any?, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) { + init(id: Int32, consumerId: Int64, resourceId: String?, target: MultiplexedRequestTarget, functionDescription: FunctionDescription, payload: Buffer, tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, expectedResponseSize: Int32?, deserializeResponse: @escaping (Buffer) -> Any?, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) { self.id = id self.consumerId = consumerId self.resourceId = resourceId @@ -46,6 +47,7 @@ private final class RequestData { self.tag = tag self.continueInBackground = continueInBackground self.automaticFloodWait = automaticFloodWait + self.expectedResponseSize = expectedResponseSize self.payload = payload self.deserializeResponse = deserializeResponse self.completed = completed @@ -138,12 +140,12 @@ private final class MultiplexedRequestManagerContext { } } - func request(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, (Buffer) -> Any?), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) -> Disposable { + func request(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, (Buffer) -> Any?), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, expectedResponseSize: Int32?, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) -> Disposable { let targetKey = MultiplexedRequestTargetKey(target: target, continueInBackground: continueInBackground) let requestId = self.nextId self.nextId += 1 - self.queuedRequests.append(RequestData(id: requestId, consumerId: consumerId, resourceId: resourceId, target: target, functionDescription: data.0, payload: data.1, tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, deserializeResponse: { buffer in + self.queuedRequests.append(RequestData(id: requestId, consumerId: consumerId, resourceId: resourceId, target: target, functionDescription: data.0, payload: data.1, tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, expectedResponseSize: expectedResponseSize, deserializeResponse: { buffer in return data.2(buffer) }, completed: { result, info in completed(result, info) @@ -241,7 +243,7 @@ private final class MultiplexedRequestManagerContext { let requestId = request.id selectedContext.requests.append(ExecutingRequestData(requestId: requestId, disposable: disposable)) let queue = self.queue - disposable.set(selectedContext.worker.rawRequest((request.functionDescription, request.payload, request.deserializeResponse), automaticFloodWait: request.automaticFloodWait).start(next: { [weak self, weak selectedContext] result, info in + disposable.set(selectedContext.worker.rawRequest((request.functionDescription, request.payload, request.deserializeResponse), automaticFloodWait: request.automaticFloodWait, expectedResponseSize: request.expectedResponseSize).start(next: { [weak self, weak selectedContext] result, info in queue.async { guard let strongSelf = self else { return @@ -341,13 +343,13 @@ final class MultiplexedRequestManager { return disposable } - func request(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true) -> Signal { + func request(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true, expectedResponseSize: Int32?) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.context.with { context in disposable.set(context.request(to: target, consumerId: consumerId, resourceId: resourceId, data: (data.0, data.1, { buffer in return data.2.parse(buffer) - }), tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, completed: { result, _ in + }), tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, expectedResponseSize: expectedResponseSize, completed: { result, _ in if let result = result as? T { subscriber.putNext(result) subscriber.putCompletion() @@ -362,13 +364,13 @@ final class MultiplexedRequestManager { } } - func requestWithAdditionalInfo(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true) -> Signal<(T, NetworkResponseInfo), (MTRpcError, Double)> { + func requestWithAdditionalInfo(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true, expectedResponseSize: Int32?) -> Signal<(T, NetworkResponseInfo), (MTRpcError, Double)> { return Signal { subscriber in let disposable = MetaDisposable() self.context.with { context in disposable.set(context.request(to: target, consumerId: consumerId, resourceId: resourceId, data: (data.0, data.1, { buffer in return data.2.parse(buffer) - }), tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, completed: { result, info in + }), tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, expectedResponseSize: expectedResponseSize, completed: { result, info in if let result = result as? T { subscriber.putNext((result, info)) subscriber.putCompletion() diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 63a2ecd68b..346d282ab2 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1671,6 +1671,10 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: updatedState.updateStory(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), story: story) case let .updateReadStories(userId, id): updatedState.readStories(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), maxId: id) + case let .updateStoriesStealthMode(stealthMode): + updatedState.updateStoryStealthMode(stealthMode) + case let .updateStoriesStealth(expireDate): + updatedState.updateStoryStealth(expireDate: expireDate) default: break } @@ -3155,7 +3159,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddScheduledMessages: OptimizeAddMessagesState? for operation in operations { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStoryStealth: if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) } @@ -4532,6 +4536,14 @@ func replayFinalState( ).postboxRepresentation) storyUpdates.append(InternalStoryUpdate.read(peerId: peerId, maxId: maxId)) + case let .UpdateStoryStealthMode(data): + var configuration = _internal_getStoryConfigurationState(transaction: transaction) + configuration.stealthModeState = Stories.StealthModeState(apiMode: data) + _internal_setStoryConfigurationState(transaction: transaction, state: configuration) + case let .UpdateStoryStealth(expireDate): + var configuration = _internal_getStoryConfigurationState(transaction: transaction) + configuration.stealthModeState.activeUntilTimestamp = expireDate + _internal_setStoryConfigurationState(transaction: transaction, state: configuration) } } diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index dfaeb00101..f4b2044975 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 160 + return 161 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index d2ec5c083d..340dfc1095 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -261,6 +261,7 @@ private enum PreferencesKeyValues: Int32 { case linksConfiguration = 29 case chatListFilterUpdates = 30 case globalPrivacySettings = 31 + case storiesConfiguration = 32 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { @@ -419,6 +420,12 @@ public struct PreferencesKeys { key.setInt32(0, value: PreferencesKeyValues.globalPrivacySettings.rawValue) return key }() + + public static let storiesConfiguration: ValueBoxKey = { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: PreferencesKeyValues.storiesConfiguration.rawValue) + return key + }() } private enum SharedDataKeyValues: Int32 { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift index 3e9abb4820..60b503f5d7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift @@ -457,5 +457,26 @@ public extension TelegramEngine.EngineData.Item { return value } } + + public struct StoryConfigurationState: TelegramEngineDataItem, PostboxViewDataItem { + public typealias Result = Stories.ConfigurationState + + public init() { + } + + var key: PostboxViewKey { + return .preferences(keys: Set([PreferencesKeys.storiesConfiguration])) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? PreferencesView else { + preconditionFailure() + } + guard let value = view.values[PreferencesKeys.storiesConfiguration]?.get(Stories.ConfigurationState.self) else { + return Stories.ConfigurationState.default + } + return value + } + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 13c7c2fcbd..c1e4b576eb 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -478,6 +478,27 @@ public enum Stories { } } + public struct StealthModeState: Equatable, Codable { + public var activeUntilTimestamp: Int32? + public var cooldownUntilTimestamp: Int32? + + public init( + activeUntilTimestamp: Int32?, + cooldownUntilTimestamp: Int32? + ) { + self.activeUntilTimestamp = activeUntilTimestamp + self.cooldownUntilTimestamp = cooldownUntilTimestamp + } + } + + public struct ConfigurationState: Equatable, Codable { + public var stealthModeState: StealthModeState + + public init(stealthModeState: StealthModeState) { + self.stealthModeState = stealthModeState + } + } + public final class SubscriptionsState: Equatable, Codable { private enum CodingKeys: CodingKey { case opaqueState @@ -1950,3 +1971,91 @@ func _internal_refreshSeenStories(postbox: Postbox, network: Network) -> Signal< |> ignoreValues } } + +extension Stories.ConfigurationState { + static var `default`: Stories.ConfigurationState { + return Stories.ConfigurationState( + stealthModeState: Stories.StealthModeState( + activeUntilTimestamp: nil, + cooldownUntilTimestamp: nil + ) + ) + } +} + +extension Stories.StealthModeState { + init(apiMode: Api.StoriesStealthMode) { + switch apiMode { + case let .storiesStealthMode(_, activeUntilDate, cooldownUntilDate): + self.init( + activeUntilTimestamp: activeUntilDate, + cooldownUntilTimestamp: cooldownUntilDate + ) + } + } +} + +public extension Stories.StealthModeState { + func actualizedNow() -> Stories.StealthModeState { + let timestamp = Int32(Date().timeIntervalSince1970) + + var activeUntilTimestamp = self.activeUntilTimestamp + var cooldownUntilTimestamp = self.cooldownUntilTimestamp + + if let activeUntilTimestampValue = activeUntilTimestamp, activeUntilTimestampValue < timestamp { + activeUntilTimestamp = nil + } + if let cooldownUntilTimestampValue = cooldownUntilTimestamp, cooldownUntilTimestampValue < timestamp { + cooldownUntilTimestamp = nil + } + + return Stories.StealthModeState( + activeUntilTimestamp: activeUntilTimestamp, + cooldownUntilTimestamp: cooldownUntilTimestamp + ) + } +} + +func _internal_getStoryConfigurationState(transaction: Transaction) -> Stories.ConfigurationState { + return transaction.getPreferencesEntry(key: PreferencesKeys.storiesConfiguration)?.get(Stories.ConfigurationState.self) ?? .default +} + +func _internal_setStoryConfigurationState(transaction: Transaction, state: Stories.ConfigurationState, force: Bool = false) { + transaction.setPreferencesEntry(key: PreferencesKeys.storiesConfiguration, value: PreferencesEntry(state)) +} + +func _internal_enableStoryStealthMode(account: Account) -> Signal { + var flags: Int32 = 0 + flags |= 1 << 0 + flags |= 1 << 1 + return account.network.request(Api.functions.stories.activateStealthMode(flags: flags)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction in + let appConfig = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? .defaultValue + + if let data = appConfig.data { + if let futurePeriod = data["stories_stealth_future_period"] as? Double, let cooldownPeriod = data["stories_stealth_cooldown_period"] as? Double { + + var futurePeriodInt32: Int32 + futurePeriodInt32 = Int32(futurePeriod) + var cooldownPeriodInt32: Int32 + cooldownPeriodInt32 = Int32(cooldownPeriod) + + #if DEBUG && false + futurePeriodInt32 = 30 + cooldownPeriodInt32 = 60 + #endif + + var config = _internal_getStoryConfigurationState(transaction: transaction) + config.stealthModeState.activeUntilTimestamp = Int32(Date().timeIntervalSince1970) + futurePeriodInt32 + config.stealthModeState.cooldownUntilTimestamp = Int32(Date().timeIntervalSince1970) + cooldownPeriodInt32 + _internal_setStoryConfigurationState(transaction: transaction, state: config, force: true) + } + } + } + |> ignoreValues + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index cb5ae41885..8662d3f14a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -310,8 +310,9 @@ public final class StorySubscriptionsContext { } let _ = (self.postbox.transaction { transaction -> Void in + var updatedStealthMode: Api.StoriesStealthMode? switch result { - case let .allStoriesNotModified(state): + case let .allStoriesNotModified(_, state, stealthMode): self.loadedStateMark = .value(state) let (currentStateValue, _) = transaction.getAllStorySubscriptions(key: subscriptionsKey) let currentState = currentStateValue.flatMap { $0.get(Stories.SubscriptionsState.self) } @@ -326,9 +327,11 @@ public final class StorySubscriptionsContext { refreshId: currentState?.refreshId ?? UInt64.random(in: 0 ... UInt64.max), hasMore: hasMore ))) - case let .allStories(flags, _, state, userStories, users): - //TODO:count + if isRefresh && !isHidden { + updatedStealthMode = stealthMode + } + case let .allStories(flags, _, state, userStories, users, stealthMode): let parsedPeers = AccumulatedPeers(transaction: transaction, chats: [], users: users) let hasMore: Bool = (flags & (1 << 0)) != 0 @@ -378,6 +381,10 @@ public final class StorySubscriptionsContext { } } + if isRefresh && !isHidden { + updatedStealthMode = stealthMode + } + transaction.replaceAllStorySubscriptions(key: subscriptionsKey, state: CodableEntry(Stories.SubscriptionsState( opaqueState: state, refreshId: UInt64.random(in: 0 ... UInt64.max), @@ -386,6 +393,12 @@ public final class StorySubscriptionsContext { updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) } + + if let updatedStealthMode = updatedStealthMode { + var configuration = _internal_getStoryConfigurationState(transaction: transaction) + configuration.stealthModeState = Stories.StealthModeState(apiMode: updatedStealthMode) + _internal_setStoryConfigurationState(transaction: transaction, state: configuration) + } } |> deliverOn(self.queue)).start(completed: { [weak self] in guard let `self` = self else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index cfdd38d0ed..0aed71eee0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1090,5 +1090,9 @@ public extension TelegramEngine { public func exportStoryLink(peerId: EnginePeer.Id, id: Int32) -> Signal { return _internal_exportStoryLink(account: self.account, peerId: peerId, id: id) } + + public func enableStoryStealthMode() -> Signal { + return _internal_enableStoryStealthMode(account: self.account) + } } } diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index e8283aa3c8..90fdba6a78 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -322,10 +322,9 @@ public final class ChatListNavigationBar: Component { let storiesUnlocked: Bool if allowAvatarsExpansion { storiesOffsetFraction = max(0.0, min(4.0, -offset / ChatListNavigationBar.storiesScrollHeight)) - - if offset <= -60.0 { + if offset <= -65.0 { storiesUnlocked = true - } else if offset >= -58.0 { + } else if offset >= -61.0 { storiesUnlocked = false } else { storiesUnlocked = self.storiesUnlocked diff --git a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift index a2ac72134f..12e9cb787c 100644 --- a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift +++ b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift @@ -67,15 +67,18 @@ public final class LottieComponent: Component { public let content: Content public let color: UIColor? public let startingPosition: StartingPosition + public let size: CGSize? public init( content: Content, color: UIColor? = nil, - startingPosition: StartingPosition = .end + startingPosition: StartingPosition = .end, + size: CGSize? = nil ) { self.content = content self.color = color self.startingPosition = startingPosition + self.size = size } public static func ==(lhs: LottieComponent, rhs: LottieComponent) -> Bool { @@ -88,6 +91,9 @@ public final class LottieComponent: Component { if lhs.startingPosition != rhs.startingPosition { return false } + if lhs.size != rhs.size { + return false + } return true } @@ -170,6 +176,7 @@ public final class LottieComponent: Component { return } if !self.isVisible { + self.scheduledPlayOnce = true return } @@ -296,9 +303,11 @@ public final class LottieComponent: Component { self.component = component self.state = state + let size = component.size ?? availableSize + var redrawImage = false - let displaySize = CGSize(width: availableSize.width * UIScreenScale, height: availableSize.height * UIScreenScale) + let displaySize = CGSize(width: size.width * UIScreenScale, height: size.height * UIScreenScale) if self.currentDisplaySize != displaySize { self.currentDisplaySize = displaySize redrawImage = true @@ -323,7 +332,7 @@ public final class LottieComponent: Component { transition.setTintColor(view: self, color: color) } - return availableSize + return size } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 0c2dc470d8..591232b90b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -77,6 +77,7 @@ swift_library( "//submodules/MediaPasteboardUI", "//submodules/WebPBinding", "//submodules/Utils/RangeSet", + "//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 241d8be961..9c21cd878a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -1144,7 +1144,6 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { listContext.state, self.focusedIdUpdated.get() ) - //|> delay(0.4, queue: .mainQueue()) |> deliverOnMainQueue).start(next: { [weak self] data, state, _ in guard let self else { return diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 6cadc3a186..07410b03a1 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -333,6 +333,10 @@ private final class StoryContainerScreenComponent: Component { private let focusedItem = ValuePromise(nil, ignoreRepeated: true) private var contentUpdatedDisposable: Disposable? + private var stealthModeActiveUntilTimestamp: Int32? + private var stealthModeDisposable: Disposable? + private var stealthModeTimer: Foundation.Timer? + private let storyItemSharedState = StoryContentItem.SharedState() private var visibleItemSetViews: [EnginePeer.Id: ItemSetView] = [:] @@ -587,6 +591,8 @@ private final class StoryContainerScreenComponent: Component { self.contentUpdatedDisposable?.dispose() self.volumeButtonsListenerShouldBeActiveDisposable?.dispose() self.headphonesDisposable?.dispose() + self.stealthModeDisposable?.dispose() + self.stealthModeTimer?.invalidate() } override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { @@ -1073,6 +1079,21 @@ private final class StoryContainerScreenComponent: Component { } } }) + + self.stealthModeDisposable = (component.context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState() + ) + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + if self.stealthModeActiveUntilTimestamp != state.stealthModeState.activeUntilTimestamp { + self.stealthModeActiveUntilTimestamp = state.stealthModeState.activeUntilTimestamp + if update { + self.state?.updated(transition: .immediate) + } + } + }) update = true } @@ -1160,6 +1181,32 @@ private final class StoryContainerScreenComponent: Component { self.component = component self.state = state + var stealthModeTimeout: Int32? + if let stealthModeActiveUntilTimestamp = self.stealthModeActiveUntilTimestamp { + let timestamp = Int32(Date().timeIntervalSince1970) + if stealthModeActiveUntilTimestamp > timestamp { + stealthModeTimeout = stealthModeActiveUntilTimestamp - timestamp + + if self.stealthModeTimer == nil { + self.stealthModeTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in + self?.state?.updated(transition: .immediate) + }) + } + } else { + stealthModeTimeout = nil + if let stealthModeTimer = self.stealthModeTimer { + self.stealthModeTimer = nil + stealthModeTimer.invalidate() + } + } + } else { + stealthModeTimeout = nil + if let stealthModeTimer = self.stealthModeTimer { + self.stealthModeTimer = nil + stealthModeTimer.invalidate() + } + } + if let pendingNavigationToItemId = self.pendingNavigationToItemId { if let slice = component.content.stateValue?.slice, slice.peer.id == pendingNavigationToItemId.peerId { if slice.item.storyItem.id == pendingNavigationToItemId.id { @@ -1430,7 +1477,8 @@ private final class StoryContainerScreenComponent: Component { }, keyboardInputData: self.inputMediaNodeDataPromise.get(), closeFriends: self.closeFriendsPromise, - sharedViewListsContext: self.sharedViewListsContext + sharedViewListsContext: self.sharedViewListsContext, + stealthModeTimeout: stealthModeTimeout )), environment: {}, containerSize: itemSetContainerSize diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 677a424c0e..2f1b4c6a51 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -34,6 +34,7 @@ import PremiumUI import AttachmentUI import StickerPackPreviewUI import TextNodeWithEntities +import TelegramStringFormatting public final class StoryAvailableReactions: Equatable { let reactionItems: [ReactionItem] @@ -113,6 +114,7 @@ public final class StoryItemSetContainerComponent: Component { public let keyboardInputData: Signal public let closeFriends: Promise<[EnginePeer]> let sharedViewListsContext: StoryItemSetViewListComponent.SharedListsContext + let stealthModeTimeout: Int32? init( context: AccountContext, @@ -145,7 +147,8 @@ public final class StoryItemSetContainerComponent: Component { toggleAmbientMode: @escaping () -> Void, keyboardInputData: Signal, closeFriends: Promise<[EnginePeer]>, - sharedViewListsContext: StoryItemSetViewListComponent.SharedListsContext + sharedViewListsContext: StoryItemSetViewListComponent.SharedListsContext, + stealthModeTimeout: Int32? ) { self.context = context self.externalState = externalState @@ -178,6 +181,7 @@ public final class StoryItemSetContainerComponent: Component { self.keyboardInputData = keyboardInputData self.closeFriends = closeFriends self.sharedViewListsContext = sharedViewListsContext + self.stealthModeTimeout = stealthModeTimeout } public static func ==(lhs: StoryItemSetContainerComponent, rhs: StoryItemSetContainerComponent) -> Bool { @@ -232,6 +236,9 @@ public final class StoryItemSetContainerComponent: Component { if lhs.pinchState != rhs.pinchState { return false } + if lhs.stealthModeTimeout != rhs.stealthModeTimeout { + return false + } return true } @@ -1794,6 +1801,14 @@ public final class StoryItemSetContainerComponent: Component { isUnsupported = true disabledPlaceholder = component.strings.Story_FooterReplyUnavailable } + + let inputPlaceholder: String + if let stealthModeTimeout = component.stealthModeTimeout { + //TODO:localize + inputPlaceholder = "Stealth Mode active – \(stringForDuration(stealthModeTimeout))" + } else { + inputPlaceholder = component.strings.Story_InputPlaceholderReplyPrivately + } var keyboardHeight = component.deviceMetrics.standardInputHeight(inLandscape: false) let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden @@ -1807,7 +1822,7 @@ public final class StoryItemSetContainerComponent: Component { theme: component.theme, strings: component.strings, style: .story, - placeholder: component.strings.Story_InputPlaceholderReplyPrivately, + placeholder: inputPlaceholder, maxLength: 4096, queryTypes: [.mention, .emoji], alwaysDarkWhenHasText: component.metrics.widthClass == .regular, @@ -3997,12 +4012,16 @@ public final class StoryItemSetContainerComponent: Component { TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: component.slice.peer.id), TelegramEngine.EngineData.Item.NotificationSettings.Global(), TelegramEngine.EngineData.Item.Contacts.Top(), - TelegramEngine.EngineData.Item.Peer.IsContact(id: component.slice.peer.id) + TelegramEngine.EngineData.Item.Peer.IsContact(id: component.slice.peer.id), + TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId) ) - |> deliverOnMainQueue).start(next: { [weak self] settings, globalSettings, topSearchPeers, isContact in + |> deliverOnMainQueue).start(next: { [weak self] settings, globalSettings, topSearchPeers, isContact, accountPeer in guard let self, let component = self.component, let controller = component.controller() else { return } + guard case let .user(accountUser) = accountPeer else { + return + } self.dismissAllTooltips() @@ -4106,17 +4125,40 @@ public final class StoryItemSetContainerComponent: Component { if !component.slice.item.storyItem.isForwardingDisabled { let saveText: String = component.strings.Story_Context_SaveToGallery items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) + return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Download" : "Chat/Context Menu/DownloadLocked"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in a(.default) - guard let self else { + guard let self, let component = self.component else { return } - self.requestSave() + + if accountUser.isPremium { + self.requestSave() + } else { + let premiumScreen = PremiumIntroScreen(context: component.context, source: .stories) + component.controller()?.push(premiumScreen) + } }))) } + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Stealth Mode", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component else { + return + } + if accountUser.isPremium { + self.sendMessageContext.requestStealthMode(view: self) + } else { + let premiumScreen = PremiumIntroScreen(context: component.context, source: .stories) + component.controller()?.push(premiumScreen) + } + }))) + if !component.slice.peer.isService && component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_CopyLink, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 82df7f8971..133d01b8f7 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -42,6 +42,7 @@ import MediaPasteboardUI import WebPBinding import ContextUI import ChatScheduleTimeController +import StoryStealthModeSheetScreen final class StoryItemSetContainerSendMessage { enum InputMode { @@ -60,6 +61,8 @@ final class StoryItemSetContainerSendMessage { weak var statusController: ViewController? var isViewingAttachedStickers = false + var currentTooltipUpdateTimer: Foundation.Timer? + var currentInputMode: InputMode = .text private var needsInputActivation = false @@ -95,6 +98,7 @@ final class StoryItemSetContainerSendMessage { self.navigationActionDisposable.dispose() self.resolvePeerByNameDisposable.dispose() self.inputMediaNodeDataDisposable?.dispose() + self.currentTooltipUpdateTimer?.invalidate() } func setup(context: AccountContext, view: StoryItemSetContainerComponent.View, inputPanelExternalState: MessageInputPanelComponent.ExternalState, keyboardInputData: Signal) { @@ -2835,4 +2839,125 @@ final class StoryItemSetContainerSendMessage { self.isViewingAttachedStickers = true view.updateIsProgressPaused() } + + func requestStealthMode(view: StoryItemSetContainerComponent.View) { + guard let component = view.component else { + return + } + + let _ = (component.context.engine.data.get( + TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState(), + TelegramEngine.EngineData.Item.Configuration.App() + ) + |> deliverOnMainQueue).start(next: { [weak self, weak view] config, appConfig in + guard let self, let view, let component = view.component, let controller = component.controller() else { + return + } + + let timestamp = Int32(Date().timeIntervalSince1970) + if let activeUntilTimestamp = config.stealthModeState.actualizedNow().activeUntilTimestamp, activeUntilTimestamp > timestamp { + let remainingActiveSeconds = activeUntilTimestamp - timestamp + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + //TODO:localize + let text = "The creators of stories you will view in the next \(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds)) won't see you in the viewers' lists." + let tooltipScreen = UndoOverlayController( + presentationData: presentationData, + content: .actionSucceeded(title: "You are in Stealth Mode", text: text, cancel: "", destructive: false), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in + return false + } + ) + weak var tooltipScreenValue: UndoOverlayController? = tooltipScreen + self.currentTooltipUpdateTimer?.invalidate() + self.currentTooltipUpdateTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self, weak view] _ in + guard let self, let view, let component = view.component else { + return + } + guard let tooltipScreenValue else { + self.currentTooltipUpdateTimer?.invalidate() + self.currentTooltipUpdateTimer = nil + return + } + + let timestamp = Int32(Date().timeIntervalSince1970) + let remainingActiveSeconds = max(1, activeUntilTimestamp - timestamp) + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + //TODO:localize + let text = "The creators of stories you will view in the next \(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds)) won't see you in the viewers' lists." + tooltipScreenValue.content = .actionSucceeded(title: "You are in Stealth Mode", text: text, cancel: "", destructive: false) + }) + + self.tooltipScreen?.dismiss(animated: true) + self.tooltipScreen = tooltipScreen + controller.present(tooltipScreen, in: .current) + + view.updateIsProgressPaused() + + return + } + + let pastPeriod: Int32 + let futurePeriod: Int32 + if let data = appConfig.data, let futurePeriodF = data["stories_stealth_future_period"] as? Double, let pastPeriodF = data["stories_stealth_past_period"] as? Double { + futurePeriod = Int32(futurePeriodF) + pastPeriod = Int32(pastPeriodF) + } else { + pastPeriod = 5 * 60 + futurePeriod = 25 * 60 + } + + let sheet = StoryStealthModeSheetScreen( + context: component.context, + cooldownUntilTimestamp: config.stealthModeState.actualizedNow().cooldownUntilTimestamp, + backwardDuration: pastPeriod, + forwardDuration: futurePeriod, + buttonAction: { [weak self, weak view] in + guard let self, let view, let component = view.component else { + return + } + + let _ = (component.context.engine.messages.enableStoryStealthMode() + |> deliverOnMainQueue).start(completed: { [weak self, weak view] in + guard let self, let view, let component = view.component, let controller = component.controller() else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + //TODO:localize + let text = "The creators of stories you viewed in the last \(timeIntervalString(strings: presentationData.strings, value: pastPeriod)) or will view in the next \(timeIntervalString(strings: presentationData.strings, value: futurePeriod)) won’t see you in the viewers’ lists." + let tooltipScreen = UndoOverlayController( + presentationData: presentationData, + content: .actionSucceeded(title: "Stealth Mode On", text: text, cancel: "", destructive: false), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in + return false + } + ) + self.tooltipScreen?.dismiss(animated: true) + self.tooltipScreen = tooltipScreen + controller.present(tooltipScreen, in: .current) + + view.updateIsProgressPaused() + + HapticFeedback().success() + }) + } + ) + sheet.wasDismissed = { [weak self, weak view] in + guard let self, let view else { + return + } + self.actionSheet = nil + view.updateIsProgressPaused() + } + self.actionSheet = sheet + view.updateIsProgressPaused() + controller.push(sheet) + }) + } } diff --git a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/BUILD new file mode 100644 index 0000000000..0fc67aed4c --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/BUILD @@ -0,0 +1,32 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "StoryStealthModeSheetScreen", + module_name = "StoryStealthModeSheetScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AccountContext", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/SheetComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/ToastComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/Markdown", + "//submodules/TelegramStringFormatting", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeInfoContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeInfoContentComponent.swift new file mode 100644 index 0000000000..42c38914a4 --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeInfoContentComponent.swift @@ -0,0 +1,307 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import MultilineTextComponent +import TelegramPresentationData +import AppBundle +import BundleIconComponent +import Markdown +import TelegramCore +import BalancedTextComponent + +public final class StoryStealthModeInfoContentComponent: Component { + public let theme: PresentationTheme + public let strings: PresentationStrings + public let backwardDuration: Int32 + public let forwardDuration: Int32 + + public init( + theme: PresentationTheme, + strings: PresentationStrings, + backwardDuration: Int32, + forwardDuration: Int32 + ) { + self.theme = theme + self.strings = strings + self.backwardDuration = backwardDuration + self.forwardDuration = forwardDuration + } + + public static func ==(lhs: StoryStealthModeInfoContentComponent, rhs: StoryStealthModeInfoContentComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.backwardDuration != rhs.backwardDuration { + return false + } + if lhs.forwardDuration != rhs.forwardDuration { + return false + } + return true + } + + private final class Item { + let icon = ComponentView() + let title = ComponentView() + let text = ComponentView() + + init() { + } + } + + public final class View: UIView { + private let scrollView: UIScrollView + private let iconBackground: UIImageView + private let iconForeground: UIImageView + + private let title = ComponentView() + private let mainText = ComponentView() + + private var items: [Item] = [] + + private var component: StoryStealthModeInfoContentComponent? + + public override init(frame: CGRect) { + self.scrollView = UIScrollView() + + self.iconBackground = UIImageView() + self.iconForeground = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.scrollView) + + self.scrollView.delaysContentTouches = false + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.scrollsToTop = false + self.scrollView.clipsToBounds = false + + self.scrollView.addSubview(self.iconBackground) + self.scrollView.addSubview(self.iconForeground) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = super.hitTest(point, with: event) { + return result + } else { + return nil + } + } + + func update(component: StoryStealthModeInfoContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let sideInset: CGFloat = 16.0 + let sideIconInset: CGFloat = 40.0 + + var contentHeight: CGFloat = 0.0 + + let iconSize: CGFloat = 90.0 + if self.iconBackground.image == nil { + let backgroundColors: [UIColor] = [UIColor(rgb: 0x3DA1FD), UIColor(rgb: 0x34C76F)] + let colors: NSArray = [backgroundColors[0].cgColor, backgroundColors[1].cgColor] + self.iconBackground.image = generateGradientFilledCircleImage(diameter: iconSize, colors: colors) + } + let iconBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize) * 0.5), y: contentHeight), size: CGSize(width: iconSize, height: iconSize)) + transition.setFrame(view: self.iconBackground, frame: iconBackgroundFrame) + + if self.iconForeground.image == nil { + self.iconForeground.image = generateTintedImage(image: UIImage(bundleImageName: "Stories/StealthModeIntroIconMain"), color: .white) + } + if let image = self.iconForeground.image { + transition.setFrame(view: self.iconForeground, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + floor((iconBackgroundFrame.width - image.size.width) * 0.5), y: iconBackgroundFrame.minY + floor((iconBackgroundFrame.height - image.size.height) * 0.5)), size: image.size)) + } + + contentHeight += iconSize + contentHeight += 15.0 + + let titleString = NSMutableAttributedString() + //TODO:localize + titleString.append(NSAttributedString(string: "Stealth Mode", font: Font.semibold(19.0), textColor: component.theme.list.itemPrimaryTextColor)) + let imageAttachment = NSTextAttachment() + imageAttachment.image = self.iconBackground.image + titleString.append(NSAttributedString(attachment: imageAttachment)) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(titleString), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.scrollView.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize)) + } + contentHeight += titleSize.height + contentHeight += 15.0 + + //TODO:localize + let text: String = "Turn Stealth Mode on to hide the fact that you viewed peoples' stories from them." + let mainText = NSMutableAttributedString() + mainText.append(parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet( + font: Font.regular(15.0), + textColor: component.theme.list.itemSecondaryTextColor + ), + bold: MarkdownAttributeSet( + font: Font.semibold(15.0), + textColor: component.theme.list.itemSecondaryTextColor + ), + link: MarkdownAttributeSet( + font: Font.regular(15.0), + textColor: component.theme.list.itemAccentColor, + additionalAttributes: [:] + ), + linkAttribute: { attributes in + return ("URL", "") + } + ))) + + let mainTextSize = self.mainText.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(mainText), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + if let mainTextView = self.mainText.view { + if mainTextView.superview == nil { + self.scrollView.addSubview(mainTextView) + } + transition.setFrame(view: mainTextView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - mainTextSize.width) * 0.5), y: contentHeight), size: mainTextSize)) + } + contentHeight += mainTextSize.height + contentHeight += 24.0 + + struct ItemDesc { + var icon: String + var title: String + var text: String + } + //TODO:localize + let itemDescs: [ItemDesc] = [ + ItemDesc( + icon: "Stories/StealthModeIntroIconHidePrevious", + title: "Hide Recent Views", + text: "Hide my views in the last **\(timeIntervalString(strings: component.strings, value: component.backwardDuration))**." + ), + ItemDesc( + icon: "Stories/StealthModeIntroIconHideNext", + title: "Hide Next Views", + text: "Hide my views in the next **\(timeIntervalString(strings: component.strings, value: component.forwardDuration))**." + ) + ] + for i in 0 ..< itemDescs.count { + if i != 0 { + contentHeight += 24.0 + } + + let item: Item + if self.items.count > i { + item = self.items[i] + } else { + item = Item() + self.items.append(item) + } + + let itemDesc = itemDescs[i] + + let iconSize = item.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: itemDesc.icon, + tintColor: component.theme.list.itemAccentColor + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let titleSize = item.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: itemDesc.title, font: Font.semibold(15.0), textColor: component.theme.list.itemPrimaryTextColor)), + maximumNumberOfLines: 0, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - sideIconInset, height: 1000.0) + ) + let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor) + let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: component.theme.list.itemSecondaryTextColor) + let textSize = item.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .markdown(text: itemDesc.text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil })), + maximumNumberOfLines: 0, + lineSpacing: 0.18 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - sideIconInset, height: 1000.0) + ) + + if let iconView = item.icon.view { + if iconView.superview == nil { + self.scrollView.addSubview(iconView) + } + transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 4.0), size: iconSize)) + } + + if let titleView = item.title.view { + if titleView.superview == nil { + self.scrollView.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: sideInset + sideIconInset, y: contentHeight), size: titleSize)) + } + contentHeight += titleSize.height + contentHeight += 2.0 + + if let textView = item.text.view { + if textView.superview == nil { + self.scrollView.addSubview(textView) + } + transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: sideInset + sideIconInset, y: contentHeight), size: textSize)) + } + contentHeight += textSize.height + } + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + let size = CGSize(width: availableSize.width, height: min(availableSize.height, contentSize.height)) + if self.scrollView.bounds.size != size || self.scrollView.contentSize != contentSize { + self.scrollView.frame = CGRect(origin: CGPoint(), size: size) + self.scrollView.contentSize = contentSize + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift new file mode 100644 index 0000000000..b6988d504c --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift @@ -0,0 +1,398 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ViewControllerComponent +import AccountContext +import SheetComponent +import ButtonComponent +import ToastComponent +import LottieComponent +import MultilineTextComponent +import Markdown +import TelegramStringFormatting + +private final class StoryStealthModeSheetContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let cooldownUntilTimestamp: Int32? + let backwardDuration: Int32 + let forwardDuration: Int32 + let dismiss: () -> Void + + init( + cooldownUntilTimestamp: Int32?, + backwardDuration: Int32, + forwardDuration: Int32, + dismiss: @escaping () -> Void + ) { + self.cooldownUntilTimestamp = cooldownUntilTimestamp + self.backwardDuration = backwardDuration + self.forwardDuration = forwardDuration + self.dismiss = dismiss + } + + static func ==(lhs: StoryStealthModeSheetContentComponent, rhs: StoryStealthModeSheetContentComponent) -> Bool { + if lhs.cooldownUntilTimestamp != rhs.cooldownUntilTimestamp { + return false + } + if lhs.backwardDuration != rhs.backwardDuration { + return false + } + if lhs.forwardDuration != rhs.forwardDuration { + return false + } + return true + } + + final class View: UIView { + private var toast: ComponentView? + private let content = ComponentView() + private let button = ComponentView() + + private var component: StoryStealthModeSheetContentComponent? + private weak var state: EmptyComponentState? + + private var timer: Timer? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.timer?.invalidate() + } + + func update(component: StoryStealthModeSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + var remainingCooldownSeconds: Int32 = 0 + if let cooldownUntilTimestamp = component.cooldownUntilTimestamp { + remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) + remainingCooldownSeconds = max(0, remainingCooldownSeconds) + } + if remainingCooldownSeconds > 0 { + if self.timer == nil { + self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + self.state?.updated(transition: .immediate) + }) + } + } else { + if let timer = self.timer { + self.timer = nil + timer.invalidate() + } + } + + let environment = environment[EnvironmentType.self].value + + let sideInset: CGFloat = 16.0 + + if remainingCooldownSeconds > 0 { + let toast: ComponentView + var toastTransition = transition + if let current = self.toast { + toast = current + } else { + toastTransition = toastTransition.withAnimation(.none) + toast = ComponentView() + self.toast = toast + } + //TODO:localize + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let toastSize = toast.update( + transition: toastTransition, + component: AnyComponent(ToastContentComponent( + icon: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "anim_infotip"), + startingPosition: .begin, + size: CGSize(width: 32.0, height: 32.0) + )), + content: AnyComponent(MultilineTextComponent( + text: .markdown(text: "Please wait until the **Stealth Mode** is ready to use again", attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil })), + maximumNumberOfLines: 0 + )) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + if let toastView = toast.view { + if toastView.superview == nil { + self.addSubview(toastView) + + if let toastView = toastView as? ToastContentComponent.View, let iconView = toastView.iconView as? LottieComponent.View { + iconView.playOnce() + } + } + toastTransition.setFrame(view: toastView, frame: CGRect(origin: CGPoint(x: sideInset, y: -sideInset - toastSize.height), size: toastSize)) + } + } else { + if let toast = self.toast { + self.toast = nil + toast.view?.removeFromSuperview() + } + } + + var contentHeight: CGFloat = 0.0 + contentHeight += 32.0 + + let contentSize = self.content.update( + transition: transition, + component: AnyComponent(StoryStealthModeInfoContentComponent( + theme: environment.theme, + strings: environment.strings, + backwardDuration: component.backwardDuration, + forwardDuration: component.forwardDuration + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + if let contentView = self.content.view { + if contentView.superview == nil { + self.addSubview(contentView) + } + transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: contentSize)) + } + contentHeight += contentSize.height + contentHeight += 41.0 + + //TODO:localize + let buttonText: String + if remainingCooldownSeconds <= 0 { + buttonText = "Enable Stealth Mode" + } else { + buttonText = "Available in \(stringForDuration(remainingCooldownSeconds))" + } + let buttonSize = self.button.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + Text(text: buttonText, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) + )), + isEnabled: remainingCooldownSeconds <= 0, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: buttonFrame) + } + contentHeight += buttonSize.height + + if environment.safeInsets.bottom.isZero { + contentHeight += 16.0 + } else { + contentHeight += environment.safeInsets.bottom + 14.0 + } + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class StoryStealthModeSheetScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let cooldownUntilTimestamp: Int32? + let backwardDuration: Int32 + let forwardDuration: Int32 + let buttonAction: (() -> Void)? + + init( + context: AccountContext, + cooldownUntilTimestamp: Int32?, + backwardDuration: Int32, + forwardDuration: Int32, + buttonAction: (() -> Void)? + ) { + self.context = context + self.cooldownUntilTimestamp = cooldownUntilTimestamp + self.backwardDuration = backwardDuration + self.forwardDuration = forwardDuration + self.buttonAction = buttonAction + } + + static func ==(lhs: StoryStealthModeSheetScreenComponent, rhs: StoryStealthModeSheetScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.cooldownUntilTimestamp != rhs.cooldownUntilTimestamp { + return false + } + if lhs.backwardDuration != rhs.backwardDuration { + return false + } + if lhs.forwardDuration != rhs.forwardDuration { + return false + } + return true + } + + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>() + private let sheetAnimateOut = ActionSlot>() + + private var component: StoryStealthModeSheetScreenComponent? + private var environment: EnvironmentType? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: StoryStealthModeSheetScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environment + + let sheetEnvironment = SheetComponentEnvironment( + isDisplaying: environment.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { [weak self] _ in + guard let self, let environment = self.environment else { + return + } + self.sheetAnimateOut.invoke(Action { _ in + if let controller = environment.controller() { + controller.dismiss(completion: nil) + } + }) + } + ) + let _ = self.sheet.update( + transition: transition, + component: AnyComponent(SheetComponent( + content: AnyComponent(StoryStealthModeSheetContentComponent( + cooldownUntilTimestamp: component.cooldownUntilTimestamp, + backwardDuration: component.backwardDuration, + forwardDuration: component.forwardDuration, + dismiss: { [weak self] in + guard let self else { + return + } + self.sheetAnimateOut.invoke(Action { [weak self] _ in + if let controller = environment.controller() { + controller.dismiss(completion: nil) + } + + guard let self else { + return + } + self.component?.buttonAction?() + }) + } + )), + backgroundColor: .color(environment.theme.list.plainBackgroundColor), + animateOut: self.sheetAnimateOut + )), + environment: { + environment + sheetEnvironment + }, + containerSize: availableSize + ) + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize)) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class StoryStealthModeSheetScreen: ViewControllerComponentContainer { + public init( + context: AccountContext, + cooldownUntilTimestamp: Int32?, + backwardDuration: Int32, + forwardDuration: Int32, + buttonAction: (() -> Void)? = nil + ) { + super.init(context: context, component: StoryStealthModeSheetScreenComponent( + context: context, + cooldownUntilTimestamp: cooldownUntilTimestamp, + backwardDuration: backwardDuration, + forwardDuration: forwardDuration, + buttonAction: buttonAction + ), navigationBarAppearance: .none, theme: .dark) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + } + + override public func dismiss(completion: (() -> Void)? = nil) { + super.dismiss(completion: { + completion?() + }) + self.wasDismissed?() + } +} diff --git a/submodules/TelegramUI/Components/ToastComponent/BUILD b/submodules/TelegramUI/Components/ToastComponent/BUILD new file mode 100644 index 0000000000..29d26d819c --- /dev/null +++ b/submodules/TelegramUI/Components/ToastComponent/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ToastComponent", + module_name = "ToastComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift b/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift new file mode 100644 index 0000000000..3d355dcc88 --- /dev/null +++ b/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift @@ -0,0 +1,106 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ComponentDisplayAdapters + +public final class ToastContentComponent: Component { + public let icon: AnyComponent + public let content: AnyComponent + + public init( + icon: AnyComponent, + content: AnyComponent + ) { + self.icon = icon + self.content = content + } + + public static func ==(lhs: ToastContentComponent, rhs: ToastContentComponent) -> Bool { + if lhs.icon != rhs.icon { + return false + } + if lhs.content != rhs.content { + return false + } + return true + } + + public final class View: UIView { + private let backgroundView: BlurredBackgroundView + private let icon = ComponentView() + private let content = ComponentView() + + public var iconView: UIView? { + return self.icon.view + } + + public var contentView: UIView? { + return self.content.view + } + + override public init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ToastContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + var contentHeight: CGFloat = 0.0 + + let leftInset: CGFloat = 9.0 + let rightInset: CGFloat = 6.0 + let verticalInset: CGFloat = 10.0 + let spacing: CGFloat = 9.0 + + let iconSize = self.icon.update( + transition: transition, + component: component.icon, + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - spacing, height: availableSize.height) + ) + let contentSize = self.content.update( + transition: transition, + component: component.content, + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset - spacing - iconSize.width, height: availableSize.height) + ) + + contentHeight += verticalInset * 2.0 + max(iconSize.height, contentSize.height) + + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: leftInset, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize)) + } + if let contentView = self.content.view { + if contentView.superview == nil { + self.addSubview(contentView) + } + transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: leftInset + iconSize.height + spacing, y: floor((contentHeight - contentSize.height) * 0.5)), size: contentSize)) + } + + let size = CGSize(width: availableSize.width, height: contentHeight) + self.backgroundView.updateColor(color: UIColor(white: 0.0, alpha: 0.7), transition: .immediate) + self.backgroundView.update(size: size, cornerRadius: 10.0, transition: transition.containedViewLayoutTransition) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DownloadLocked.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DownloadLocked.imageset/Contents.json new file mode 100644 index 0000000000..4444d5f412 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DownloadLocked.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "IconDownloadLocked.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DownloadLocked.imageset/IconDownloadLocked.svg b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DownloadLocked.imageset/IconDownloadLocked.svg new file mode 100644 index 0000000000..83f499ad6e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DownloadLocked.imageset/IconDownloadLocked.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Eye.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Eye.imageset/Contents.json new file mode 100644 index 0000000000..9f4a1173a4 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Eye.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "eye_301.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Eye.imageset/eye_301.svg b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Eye.imageset/eye_301.svg new file mode 100644 index 0000000000..38d3ca746a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Eye.imageset/eye_301.svg @@ -0,0 +1,3 @@ + + + diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/EyeLocked.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/EyeLocked.imageset/Contents.json new file mode 100644 index 0000000000..98ff298312 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/EyeLocked.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "IconEyeLocked.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/EyeLocked.imageset/IconEyeLocked.svg b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/EyeLocked.imageset/IconEyeLocked.svg new file mode 100644 index 0000000000..b40fc127aa --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/EyeLocked.imageset/IconEyeLocked.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconHideNext.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconHideNext.imageset/Contents.json new file mode 100644 index 0000000000..6095497252 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconHideNext.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "rewind_301.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconHideNext.imageset/rewind_301.svg b/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconHideNext.imageset/rewind_301.svg new file mode 100644 index 0000000000..2ef6beb95c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconHideNext.imageset/rewind_301.svg @@ -0,0 +1,4 @@ + + + + diff --git a/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconHidePrevious.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconHidePrevious.imageset/Contents.json new file mode 100644 index 0000000000..4dcac45b86 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconHidePrevious.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "rewind_30.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconHidePrevious.imageset/rewind_30.svg b/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconHidePrevious.imageset/rewind_30.svg new file mode 100644 index 0000000000..ca6090d027 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconHidePrevious.imageset/rewind_30.svg @@ -0,0 +1,4 @@ + + + + diff --git a/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconMain.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconMain.imageset/Contents.json new file mode 100644 index 0000000000..1ee5ae4489 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconMain.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "eye_30.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconMain.imageset/eye_30.svg b/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconMain.imageset/eye_30.svg new file mode 100644 index 0000000000..90f87efa94 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/StealthModeIntroIconMain.imageset/eye_30.svg @@ -0,0 +1,3 @@ + + + diff --git a/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift b/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift index ed8dbdfd8a..7f9abd7acf 100644 --- a/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift @@ -380,7 +380,7 @@ public struct MediaAutoDownloadSettings: Codable, Equatable { photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false), video: MediaAutoDownloadCategory(contacts: false, otherPrivate: false, groups: false, channels: false, sizeLimit: 1 * mb, predownload: false), file: MediaAutoDownloadCategory(contacts: false, otherPrivate: false, groups: false, channels: false, sizeLimit: 1 * mb, predownload: false), - stories: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 20 * mb, predownload: false) + stories: MediaAutoDownloadCategory(contacts: false, otherPrivate: false, groups: false, channels: false, sizeLimit: 20 * mb, predownload: false) ), medium: MediaAutoDownloadCategories( basePreset: .medium, diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index b45611a178..0ef17499da 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -19,6 +19,7 @@ import AccountContext import AnimatedAvatarSetNode final class UndoOverlayControllerNode: ViewControllerTracingNode { + private let presentationData: PresentationData private let elevatedLayout: Bool private let placementPosition: UndoOverlayController.Position private var statusNode: RadialStatusNode? @@ -60,6 +61,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { private var fetchResourceDisposable: Disposable? init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, placementPosition: UndoOverlayController.Position, action: @escaping (UndoOverlayAction) -> Bool, dismiss: @escaping () -> Void) { + self.presentationData = presentationData self.elevatedLayout = elevatedLayout self.placementPosition = placementPosition self.content = content @@ -1246,12 +1248,23 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.titleNode.attributedText = nil } self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white) + self.renewWithCurrentContent() + case let .actionSucceeded(title, text, _, destructive): + var undoTextColor = self.presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0) + if destructive { + undoTextColor = UIColor(rgb: 0xff7b74) + } + + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + self.textNode.attributedText = attributedText default: break } - self.renewWithCurrentContent() - if let validLayout = self.validLayout { self.containerLayoutUpdated(layout: validLayout, transition: .immediate) }