This commit is contained in:
Ali 2023-07-25 22:08:46 +04:00
parent d8501b41c2
commit 29f29e927e
46 changed files with 1590 additions and 71 deletions

View File

@ -71,7 +71,7 @@
- (MTQueue *)messageServiceQueue;
- (void)requestTransportTransaction;
- (void)requestSecureTransportReset;
- (void)resetSessionInfo;
- (void)resetSessionInfo:(bool)ifActive;
- (void)requestTimeResync;
- (void)_messageResendRequestFailed:(int64_t)messageId;

View File

@ -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);

View File

@ -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;

View File

@ -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];
}

View File

@ -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:

View File

@ -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()

View File

@ -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

View File

@ -8447,6 +8447,21 @@ public extension Api.functions.stickers {
})
}
}
public extension Api.functions.stories {
static func activateStealthMode(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
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()

View File

@ -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 {

View File

@ -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<Data, NoError> {
return Signal<Data, MTRpcError> { 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<Data, NoError> {
return Signal<Data, MTRpcError> { 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<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>)) -> Signal<T, MTRpcError> {
func request<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), expectedResponseSize: Int32? = nil) -> Signal<T, MTRpcError> {
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<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), automaticFloodWait: Bool = true, failOnServerErrors: Bool = false) -> Signal<(T, Double), (MTRpcError, Double)> {
func requestWithAdditionalData<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), 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)) {

View File

@ -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 {

View File

@ -103,14 +103,14 @@ private struct DownloadWrapper {
self.useMainConnection = useMainConnection
}
func request<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), tag: MediaResourceFetchTag?, continueInBackground: Bool) -> Signal<(T, NetworkResponseInfo), MTRpcError> {
func request<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), 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
}

View File

@ -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<T>(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true) -> Signal<T, MTRpcError> {
func request<T>(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true, expectedResponseSize: Int32?) -> Signal<T, MTRpcError> {
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<T>(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true) -> Signal<(T, NetworkResponseInfo), (MTRpcError, Double)> {
func requestWithAdditionalInfo<T>(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), 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()

View File

@ -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)
}
}

View File

@ -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! {

View File

@ -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 {

View File

@ -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
}
}
}
}

View File

@ -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<Never, NoError> {
var flags: Int32 = 0
flags |= 1 << 0
flags |= 1 << 1
return account.network.request(Api.functions.stories.activateStealthMode(flags: flags))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { result -> Signal<Never, NoError> 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
}
}

View File

@ -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 {

View File

@ -1090,5 +1090,9 @@ public extension TelegramEngine {
public func exportStoryLink(peerId: EnginePeer.Id, id: Int32) -> Signal<String?, NoError> {
return _internal_exportStoryLink(account: self.account, peerId: peerId, id: id)
}
public func enableStoryStealthMode() -> Signal<Never, NoError> {
return _internal_enableStoryStealthMode(account: self.account)
}
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -77,6 +77,7 @@ swift_library(
"//submodules/MediaPasteboardUI",
"//submodules/WebPBinding",
"//submodules/Utils/RangeSet",
"//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen",
],
visibility = [
"//visibility:public",

View File

@ -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

View File

@ -333,6 +333,10 @@ private final class StoryContainerScreenComponent: Component {
private let focusedItem = ValuePromise<StoryId?>(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

View File

@ -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<ChatEntityKeyboardInputNode.InputData, NoError>
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<ChatEntityKeyboardInputNode.InputData, NoError>,
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)

View File

@ -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<ChatEntityKeyboardInputNode.InputData, NoError>) {
@ -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)) wont 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)
})
}
}

View File

@ -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",
],
)

View File

@ -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<Empty>()
let title = ComponentView<Empty>()
let text = ComponentView<Empty>()
init() {
}
}
public final class View: UIView {
private let scrollView: UIScrollView
private let iconBackground: UIImageView
private let iconForeground: UIImageView
private let title = ComponentView<Empty>()
private let mainText = ComponentView<Empty>()
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<Empty>, 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<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -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<Empty>?
private let content = ComponentView<Empty>()
private let button = ComponentView<Empty>()
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<EnvironmentType>, 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<Empty>
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<EnvironmentType>, 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<Action<Void>>()
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<ViewControllerComponentContainer.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<ViewControllerComponentContainer.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?()
}
}

View File

@ -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",
],
)

View File

@ -0,0 +1,106 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ComponentDisplayAdapters
public final class ToastContentComponent: Component {
public let icon: AnyComponent<Empty>
public let content: AnyComponent<Empty>
public init(
icon: AnyComponent<Empty>,
content: AnyComponent<Empty>
) {
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<Empty>()
private let content = ComponentView<Empty>()
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<Empty>, 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<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "IconDownloadLocked.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 19.748C13.3608 19.9125 12.6906 20 12 20C7.58172 20 4 16.4183 4 12C4 7.58172 7.58172 4 12 4C16.0796 4 19.446 7.05369 19.9381 11" stroke="white" stroke-width="1.33" stroke-linecap="round"/>
<path d="M12 14V8.5" stroke="white" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 12.5L12 15.5L15 12.5" stroke="white" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 12.835C18.252 12.835 16.835 14.252 16.835 16V17.5307C16.3253 17.9588 16 18.6076 16 19.3333V21.6667C16 22.9544 17.0242 24 18.2857 24H21.7143C22.9758 24 24 22.9544 24 21.6667V19.3333C24 18.6076 23.6747 17.9588 23.165 17.5307V16C23.165 14.252 21.748 12.835 20 12.835ZM21.835 17.0032V16C21.835 14.9866 21.0134 14.165 20 14.165C18.9866 14.165 18.165 14.9866 18.165 16V17.0032C18.205 17.0011 18.2452 17 18.2857 17H21.7143C21.7548 17 21.795 17.0011 21.835 17.0032Z" fill="white" style="mix-blend-mode:overlay"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 12.835C18.252 12.835 16.835 14.252 16.835 16V17.5307C16.3253 17.9588 16 18.6076 16 19.3333V21.6667C16 22.9544 17.0242 24 18.2857 24H21.7143C22.9758 24 24 22.9544 24 21.6667V19.3333C24 18.6076 23.6747 17.9588 23.165 17.5307V16C23.165 14.252 21.748 12.835 20 12.835ZM21.835 17.0032V16C21.835 14.9866 21.0134 14.165 20 14.165C18.9866 14.165 18.165 14.9866 18.165 16V17.0032C18.205 17.0011 18.2452 17 18.2857 17H21.7143C21.7548 17 21.795 17.0011 21.835 17.0032Z" fill="white" fill-opacity="0.4" style="mix-blend-mode:overlay"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "eye_301.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.47024 3.52973C4.21054 3.27004 3.78948 3.27004 3.52979 3.52973C3.27009 3.78943 3.27009 4.21049 3.52979 4.47019L19.5298 20.4702C19.7895 20.7299 20.2105 20.7299 20.4702 20.4702C20.7299 20.2105 20.7299 19.7894 20.4702 19.5297L17.4681 16.5276C19.2332 15.3101 20.384 13.7481 20.9455 12.8656C21.3679 12.2017 21.3532 11.361 20.8984 10.7134C19.8408 9.20775 16.8814 5.73438 12.0002 5.73438C10.334 5.73438 8.89172 6.13908 7.67259 6.73209L4.47024 3.52973ZM8.67783 7.73733L9.87465 8.93415C10.4777 8.51528 11.2102 8.26977 12.0001 8.26977C14.0601 8.26977 15.7301 9.93974 15.7301 11.9998C15.7301 12.7896 15.4846 13.5221 15.0657 14.1252L16.5091 15.5686C18.1655 14.4907 19.2662 13.0274 19.8234 12.1516C19.9591 11.9382 19.9529 11.6812 19.81 11.4779C18.82 10.0684 16.2073 7.06438 12.0002 7.06438C10.7522 7.06438 9.64448 7.3287 8.67783 7.73733ZM14.1011 13.1606C14.2916 12.8166 14.4001 12.4209 14.4001 11.9998C14.4001 10.6743 13.3255 9.59977 12.0001 9.59977C11.579 9.59977 11.1832 9.70821 10.8392 9.89869L14.1011 13.1606ZM5.77358 7.91384C4.47869 8.91015 3.58879 10.0203 3.10194 10.7134C2.64713 11.361 2.63243 12.2017 3.05484 12.8656C4.0682 14.4583 7.00115 18.2644 12.0002 18.2644C13.2924 18.2644 14.4465 18.0101 15.4651 17.6054L14.4231 16.5634C13.6864 16.7961 12.879 16.9344 12.0002 16.9344C7.72658 16.9344 5.14002 13.6652 4.17696 12.1516C4.04117 11.9382 4.04746 11.6812 4.1903 11.4779C4.66401 10.8034 5.50924 9.76394 6.72289 8.86315L5.77358 7.91384ZM8.51844 10.6586C8.35801 11.0748 8.27006 11.527 8.27006 11.9998C8.27006 14.0598 9.94004 15.7298 12.0001 15.7298C12.4728 15.7298 12.925 15.6418 13.3412 15.4814L12.2471 14.3872C12.1659 14.3955 12.0835 14.3998 12.0001 14.3998C10.6746 14.3998 9.60006 13.3252 9.60006 11.9998C9.60006 11.9164 9.60431 11.834 9.61262 11.7528L8.51844 10.6586Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "IconEyeLocked.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.47037 3.52973C4.21067 3.27004 3.78962 3.27004 3.52992 3.52973C3.27022 3.78943 3.27022 4.21049 3.52992 4.47019L13.7099 14.6502C13.9246 14.8649 14.2495 14.9021 14.5024 14.7618L14.5046 14.7641C14.508 14.761 14.5114 14.7579 14.5148 14.7548C14.5632 14.7266 14.6089 14.6917 14.6504 14.6502C14.6919 14.6087 14.7267 14.5631 14.755 14.5147C15.3607 13.8515 15.7302 12.9688 15.7302 11.9999C15.7302 9.93983 14.0602 8.26985 12.0002 8.26985C11.2103 8.26985 10.4778 8.51535 9.87483 8.93419L8.67856 7.73793C9.64518 7.32933 10.7528 7.06502 12.0008 7.06502C16.117 7.06502 18.707 9.94002 19.7462 11.3878C19.9603 11.6861 20.3758 11.7544 20.6742 11.5402C20.9726 11.3261 21.0408 10.9106 20.8267 10.6122C19.7141 9.06223 16.7778 5.73502 12.0008 5.73502C10.3346 5.73502 8.89242 6.1397 7.67332 6.73268L4.47037 3.52973ZM10.8394 9.89874L14.1013 13.1607C14.2918 12.8166 14.4002 12.4209 14.4002 11.9999C14.4002 10.6744 13.3257 9.59985 12.0002 9.59985C11.5791 9.59985 11.1834 9.70828 10.8394 9.89874ZM5.77418 7.9145C4.47929 8.91081 3.5894 10.0209 3.10255 10.7141C2.64774 11.3616 2.63304 12.2024 3.05545 12.8663C4.06881 14.4589 7.00176 18.265 12.0008 18.265C12.7635 18.265 13.4808 18.1758 14.1521 18.0181C14.5097 17.9342 14.7314 17.5762 14.6475 17.2187C14.5635 16.8612 14.2056 16.6394 13.848 16.7234C13.2741 16.8582 12.6588 16.935 12.0008 16.935C7.72719 16.935 5.14063 13.6659 4.17757 12.1523C4.04178 11.9389 4.04807 11.6819 4.19091 11.4785C4.66462 10.8041 5.50984 9.7646 6.72349 8.86381L5.77418 7.9145ZM8.51851 10.6588C8.35811 11.075 8.27018 11.5272 8.27018 11.9999C8.27018 14.0599 9.94016 15.7299 12.0002 15.7299C12.4729 15.7299 12.925 15.6419 13.3412 15.4815L12.247 14.3873C12.1659 14.3956 12.0835 14.3999 12.0002 14.3999C10.6747 14.3999 9.60018 13.3253 9.60018 11.9999C9.60018 11.9165 9.60443 11.8342 9.61272 11.753L8.51851 10.6588Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 12.835C18.252 12.835 16.835 14.252 16.835 16V17.5307C16.3253 17.9588 16 18.6076 16 19.3333V21.6666C16 22.9544 17.0242 24 18.2857 24H21.7143C22.9758 24 24 22.9544 24 21.6666V19.3333C24 18.6076 23.6747 17.9588 23.165 17.5307V16C23.165 14.252 21.748 12.835 20 12.835ZM21.835 17.0032V16C21.835 14.9865 21.0134 14.165 20 14.165C18.9866 14.165 18.165 14.9865 18.165 16V17.0032C18.205 17.001 18.2452 17 18.2857 17H21.7143C21.7548 17 21.795 17.001 21.835 17.0032Z" fill="white" style="mix-blend-mode:overlay"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 12.835C18.252 12.835 16.835 14.252 16.835 16V17.5307C16.3253 17.9588 16 18.6076 16 19.3333V21.6666C16 22.9544 17.0242 24 18.2857 24H21.7143C22.9758 24 24 22.9544 24 21.6666V19.3333C24 18.6076 23.6747 17.9588 23.165 17.5307V16C23.165 14.252 21.748 12.835 20 12.835ZM21.835 17.0032V16C21.835 14.9865 21.0134 14.165 20 14.165C18.9866 14.165 18.165 14.9865 18.165 16V17.0032C18.205 17.001 18.2452 17 18.2857 17H21.7143C21.7548 17 21.795 17.001 21.835 17.0032Z" fill="white" fill-opacity="0.4" style="mix-blend-mode:overlay"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "rewind_301.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,4 @@
<svg width="31" height="30" viewBox="0 0 31 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.5 2.99879C13.5 2.47292 14.092 2.16472 14.5228 2.46629L17.7393 4.71786C18.109 4.97663 18.109 5.5241 17.7393 5.78286L14.5228 8.03444C14.092 8.336 13.5 8.0278 13.5 7.50193V5.88011C9.30553 6.7957 6.16504 10.5313 6.16504 15.0004C6.16504 20.1559 10.3445 24.3354 15.5 24.3354C20.6556 24.3354 24.835 20.1559 24.835 15.0004C24.835 11.7832 23.2082 8.94581 20.7292 7.26622C20.4251 7.06022 20.3457 6.64674 20.5517 6.34268C20.7577 6.03863 21.1711 5.95914 21.4752 6.16515C24.3038 8.08162 26.165 11.3235 26.165 15.0004C26.165 20.8905 21.3902 25.6654 15.5 25.6654C9.60992 25.6654 4.83504 20.8905 4.83504 15.0004C4.83504 9.79384 8.56591 5.45871 13.5 4.52259V2.99879Z" fill="#4AA1FF"/>
<path d="M10.2328 19C9.80311 19 9.57752 18.7637 9.57752 18.4146C9.57752 18.146 9.66883 17.9849 9.96424 17.7056L12.3329 15.3584C13.3212 14.3809 13.5682 13.9888 13.5682 13.3979C13.5682 12.7051 13.0365 12.2056 12.2899 12.2056C11.5917 12.2056 11.1029 12.5601 10.829 13.2583C10.6893 13.5376 10.5282 13.6826 10.2113 13.6826C9.83533 13.6826 9.60975 13.4517 9.60975 13.1079C9.60975 13.0059 9.62586 12.9092 9.65271 12.8179C9.86219 11.98 10.7914 11.0938 12.3007 11.0938C13.8529 11.0938 14.9218 12.0122 14.9218 13.312C14.9218 14.2197 14.4867 14.832 13.203 16.0889L11.4252 17.8398V17.8667H14.5189C14.8842 17.8667 15.1097 18.0923 15.1097 18.436C15.1097 18.7744 14.8842 19 14.5189 19H10.2328ZM18.7047 19.1558C17.5123 19.1558 16.4811 18.5864 16.0997 17.7432C16.0245 17.5767 15.9762 17.4155 15.9762 17.2168C15.9762 16.8569 16.2071 16.6313 16.5724 16.6313C16.8624 16.6313 17.0397 16.7441 17.1847 17.0288C17.4479 17.6572 17.9635 18.0278 18.7101 18.0278C19.6285 18.0278 20.2784 17.3779 20.2784 16.4595C20.2784 15.5679 19.6285 14.9341 18.7154 14.9341C18.2267 14.9341 17.8024 15.1328 17.507 15.4229C17.1847 15.7451 17.0504 15.8149 16.7335 15.8149C16.2877 15.8149 16.0675 15.498 16.0782 15.1221C16.0782 15.0898 16.0782 15.0684 16.0836 15.0361L16.3307 12.1089C16.379 11.4966 16.6905 11.2495 17.2975 11.2495H20.6383C20.9928 11.2495 21.2238 11.4751 21.2238 11.8135C21.2238 12.1572 20.9928 12.3828 20.6383 12.3828H17.507L17.3029 14.6763H17.3297C17.6573 14.1929 18.2965 13.8921 19.0753 13.8921C20.5577 13.8921 21.6158 14.9395 21.6158 16.4219C21.6158 18.0439 20.4235 19.1558 18.7047 19.1558Z" fill="#4AA1FF"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "rewind_30.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,4 @@
<svg width="31" height="30" viewBox="0 0 31 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5 2.99879C17.5 2.47292 16.908 2.16472 16.4772 2.46629L13.2607 4.71786C12.891 4.97663 12.891 5.5241 13.2607 5.78286L16.4772 8.03444C16.908 8.336 17.5 8.0278 17.5 7.50193V5.88011C21.6945 6.7957 24.835 10.5313 24.835 15.0004C24.835 20.1559 20.6555 24.3354 15.5 24.3354C10.3444 24.3354 6.16496 20.1559 6.16496 15.0004C6.16496 11.7832 7.79182 8.94581 10.2708 7.26622C10.5749 7.06022 10.6543 6.64674 10.4483 6.34268C10.2423 6.03863 9.82885 5.95914 9.5248 6.16515C6.69618 8.08162 4.83496 11.3235 4.83496 15.0004C4.83496 20.8905 9.60985 25.6654 15.5 25.6654C21.3901 25.6654 26.165 20.8905 26.165 15.0004C26.165 9.79384 22.4341 5.45871 17.5 4.52259V2.99879Z" fill="#4AA1FF"/>
<path d="M15.5806 19.1558C14.3882 19.1558 13.3569 18.5864 12.9756 17.7432C12.9004 17.5767 12.8521 17.4155 12.8521 17.2168C12.8521 16.8569 13.083 16.6313 13.4482 16.6313C13.7383 16.6313 13.9155 16.7441 14.0605 17.0288C14.3237 17.6572 14.8394 18.0278 15.5859 18.0278C16.5044 18.0278 17.1543 17.3779 17.1543 16.4595C17.1543 15.5679 16.5044 14.9341 15.5913 14.9341C15.1025 14.9341 14.6782 15.1328 14.3828 15.4229C14.0605 15.7451 13.9263 15.8149 13.6094 15.8149C13.1636 15.8149 12.9434 15.498 12.9541 15.1221C12.9541 15.0898 12.9541 15.0684 12.9595 15.0361L13.2065 12.1089C13.2549 11.4966 13.5664 11.2495 14.1733 11.2495H17.5142C17.8687 11.2495 18.0996 11.4751 18.0996 11.8135C18.0996 12.1572 17.8687 12.3828 17.5142 12.3828H14.3828L14.1787 14.6763H14.2056C14.5332 14.1929 15.1724 13.8921 15.9512 13.8921C17.4336 13.8921 18.4917 14.9395 18.4917 16.4219C18.4917 18.0439 17.2993 19.1558 15.5806 19.1558Z" fill="#4AA1FF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "eye_30.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,3 @@
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.1284 10.8718C12.5052 10.2485 11.4946 10.2485 10.8714 10.8718C10.2481 11.495 10.2481 12.5056 10.8714 13.1288L58.8714 61.1288C59.4946 61.7521 60.5052 61.7521 61.1284 61.1288C61.7517 60.5056 61.7517 59.495 61.1284 58.8718L50.6296 48.3729C55.9529 44.901 59.4407 40.2205 61.1539 37.5281C61.9912 36.2121 61.9597 34.5655 61.0632 33.2891C57.9919 28.9164 49.6337 19.2003 36.0011 19.2003C31.4602 19.2003 27.5045 20.2783 24.1344 21.8777L13.1284 10.8718ZM28.7186 26.4619L32.1637 29.907C33.2746 29.206 34.5904 28.8003 36.0011 28.8003C39.9775 28.8003 43.2011 32.0238 43.2011 36.0003C43.2011 37.4109 42.7954 38.7268 42.0944 39.8377L45.5395 43.2828C47.0837 41.2633 48.0011 38.7389 48.0011 36.0003C48.0011 29.3729 42.6285 24.0003 36.0011 24.0003C33.2625 24.0003 30.7381 24.9177 28.7186 26.4619ZM10.939 33.2891C12.4199 31.1807 15.13 27.83 19.0678 24.9252L25.1061 30.9636C24.3969 32.4951 24.0011 34.2015 24.0011 36.0003C24.0011 42.6277 29.3737 48.0003 36.0011 48.0003C37.7999 48.0003 39.5063 47.6045 41.0378 46.8953L45.2588 51.1163C42.5113 52.1564 39.427 52.8003 36.0011 52.8003C22.0922 52.8003 13.8129 42.1874 10.8483 37.5281C10.011 36.2121 10.0425 34.5655 10.939 33.2891ZM28.8011 36.0003C28.8011 35.5788 28.8373 35.1658 28.9068 34.7642L37.2372 43.0946C36.8356 43.1641 36.4226 43.2003 36.0011 43.2003C32.0246 43.2003 28.8011 39.9767 28.8011 36.0003Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -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,

View File

@ -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)
}