Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2026-03-16 13:42:14 +01:00
commit b64d4a23fe
40 changed files with 704 additions and 349 deletions

View file

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

View file

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

View file

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

View file

@ -385,7 +385,8 @@ plist_fragment(
"""
<key>CFBundleVersion</key>
<string>{buildNumber}</string>
"""
""",
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 > $@ <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>{telegramVersion}</string>
"""
<string>$$version</string>
</dict>
</plist>
PLIST
""",
)
plist_fragment(

View file

@ -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 > $@ <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>{telegramVersion}</string>
"""
<string>$$version</string>
</dict>
</plist>
PLIST
""",
)
plist_fragment(

View file

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

@ -1 +1 @@
Subproject commit 39cc2529241335d20afcb6700c587d064e7a2870
Subproject commit f6aef7db5b649ffc3b7497ecc83a08ad3ab19559

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -928,6 +928,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private var scrubbingFrameDisposable: Disposable?
private var isPlaying = false
private var hasStartedOnce = false
private let isPlayingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let isInteractingPromise = ValuePromise<Bool>(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

View file

@ -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<Int64>, _ 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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Never, NoError> 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<Api.Bool, NoError> 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 {

View file

@ -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<Float, NoError> {
#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<MediaResourceId>()

View file

@ -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<Never, NoError> 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<Never, NoError>
if timeout != Int32.max {
let minPeerTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - timeout
allSignal = mediaBox.storageBox.all(peerId: peerId, excludeType: MediaResourceUserContentType.story.rawValue)
|> mapToSignal { peerResourceIds -> Signal<Never, NoError> 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<Never, NoError> 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<Never, NoError>
if storyTimeout != Int32.max {
let minPeerTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - storyTimeout
storySignal = mediaBox.storageBox.all(peerId: peerId, onlyType: MediaResourceUserContentType.story.rawValue)
|> mapToSignal { peerResourceIds -> Signal<Never, NoError> 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)

View file

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

View file

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

View file

@ -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<Bool>()
private var didSetReady: Bool = false
var isReady: Signal<Bool, NoError> {
@ -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
}

View file

@ -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<PresentationData>()
private var ignoreListBackgroundUpdates: Bool = false
private let ready = Promise<Bool>()
private var didSetReady: Bool = false
var isReady: Signal<Bool, NoError> {
@ -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
}

View file

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

View file

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

View file

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

View file

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

View file

@ -46,6 +46,7 @@ swift_library(
"//submodules/SegmentedControlNode",
"//submodules/TelegramUIPreferences",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/EdgeEffect",
],
visibility = [
"//visibility:public",

View file

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

View file

@ -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<Bool>
init(
context: AccountContext,
overNavigationContainer: UIView,
makeStorageUsageExceptionsScreen: @escaping (CacheStorageSettings.PeerStorageCategory) -> ViewController?,
peer: EnginePeer?,
ready: Promise<Bool>
) {
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<Empty>()
private let navigationDoneButton = ComponentView<Empty>()
private let edgeEffectView: EdgeEffectView
private let headerView = ComponentView<Empty>()
private let headerOffsetContainer: UIView
private let headerDescriptionView = ComponentView<Empty>()
@ -768,7 +768,8 @@ final class StorageUsageScreenComponent: Component {
private var doneStatusNode: RadialStatusNode?
private let scrollContainerView: UIView
private let topContentOverlayView: UIView
private let pieChartView = ComponentView<Empty>()
private let chartTotalLabel = ComponentView<Empty>()
private let categoriesView = ComponentView<Empty>()
@ -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<Bool>()
override public var ready: Promise<Bool> {
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<Bool>()
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)))
}

View file

@ -551,80 +551,94 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
}
}
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
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<RecaptchaClient>
if let current = self.recaptchaClientsBySiteKey[siteKey] {
recaptchaClient = current
} else {
recaptchaClient = Promise<RecaptchaClient>()
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<String?, NoError> 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<String?, NoError> { subscriber in
let recaptchaClient: Promise<RecaptchaClient>
if let current = self.recaptchaClientsBySiteKey[siteKey] {
recaptchaClient = current
} else {
recaptchaClient = Promise<RecaptchaClient>()
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<String?, NoError> 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))

View file

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

View file

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

View file

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

View file

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

View file

@ -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<AccountRecordId?, NoError>, watchTasks: Signal<AccountRecordId?, NoError>, inForeground: Signal<Bool, NoError>, hasActiveAudioSession: Signal<Bool, NoError>, notificationManager: SharedNotificationManager?, mediaManager: MediaManager, callManager: PresentationCallManager?, accountUserInterfaceInUse: @escaping (AccountRecordId) -> Signal<Bool, NoError>, 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 = [:]