diff --git a/submodules/SettingsUI/Sources/DebugController.swift b/submodules/SettingsUI/Sources/DebugController.swift index 1d6001ac45..e1ad7b484e 100644 --- a/submodules/SettingsUI/Sources/DebugController.swift +++ b/submodules/SettingsUI/Sources/DebugController.swift @@ -51,6 +51,7 @@ private enum DebugControllerSection: Int32 { private enum DebugControllerEntry: ItemListNodeEntry { case sendLogs(PresentationTheme) case sendOneLog(PresentationTheme) + case sendShareLogs case sendNotificationLogs(PresentationTheme) case sendCriticalLogs(PresentationTheme) case accounts(PresentationTheme) @@ -84,7 +85,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { - case .sendLogs, .sendOneLog, .sendNotificationLogs, .sendCriticalLogs: + case .sendLogs, .sendOneLog, .sendShareLogs, .sendNotificationLogs, .sendCriticalLogs: return DebugControllerSection.logs.rawValue case .accounts: return DebugControllerSection.logs.rawValue @@ -109,58 +110,60 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 0 case .sendOneLog: return 1 - case .sendNotificationLogs: + case .sendShareLogs: return 2 - case .sendCriticalLogs: + case .sendNotificationLogs: return 3 - case .accounts: + case .sendCriticalLogs: return 4 - case .logToFile: + case .accounts: return 5 - case .logToConsole: + case .logToFile: return 6 - case .redactSensitiveData: + case .logToConsole: return 7 - case .enableRaiseToSpeak: + case .redactSensitiveData: return 8 - case .keepChatNavigationStack: + case .enableRaiseToSpeak: return 9 - case .skipReadHistory: + case .keepChatNavigationStack: return 10 - case .crashOnSlowQueries: + case .skipReadHistory: return 11 - case .clearTips: + case .crashOnSlowQueries: return 12 - case .reimport: + case .clearTips: return 13 - case .resetData: + case .reimport: return 14 - case .resetDatabase: + case .resetData: return 15 - case .resetDatabaseAndCache: + case .resetDatabase: return 16 - case .resetHoles: + case .resetDatabaseAndCache: return 17 - case .reindexUnread: + case .resetHoles: return 18 - case .resetBiometricsData: + case .reindexUnread: return 19 - case .optimizeDatabase: + case .resetBiometricsData: return 20 - case .photoPreview: + case .optimizeDatabase: return 21 - case .knockoutWallpaper: + case .photoPreview: return 22 - case .alternativeFolderTabs: + case .knockoutWallpaper: return 23 - case .playerEmbedding: + case .alternativeFolderTabs: return 24 - case .playlistPlayback: + case .playerEmbedding: return 25 - case .voiceConference: + case .playlistPlayback: return 26 + case .voiceConference: + return 27 case let .preferredVideoCodec(index, _, _, _): - return 27 + index + return 28 + index case .disableVideoAspectScaling: return 100 case .enableVoipTcp: @@ -327,9 +330,77 @@ private enum DebugControllerEntry: ItemListNodeEntry { arguments.presentController(actionSheet, nil) }) }) + case let .sendShareLogs: + return ItemListDisclosureItem(presentationData: presentationData, title: "Send Share Logs (Up to 40 MB)", label: "", sectionId: self.section, style: .blocks, action: { + let _ = (Logger.shared.collectLogs(prefix: "/share-logs") + |> deliverOnMainQueue).start(next: { logs in + let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationData: presentationData) + + var items: [ActionSheetButtonItem] = [] + + if let context = arguments.context { + items.append(ActionSheetButtonItem(title: "Via Telegram", color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled])) + controller.peerSelected = { [weak controller] peerId in + if let strongController = controller { + strongController.dismiss() + + let lineFeed = "\n".data(using: .utf8)! + var logData: Data = Data() + for (name, path) in logs { + if !logData.isEmpty { + logData.append(lineFeed) + logData.append(lineFeed) + } + + logData.append("------ File: \(name) ------\n".data(using: .utf8)!) + + if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { + logData.append(data) + } + } + + let id = arc4random64() + let fileResource = LocalFileMediaResource(fileId: id, size: logData.count, isSecretRelated: false) + context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: logData) + + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: logData.count, attributes: [.FileName(fileName: "Log-iOS-Full.txt")]) + let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil) + + let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() + } + } + arguments.pushController(controller) + })) + } + items.append(ActionSheetButtonItem(title: "Via Email", color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + let composeController = MFMailComposeViewController() + composeController.mailComposeDelegate = arguments.mailComposeDelegate + composeController.setSubject("Telegram Logs") + for (name, path) in logs { + if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) { + composeController.addAttachmentData(data, mimeType: "application/text", fileName: name) + } + } + arguments.getRootController()?.present(composeController, animated: true, completion: nil) + })) + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + arguments.presentController(actionSheet, nil) + }) + }) case let .sendNotificationLogs(theme): return ItemListDisclosureItem(presentationData: presentationData, title: "Send Notification Logs", label: "", sectionId: self.section, style: .blocks, action: { - let _ = (Logger(basePath: arguments.sharedContext.basePath + "/notificationServiceLogs").collectLogs() + let _ = (Logger(rootPath: arguments.sharedContext.basePath, basePath: arguments.sharedContext.basePath + "/notificationServiceLogs").collectLogs() |> deliverOnMainQueue).start(next: { logs in guard let context = arguments.context else { return @@ -708,6 +779,7 @@ private func debugControllerEntries(presentationData: PresentationData, loggingS entries.append(.sendLogs(presentationData.theme)) entries.append(.sendOneLog(presentationData.theme)) + entries.append(.sendShareLogs) entries.append(.sendNotificationLogs(presentationData.theme)) entries.append(.sendCriticalLogs(presentationData.theme)) entries.append(.accounts(presentationData.theme)) diff --git a/submodules/TelegramCore/Sources/Log.swift b/submodules/TelegramCore/Sources/Log.swift index 4ea33111c9..11ad4da33c 100644 --- a/submodules/TelegramCore/Sources/Log.swift +++ b/submodules/TelegramCore/Sources/Log.swift @@ -77,6 +77,7 @@ public final class Logger { private let maxShortLength: Int = 1 * 1024 * 1024 private let maxFiles: Int = 20 + private let rootPath: String private let basePath: String private var file: (ManagedFile, Int)? private var shortFile: (ManagedFile, Int)? @@ -114,22 +115,30 @@ public final class Logger { return sharedLogger } else { assertionFailure() - let tempLogger = Logger(basePath: "") + let tempLogger = Logger(rootPath: "", basePath: "") tempLogger.logToFile = false tempLogger.logToConsole = false return tempLogger } } - public init(basePath: String) { + public init(rootPath: String, basePath: String) { + self.rootPath = rootPath self.basePath = basePath } - public func collectLogs() -> Signal<[(String, String)], NoError> { + public func collectLogs(prefix: String? = nil) -> Signal<[(String, String)], NoError> { return Signal { subscriber in self.queue.async { + let logsPath: String + if let prefix = prefix { + logsPath = self.rootPath + prefix + } else { + logsPath = self.basePath + } + var result: [(Date, String, String)] = [] - if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: self.basePath), includingPropertiesForKeys: [URLResourceKey.creationDateKey], options: []) { + if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: logsPath), includingPropertiesForKeys: [URLResourceKey.creationDateKey], options: []) { for url in files { if url.lastPathComponent.hasPrefix("log-") { if let creationDate = (try? url.resourceValues(forKeys: Set([.creationDateKey])))?.creationDate { diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 988121e17d..526e5c6a8e 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -444,7 +444,7 @@ final class SharedApplicationContext { let logsPath = rootPath + "/logs" let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil) - Logger.setSharedLogger(Logger(basePath: logsPath)) + Logger.setSharedLogger(Logger(rootPath: rootPath, basePath: logsPath)) if let contents = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: rootPath + "/accounts-metadata"), includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants]) { for url in contents { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 4b00ae1660..57bff95e86 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -155,6 +155,18 @@ private func calculateSlowmodeActiveUntilTimestamp(account: Account, untilTimest } } +private struct ScrolledToMessageId: Equatable { + struct AllowedReplacementDirections: OptionSet { + var rawValue: Int32 + + static let up = AllowedReplacementDirections(rawValue: 1 << 0) + static let down = AllowedReplacementDirections(rawValue: 1 << 1) + } + + var id: MessageId + var allowedReplacementDirection: AllowedReplacementDirections +} + public final class ChatControllerImpl: TelegramBaseController, ChatController, GalleryHiddenMediaTarget, UIDropInteractionDelegate { private var validLayout: ContainerViewLayout? @@ -358,7 +370,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G public var purposefulAction: (() -> Void)? - private let scrolledToMessageId = ValuePromise(nil, ignoreRepeated: true) + private let scrolledToMessageId = ValuePromise(nil, ignoreRepeated: true) + private var scrolledToMessageIdValue: ScrolledToMessageId? = nil { + didSet { + self.scrolledToMessageId.set(self.scrolledToMessageIdValue) + } + } public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil) { let _ = ChatControllerCount.modify { value in @@ -3247,23 +3264,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let topPinnedMessage: Signal switch self.chatLocation { case let .peer(peerId): - let replyHistory: Signal = (chatHistoryViewForLocation(ChatHistoryLocationInput(content: .Initial(count: 100), id: 0), context: self.context, chatLocation: .peer(peerId), chatLocationContextHolder: Atomic(value: nil), scheduled: false, fixedCombinedReadStates: nil, tagMask: MessageTags.pinned, additionalData: []) - |> castError(Bool.self) - |> mapToSignal { update -> Signal in - switch update { - case let .Loading(_, type): - if case .Generic(.FillHole) = type { - return .fail(true) - } - case let .HistoryView(_, type, _, _, _, _, _): - if case .Generic(.FillHole) = type { - return .fail(true) - } - } - return .single(update) - }) - |> restartIfError - struct ReferenceMessage { var id: MessageId var isScrolled: Bool @@ -3279,55 +3279,181 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatDisplayNode.historyNode.topVisibleMessageRange.get() ) |> map { scrolledToMessageId, topVisibleMessageRange -> ReferenceMessage? in + let topVisibleMessage = topVisibleMessageRange?.upperBound + if let scrolledToMessageId = scrolledToMessageId { - return ReferenceMessage(id: scrolledToMessageId, isScrolled: true) - } else if let topVisibleMessageRange = topVisibleMessageRange { - return ReferenceMessage(id: topVisibleMessageRange.upperBound, isScrolled: false) + if let topVisibleMessage = topVisibleMessage { + if scrolledToMessageId.allowedReplacementDirection.contains(.up) && topVisibleMessage < scrolledToMessageId.id { + return ReferenceMessage(id: topVisibleMessage, isScrolled: false) + } + } + return ReferenceMessage(id: scrolledToMessageId.id, isScrolled: true) + } else if let topVisibleMessage = topVisibleMessage { + return ReferenceMessage(id: topVisibleMessage, isScrolled: false) } else { return nil } } } - topPinnedMessage = combineLatest( - replyHistory, - referenceMessage - ) - |> map { update, referenceMessage -> ChatPinnedMessage? in - var message: ChatPinnedMessage? + let context = self.context + + func replyHistorySignal(anchorMessageId: MessageId?, count: Int) -> Signal { + let location: ChatHistoryLocation + if let anchorMessageId = anchorMessageId { + location = .InitialSearch(location: .id(anchorMessageId), count: count, highlight: false) + } else { + location = .Initial(count: count) + } + + return (chatHistoryViewForLocation(ChatHistoryLocationInput(content: location, id: 0), context: context, chatLocation: .peer(peerId), chatLocationContextHolder: Atomic(value: nil), scheduled: false, fixedCombinedReadStates: nil, tagMask: MessageTags.pinned, additionalData: []) + |> castError(Bool.self) + |> mapToSignal { update -> Signal in + switch update { + case let .Loading(_, type): + if case .Generic(.FillHole) = type { + return .fail(true) + } + case let .HistoryView(_, type, _, _, _, _, _): + if case .Generic(.FillHole) = type { + return .fail(true) + } + } + return .single(update) + }) + |> restartIfError + } + + let topMessage = replyHistorySignal(anchorMessageId: nil, count: 3) + |> map { update -> Message? in switch update { case .Loading: - break - case let .HistoryView(view, _, _, _, _, _, _): - let topMessageId: MessageId - if view.entries.isEmpty { - return nil + return nil + case let .HistoryView(viewValue, _, _, _, _, _, _): + return viewValue.entries.last?.message + } + } + + let loadCount = 100 + + let adjustedReplyHistory: Signal<[Message], NoError> + if latest { + adjustedReplyHistory = replyHistorySignal(anchorMessageId: nil, count: loadCount) + |> map { view -> [Message] in + switch view { + case .Loading: + return [] + case let .HistoryView(viewValue, _, _, _, _, _, _): + return viewValue.entries.map(\.message) } - topMessageId = view.entries[view.entries.count - 1].message.id - for i in 0 ..< view.entries.count { - let entry = view.entries[i] - var matches = false - if message == nil { - matches = true - } else if let referenceMessage = referenceMessage { - if referenceMessage.isScrolled { - if entry.message.id < referenceMessage.id { - matches = true + } + } else { + adjustedReplyHistory = (Signal<[Message], NoError> { subscriber in + var referenceMessageValue: ReferenceMessage? + var view: ChatHistoryViewUpdate? + + let updateState: () -> Void = { + guard let view = view else { + return + } + guard case let .HistoryView(viewValue, _, _, _, _, _, _) = view else { + subscriber.putNext([]) + subscriber.putCompletion() + return + } + + if let referenceId = referenceMessageValue?.id { + if viewValue.entries.count < loadCount { + subscriber.putNext(viewValue.entries.map(\.message)) + } else if referenceId < viewValue.entries[1].message.id { + if viewValue.earlierId != nil { + subscriber.putCompletion() + } else { + subscriber.putNext(viewValue.entries.map(\.message)) + } + } else if referenceId > viewValue.entries[viewValue.entries.count - 2].message.id { + if viewValue.laterId != nil { + subscriber.putCompletion() + } else { + subscriber.putNext(viewValue.entries.map(\.message)) } } else { - if entry.message.id <= referenceMessage.id { - matches = true - } + subscriber.putNext(viewValue.entries.map(\.message)) } } else { - matches = true - } - if matches { - message = ChatPinnedMessage(message: entry.message, topMessageId: topMessageId) + if viewValue.holeLater || viewValue.laterId != nil { + subscriber.putCompletion() + } else { + subscriber.putNext(viewValue.entries.map(\.message)) + } } } - break + + var initializedView = false + let viewDisposable = MetaDisposable() + + let referenceDisposable = (referenceMessage + |> deliverOnMainQueue).start(next: { referenceMessage in + referenceMessageValue = referenceMessage + if !initializedView { + initializedView = true + //print("reload at \(String(describing: referenceMessage?.id)) disposable \(unsafeBitCast(viewDisposable, to: UInt64.self))") + viewDisposable.set((replyHistorySignal(anchorMessageId: referenceMessage?.id, count: loadCount) + |> deliverOnMainQueue).start(next: { next in + view = next + updateState() + })) + } + updateState() + }) + + return ActionDisposable { + //print("dispose \(unsafeBitCast(viewDisposable, to: UInt64.self))") + referenceDisposable.dispose() + viewDisposable.dispose() + } } + |> runOn(.mainQueue())) + |> restart + } + + topPinnedMessage = combineLatest(queue: .mainQueue(), + adjustedReplyHistory, + topMessage, + referenceMessage + ) + |> map { messages, topMessage, referenceMessage -> ChatPinnedMessage? in + var message: ChatPinnedMessage? + + let topMessageId: MessageId + if messages.isEmpty { + return nil + } + topMessageId = topMessage?.id ?? messages[messages.count - 1].id + //print("reference: \(String(describing: referenceMessage?.id.id)) entries: \(view.entries.map(\.index.id.id))") + for i in 0 ..< messages.count { + let entry = messages[i] + var matches = false + if message == nil { + matches = true + } else if let referenceMessage = referenceMessage { + if referenceMessage.isScrolled { + if entry.id < referenceMessage.id { + matches = true + } + } else { + if entry.id <= referenceMessage.id { + matches = true + } + } + } else { + matches = true + } + if matches { + message = ChatPinnedMessage(message: entry, topMessageId: topMessageId) + } + } + return message } |> distinctUntilChanged @@ -3355,12 +3481,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - self.chatDisplayNode.historyNode.beganDragging = { [weak self] in + self.chatDisplayNode.historyNode.didScrollWithOffset = { [weak self] offset, _, _ in guard let strongSelf = self else { return } - strongSelf.scrolledToMessageId.set(nil) + if offset > 0.0 { + if var scrolledToMessageIdValue = strongSelf.scrolledToMessageIdValue { + scrolledToMessageIdValue.allowedReplacementDirection.insert(.up) + strongSelf.scrolledToMessageIdValue = scrolledToMessageIdValue + } + } else if offset < 0.0 { + strongSelf.scrolledToMessageIdValue = nil + } } self.chatDisplayNode.peerView = self.peerView @@ -3690,7 +3823,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId) controllerInteraction.highlightedState = highlightedState strongSelf.updateItemNodesHighlightedStates(animated: false) - strongSelf.scrolledToMessageId.set(index.id) + strongSelf.scrolledToMessageIdValue = ScrolledToMessageId(id: index.id, allowedReplacementDirection: []) strongSelf.messageContextDisposable.set((Signal.complete() |> delay(0.7, queue: Queue.mainQueue())).start(completed: { if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { @@ -3709,7 +3842,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - strongSelf.scrolledToMessageId.set(nil) + strongSelf.scrolledToMessageIdValue = nil } self.chatDisplayNode.historyNode.maxVisibleMessageIndexUpdated = { [weak self] index in diff --git a/submodules/TelegramUI/Sources/NotificationContentContext.swift b/submodules/TelegramUI/Sources/NotificationContentContext.swift index 9c45f18203..5c77c61151 100644 --- a/submodules/TelegramUI/Sources/NotificationContentContext.swift +++ b/submodules/TelegramUI/Sources/NotificationContentContext.swift @@ -25,10 +25,10 @@ private var sharedAccountContext: SharedAccountContext? private var installedSharedLogger = false -private func setupSharedLogger(_ path: String) { +private func setupSharedLogger(rootPath: String, path: String) { if !installedSharedLogger { installedSharedLogger = true - Logger.setSharedLogger(Logger(basePath: path)) + Logger.setSharedLogger(Logger(rootPath: rootPath, basePath: path)) } } @@ -107,7 +107,7 @@ public final class NotificationViewControllerImpl { let logsPath = rootPath + "/notificationcontent-logs" let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil) - setupSharedLogger(logsPath) + setupSharedLogger(rootPath: rootPath, path: logsPath) accountsPath = rootPath diff --git a/submodules/TelegramUI/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Sources/ShareExtensionContext.swift index a5cd430eeb..5e95b25fc9 100644 --- a/submodules/TelegramUI/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Sources/ShareExtensionContext.swift @@ -35,10 +35,10 @@ private var globalInternalContext: InternalContext? private var installedSharedLogger = false -private func setupSharedLogger(_ path: String) { +private func setupSharedLogger(rootPath: String, path: String) { if !installedSharedLogger { installedSharedLogger = true - Logger.setSharedLogger(Logger(basePath: path)) + Logger.setSharedLogger(Logger(rootPath: rootPath, basePath: path)) } } @@ -138,7 +138,7 @@ public class ShareRootControllerImpl { let logsPath = rootPath + "/share-logs" let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil) - setupSharedLogger(logsPath) + setupSharedLogger(rootPath: rootPath, path: logsPath) let applicationBindings = TelegramApplicationBindings(isMainApp: false, containerPath: self.initializationData.appGroupPath, appSpecificScheme: "tg", openUrl: { _ in }, openUniversalUrl: { _, completion in