Files
letro-ios/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineBubbleLayout.swift
Stefan Ceriu 8069d36249 Various optimisations (#1878)
* Cache TimelineBubbleLayout subview sizes

* Cache MessageText sizes, avoid extra updates

* Only use the `CollapsibleReactionLayout` if there's more than 5 reactions on a particular message

* Upgrade Sentry to 8.13.0, disable various options as they're not useful and impact performance

* Address PR comments, fix unit tests
2023-10-11 16:21:45 +03:00

110 lines
4.6 KiB
Swift

//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
/// A custom layout used for quotes and content when using the bubbles timeline style.
///
/// A custom layout is required as the embedded quote bubbles should fill the entire width of
/// the message bubble, without causing the width of the bubble to fill all of the available space.
struct TimelineBubbleLayout: Layout {
struct Cache {
var sizes = [Int: [ProposedViewSize: CGSize]]()
}
/// The spacing between the components in the bubble.
let spacing: CGFloat
/// Layout priority constants for the bubble content. These priorities are abused within
/// `TimelineBubbleLayout` to create the layout we would like. They aren't
/// used in the expected way that SwiftUI would normally use layout priorities.
enum Priority {
/// The priority of hidden quote bubbles that are only used for layout calculations.
static let hiddenQuote: Double = -1
/// The priority of visible quote bubbles that are placed in the view with a full width.
static let visibleQuote: Double = 0
/// The priority of regular text that is used for layout calculations and placed in the view.
static let regularText: Double = 1
}
func makeCache(subviews: Subviews) -> Cache {
Cache()
}
func updateCache(_ cache: inout Cache, subviews: Subviews) {
// A subview changed, reset everything
cache = Cache()
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) -> CGSize {
guard !subviews.isEmpty else { return .zero }
// Calculate the natural size using the regular text and non-greedy quote bubbles.
let layoutSubviews = subviews.filter { $0.priority != Priority.visibleQuote }
let subviewSizes = layoutSubviews.map { size(for: $0, subviews: subviews, proposedSize: proposal, cache: &cache) }
let maxWidth = subviewSizes.map(\.width).reduce(0, max)
let totalHeight = subviewSizes.map(\.height).reduce(0, +)
let totalSpacing = CGFloat(layoutSubviews.count - 1) * spacing
return CGSize(width: maxWidth, height: totalHeight + totalSpacing)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) {
guard !subviews.isEmpty else { return }
// Calculate the width using the regular text and the non-greedy quote bubbles.
let layoutSubviews = subviews.filter { $0.priority != Priority.visibleQuote }
let maxWidth = layoutSubviews.map { size(for: $0, subviews: subviews, proposedSize: proposal, cache: &cache).width }.reduce(0, max)
// Place the regular text and greedy quote bubbles using the calculated width.
let visibleSubviews = subviews.filter { $0.priority != Priority.hiddenQuote }
let subviewSizes = visibleSubviews.map { size(for: $0, subviews: subviews, proposedSize: ProposedViewSize(width: maxWidth, height: proposal.height), cache: &cache) }
var y = bounds.minY
for index in visibleSubviews.indices {
let height = subviewSizes[index].height
visibleSubviews[index].place(at: CGPoint(x: bounds.minX, y: y),
anchor: .topLeading,
proposal: ProposedViewSize(width: maxWidth, height: height))
y += height + spacing
}
}
// MARK: - Private
private func size(for subview: LayoutSubview, subviews: LayoutSubviews, proposedSize: ProposedViewSize, cache: inout Cache) -> CGSize {
guard let index = subviews.firstIndex(of: subview) else {
fatalError()
}
if cache.sizes[index] == nil {
cache.sizes[index] = [:]
}
if let cachedSize = cache.sizes[index]?[proposedSize] {
return cachedSize
}
let size = subview.sizeThatFits(proposedSize)
cache.sizes[index]?[proposedSize] = size
return size
}
}