diff --git a/.bazelrc b/.bazelrc index 1ce9174344..9127415f6a 100644 --- a/.bazelrc +++ b/.bazelrc @@ -22,14 +22,36 @@ build --strategy=SwiftCompile=worker #common --registry=https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/ -# SourceKit BSP: Swift indexing features -common --features=swift.index_while_building -common --features=swift.use_global_index_store -common --features=swift.use_global_module_cache -common --features=oso_prefix_is_pwd +# rules_swift flags migration +# --swiftcopt and --host_swiftcopt are deprecated +common --flag_alias=swiftcopt=@build_bazel_rules_swift//swift:copt +common --flag_alias=host_swiftcopt=@build_bazel_rules_swift//swift:exec_copt +#common --swiftcopt=-whole-module-optimization +#common --host_swiftcopt=-whole-module-optimization -# SourceKit BSP: Index build config (used for background indexing) +common --check_visibility=false + +# All of the following are Debug/Index setup configs inspired by the default rules_xcodeproj template + +common --verbose_failures +common --cache_computed_file_digests=500000 +common --action_cache_store_output_metadata +common --experimental_use_cpp_compile_action_args_params_file +common --define=apple.experimental.tree_artifact_outputs=1 +common --features=apple.swizzle_absolute_xcttestsourcelocation +common --features=oso_prefix_is_pwd +common --features=relative_ast_path +common --features=swift.cacheable_swiftmodules +common --features=swift.index_while_building +common --features=swift.use_global_module_cache +common --features=swift.emit_swiftsourceinfo +common --nolegacy_important_outputs +build --noworker_sandboxing +build --spawn_strategy=remote,worker,local + +# Only for BSP builds common:index_build --experimental_convenience_symlinks=ignore +common:index_build --bes_backend= --bes_results_url= common:index_build --show_result=0 common:index_build --noshow_loading_progress common:index_build --noshow_progress @@ -37,3 +59,4 @@ common:index_build --noannounce_rc common:index_build --noshow_timestamps common:index_build --curses=no common:index_build --color=no + diff --git a/.vscode/settings.json b/.vscode/settings.json index d1e808574b..ba34a33e56 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,5 +23,7 @@ ] } }, - "swift.sourcekit-lsp.serverPath": "${workspaceFolder}/build-input/sourcekit-lsp" + "swift.sourcekit-lsp.serverPath": "${workspaceFolder}/build-input/sourcekit-lsp", + "sourcekit-bazel-bsp.rulesAppleName": "build_bazel_rules_apple", + "cmake.sourceDirectory": "/Users/ali/build/telegram/telegram-ios/submodules/rlottie/rlottie/test" } diff --git a/BUILD.bazel b/BUILD.bazel index 05e5d1c325..4292181ff1 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -1,5 +1,7 @@ load("@sourcekit_bazel_bsp//rules:setup_sourcekit_bsp.bzl", "setup_sourcekit_bsp") +exports_files(["versions.json"]) + setup_sourcekit_bsp( name = "setup_sourcekit_bsp", bazel_wrapper = "./build-input/bazel-8.4.2-darwin-arm64", @@ -18,17 +20,12 @@ setup_sourcekit_bsp( "submodules/**/*.cpp", ], aquery_flags = [ - "define=telegramVersion=12.5", - "define=buildNumber=100000", ], index_flags = [ "config=index_build", - "define=telegramVersion=12.5", - "define=buildNumber=100000", ], index_build_batch_size = 10, targets = [ "//Telegram:Telegram", ], - merge_lsp_config = False, ) diff --git a/Telegram/BUILD b/Telegram/BUILD index 15f6fe2c5f..a1cf7d9524 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -385,7 +385,8 @@ plist_fragment( """ CFBundleVersion {buildNumber} - """ + """, + defaults = {"buildNumber": "1"}, ) plist_fragment( @@ -574,14 +575,23 @@ plist_fragment( ]) ) -plist_fragment( +genrule( name = "VersionInfoPlist", - extension = "plist", - template = - """ + srcs = ["//:versions.json"], + outs = ["VersionInfoPlist.plist"], + cmd = """ + version=$$(python3 -c "import json; print(json.load(open('$(location //:versions.json)'))['app'])") + cat > $@ < + + + CFBundleShortVersionString - {telegramVersion} - """ + $$version + + +PLIST + """, ) plist_fragment( diff --git a/Tests/TelegramCoreBuildTest/BUILD b/Tests/TelegramCoreBuildTest/BUILD index 8b4227a100..e5038bea6f 100644 --- a/Tests/TelegramCoreBuildTest/BUILD +++ b/Tests/TelegramCoreBuildTest/BUILD @@ -43,14 +43,23 @@ plist_fragment( """ ) -plist_fragment( +genrule( name = "VersionInfoPlist", - extension = "plist", - template = - """ + srcs = ["//:versions.json"], + outs = ["VersionInfoPlist.plist"], + cmd = """ + version=$$(python3 -c "import json; print(json.load(open('$(location //:versions.json)'))['app'])") + cat > $@ < + + + CFBundleShortVersionString - {telegramVersion} - """ + $$version + + +PLIST + """, ) plist_fragment( diff --git a/build-system/Make/Make.py b/build-system/Make/Make.py index ebff8097e5..457e5d9517 100644 --- a/build-system/Make/Make.py +++ b/build-system/Make/Make.py @@ -212,7 +212,6 @@ class BazelCommandLine: def get_define_arguments(self): return [ '--define=buildNumber={}'.format(self.build_number), - '--define=telegramVersion={}'.format(self.build_environment.app_version) ] def get_project_generation_arguments(self): diff --git a/build-system/bazel-rules/rules_apple b/build-system/bazel-rules/rules_apple index 39cc252924..f6aef7db5b 160000 --- a/build-system/bazel-rules/rules_apple +++ b/build-system/bazel-rules/rules_apple @@ -1 +1 @@ -Subproject commit 39cc2529241335d20afcb6700c587d064e7a2870 +Subproject commit f6aef7db5b649ffc3b7497ecc83a08ad3ab19559 diff --git a/build-system/bazel-utils/plist_fragment.bzl b/build-system/bazel-utils/plist_fragment.bzl index ac40575332..64ef8d4076 100644 --- a/build-system/bazel-utils/plist_fragment.bzl +++ b/build-system/bazel-utils/plist_fragment.bzl @@ -18,6 +18,8 @@ def _plist_fragment(ctx): resolved_values = dict() for key in found_keys: value = ctx.var.get(key, None) + if value == None: + value = ctx.attr.defaults.get(key, None) if value == None: fail("Expected value for --define={} was not found".format(key)) resolved_values[key] = value @@ -38,6 +40,7 @@ plist_fragment = rule( attrs = { "extension": attr.string(mandatory = True), "template": attr.string(mandatory = True), + "defaults": attr.string_dict(), }, outputs = { "out": "%{name}.%{extension}" diff --git a/scripts/lldb_build_common.sh b/scripts/lldb_build_common.sh index 8633902f40..223417bc5b 100755 --- a/scripts/lldb_build_common.sh +++ b/scripts/lldb_build_common.sh @@ -10,12 +10,9 @@ WORKSPACE_ROOT=$(pwd) BAZEL_CMD="./build-input/bazel-8.4.2-darwin-arm64" export ADDITIONAL_FLAGS=() -TELEGRAM_VERSION=$(python3 -c "import json; print(json.load(open('${WORKSPACE_ROOT}/versions.json'))['app'])") ADDITIONAL_FLAGS+=("--keep_going") ADDITIONAL_FLAGS+=("--color=yes") -ADDITIONAL_FLAGS+=("--define=telegramVersion=${TELEGRAM_VERSION}") -ADDITIONAL_FLAGS+=("--define=buildNumber=100000") if [ -n "${BAZEL_EXTRA_BUILD_FLAGS:-}" ]; then ADDITIONAL_FLAGS+=("${BAZEL_EXTRA_BUILD_FLAGS[@]}") diff --git a/submodules/ContextUI/Sources/PinchSourceContainerNode.swift b/submodules/ContextUI/Sources/PinchSourceContainerNode.swift index bc2cc63ac6..e04b59d7fd 100644 --- a/submodules/ContextUI/Sources/PinchSourceContainerNode.swift +++ b/submodules/ContextUI/Sources/PinchSourceContainerNode.swift @@ -146,7 +146,7 @@ public final class PinchSourceContainerNode: ASDisplayNode, ASGestureRecognizerD public var maxPinchScale: CGFloat = 10.0 - private var isActive: Bool = false + public private(set) var isActive: Bool = false public var activate: ((PinchSourceContainerNode) -> Void)? public var scaleUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? @@ -223,4 +223,4 @@ public final class PinchSourceContainerNode: ASDisplayNode, ASGestureRecognizerD } self.contentNode.frame = naturalContentFrame } -} \ No newline at end of file +} diff --git a/submodules/Display/Source/ListViewProtocol.swift b/submodules/Display/Source/ListViewProtocol.swift index d66509ef95..51a838d7e4 100644 --- a/submodules/Display/Source/ListViewProtocol.swift +++ b/submodules/Display/Source/ListViewProtocol.swift @@ -30,6 +30,7 @@ public protocol ListView: ASDisplayNode { // MARK: - Read-only State Properties var insets: UIEdgeInsets { get } + var visibleSize: CGSize { get } var isTracking: Bool { get } var trackingOffset: CGFloat { get } var beganTrackingAtTopOrigin: Bool { get } diff --git a/submodules/GalleryUI/Sources/GalleryFooterNode.swift b/submodules/GalleryUI/Sources/GalleryFooterNode.swift index a2571990d0..af4656ee40 100644 --- a/submodules/GalleryUI/Sources/GalleryFooterNode.swift +++ b/submodules/GalleryUI/Sources/GalleryFooterNode.swift @@ -78,7 +78,7 @@ public final class GalleryFooterNode: ASDisplayNode { self.currentThumbnailPanelHeight = thumbnailPanelHeight self.currentFooterContentNode = footerContentNode if let footerContentNode = footerContentNode { - footerContentNode.setVisibilityAlpha(self.visibilityAlpha, animated: transition.isAnimated) + footerContentNode.setVisibilityAlpha(self.visibilityAlpha, animated: false) footerContentNode.controllerInteraction = self.controllerInteraction footerContentNode.requestLayout = { [weak self] transition in if let strongSelf = self, let (currentLayout, navigationBarHeight, currentThumbnailPanelHeight, isHidden) = strongSelf.currentLayout { diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 7de25df73e..8c4bc2ac28 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -928,6 +928,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var scrubbingFrameDisposable: Disposable? private var isPlaying = false + private var hasStartedOnce = false private let isPlayingPromise = ValuePromise(false, ignoreRepeated: true) private let isInteractingPromise = ValuePromise(false, ignoreRepeated: true) private var areControlsVisible: Bool = true @@ -1679,10 +1680,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var isPlaying = false var isPaused = true var seekable = hintSeekable - var hasStarted = false + var hasStarted = strongSelf.hasStartedOnce var displayProgress = true if let value = value { - hasStarted = value.timestamp > 0 + if value.timestamp > 0 { + hasStarted = true + } if let zoomableContent = strongSelf.zoomableContent, !value.dimensions.width.isZero && !value.dimensions.height.isZero { let videoSize = CGSize(width: value.dimensions.width * 2.0, height: value.dimensions.height * 2.0) @@ -1750,6 +1753,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } + strongSelf.hasStartedOnce = hasStarted + if !disablePlayerControls && strongSelf.isCentral == true && isPlaying { strongSelf.isPlayingPromise.set(true) strongSelf.isPlaying = true diff --git a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift index d5ee06bab1..98029012c8 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift @@ -268,7 +268,11 @@ private final class MediaPlayerScrubbingBufferingNode: ASDisplayNode { self.foregroundNode.isLayerBacked = true self.foregroundNode.displayWithoutProcessing = true self.foregroundNode.displaysAsynchronously = false - self.foregroundNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: color) + if case .round = lineCap { + self.foregroundNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: color) + } else { + self.foregroundNode.backgroundColor = color + } super.init() @@ -278,6 +282,9 @@ private final class MediaPlayerScrubbingBufferingNode: ASDisplayNode { func updateStatus(_ ranges: RangeSet, _ size: Int64) { self.ranges = (ranges, size) + /*#if DEBUG + self.ranges = (RangeSet(0 ..< size / 2), size) + #endif*/ if !self.bounds.width.isZero { self.updateLayout(size: self.bounds.size, transition: .animated(duration: 0.15, curve: .easeInOut)) } @@ -424,7 +431,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { backgroundNode.displaysAsynchronously = false backgroundNode.displayWithoutProcessing = true - let bufferingNode = MediaPlayerScrubbingBufferingNode(color: bufferingColor, lineCap: lineCap, lineHeight: lineHeight) + let bufferingNode = MediaPlayerScrubbingBufferingNode(color: bufferingColor, lineCap: scrubberHandle == .none ? .square : lineCap, lineHeight: lineHeight) let foregroundContentNode = ASImageNode() foregroundContentNode.isLayerBacked = true @@ -434,7 +441,11 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { switch lineCap { case .round: backgroundNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: backgroundColor) - foregroundContentNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: foregroundColor) + if case .none = scrubberHandle { + foregroundContentNode.backgroundColor = foregroundColor + } else { + foregroundContentNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: foregroundColor) + } case .square: backgroundNode.backgroundColor = backgroundColor foregroundContentNode.backgroundColor = foregroundColor @@ -443,13 +454,16 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { let foregroundNode = MediaPlayerScrubbingForegroundNode() foregroundNode.isLayerBacked = true foregroundNode.clipsToBounds = true + if case .round = lineCap { + foregroundNode.layer.cornerRadius = lineHeight * 0.5 + } var handleNodeImpl: ASImageNode? var highlightedHandleNodeImpl: ASImageNode? var handleNodeContainerImpl: MediaPlayerScrubbingNodeButton? switch scrubberHandle { - case .none: + case .none: let handleNode = ASImageNode() handleNode.isLayerBacked = true handleNodeImpl = handleNode @@ -750,7 +764,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { switch self.contentNodes { case let .standard(node): let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate - node.foregroundContentNode.backgroundColor = collapsed ? .white : nil + node.foregroundContentNode.backgroundColor = collapsed ? .white : (node.handle == .none ? .white : nil) if let handleNode = node.handleNodeContainer { transition.updateAlpha(node: node.foregroundContentNode, alpha: collapsed ? 0.45 : 1.0) diff --git a/submodules/Postbox/Sources/TimeBasedCleanup.swift b/submodules/Postbox/Sources/TimeBasedCleanup.swift index 978ec8e094..12b4e864ba 100644 --- a/submodules/Postbox/Sources/TimeBasedCleanup.swift +++ b/submodules/Postbox/Sources/TimeBasedCleanup.swift @@ -198,87 +198,6 @@ private func scanFiles(at path: String, olderThan minTimestamp: Int32, includeSu return result } -/*private func mapFiles(paths: [String], inodes: inout [InodeInfo], removeSize: UInt64, mainStoragePath: String, storageBox: StorageBox) { - var removedSize: UInt64 = 0 - inodes.sort(by: { lhs, rhs in - return lhs.timestamp < rhs.timestamp - }) - - var inodesToDelete = Set<__darwin_ino64_t>() - - for inode in inodes { - inodesToDelete.insert(inode.inode) - removedSize += UInt64(inode.size) - if removedSize >= removeSize { - break - } - } - - if inodesToDelete.isEmpty { - return - } - - let pathBuffer = malloc(2048).assumingMemoryBound(to: Int8.self) - defer { - free(pathBuffer) - } - - var unlinkedResourceIds: [Data] = [] - - for path in paths { - let isMainPath = path == mainStoragePath - if let dp = opendir(path) { - while true { - guard let dirp = readdir(dp) else { - break - } - - if strncmp(&dirp.pointee.d_name.0, ".", 1024) == 0 { - continue - } - if strncmp(&dirp.pointee.d_name.0, "..", 1024) == 0 { - continue - } - strncpy(pathBuffer, path, 1024) - strncat(pathBuffer, "/", 1024) - strncat(pathBuffer, &dirp.pointee.d_name.0, 1024) - - //puts(pathBuffer) - //puts("\n") - - var value = stat() - if stat(pathBuffer, &value) == 0 { - if (((value.st_mode) & S_IFMT) == S_IFDIR) { - if let subPath = String(data: Data(bytes: pathBuffer, count: strnlen(pathBuffer, 1024)), encoding: .utf8) { - mapFiles(paths: <#T##[String]#>, inodes: &<#T##[InodeInfo]#>, removeSize: remov, mainStoragePath: mainStoragePath, storageBox: storageBox) - } - } else { - if inodesToDelete.contains(value.st_ino) { - if isMainPath { - let nameLength = strnlen(&dirp.pointee.d_name.0, 1024) - let nameData = Data(bytesNoCopy: &dirp.pointee.d_name.0, count: Int(nameLength), deallocator: .none) - withExtendedLifetime(nameData, { - if let fileName = String(data: nameData, encoding: .utf8) { - if let idData = MediaBox.idForFileName(name: fileName).data(using: .utf8) { - unlinkedResourceIds.append(idData) - } - } - }) - } - unlink(pathBuffer) - } - } - } - } - closedir(dp) - } - } - - if !unlinkedResourceIds.isEmpty { - storageBox.remove(ids: unlinkedResourceIds) - } -}*/ - private func statForDirectory(path: String) -> Int64 { if #available(macOS 10.13, *) { var s = darwin_dirstat() @@ -482,10 +401,10 @@ private final class TimeBasedCleanupImpl { } //let fileName = filePath.lastPathComponent - if remainingSize >= bytesLimit { + if remainingSize <= Int64(bytesLimit) { return false } - + return true } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift index a46563c51a..4c6ef24fcc 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift @@ -213,6 +213,10 @@ final class VideoChatParticipantVideoComponent: Component { private var referenceLocation: ReferenceLocation? private var loadingEffectView: VideoChatVideoLoadingEffectView? + public var isPinchToZoomActive: Bool { + return self.pinchContainerNode.isActive + } + override init(frame: CGRect) { self.backgroundGradientView = UIImageView() self.pinchContainerNode = PinchSourceContainerNode() diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index 11529cf236..5aa0a630c3 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -693,7 +693,7 @@ final class VideoChatParticipantsComponent: Component { private var appliedGridIsEmpty: Bool = true - private var isPinchToZoomActive: Bool = false + private(set) var isPinchToZoomActive: Bool = false private var stopRequestingNonCentralVideo: Bool = false private var stopRequestingNonCentralVideoTimer: Foundation.Timer? diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index 5449297515..def60adbb4 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -337,6 +337,13 @@ final class VideoChatScreenComponent: Component { var maxVideoQuality: Int = Int.max + private var isPinchToZoomActive: Bool { + if let participantsView = self.participants.view as? VideoChatParticipantsComponent.View { + return participantsView.isPinchToZoomActive + } + return false + } + override init(frame: CGRect) { self.containerView = UIView() self.containerView.clipsToBounds = true @@ -1609,7 +1616,7 @@ final class VideoChatScreenComponent: Component { } if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, let members { - if CFAbsoluteTimeGetCurrent() > self.focusedSpeakerAutoSwitchDeadline, !expandedParticipantsVideoState.isMainParticipantPinned, let participant = members.participants.first(where: { participant in + if CFAbsoluteTimeGetCurrent() > self.focusedSpeakerAutoSwitchDeadline, !expandedParticipantsVideoState.isMainParticipantPinned, !self.isPinchToZoomActive, let participant = members.participants.first(where: { participant in if let callState = self.callState, participant.id == .peer(callState.myPeerId) { return false } @@ -1966,7 +1973,7 @@ final class VideoChatScreenComponent: Component { } if let expandedParticipantsVideoState = self.expandedParticipantsVideoState { - if CFAbsoluteTimeGetCurrent() > self.focusedSpeakerAutoSwitchDeadline, !expandedParticipantsVideoState.isMainParticipantPinned, let participant = members.participants.first(where: { participant in + if CFAbsoluteTimeGetCurrent() > self.focusedSpeakerAutoSwitchDeadline, !expandedParticipantsVideoState.isMainParticipantPinned, !self.isPinchToZoomActive, let participant = members.participants.first(where: { participant in if let callState = self.callState, participant.id == .peer(callState.myPeerId) { return false } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift index b47e6d164a..dbfabab361 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift @@ -316,8 +316,8 @@ extension VideoChatScreenComponent.View { ] let videoQualityTitle = qualityList.first(where: { $0.0 == self.maxVideoQuality })?.1 ?? "" - items.append(.action(ContextMenuActionItem(text: environment.strings.VideoChat_IncomingVideoQuality_Title, textColor: .primary, textLayout: .secondLineWithValue(videoQualityTitle), icon: { _ in - return nil + items.append(.action(ContextMenuActionItem(text: environment.strings.VideoChat_IncomingVideoQuality_Title, textColor: .primary, textLayout: .secondLineWithValue(videoQualityTitle), icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] c, _ in guard let self else { c?.dismiss(completion: nil) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatShareScreenContextItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatShareScreenContextItem.swift index 70360e3234..d84df8a4c2 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatShareScreenContextItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatShareScreenContextItem.swift @@ -121,11 +121,10 @@ private final class VoiceChatShareScreenContextItemNode: ASDisplayNode, ContextM let standardIconWidth: CGFloat = 32.0 var rightTextInset: CGFloat = sideInset if !iconSize.width.isZero { - rightTextInset = max(iconSize.width, standardIconWidth) + iconSideInset + sideInset + rightTextInset = max(iconSize.width, standardIconWidth) + iconSideInset } let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude)) - let verticalSpacing: CGFloat = 2.0 let combinedTextHeight = textSize.height + verticalSpacing diff --git a/submodules/TelegramCore/Sources/State/PaidMessages.swift b/submodules/TelegramCore/Sources/State/PaidMessages.swift index fc41251860..d576b283a7 100644 --- a/submodules/TelegramCore/Sources/State/PaidMessages.swift +++ b/submodules/TelegramCore/Sources/State/PaidMessages.swift @@ -96,23 +96,29 @@ func _internal_reinstateNoPaidMessagesException(account: Account, scopePeerId: P return (transaction.getPeer(scopePeerId).flatMap(apiInputPeer), transaction.getPeer(peerId).flatMap(apiInputUser)) } |> mapToSignal { scopeInputPeer, inputUser -> Signal in + var scopeInputPeer = scopeInputPeer if scopePeerId != account.peerId { if scopeInputPeer == nil { return .never() } } else { - return .never() + scopeInputPeer = nil } guard let inputUser else { return .never() } var flags: Int32 = 0 flags |= (1 << 2) - flags |= (1 << 1) + if scopeInputPeer != nil { + flags |= (1 << 1) + } return account.network.request(Api.functions.account.toggleNoPaidMessagesException(flags: flags, parentPeer: scopeInputPeer, userId: inputUser)) |> `catch` { _ -> Signal in return .single(.boolFalse) } |> mapToSignal { _ in + if scopePeerId == account.peerId { + account.viewTracker.forceUpdateCachedPeerData(peerId: peerId) + } return account.postbox.transaction { transaction -> Void in if scopePeerId != account.peerId { guard var data = transaction.getMessageHistoryThreadInfo(peerId: scopePeerId, threadId: peerId.toInt64())?.data.get(MessageHistoryThreadData.self) else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift index c7c38cb20b..e51b9a840e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift @@ -325,6 +325,31 @@ func _internal_renderStorageUsageStatsMessages(account: Account, stats: StorageU } func _internal_clearStorage(account: Account, peerId: EnginePeer.Id?, categories: [StorageUsageStats.CategoryKey], includeMessages: [Message], excludeMessages: [Message]) -> Signal { + #if DEBUG + if "".isEmpty { + return Signal { subscriber in + var value: Float = 0.0 + subscriber.putNext(value) + let timer = SwiftSignalKit.Timer(timeout: 0.1, repeat: true, completion: { + if value != 1.0 { + value += 0.1 + if value >= 1.0 { + value = 1.0 + subscriber.putNext(value) + subscriber.putCompletion() + } else { + subscriber.putNext(value) + } + } + }, queue: .mainQueue()) + timer.start() + return ActionDisposable { + timer.invalidate() + } + } + } + #endif + let mediaBox = account.postbox.mediaBox return Signal { subscriber in var includeResourceIds = Set() diff --git a/submodules/TelegramCore/Sources/Utils/AutomaticCacheEviction.swift b/submodules/TelegramCore/Sources/Utils/AutomaticCacheEviction.swift index e4773b42e8..fe0cae68e7 100644 --- a/submodules/TelegramCore/Sources/Utils/AutomaticCacheEviction.swift +++ b/submodules/TelegramCore/Sources/Utils/AutomaticCacheEviction.swift @@ -134,115 +134,119 @@ final class AutomaticCacheEvictionContext { } } - if timeout == Int32.max { - return .complete() - } + let storyTimeout: Int32 = settings.categoryStorageTimeout[.stories] ?? (2 * 24 * 60 * 60) - let minPeerTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - timeout - //let minPeerTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - - let allSignal = mediaBox.storageBox.all(peerId: peerId, excludeType: MediaResourceUserContentType.story.rawValue) - |> mapToSignal { peerResourceIds -> Signal in - return Signal { subscriber in - var isCancelled = false - - processingQueue.justDispatch { - var removeIds: [MediaResourceId] = [] - var removeRawIds: [Data] = [] - var localCounter = 0 - for resourceId in peerResourceIds { - localCounter += 1 - if localCounter % 100 == 0 { - if isCancelled { - subscriber.putCompletion() - return - } - } - - removeRawIds.append(resourceId) - let id = MediaResourceId(String(data: resourceId, encoding: .utf8)!) - let resourceTimestamp = mediaBox.resourceUsageWithInfo(id: id) - if resourceTimestamp != 0 && resourceTimestamp < minPeerTimestamp { - removeIds.append(id) - } - } + let allSignal: Signal + if timeout != Int32.max { + let minPeerTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - timeout + + allSignal = mediaBox.storageBox.all(peerId: peerId, excludeType: MediaResourceUserContentType.story.rawValue) + |> mapToSignal { peerResourceIds -> Signal in + return Signal { subscriber in + var isCancelled = false - if !removeIds.isEmpty { - Logger.shared.log("AutomaticCacheEviction", "peer \(peerId): cleaning \(removeIds.count) resources") - - let _ = mediaBox.removeCachedResourcesWithResult(removeIds).start(next: { actualIds in - var actualRawIds: [Data] = [] - for id in actualIds { - if let data = id.stringRepresentation.data(using: .utf8) { - actualRawIds.append(data) + processingQueue.justDispatch { + var removeIds: [MediaResourceId] = [] + var localCounter = 0 + for resourceId in peerResourceIds { + localCounter += 1 + if localCounter % 100 == 0 { + if isCancelled { + subscriber.putCompletion() + return } } - mediaBox.storageBox.remove(ids: actualRawIds) - - subscriber.putCompletion() - }) - } else { - subscriber.putCompletion() - } - } - - return ActionDisposable { - isCancelled = true - } - } - } - - let storySignal = mediaBox.storageBox.all(peerId: peerId, onlyType: MediaResourceUserContentType.story.rawValue) - |> mapToSignal { peerResourceIds -> Signal in - return Signal { subscriber in - var isCancelled = false - - processingQueue.justDispatch { - var removeIds: [MediaResourceId] = [] - var removeRawIds: [Data] = [] - var localCounter = 0 - for resourceId in peerResourceIds { - localCounter += 1 - if localCounter % 100 == 0 { - if isCancelled { - subscriber.putCompletion() - return + let id = MediaResourceId(String(data: resourceId, encoding: .utf8)!) + let resourceTimestamp = mediaBox.resourceUsageWithInfo(id: id) + if resourceTimestamp != 0 && resourceTimestamp < minPeerTimestamp { + removeIds.append(id) } } - removeRawIds.append(resourceId) - let id = MediaResourceId(String(data: resourceId, encoding: .utf8)!) - let resourceTimestamp = mediaBox.resourceUsageWithInfo(id: id) - if resourceTimestamp != 0 && resourceTimestamp < minPeerTimestamp { - removeIds.append(id) + if !removeIds.isEmpty { + Logger.shared.log("AutomaticCacheEviction", "peer \(peerId): cleaning \(removeIds.count) resources") + + let _ = mediaBox.removeCachedResourcesWithResult(removeIds).start(next: { actualIds in + var actualRawIds: [Data] = [] + for id in actualIds { + if let data = id.stringRepresentation.data(using: .utf8) { + actualRawIds.append(data) + } + } + + mediaBox.storageBox.remove(ids: actualRawIds) + + subscriber.putCompletion() + }) + } else { + subscriber.putCompletion() } } - if !removeIds.isEmpty { - Logger.shared.log("AutomaticCacheEviction", "peer \(peerId): cleaning \(removeIds.count) resources") - - let _ = mediaBox.removeCachedResourcesWithResult(removeIds).start(next: { actualIds in - var actualRawIds: [Data] = [] - for id in actualIds { - if let data = id.stringRepresentation.data(using: .utf8) { - actualRawIds.append(data) + return ActionDisposable { + isCancelled = true + } + } + } + } else { + allSignal = .complete() + } + + let storySignal: Signal + if storyTimeout != Int32.max { + let minPeerTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - storyTimeout + storySignal = mediaBox.storageBox.all(peerId: peerId, onlyType: MediaResourceUserContentType.story.rawValue) + |> mapToSignal { peerResourceIds -> Signal in + return Signal { subscriber in + var isCancelled = false + + processingQueue.justDispatch { + var removeIds: [MediaResourceId] = [] + var localCounter = 0 + for resourceId in peerResourceIds { + localCounter += 1 + if localCounter % 100 == 0 { + if isCancelled { + subscriber.putCompletion() + return } } - mediaBox.storageBox.remove(ids: actualRawIds) + let id = MediaResourceId(String(data: resourceId, encoding: .utf8)!) + let resourceTimestamp = mediaBox.resourceUsageWithInfo(id: id) + if resourceTimestamp != 0 && resourceTimestamp < minPeerTimestamp { + removeIds.append(id) + } + } + + if !removeIds.isEmpty { + Logger.shared.log("AutomaticCacheEviction", "peer \(peerId): cleaning \(removeIds.count) resources") + let _ = mediaBox.removeCachedResourcesWithResult(removeIds).start(next: { actualIds in + var actualRawIds: [Data] = [] + for id in actualIds { + if let data = id.stringRepresentation.data(using: .utf8) { + actualRawIds.append(data) + } + } + + mediaBox.storageBox.remove(ids: actualRawIds) + + subscriber.putCompletion() + }) + } else { subscriber.putCompletion() - }) - } else { - subscriber.putCompletion() + } + } + + return ActionDisposable { + isCancelled = true } } - - return ActionDisposable { - isCancelled = true - } } + } else { + storySignal = .complete() } return allSignal |> then(storySignal) diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index c4f433f965..4c75039caa 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -2404,7 +2404,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg if additionalSideInsets.right > 0.0 { textFieldInsets.right += additionalSideInsets.right / 3.0 } - if inputHasText || self.extendedSearchLayout || hasMediaDraft || hasForward || hasSlowmodeButton { + if inputHasText || self.extendedSearchLayout || hasMediaDraft || hasForward || hasSlowmodeButton || isEditingMedia { } else { if let customRightAction = self.customRightAction, case .empty = customRightAction { textFieldInsets.right = 8.0 @@ -3033,7 +3033,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg if self.extendedSearchLayout { nextButtonTopRight.x -= 46.0 } else if hasSlowmodeButton { - } else if inputHasText || hasMediaDraft || hasForward { + } else if inputHasText || hasMediaDraft || hasForward || isEditingMedia { nextButtonTopRight.x -= sendActionButtonsSize.width } for (item, button) in self.accessoryItemButtons.reversed() { @@ -3182,7 +3182,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } var mediaActionButtonsFrame = CGRect(origin: CGPoint(x: textInputContainerBackgroundFrame.maxX + 6.0, y: textInputContainerBackgroundFrame.maxY - mediaActionButtonsSize.height), size: mediaActionButtonsSize) - if inputHasText || self.extendedSearchLayout || hasMediaDraft || interfaceState.interfaceState.forwardMessageIds != nil || hasSlowmodeButton { + if inputHasText || self.extendedSearchLayout || hasMediaDraft || interfaceState.interfaceState.forwardMessageIds != nil || hasSlowmodeButton || isEditingMedia { mediaActionButtonsFrame.origin.x = width + 8.0 } transition.updateFrame(node: self.mediaActionButtons, frame: mediaActionButtonsFrame) @@ -3230,7 +3230,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg var sendActionButtonsFrame = CGRect(origin: CGPoint(x: textInputContainerBackgroundFrame.maxX - sendActionButtonsSize.width, y: textInputContainerBackgroundFrame.maxY - sendActionButtonsSize.height), size: sendActionButtonsSize) let sendActionsScale: CGFloat - if inputHasText || hasMediaDraft || hasForward { + if inputHasText || hasMediaDraft || hasForward || isEditingMedia { sendActionsScale = 1.0 } else { sendActionsScale = 0.001 @@ -4369,6 +4369,11 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg if presentationInterfaceState.interfaceState.mediaDraftState != nil { keepSendButtonEnabled = true } + if let editMessageState = presentationInterfaceState.editMessageState { + if case let .media(value) = editMessageState.content, !value.isEmpty { + keepSendButtonEnabled = true + } + } hasForward = presentationInterfaceState.interfaceState.forwardMessageIds != nil hideMicButtonBackground = presentationInterfaceState.inputTextPanelState.mediaRecordingState != nil diff --git a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift index ab60255310..9d00e1054b 100644 --- a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift +++ b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift @@ -1010,7 +1010,7 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { if (leftTitleInset == leftInset) != (rightTitleInset == rightInset) { if rightTitleInset == rightInset { - rightTitleInset = leftTitleInset + rightTitleInset = max(rightInset, 16.0) } else if leftTitleInset == leftInset { leftTitleInset = rightTitleInset } @@ -1041,6 +1041,9 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { let titleSize = titleView.updateLayout(availableSize: CGSize(width: size.width - leftTitleInset - rightTitleInset, height: nominalHeight), transition: titleViewTransition) var titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) * 0.5), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize) + if titleFrame.origin.x + titleFrame.width > size.width - rightTitleInset { + titleFrame.origin.x = size.width - rightTitleInset - titleFrame.width + } if titleFrame.origin.x < leftTitleInset { titleFrame.origin.x = leftTitleInset + floorToScreenPixels((size.width - leftTitleInset - rightTitleInset - titleFrame.width) * 0.5) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift index 1f69cbc65a..b1600c7312 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift @@ -69,14 +69,18 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { private let groupsInCommonContext: GroupsInCommonContext weak var parentController: ViewController? - + + private let listBackgroundView: UIImageView + private let listMaskView: UIImageView private let listNode: ListView private var state: GroupsInCommonState? private var currentEntries: [GroupsInCommonListEntry] = [] private var enqueuedTransactions: [GroupsInCommonListTransaction] = [] private var currentParams: (size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData)? - + + private var ignoreListBackgroundUpdates: Bool = false + private let ready = Promise() private var didSetReady: Bool = false var isReady: Signal { @@ -114,11 +118,25 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { self.listNode.accessibilityPageScrolledString = { row, count in return presentationData.strings.VoiceOver_ScrollStatus(row, count).string } - + + self.listBackgroundView = UIImageView() + self.listBackgroundView.image = generateStretchableFilledCircleImage(diameter: 26.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate) + self.listMaskView = UIImageView() + self.listMaskView.image = generateImage(CGSize(width: 16.0 + 26.0 * 2.0 + 16.0, height: 2.0 * 2.0 + 26.0 * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 16.0, y: 2.0), size: CGSize(width: 26.0 * 2.0, height: 26.0 * 2.0))) + })?.stretchableImage(withLeftCapWidth: 16 + 26, topCapHeight: 2 + 26).withRenderingMode(.alwaysTemplate) + super.init() - + self.listNode.preloadPages = true + self.view.addSubview(self.listBackgroundView) self.addSubnode(self.listNode) + self.view.addSubview(self.listMaskView) self.disposable = (groupsInCommonContext.state |> deliverOnMainQueue).startStrict(next: { [weak self] state in @@ -139,6 +157,23 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { strongSelf.groupsInCommonContext.loadMore() } } + + self.listNode.visibleContentOffsetChanged = { [weak self] _, transition in + guard let self else { + return + } + if !self.ignoreListBackgroundUpdates { + self.updateListBackground(transition: transition) + } + } + self.listNode.displayedItemRangeChanged = { [weak self] _, _ in + guard let self else { + return + } + if !self.ignoreListBackgroundUpdates { + self.updateListBackground(transition: .immediate) + } + } } deinit { @@ -161,9 +196,10 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { let isFirstLayout = self.currentParams == nil self.currentParams = (size, isScrollingLockedAtTop, presentationData) + self.ignoreListBackgroundUpdates = true transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) - + var scrollToItem: ListViewScrollToItem? if isScrollingLockedAtTop { switch self.listNode.visibleContentOffset() { @@ -174,10 +210,16 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { } } - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset + 16.0, bottom: bottomInset, right: sideInset + 16.0), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.scrollEnabled = !isScrollingLockedAtTop - + + self.ignoreListBackgroundUpdates = false + self.updateListBackground(transition: transition) + + self.listBackgroundView.tintColor = presentationData.theme.list.itemBlocksBackgroundColor + self.listMaskView.tintColor = presentationData.theme.list.blocksBackgroundColor + if isFirstLayout, let state = self.state { self.updatePeers(state: state, presentationData: presentationData) } @@ -221,6 +263,35 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { }) } + private func updateListBackground(transition: ContainedViewLayoutTransition) { + guard self.listNode.visibleSize.width != 0.0 else { + return + } + + var distanceToTop: CGFloat = -100.0 + var distanceToBottom: CGFloat = -100.0 + switch self.listNode.visibleContentOffset() { + case let .known(topOffset): + distanceToTop = -topOffset + self.listNode.insets.top + default: + break + } + switch self.listNode.visibleBottomContentOffset() { + case let .known(bottomOffset): + distanceToBottom = -bottomOffset + self.listNode.insets.bottom + default: + break + } + + distanceToTop = max(-100.0, distanceToTop) + distanceToBottom = max(-100.0, distanceToBottom) + + let listBackgroundFrame = CGRect(origin: CGPoint(x: 16.0, y: distanceToTop), size: CGSize(width: max(1.0, self.listNode.visibleSize.width - 16.0 * 2.0), height: max(1.0, self.listNode.visibleSize.height - distanceToBottom - distanceToTop))) + let listMaskFrame = CGRect(origin: CGPoint(x: 0.0, y: listBackgroundFrame.minY - 2.0), size: CGSize(width: listBackgroundFrame.width + 16.0 * 2.0, height: listBackgroundFrame.height + 2.0 * 2.0)) + transition.updateFrame(view: self.listBackgroundView, frame: listBackgroundFrame) + transition.updateFrame(view: self.listMaskView, frame: listMaskFrame) + } + func findLoadedMessage(id: MessageId) -> Message? { return nil } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift index 284992c656..7ba0773cb5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift @@ -286,6 +286,8 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { weak var parentController: ViewController? + private let listBackgroundView: UIImageView + private let listMaskView: UIImageView private let listNode: ListView private var currentEntries: [PeerMembersListEntry] = [] private var enclosingPeer: Peer? @@ -296,6 +298,8 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { private var currentParams: (size: CGSize, isScrollingLockedAtTop: Bool)? private let presentationDataPromise = Promise() + private var ignoreListBackgroundUpdates: Bool = false + private let ready = Promise() private var didSetReady: Bool = false var isReady: Signal { @@ -325,10 +329,24 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { return presentationData.strings.VoiceOver_ScrollStatus(row, count).string } + self.listBackgroundView = UIImageView() + self.listBackgroundView.image = generateStretchableFilledCircleImage(diameter: 26.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate) + self.listMaskView = UIImageView() + self.listMaskView.image = generateImage(CGSize(width: 16.0 + 26.0 * 2.0 + 16.0, height: 26.0 * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: CGSize(width: 26.0 * 2.0, height: 26.0 * 2.0))) + })?.stretchableImage(withLeftCapWidth: 16 + 26, topCapHeight: 26).withRenderingMode(.alwaysTemplate) + super.init() self.listNode.preloadPages = true + self.view.addSubview(self.listBackgroundView) self.addSubnode(self.listNode) + self.view.addSubview(self.listMaskView) self.disposable = (combineLatest(queue: .mainQueue(), membersContext.state, @@ -353,6 +371,23 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { strongSelf.membersContext.loadMore() } } + + self.listNode.visibleContentOffsetChanged = { [weak self] _, transition in + guard let self else { + return + } + if !self.ignoreListBackgroundUpdates { + self.updateListBackground(transition: transition) + } + } + self.listNode.displayedItemRangeChanged = { [weak self] _, _ in + guard let self else { + return + } + if !self.ignoreListBackgroundUpdates { + self.updateListBackground(transition: .immediate) + } + } } deinit { @@ -376,6 +411,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { self.currentParams = (size, isScrollingLockedAtTop) self.presentationDataPromise.set(.single(presentationData)) + self.ignoreListBackgroundUpdates = true transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) @@ -388,10 +424,16 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: duration), directionHint: .Up) } } - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset + 16.0, bottom: bottomInset, right: sideInset + 16.0), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.listNode.scrollEnabled = !isScrollingLockedAtTop + self.ignoreListBackgroundUpdates = false + self.updateListBackground(transition: transition) + + self.listBackgroundView.tintColor = presentationData.theme.list.itemBlocksBackgroundColor + self.listMaskView.tintColor = presentationData.theme.list.blocksBackgroundColor + if isFirstLayout, let enclosingPeer = self.enclosingPeer, let state = self.currentState { self.updateState(enclosingPeer: enclosingPeer, state: state, presentationData: presentationData) } @@ -471,6 +513,35 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { }) } + private func updateListBackground(transition: ContainedViewLayoutTransition) { + guard self.listNode.visibleSize.width != 0.0 else { + return + } + + var distanceToTop: CGFloat = -100.0 + var distanceToBottom: CGFloat = -100.0 + switch self.listNode.visibleContentOffset() { + case let .known(topOffset): + distanceToTop = -topOffset + self.listNode.insets.top + default: + break + } + switch self.listNode.visibleBottomContentOffset() { + case let .known(bottomOffset): + distanceToBottom = -bottomOffset + self.listNode.insets.bottom + default: + break + } + + distanceToTop = max(-100.0, distanceToTop) + distanceToBottom = max(-100.0, distanceToBottom) + + let listBackgroundFrame = CGRect(origin: CGPoint(x: 16.0, y: distanceToTop), size: CGSize(width: max(1.0, self.listNode.visibleSize.width - 16.0 * 2.0), height: max(1.0, self.listNode.visibleSize.height - distanceToBottom - distanceToTop))) + let listMaskFrame = CGRect(origin: CGPoint(x: 0.0, y: listBackgroundFrame.minY), size: CGSize(width: listBackgroundFrame.width + 16.0 * 2.0, height: listBackgroundFrame.height)) + transition.updateFrame(view: self.listBackgroundView, frame: listBackgroundFrame) + transition.updateFrame(view: self.listMaskView, frame: listMaskFrame) + } + func findLoadedMessage(id: MessageId) -> Message? { return nil } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index fbfeabc8f3..c88a28f342 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -962,7 +962,11 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat if self.currentPaneKey == .gifts { backgroundColor = presentationData.theme.list.blocksBackgroundColor } else { - backgroundColor = presentationData.theme.list.blocksBackgroundColor.mixedWith(presentationData.theme.list.plainBackgroundColor, alpha: expansionFraction) + if self.currentPaneKey == .stories || self.currentPaneKey == .storyArchive { + backgroundColor = presentationData.theme.list.blocksBackgroundColor.mixedWith(presentationData.theme.list.plainBackgroundColor, alpha: expansionFraction) + } else { + backgroundColor = presentationData.theme.list.blocksBackgroundColor + } } self.backgroundColor = backgroundColor diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift index 2ea49ad1ed..cf9db2dc9c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift @@ -682,6 +682,27 @@ extension PeerInfoScreenNode { } let itemsCount = items.count + + /*if let cachedData = data.cachedData as? CachedUserData { + var hasPaidFee = false + if let paidMessageStars = cachedData.peerStatusSettings?.paidMessageStars, paidMessageStars != .zero { + hasPaidFee = true + } + if !hasPaidFee { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Return Fee", icon: { theme in + generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Paid"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + guard let self, let peer = self.data?.peer else { + return + } + + let _ = self.context.engine.peers.reinstateNoPaidMessagesException(scopePeerId: self.context.account.peerId, peerId: peer.id).startStandalone() + }))) + } + }*/ if canSetupAutoremoveTimeout { let strings = strongSelf.presentationData.strings diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift index 330bba5457..48aa418161 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift @@ -187,8 +187,8 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { super.init() - self.addSubnode(self.backgroundNode) - self.addSubnode(self.separatorNode) + //self.addSubnode(self.backgroundNode) + //self.addSubnode(self.separatorNode) self.addSubnode(self.selectionPanel) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index 688647ec89..bd7343013d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -680,6 +680,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme var onTagTapImpl: (() -> Void)? var didScrollImpl: (() -> Void)? var coveringInsetOffsetUpdatedImpl: ((ContainedViewLayoutTransition) -> Void)? + var scrollingOffsetUpdatedImpl: ((ContainedViewLayoutTransition) -> Void)? var onBeginFastScrollingImpl: (() -> Void)? var getShimmerColorsImpl: (() -> SparseItemGrid.ShimmerColors)? var updateShimmerLayersImpl: ((SparseItemGridDisplayItem) -> Void)? @@ -1054,6 +1055,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme } func scrollingOffsetUpdated(transition: ContainedViewLayoutTransition) { + self.scrollingOffsetUpdatedImpl?(transition) } func onBeginFastScrolling() { @@ -1132,6 +1134,8 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, private let contextGestureContainerNode: ContextControllerSourceNode private let itemGrid: SparseItemGrid private let itemGridBinding: SparseItemGridBindingImpl + private let listBackgroundView: UIImageView + private let listMaskView: UIImageView private let directMediaImageCache: DirectMediaImageCache private var items: SparseItemGrid.Items? private var didUpdateItemsOnce: Bool = false @@ -1271,8 +1275,23 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, self.calendarSource = nil } + self.listBackgroundView = UIImageView() + self.listBackgroundView.image = generateStretchableFilledCircleImage(diameter: 26.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate) + self.listMaskView = UIImageView() + self.listMaskView.image = generateImage(CGSize(width: 16.0 + 26.0 * 2.0 + 16.0, height: 26.0 * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: CGSize(width: 26.0 * 2.0, height: 26.0 * 2.0))) + })?.stretchableImage(withLeftCapWidth: 16 + 26, topCapHeight: 26).withRenderingMode(.alwaysTemplate) + self.listMaskView.image = nil + self.listBackgroundView.isHidden = true + self.listMaskView.isHidden = true + super.init() - + if self.initialMessageIndex != nil { self.didSetReady = true self.ready.set(.single(true)) @@ -1342,6 +1361,13 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, strongSelf.tabBarOffsetUpdated?(transition) } + self.itemGridBinding.scrollingOffsetUpdatedImpl = { [weak self] transition in + guard let strongSelf = self else { + return + } + strongSelf.updateListBackground(transition: transition) + } + var processedOnBeginFastScrolling = false self.itemGridBinding.onBeginFastScrollingImpl = { [weak self] in guard let strongSelf = self else { @@ -1421,6 +1447,8 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, self.contextGestureContainerNode.isGestureEnabled = !useListItems self.contextGestureContainerNode.addSubnode(self.itemGrid) self.addSubnode(self.contextGestureContainerNode) + self.view.insertSubview(self.listBackgroundView, at: 0) + self.view.addSubview(self.listMaskView) self.contextGestureContainerNode.shouldBegin = { [weak self] point in guard let strongSelf = self else { @@ -2239,7 +2267,8 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, fixedItemHeight = nil } - self.itemGrid.update(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: nil, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none) + let listSideInset = isList ? sideInset + 16.0 : sideInset + self.itemGrid.update(size: size, insets: UIEdgeInsets(top: topInset, left: listSideInset, bottom: bottomInset, right: listSideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: nil, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none) if let initialMessageIndexValue = self.initialMessageIndex, items.items.contains(where: { item in if let _ = item as? VisualMediaItem { return true @@ -2257,9 +2286,44 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, } } } + + self.listBackgroundView.isHidden = !isList + self.listMaskView.isHidden = !isList + if isList { + self.listBackgroundView.tintColor = presentationData.theme.list.itemBlocksBackgroundColor + self.listMaskView.tintColor = presentationData.theme.list.blocksBackgroundColor + self.updateListBackground(transition: transition) + } } } + private func updateListBackground(transition: ContainedViewLayoutTransition) { + guard let (size, topInset, _, bottomInset, _, _, _, _, _, _) = self.currentParams else { + return + } + let _ = bottomInset + guard size.width != 0.0 else { + return + } + + var distanceToTop: CGFloat = -100.0 + var distanceToBottom: CGFloat = -100.0 + + let scrollingOffset = self.itemGrid.scrollingOffset + distanceToTop = -scrollingOffset + topInset + + let contentBottomOffset = self.itemGrid.contentBottomOffset + distanceToBottom = -contentBottomOffset + size.height + + distanceToTop = max(-100.0, distanceToTop) + distanceToBottom = max(-100.0, distanceToBottom) + + let listBackgroundFrame = CGRect(origin: CGPoint(x: 16.0, y: distanceToTop), size: CGSize(width: max(1.0, size.width - 16.0 * 2.0), height: max(1.0, size.height - distanceToBottom - distanceToTop))) + let listMaskFrame = CGRect(origin: CGPoint(x: 0.0, y: listBackgroundFrame.minY), size: CGSize(width: listBackgroundFrame.width + 16.0 * 2.0, height: listBackgroundFrame.height)) + transition.updateFrame(view: self.listBackgroundView, frame: listBackgroundFrame) + transition.updateFrame(view: self.listMaskView, frame: listMaskFrame) + } + public func currentTopTimestamp() -> Int32? { var timestamp: Int32? self.itemGrid.forEachVisibleItem { item in diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD index 4b984b59f0..d935fec442 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD +++ b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD @@ -46,6 +46,7 @@ swift_library( "//submodules/SegmentedControlNode", "//submodules/TelegramUIPreferences", "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/EdgeEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift index 6c50be3b61..57576d65b8 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift @@ -561,7 +561,7 @@ final class StorageUsagePanelContainerComponent: Component { self.panelsBackgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor self.topPanelSeparatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor self.topPanelBackgroundView.backgroundColor = component.theme.list.itemBlocksBackgroundColor - self.topPanelMergedBackgroundView.backgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor + self.topPanelMergedBackgroundView.backgroundColor = component.theme.list.blocksBackgroundColor } let topPanelCoverHeight: CGFloat = 10.0 diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 87729ec7df..827f5feb2d 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -25,6 +25,7 @@ import GalleryData import AnimatedTextComponent import BottomButtonPanelComponent import GlassBackgroundComponent +import EdgeEffect #if DEBUG import os.signpost @@ -124,20 +125,17 @@ final class StorageUsageScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let overNavigationContainer: UIView let makeStorageUsageExceptionsScreen: (CacheStorageSettings.PeerStorageCategory) -> ViewController? let peer: EnginePeer? let ready: Promise init( context: AccountContext, - overNavigationContainer: UIView, makeStorageUsageExceptionsScreen: @escaping (CacheStorageSettings.PeerStorageCategory) -> ViewController?, peer: EnginePeer?, ready: Promise ) { self.context = context - self.overNavigationContainer = overNavigationContainer self.makeStorageUsageExceptionsScreen = makeStorageUsageExceptionsScreen self.peer = peer self.ready = ready @@ -755,6 +753,8 @@ final class StorageUsageScreenComponent: Component { private let navigationEditButton = ComponentView() private let navigationDoneButton = ComponentView() + private let edgeEffectView: EdgeEffectView + private let headerView = ComponentView() private let headerOffsetContainer: UIView private let headerDescriptionView = ComponentView() @@ -768,7 +768,8 @@ final class StorageUsageScreenComponent: Component { private var doneStatusNode: RadialStatusNode? private let scrollContainerView: UIView - + private let topContentOverlayView: UIView + private let pieChartView = ComponentView() private let chartTotalLabel = ComponentView() private let categoriesView = ComponentView() @@ -811,7 +812,11 @@ final class StorageUsageScreenComponent: Component { self.headerOffsetContainer = SparseContainerView() self.scrollContainerView = UIView() - + + self.topContentOverlayView = UIView() + self.topContentOverlayView.isUserInteractionEnabled = false + self.topContentOverlayView.alpha = 0.0 + self.scrollView = ScrollViewImpl() self.keepDurationSectionContainerView = UIView() @@ -823,6 +828,8 @@ final class StorageUsageScreenComponent: Component { self.navigationRightButtonsBackground = GlassBackgroundView() + self.edgeEffectView = EdgeEffectView() + super.init(frame: frame) self.scrollView.delaysContentTouches = true @@ -848,6 +855,8 @@ final class StorageUsageScreenComponent: Component { self.scrollView.layer.addSublayer(self.headerProgressBackgroundLayer) self.scrollView.layer.addSublayer(self.headerProgressForegroundLayer) + + self.addSubview(self.edgeEffectView) } required init?(coder: NSCoder) { @@ -961,6 +970,7 @@ final class StorageUsageScreenComponent: Component { if let panelContainerView = self.panelContainer.view as? StorageUsagePanelContainerComponent.View { panelContainerView.updateNavigationMergeFactor(value: 1.0 - expansionDistanceFactor, transition: transition) } + self.topContentOverlayView.alpha = 1.0 - expansionDistanceFactor var offsetFraction: CGFloat = abs(headerOffset - minOffset) / 60.0 offsetFraction = min(1.0, max(0.0, offsetFraction)) @@ -1105,10 +1115,10 @@ final class StorageUsageScreenComponent: Component { } if self.headerOffsetContainer.superview == nil { - component.overNavigationContainer.addSubview(self.headerOffsetContainer) + self.addSubview(self.headerOffsetContainer) } if self.navigationRightButtonsBackground.superview == nil { - component.overNavigationContainer.addSubview(self.navigationRightButtonsBackground) + self.addSubview(self.navigationRightButtonsBackground) } var wasLockedAtPanels = false @@ -1222,6 +1232,11 @@ final class StorageUsageScreenComponent: Component { self.backgroundColor = environment.theme.list.blocksBackgroundColor + let edgeEffectHeight: CGFloat = environment.navigationHeight + 24.0 + let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: edgeEffectHeight)) + transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) + self.edgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: min(64.0, edgeEffectHeight), transition: transition) + var contentHeight: CGFloat = 0.0 let topInset: CGFloat = 19.0 @@ -2219,10 +2234,16 @@ final class StorageUsageScreenComponent: Component { self.scrollContainerView.addSubview(panelContainerView) } transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: panelContainerSize)) + if self.topContentOverlayView.superview == nil { + self.scrollContainerView.insertSubview(self.topContentOverlayView, belowSubview: panelContainerView) + } + self.topContentOverlayView.backgroundColor = environment.theme.list.blocksBackgroundColor + transition.setFrame(view: self.topContentOverlayView, frame: CGRect(origin: CGPoint(x: 0.0, y: panelContainerView.frame.minY - availableSize.height), size: availableSize)) } contentHeight += panelContainerSize.height } else { self.panelContainer.view?.removeFromSuperview() + self.topContentOverlayView.removeFromSuperview() } self.ignoreScrolling = true @@ -3315,8 +3336,6 @@ final class StorageUsageScreenComponent: Component { public final class StorageUsageScreen: ViewControllerComponentContainer { private let context: AccountContext - private let overNavigationContainer: UIView - private let readyValue = Promise() override public var ready: Promise { return self.readyValue @@ -3327,19 +3346,13 @@ public final class StorageUsageScreen: ViewControllerComponentContainer { public init(context: AccountContext, makeStorageUsageExceptionsScreen: @escaping (CacheStorageSettings.PeerStorageCategory) -> ViewController?, peer: EnginePeer? = nil, focusOnItemTag: StorageUsageEntryTag? = nil) { self.context = context - self.overNavigationContainer = SparseContainerView() - let componentReady = Promise() - super.init(context: context, component: StorageUsageScreenComponent(context: context, overNavigationContainer: self.overNavigationContainer, makeStorageUsageExceptionsScreen: makeStorageUsageExceptionsScreen, peer: peer, ready: componentReady), navigationBarAppearance: .default) + super.init(context: context, component: StorageUsageScreenComponent(context: context, makeStorageUsageExceptionsScreen: makeStorageUsageExceptionsScreen, peer: peer, ready: componentReady), navigationBarAppearance: .transparent) if peer != nil { self.navigationPresentation = .modal } - if let navigationBar = self.navigationBar { - navigationBar.customOverBackgroundContentView.insertSubview(self.overNavigationContainer, at: 0) - } - self.readyValue.set(componentReady.get() |> timeout(0.3, queue: .mainQueue(), alternate: .single(true))) } diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index f28a25671b..042bcda543 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -551,80 +551,94 @@ private func extractAccountManagerState(records: AccountRecordsView CallSessionManagerImplementationVersion in - CallSessionManagerImplementationVersion(version: version, supportsVideo: supportsVideo) - }, appData: self.regularDeviceToken.get() - |> map { token in - let tokenEnvironment: String - #if DEBUG - tokenEnvironment = "sandbox" - #else - tokenEnvironment = "production" - #endif - - let data = buildConfig.bundleData(withAppToken: token, tokenType: "apns", tokenEnvironment: tokenEnvironment, signatureDict: signatureDict) - if let data = data, let _ = String(data: data, encoding: .utf8) { - } else { - Logger.shared.log("data", "can't deserialize") - } - return data - }, externalRequestVerificationStream: self.firebaseRequestVerificationSecretStream.get(), externalRecaptchaRequestVerification: { method, siteKey in - return Signal { subscriber in - let recaptchaClient: Promise - if let current = self.recaptchaClientsBySiteKey[siteKey] { - recaptchaClient = current - } else { - recaptchaClient = Promise() - self.recaptchaClientsBySiteKey[siteKey] = recaptchaClient - - Recaptcha.fetchClient(withSiteKey: siteKey) { client, error in - Queue.mainQueue().async { - guard let client else { - Logger.shared.log("App \(self.episodeId)", "RecaptchaClient creation error: \(String(describing: error)).") - return - } - recaptchaClient.set(.single(client)) - } - } - } + let networkArguments = NetworkInitializationArguments( + apiId: apiId, + apiHash: apiHash, + languagesCategory: languagesCategory, + appVersion: appVersion, + voipMaxLayer: PresentationCallManagerImpl.voipMaxLayer, + voipVersions: PresentationCallManagerImpl.voipVersions(includeExperimental: true, includeReference: false).map { version, supportsVideo -> CallSessionManagerImplementationVersion in + CallSessionManagerImplementationVersion(version: version, supportsVideo: supportsVideo) + }, + appData: self.regularDeviceToken.get() |> map { token in + let tokenEnvironment: String + #if DEBUG + tokenEnvironment = "sandbox" + #else + tokenEnvironment = "production" + #endif - return (recaptchaClient.get() - |> take(1) - |> mapToSignal { recaptchaClient -> Signal in - return Signal { subscriber in - var recaptchaAction: RecaptchaAction? - switch method { - case "signup": - recaptchaAction = RecaptchaAction.signup - default: - break - } + let data = buildConfig.bundleData(withAppToken: token, tokenType: "apns", tokenEnvironment: tokenEnvironment, signatureDict: signatureDict) + if let data = data, let _ = String(data: data, encoding: .utf8) { + } else { + Logger.shared.log("data", "can't deserialize") + } + return data + }, + externalRequestVerificationStream: self.firebaseRequestVerificationSecretStream.get(), + externalRecaptchaRequestVerification: { method, siteKey in + return Signal { subscriber in + let recaptchaClient: Promise + if let current = self.recaptchaClientsBySiteKey[siteKey] { + recaptchaClient = current + } else { + recaptchaClient = Promise() + self.recaptchaClientsBySiteKey[siteKey] = recaptchaClient - guard let recaptchaAction else { - subscriber.putNext(nil) - subscriber.putCompletion() - - return EmptyDisposable - } - recaptchaClient.execute(withAction: recaptchaAction) { token, error in - if let token { - subscriber.putNext(token) - Logger.shared.log("App \(self.episodeId)", "RecaptchaClient executed successfully") - } else { - subscriber.putNext(nil) - Logger.shared.log("App \(self.episodeId)", "RecaptchaClient execute error: \(String(describing: error))") + Recaptcha.fetchClient(withSiteKey: siteKey) { client, error in + Queue.mainQueue().async { + guard let client else { + Logger.shared.log("App \(self.episodeId)", "RecaptchaClient creation error: \(String(describing: error)).") + return + } + recaptchaClient.set(.single(client)) } - subscriber.putCompletion() - } - - return ActionDisposable { } } - |> runOn(Queue.mainQueue()) - }).startStandalone(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) - } - |> runOn(Queue.mainQueue()) - }, autolockDeadine: autolockDeadine, encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: !buildConfig.isAppStoreBuild, isICloudEnabled: buildConfig.isICloudEnabled) + + return (recaptchaClient.get() + |> take(1) + |> mapToSignal { recaptchaClient -> Signal in + return Signal { subscriber in + var recaptchaAction: RecaptchaAction? + switch method { + case "signup": + recaptchaAction = RecaptchaAction.signup + default: + break + } + + guard let recaptchaAction else { + subscriber.putNext(nil) + subscriber.putCompletion() + + return EmptyDisposable + } + recaptchaClient.execute(withAction: recaptchaAction) { token, error in + if let token { + subscriber.putNext(token) + Logger.shared.log("App \(self.episodeId)", "RecaptchaClient executed successfully") + } else { + subscriber.putNext(nil) + Logger.shared.log("App \(self.episodeId)", "RecaptchaClient execute error: \(String(describing: error))") + } + subscriber.putCompletion() + } + + return ActionDisposable { + } + } + |> runOn(Queue.mainQueue()) + }).startStandalone(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) + } + |> runOn(Queue.mainQueue()) + }, + autolockDeadine: autolockDeadine, + encryptionProvider: OpenSSLEncryptionProvider(), + deviceModelName: nil, + useBetaFeatures: !buildConfig.isAppStoreBuild, + isICloudEnabled: buildConfig.isICloudEnabled + ) guard let appGroupUrl = maybeAppGroupUrl else { self.mainWindow?.presentNative(UIAlertController(title: nil, message: "Error 2", preferredStyle: .alert)) diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 09e0372e91..9462b2d182 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -838,6 +838,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.wrappingNode.contentNode.addSubnode(self.contentContainerNode) self.contentContainerNode.contentNode.addSubnode(self.backgroundNode) self.contentContainerNode.contentNode.addSubnode(self.historyNodeContainer) + + self.contentContainerNode.contentNode.addSubnode(self.messageTransitionNode) + self.contentContainerNode.contentNode.addSubnode(self.floatingTopicsPanelContainer) if let navigationBar = self.navigationBar { @@ -863,8 +866,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.inputPanelContainerNode.addSubnode(self.inputPanelClippingNode) self.inputPanelContainerNode.addSubnode(self.inputPanelOverlayNode) self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundNode) + + self.wrappingNode.contentNode.addSubnode(self.messageTransitionNode.overlayContainerNode) - self.wrappingNode.contentNode.addSubnode(self.messageTransitionNode) self.contentContainerNode.contentNode.addSubnode(self.navigateButtons) self.wrappingNode.contentNode.addSubnode(self.presentationContextMarker) self.contentContainerNode.contentNode.addSubnode(self.contentDimNode) @@ -2028,7 +2032,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { messageTransitionNode.bounds = previousMessageTransitionNode.bounds messageTransitionNode.transform = previousMessageTransitionNode.transform - self.wrappingNode.contentNode.insertSubnode(self.messageTransitionNode, aboveSubnode: previousMessageTransitionNode) + previousMessageTransitionNode.supernode?.insertSubnode(self.messageTransitionNode, aboveSubnode: previousMessageTransitionNode) self.emptyType = nil self.isLoadingValue = false @@ -2330,7 +2334,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { bottomBackgroundEdgeEffectNode = value self.bottomBackgroundEdgeEffectNode = value value.isUserInteractionEnabled = false - self.historyNodeContainer.view.superview?.insertSubview(value.view, aboveSubview: self.historyNodeContainer.view) + self.historyNodeContainer.view.superview?.insertSubview(value.view, aboveSubview: self.messageTransitionNode.view) } } } @@ -2531,7 +2535,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { topBackgroundEdgeEffectNode = value self.topBackgroundEdgeEffectNode = value value.isUserInteractionEnabled = false - self.historyNodeContainer.view.superview?.insertSubview(value.view, aboveSubview: self.historyNodeContainer.view) + self.historyNodeContainer.view.superview?.insertSubview(value.view, aboveSubview: self.messageTransitionNode.view) } } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 0bf833a029..15bbad296b 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -433,6 +433,12 @@ func chatHistoryEntriesForView( var i = 0 let unreadEntry: ChatHistoryEntry = .UnreadEntry(maxReadIndex, presentationData) for entry in entries { + if case let .MessageGroupEntry(_, messages, _) = entry { + if !messages.isEmpty && maxReadIndex >= messages[0].0.index { + i += 1 + continue + } + } if entry > unreadEntry { if i != 0 { entries.insert(unreadEntry, at: i) diff --git a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift index 00ba078433..b990c02468 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift @@ -65,7 +65,7 @@ func chatHistoryViewForLocation( let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData) - if view.isLoading { + if view.isLoading || (view.entries.isEmpty && (view.holeEarlier || view.holeLater)) { return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType)) } @@ -147,7 +147,7 @@ func chatHistoryViewForLocation( if preloaded { return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, flashIndicators: false, originalScrollPosition: nil, initialData: combinedInitialData, id: location.id) } else { - if view.isLoading { + if view.isLoading || (view.entries.isEmpty && (view.holeEarlier || view.holeLater)) { return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType)) } var scrollPosition: ChatHistoryViewScrollPosition? @@ -311,7 +311,7 @@ func chatHistoryViewForLocation( let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData) - if view.isLoading { + if view.isLoading || (view.entries.isEmpty && (view.holeEarlier || view.holeLater)) { return ChatHistoryViewUpdate.Loading(initialData: combinedInitialData, type: .Generic(type: updateType)) } @@ -461,7 +461,7 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea case .Loading: return nil case let .HistoryView(view, _, _, _, _, _, _): - if view.isLoading { + if view.isLoading || (view.entries.isEmpty && (view.holeEarlier || view.holeLater)) { return nil } return view.entries.isEmpty diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index 6f493bf636..82217835b6 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -337,21 +337,25 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran private let source: ChatMessageTransitionNodeImpl.Source private let getContentAreaInScreenSpace: () -> CGRect + private let portalSourceView: PortalSourceView private let scrollingContainer: ASDisplayNode private let containerNode: ASDisplayNode private let clippingNode: ASDisplayNode + private var portalTargetView: PortalView? weak var overlayController: OverlayTransitionContainerController? var animationEnded: (() -> Void)? var updateAfterCompletion: Bool = false - init(itemNode: ChatMessageItemNodeProtocol, contextSourceNode: ContextExtractedContentContainingNode, source: ChatMessageTransitionNodeImpl.Source, getContentAreaInScreenSpace: @escaping () -> CGRect) { + init(itemNode: ChatMessageItemNodeProtocol, contextSourceNode: ContextExtractedContentContainingNode, source: ChatMessageTransitionNodeImpl.Source, overlayContainerNode: ASDisplayNode, getContentAreaInScreenSpace: @escaping () -> CGRect) { + self.portalSourceView = PortalSourceView() + self.itemNode = itemNode self.getContentAreaInScreenSpace = getContentAreaInScreenSpace self.clippingNode = ASDisplayNode() - self.clippingNode.clipsToBounds = true + self.clippingNode.clipsToBounds = false self.scrollingContainer = ASDisplayNode() self.containerNode = ASDisplayNode() @@ -359,10 +363,18 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran self.source = source super.init() + + self.view.addSubview(self.portalSourceView) - self.addSubnode(self.clippingNode) + self.portalSourceView.addSubview(self.clippingNode.view) self.clippingNode.addSubnode(self.scrollingContainer) self.scrollingContainer.addSubnode(self.containerNode) + + if let portalTargetView = PortalView(matchPosition: true) { + self.portalTargetView = portalTargetView + self.portalSourceView.addPortal(view: portalTargetView) + overlayContainerNode.view.addSubview(portalTargetView.view) + } } deinit { @@ -374,6 +386,13 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran } func beginAnimation() { + if let portalTargetView = self.portalTargetView { + portalTargetView.view.alpha = 0.0 + portalTargetView.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) + + self.portalSourceView.layer.animateAlpha(from: 0.01, to: 1.0, duration: 0.12) + } + let verticalDuration: Double = ChatMessageTransitionNodeImpl.animationDuration let horizontalDuration: Double = verticalDuration let delay: Double = 0.0 @@ -970,6 +989,8 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran private var decorationItemNodes: [DecorationItemNodeImpl] = [] private var messageReactionContexts: [MessageReactionContext] = [] private var customOffsetHandlers: [CustomOffsetHandlerImpl] = [] + + public let overlayContainerNode: ASDisplayNode var hasScheduledTransitions: Bool { return !self.currentPendingItems.isEmpty @@ -983,6 +1004,7 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran self.listNode = listNode self.getContentAreaInScreenSpace = getContentAreaInScreenSpace self.onTransitionEvent = onTransitionEvent + self.overlayContainerNode = ASDisplayNode() super.init() @@ -1066,7 +1088,7 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran } if let contextSourceNode = contextSourceNode { - let animatingItemNode = AnimatingItemNode(itemNode: itemNode, contextSourceNode: contextSourceNode, source: source, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace) + let animatingItemNode = AnimatingItemNode(itemNode: itemNode, contextSourceNode: contextSourceNode, source: source, overlayContainerNode: self.overlayContainerNode, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace) animatingItemNode.updateLayout(size: self.bounds.size) self.animatingItemNodes.append(animatingItemNode) diff --git a/submodules/TelegramUI/Sources/SharedWakeupManager.swift b/submodules/TelegramUI/Sources/SharedWakeupManager.swift index e259fd8693..4e6bb1fcc7 100644 --- a/submodules/TelegramUI/Sources/SharedWakeupManager.swift +++ b/submodules/TelegramUI/Sources/SharedWakeupManager.swift @@ -50,6 +50,8 @@ private struct AccountTasks { } } +private let backgroundTaskSubmissionDelay: Double = 10.0 + private struct PendingMediaUploadKey: Hashable { let accountId: AccountRecordId let messageId: MessageId @@ -99,7 +101,8 @@ public final class SharedWakeupManager { private var backgroundProcessingTaskId: String? private var backgroundProcessingTaskLaunched: Bool = false private var backgroundProcessingTaskCancellationRequestedByApp: Bool = false - + private var pendingBackgroundProcessingTaskTimer: SwiftSignalKit.Timer? + private var pendingStoryUploadsByKey: [PendingStoryUploadKey: Float] = [:] private var pendingStoryUploadStatusesByKey: [PendingStoryUploadKey: PendingStoryUploadStatus] = [:] private var backgroundStoryProcessingTaskProgressByKey: [PendingStoryUploadKey: Float] = [:] @@ -107,7 +110,8 @@ public final class SharedWakeupManager { private var backgroundStoryProcessingTaskId: String? private var backgroundStoryProcessingTaskLaunched: Bool = false private var backgroundStoryProcessingTaskCancellationRequestedByApp: Bool = false - + private var pendingBackgroundStoryProcessingTaskTimer: SwiftSignalKit.Timer? + public init(beginBackgroundTask: @escaping (String, @escaping () -> Void) -> UIBackgroundTaskIdentifier?, endBackgroundTask: @escaping (UIBackgroundTaskIdentifier) -> Void, backgroundTimeRemaining: @escaping () -> Double, acquireIdleExtension: @escaping () -> Disposable?, activeAccounts: Signal<(primary: Account?, accounts: [(AccountRecordId, Account)]), NoError>, liveLocationPolling: Signal, watchTasks: Signal, inForeground: Signal, hasActiveAudioSession: Signal, notificationManager: SharedNotificationManager?, mediaManager: MediaManager, callManager: PresentationCallManager?, accountUserInterfaceInUse: @escaping (AccountRecordId) -> Signal, presentationData: @escaping () -> PresentationData?) { assert(Queue.mainQueue().isCurrent()) @@ -155,6 +159,10 @@ public final class SharedWakeupManager { } strongSelf.allowBackgroundTimeExtensionDeadlineTimer?.invalidate() strongSelf.allowBackgroundTimeExtensionDeadlineTimer = nil + strongSelf.pendingBackgroundProcessingTaskTimer?.invalidate() + strongSelf.pendingBackgroundProcessingTaskTimer = nil + strongSelf.pendingBackgroundStoryProcessingTaskTimer?.invalidate() + strongSelf.pendingBackgroundStoryProcessingTaskTimer = nil } strongSelf.updateBackgroundProcessingTaskStateFromPendingMediaUploads() strongSelf.updateBackgroundProcessingTaskStateFromPendingStoryUploads() @@ -356,6 +364,8 @@ public final class SharedWakeupManager { self.pendingStoryUploadsDisposable?.dispose() self.managedPausedInBackgroundPlayer?.dispose() self.keepIdleDisposable?.dispose() + self.pendingBackgroundProcessingTaskTimer?.invalidate() + self.pendingBackgroundStoryProcessingTaskTimer?.invalidate() if let (taskId, _, timer) = self.currentTask { timer.invalidate() self.endBackgroundTask(taskId) @@ -369,19 +379,30 @@ public final class SharedWakeupManager { let shouldHaveTask = !self.pendingMediaUploadsByKey.isEmpty && !self.inForeground let hadTask = self.backgroundProcessingTaskId != nil - + if shouldHaveTask { - if !hadTask { - self.startBackgroundProcessingTaskIfNeeded() + if !hadTask && self.pendingBackgroundProcessingTaskTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: backgroundTaskSubmissionDelay, repeat: false, completion: { [weak self] in + guard let self else { + return + } + self.pendingBackgroundProcessingTaskTimer = nil + self.startBackgroundProcessingTaskIfNeeded() + }, queue: .mainQueue()) + self.pendingBackgroundProcessingTaskTimer = timer + timer.start() } } else { + self.pendingBackgroundProcessingTaskTimer?.invalidate() + self.pendingBackgroundProcessingTaskTimer = nil + if let backgroundProcessingTaskId = self.backgroundProcessingTaskId { if !self.backgroundProcessingTaskCancellationRequestedByApp { self.backgroundProcessingTaskCancellationRequestedByApp = true BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: backgroundProcessingTaskId) Logger.shared.log("Wakeup", "Requested BG task cancellation by app: \(backgroundProcessingTaskId)") } - + if !self.backgroundProcessingTaskLaunched { self.backgroundProcessingTaskId = nil self.backgroundProcessingTaskProgressByKey = [:] @@ -399,19 +420,30 @@ public final class SharedWakeupManager { let shouldHaveTask = !self.pendingStoryUploadStatusesByKey.isEmpty && !self.inForeground let hadTask = self.backgroundStoryProcessingTaskId != nil - + if shouldHaveTask { - if !hadTask { - self.startBackgroundStoryProcessingTaskIfNeeded() + if !hadTask && self.pendingBackgroundStoryProcessingTaskTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: backgroundTaskSubmissionDelay, repeat: false, completion: { [weak self] in + guard let self else { + return + } + self.pendingBackgroundStoryProcessingTaskTimer = nil + self.startBackgroundStoryProcessingTaskIfNeeded() + }, queue: .mainQueue()) + self.pendingBackgroundStoryProcessingTaskTimer = timer + timer.start() } } else { + self.pendingBackgroundStoryProcessingTaskTimer?.invalidate() + self.pendingBackgroundStoryProcessingTaskTimer = nil + if let backgroundStoryProcessingTaskId = self.backgroundStoryProcessingTaskId { if !self.backgroundStoryProcessingTaskCancellationRequestedByApp { self.backgroundStoryProcessingTaskCancellationRequestedByApp = true BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: backgroundStoryProcessingTaskId) Logger.shared.log("Wakeup", "Requested story BG task cancellation by app: \(backgroundStoryProcessingTaskId)") } - + if !self.backgroundStoryProcessingTaskLaunched { self.backgroundStoryProcessingTaskId = nil self.backgroundStoryProcessingTaskProgressByKey = [:]