mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:06:00 +00:00
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
This commit is contained in:
@@ -21,6 +21,10 @@ import SwiftUI
|
||||
/// 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
|
||||
|
||||
@@ -36,13 +40,23 @@ struct TimelineBubbleLayout: Layout {
|
||||
static let regularText: Double = 1
|
||||
}
|
||||
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
||||
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 subviewSizes = layoutSubviews.map { $0.sizeThatFits(proposal) }
|
||||
let maxWidth = subviewSizes.map(\.width).reduce(0, max)
|
||||
let totalHeight = subviewSizes.map(\.height).reduce(0, +)
|
||||
let totalSpacing = CGFloat(layoutSubviews.count - 1) * spacing
|
||||
@@ -50,16 +64,17 @@ struct TimelineBubbleLayout: Layout {
|
||||
return CGSize(width: maxWidth, height: totalHeight + totalSpacing)
|
||||
}
|
||||
|
||||
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
|
||||
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 { $0.sizeThatFits(proposal).width }.reduce(0, max)
|
||||
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 { $0.sizeThatFits(ProposedViewSize(width: maxWidth, height: proposal.height)) }
|
||||
|
||||
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 {
|
||||
@@ -70,4 +85,25 @@ struct TimelineBubbleLayout: Layout {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
+32
-13
@@ -32,9 +32,9 @@ struct TimelineReactionsView: View {
|
||||
guard isLayoutRTL else { return layoutDirection }
|
||||
return layoutDirection == .leftToRight ? .rightToLeft : .leftToRight
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
CollapsibleReactionLayout(itemSpacing: 4, rowSpacing: 4, collapsed: collapsed, rowsBeforeCollapsible: 2) {
|
||||
layout {
|
||||
ForEach(reactions, id: \.self) { reaction in
|
||||
TimelineReactionButton(reaction: reaction) { key in
|
||||
feedbackGenerator.impactOccurred()
|
||||
@@ -45,14 +45,18 @@ struct TimelineReactionsView: View {
|
||||
.reactionLayoutItem(.reaction)
|
||||
.environment(\.layoutDirection, layoutDirection)
|
||||
}
|
||||
Button {
|
||||
collapsed.toggle()
|
||||
} label: {
|
||||
TimelineCollapseButtonLabel(collapsed: collapsed)
|
||||
.transaction { $0.animation = nil }
|
||||
|
||||
if isCollapsible {
|
||||
Button {
|
||||
collapsed.toggle()
|
||||
} label: {
|
||||
TimelineCollapseButtonLabel(collapsed: collapsed)
|
||||
.transaction { $0.animation = nil }
|
||||
}
|
||||
.reactionLayoutItem(.expandCollapse)
|
||||
.environment(\.layoutDirection, layoutDirection)
|
||||
}
|
||||
.reactionLayoutItem(.expandCollapse)
|
||||
.environment(\.layoutDirection, layoutDirection)
|
||||
|
||||
Button {
|
||||
context.send(viewAction: .displayEmojiPicker(itemID: itemID))
|
||||
} label: {
|
||||
@@ -64,6 +68,23 @@ struct TimelineReactionsView: View {
|
||||
.animation(.easeInOut(duration: 0.1).disabledDuringTests(), value: reactions)
|
||||
.padding(.leading, 4)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var isCollapsible: Bool {
|
||||
reactions.count > 5
|
||||
}
|
||||
|
||||
private var layout: AnyLayout {
|
||||
if isCollapsible {
|
||||
return AnyLayout(CollapsibleReactionLayout(itemSpacing: 4,
|
||||
rowSpacing: 4,
|
||||
collapsed: collapsed,
|
||||
rowsBeforeCollapsible: 2))
|
||||
}
|
||||
|
||||
return AnyLayout(HStackLayout(spacing: 4.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// The pill shape for the label that surrounds both the reaction and collapse buttons.
|
||||
@@ -159,10 +180,8 @@ struct TimelineReactionAddMoreButtonLabel: View {
|
||||
Image(asset: Asset.Images.addReaction)
|
||||
.resizable()
|
||||
.frame(width: addMoreButtonIconSize, height: addMoreButtonIconSize)
|
||||
// Vertical sizing is done by the layout so that the add more button
|
||||
// matches the height of the text based buttons.
|
||||
.padding(.horizontal, 10)
|
||||
.frame(maxHeight: .infinity, alignment: .center)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 12)
|
||||
.foregroundColor(.compound.iconSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user