mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
Reactions: Add more button (#1314)
* Add expand/collapse UI for reactions - Adds a CollapsibleFlowLayout for controlling the layout - Adds tests for this layout and some mocks for testing layouts generally - Improves the rendering of the reaction buttons which were not pixel perfect - Adds the UI for the expand collapse buttons including the count of hidden items in the collapsed state. * Add comment for reactionsCollapsed binding. * Add the add more button and change the layout to be reactions specific and account for the add more button specifics. * Update CollapsibleReactionLayout.swift * Rename lines to rows, to hopefully ease the incoming merge. * Add LayoutValueKey to organise subviews by item type. - create ReactionLayoutItemType to tag subviews with ReactionLayoutItem - Add LayoutValueKey support to the layout mocks - Fix some mistakes in the merge - Make the test code a bit more DRY - Make the animation of the addMore and collapse buttons consistent * Fix naming of the add more button label * Address comments and fix padding - Fix typos - Use pdf/svg for icon - Fix incorrect padding from a bad merge * Fix "Add more" button padding to match designs * Update snapshot test reference images. * Fix naming, add Preserves Vector Data
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 611 B After Width: | Height: | Size: 611 B |
ElementX/Resources/Assets.xcassets/images/timeline/timeline-reaction-add-more.imageset/Contents.json
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "timeline-reaction-add-more.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 1.333252 1.333374 cm
|
||||
0.396078 0.427451 0.466667 scn
|
||||
12.000000 14.000000 m
|
||||
11.631810 14.000000 11.333334 13.701523 11.333334 13.333333 c
|
||||
11.333334 12.000000 l
|
||||
10.000000 12.000000 l
|
||||
9.631810 12.000000 9.333334 11.701523 9.333334 11.333334 c
|
||||
9.333334 10.965144 9.631811 10.666667 10.000001 10.666667 c
|
||||
11.333334 10.666667 l
|
||||
11.333334 9.333332 l
|
||||
11.333334 8.965143 11.631810 8.666666 12.000000 8.666666 c
|
||||
12.368190 8.666666 12.666666 8.965143 12.666666 9.333333 c
|
||||
12.666666 10.666667 l
|
||||
14.000000 10.666667 l
|
||||
14.368190 10.666667 14.666666 10.965143 14.666666 11.333334 c
|
||||
14.666666 11.701523 14.368189 12.000000 13.999999 12.000000 c
|
||||
12.666666 12.000000 l
|
||||
12.666666 13.333334 l
|
||||
12.666666 13.701524 12.368190 14.000000 12.000000 14.000000 c
|
||||
h
|
||||
3.333333 8.333333 m
|
||||
3.333333 8.886666 3.780000 9.333333 4.333333 9.333333 c
|
||||
4.886666 9.333333 5.333333 8.886666 5.333333 8.333333 c
|
||||
5.333333 7.780000 4.886666 7.333333 4.333333 7.333333 c
|
||||
3.780000 7.333333 3.333333 7.780000 3.333333 8.333333 c
|
||||
h
|
||||
9.000000 7.333333 m
|
||||
9.553333 7.333333 9.999999 7.780000 9.999999 8.333333 c
|
||||
9.999999 8.886666 9.553333 9.333333 9.000000 9.333333 c
|
||||
8.446667 9.333333 8.000000 8.886666 8.000000 8.333333 c
|
||||
8.000000 7.780000 8.446667 7.333333 9.000000 7.333333 c
|
||||
h
|
||||
6.666677 3.000000 m
|
||||
8.220009 3.000000 9.540010 3.973333 10.073343 5.333334 c
|
||||
3.260010 5.333334 l
|
||||
3.793343 3.973333 5.113343 3.000000 6.666677 3.000000 c
|
||||
h
|
||||
1.333333 6.666667 m
|
||||
1.333333 9.612185 3.721148 12.000000 6.666667 12.000000 c
|
||||
7.073877 12.000000 7.469647 11.954485 7.849466 11.868532 c
|
||||
8.208576 11.787266 8.565571 12.012502 8.646837 12.371612 c
|
||||
8.728104 12.730721 8.502867 13.087715 8.143758 13.168982 c
|
||||
7.668022 13.276641 7.173560 13.333333 6.666667 13.333333 c
|
||||
2.984768 13.333333 0.000000 10.348565 0.000000 6.666667 c
|
||||
0.000000 2.984768 2.984768 0.000000 6.666667 0.000000 c
|
||||
10.348566 0.000000 13.333334 2.984768 13.333334 6.666667 c
|
||||
13.333334 6.808108 13.328918 6.948606 13.320211 7.088020 c
|
||||
13.297259 7.455494 12.980757 7.734784 12.613283 7.711831 c
|
||||
12.245810 7.688879 11.966518 7.372376 11.989471 7.004903 c
|
||||
11.996452 6.893137 12.000000 6.780360 12.000000 6.666667 c
|
||||
12.000000 3.721148 9.612185 1.333333 6.666667 1.333333 c
|
||||
3.721148 1.333333 1.333333 3.721148 1.333333 6.666667 c
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
2295
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 16.000000 16.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000002385 00000 n
|
||||
0000002408 00000 n
|
||||
0000002581 00000 n
|
||||
0000002655 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
2714
|
||||
%%EOF
|
||||
@@ -449,6 +449,8 @@
|
||||
"screen_room_details_topic_title" = "Topic";
|
||||
"screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again.";
|
||||
"screen_room_retry_send_menu_remove_action" = "Remove";
|
||||
"screen_room_reactions_show_more" = "Show more";
|
||||
"screen_room_reactions_show_less" = "Show less";
|
||||
"screen_session_verification_cancelled_title" = "Verification cancelled";
|
||||
"screen_session_verification_positive_button_ready" = "Start";
|
||||
"screen_signout_confirmation_dialog_submit" = "Sign out";
|
||||
|
||||
@@ -42,6 +42,7 @@ internal enum Asset {
|
||||
internal static let locationPointerFull = ImageAsset(name: "images/location-pointer-full")
|
||||
internal static let locationPointer = ImageAsset(name: "images/location-pointer")
|
||||
internal static let timelineComposerSendMessage = ImageAsset(name: "images/timeline-composer-send-message")
|
||||
internal static let timelineReactionAddMore = ImageAsset(name: "images/timeline-reaction-add-more")
|
||||
internal static let waitingGradient = ImageAsset(name: "images/waiting-gradient")
|
||||
}
|
||||
}
|
||||
|
||||
+131
-45
@@ -15,11 +15,12 @@
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
/// A flow layout that will show a collapse/expand button when the layout wraps over a defined number of rows.
|
||||
/// With n subviews passed to the layout, n-1 first views represent the main views to be laid out.
|
||||
/// The nth subview is the collapse/expand button which is only shown when the layout overflows `rowsBeforeCollapsible` number of rows.
|
||||
/// When the button is shown it is tagged on the end of the collapsed or expanded layout.
|
||||
struct CollapsibleFlowLayout: Layout {
|
||||
/// A flow layout for reactions that will show a collapse/expand button when the layout wraps over a defined number of rows.
|
||||
/// It displays an add more button when there are greater than 0 reactions and always displays the reaction and add more button
|
||||
/// on the same row (moving them both to a new row if necessary).
|
||||
/// Each subview should be marked with the appropriate `ReactionLayoutItem` using the `reactionLayoutItem` modified
|
||||
/// so the layout can appropriately treat each type of item.
|
||||
struct CollapsibleReactionLayout: Layout {
|
||||
static let pointOffscreen = CGPoint(x: -10000, y: -10000)
|
||||
/// The horizontal spacing between items
|
||||
var itemSpacing: CGFloat = 0
|
||||
@@ -31,61 +32,76 @@ struct CollapsibleFlowLayout: Layout {
|
||||
var rowsBeforeCollapsible: Int?
|
||||
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: some FlowLayoutSubviews, cache: inout ()) -> CGSize {
|
||||
let collapseButton = subviews[subviews.count - 1]
|
||||
var subviewsWithoutCollapseButton = subviews
|
||||
subviewsWithoutCollapseButton.removeLast()
|
||||
// Calculate the layout of the rows without the button
|
||||
let rowsNoButton = calculateRows(proposal: proposal, subviews: Array(subviewsWithoutCollapseButton))
|
||||
guard let subviewsByType = getSubviewsByItemType(subviews: Array(subviews)), subviewsByType.reactions.count > 0 else {
|
||||
return .zero
|
||||
}
|
||||
|
||||
// Calculate the layout of the rows with the reactions button and add more button
|
||||
let reactionsAndAddMore = calculateRows(proposal: proposal, subviews: Array(subviewsByType.reactions + [subviewsByType.addMoreButton]))
|
||||
// If we have extended beyond the defined number of rows we are showing the expand/collapse ui
|
||||
if let rowsBeforeCollapsible, rowsNoButton.count > rowsBeforeCollapsible {
|
||||
if let rowsBeforeCollapsible, reactionsAndAddMore.count > rowsBeforeCollapsible {
|
||||
if collapsed {
|
||||
// Truncate to `rowsBeforeCollapsible` number of rows and replace the item at the end of the last row with the button
|
||||
let collapsedRows = Array(rowsNoButton.prefix(rowsBeforeCollapsible))
|
||||
let (collapsedRowsWithButton, _) = replaceTrailingItemsWithButton(rowWidth: proposal.width ?? 0, rows: collapsedRows, button: collapseButton)
|
||||
let size = sizeThatFits(proposal: proposal, rows: collapsedRowsWithButton)
|
||||
let collapsedRows = Array(reactionsAndAddMore.prefix(rowsBeforeCollapsible))
|
||||
let (collapsedRowsWithButtons, _) = replaceTrailingItemsWithButtons(rowWidth: proposal.width ?? 0,
|
||||
rows: collapsedRows,
|
||||
collapseButton: subviewsByType.collapseButton,
|
||||
addMoreButton: subviewsByType.addMoreButton)
|
||||
let size = sizeThatFits(proposal: proposal, rows: collapsedRowsWithButtons)
|
||||
return size
|
||||
} else {
|
||||
// Show all subviews with the button at the end
|
||||
let rowsWithButton = calculateRows(proposal: proposal, subviews: Array(subviews))
|
||||
let size = sizeThatFits(proposal: proposal, rows: rowsWithButton)
|
||||
var rowsWithButtons = calculateRows(proposal: proposal, subviews: Array(subviews))
|
||||
ensureCollapseAndAddMoreButtonsAreOnTheSameRow(&rowsWithButtons)
|
||||
let size = sizeThatFits(proposal: proposal, rows: rowsWithButtons)
|
||||
return size
|
||||
}
|
||||
} else {
|
||||
// Otherwise we are just calculating the size of all items without the button
|
||||
return sizeThatFits(proposal: proposal, rows: rowsNoButton)
|
||||
return sizeThatFits(proposal: proposal, rows: reactionsAndAddMore)
|
||||
}
|
||||
}
|
||||
|
||||
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: some FlowLayoutSubviews, cache: inout ()) {
|
||||
let collapseButton = subviews[subviews.count - 1]
|
||||
var subviewsWithoutCollapseButton = subviews
|
||||
subviewsWithoutCollapseButton.removeLast()
|
||||
// Calculate the layout of the rows without the button
|
||||
let rowsNoButton = calculateRows(proposal: ProposedViewSize(bounds.size), subviews: Array(subviewsWithoutCollapseButton))
|
||||
guard let subviewsByType = getSubviewsByItemType(subviews: Array(subviews)), subviewsByType.reactions.count > 0 else {
|
||||
subviews.forEach { subview in
|
||||
subview.place(at: Self.pointOffscreen, anchor: .leading, proposal: .zero)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate the layout of the rows with the reactions button and add more button
|
||||
let reactionsAndAddMore = calculateRows(proposal: ProposedViewSize(bounds.size), subviews: Array(subviewsByType.reactions + [subviewsByType.addMoreButton]))
|
||||
// If we have extended beyond the defined number of rows we are showing the expand/collapse ui
|
||||
if let rowsBeforeCollapsible, rowsNoButton.count > rowsBeforeCollapsible {
|
||||
if let rowsBeforeCollapsible, reactionsAndAddMore.count > rowsBeforeCollapsible {
|
||||
if collapsed {
|
||||
// Truncate to `rowsBeforeCollapsible` number of rows and replace the item at the end of the last row with the button
|
||||
let collapsedRows = Array(rowsNoButton.prefix(rowsBeforeCollapsible))
|
||||
let (collapsedRowsWithButton, subviewsToHide) = replaceTrailingItemsWithButton(rowWidth: bounds.width, rows: collapsedRows, button: collapseButton)
|
||||
let remainingSubviews = subviewsToHide + Array(rowsNoButton.suffix(rowsNoButton.count - rowsBeforeCollapsible)).joined()
|
||||
placeSubviews(in: bounds, rows: collapsedRowsWithButton)
|
||||
let collapsedRows = Array(reactionsAndAddMore.prefix(rowsBeforeCollapsible))
|
||||
let (collapsedRowsWithButtons, subviewsToHide) = replaceTrailingItemsWithButtons(rowWidth: bounds.width,
|
||||
rows: collapsedRows,
|
||||
collapseButton: subviewsByType.collapseButton,
|
||||
addMoreButton: subviewsByType.addMoreButton)
|
||||
|
||||
var remainingSubviews = subviewsToHide + Array(reactionsAndAddMore.suffix(reactionsAndAddMore.count - rowsBeforeCollapsible)).joined()
|
||||
// remove the add button which was in initial rows calculation
|
||||
remainingSubviews.removeLast()
|
||||
placeSubviews(in: bounds, rows: collapsedRowsWithButtons)
|
||||
// "Remove" (place with a proposed zero frame) any additional subviews
|
||||
remainingSubviews.forEach { subview in
|
||||
subview.place(at: Self.pointOffscreen, anchor: .leading, proposal: .zero)
|
||||
}
|
||||
|
||||
} else {
|
||||
// Show all subviews with the button at the end
|
||||
let rowsWithButton = calculateRows(proposal: ProposedViewSize(bounds.size), subviews: Array(subviews))
|
||||
placeSubviews(in: bounds, rows: rowsWithButton)
|
||||
// Show all subviews with the buttons at the end
|
||||
var rowsWithButtons = calculateRows(proposal: ProposedViewSize(bounds.size), subviews: Array(subviews))
|
||||
ensureCollapseAndAddMoreButtonsAreOnTheSameRow(&rowsWithButtons)
|
||||
placeSubviews(in: bounds, rows: rowsWithButtons)
|
||||
}
|
||||
} else {
|
||||
// Otherwise we are just calculating the size of all items without the button
|
||||
placeSubviews(in: bounds, rows: rowsNoButton)
|
||||
// "Remove"(place with a proposed zero frame) the button
|
||||
collapseButton.place(at: Self.pointOffscreen, anchor: .leading, proposal: .zero)
|
||||
// Otherwise we are just placing the reactions and add button
|
||||
placeSubviews(in: bounds, rows: reactionsAndAddMore)
|
||||
// "Remove"(place with a proposed zero frame) the collapse button
|
||||
subviewsByType.collapseButton.place(at: Self.pointOffscreen, anchor: .leading, proposal: .zero)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,27 +161,40 @@ struct CollapsibleFlowLayout: Layout {
|
||||
/// - rows: The input list of rows
|
||||
/// - button: The button to replace the trailing items
|
||||
/// - Returns: The new rows structure with button replaced and the subviews remove from the input to make space for the button
|
||||
private func replaceTrailingItemsWithButton(rowWidth: CGFloat, rows: [[FlowLayoutSubview]], button: FlowLayoutSubview) -> ([[FlowLayoutSubview]], [FlowLayoutSubview]) {
|
||||
private func replaceTrailingItemsWithButtons(rowWidth: CGFloat, rows: [[FlowLayoutSubview]], collapseButton: FlowLayoutSubview, addMoreButton: FlowLayoutSubview) -> ([[FlowLayoutSubview]], [FlowLayoutSubview]) {
|
||||
var rows = rows
|
||||
let lastRow = rows[rows.count - 1]
|
||||
let buttonSize = button.sizeThatFits(.unspecified)
|
||||
let collapseButtonSize = collapseButton.sizeThatFits(.unspecified)
|
||||
let addMoreButtonSize = addMoreButton.sizeThatFits(.unspecified)
|
||||
let buttonsWidth = collapseButtonSize.width + itemSpacing + addMoreButtonSize.width
|
||||
var rowX: CGFloat = 0
|
||||
for (i, subview) in lastRow.enumerated() {
|
||||
let size = subview.sizeThatFits(.unspecified)
|
||||
let horizontalSpacing = i == 0 ? 0 : itemSpacing
|
||||
rowX += size.width + horizontalSpacing
|
||||
if rowX > (rowWidth - (buttonSize.width + horizontalSpacing)) {
|
||||
let lastRowWithButton = Array(lastRow.prefix(i)) + [button]
|
||||
if rowX > (rowWidth - (buttonsWidth + horizontalSpacing)) {
|
||||
let lastRowWithButton = Array(lastRow.prefix(i)) + [collapseButton, addMoreButton]
|
||||
let subviewsToHide = Array(lastRow.suffix(lastRow.count - i))
|
||||
rows[rows.count - 1] = lastRowWithButton
|
||||
return (rows, subviewsToHide)
|
||||
}
|
||||
}
|
||||
let lastRowWithButton = Array(lastRow) + [button]
|
||||
let lastRowWithButton = Array(lastRow) + [collapseButton, addMoreButton]
|
||||
rows[rows.count - 1] = lastRowWithButton
|
||||
return (rows, [])
|
||||
}
|
||||
|
||||
private func ensureCollapseAndAddMoreButtonsAreOnTheSameRow(_ rows: inout [[FlowLayoutSubview]]) {
|
||||
guard var lastRow = rows.last, lastRow.count == 1 else {
|
||||
return
|
||||
}
|
||||
var secondLastRow = rows[rows.count - 2]
|
||||
let collapseButton = secondLastRow.removeLast()
|
||||
lastRow.prepend(collapseButton)
|
||||
rows[rows.count - 2] = secondLastRow
|
||||
rows[rows.count - 1] = lastRow
|
||||
}
|
||||
|
||||
/// Given a list of rows place them in the layout.
|
||||
/// - Parameters:
|
||||
/// - bounds: The bounds of the parent
|
||||
@@ -173,20 +202,60 @@ struct CollapsibleFlowLayout: Layout {
|
||||
private func placeSubviews(in bounds: CGRect, rows: [[FlowLayoutSubview]]) {
|
||||
var rowY: CGFloat = bounds.minY
|
||||
var rowHeight: CGFloat = 0
|
||||
for (i, row) in rows.enumerated() {
|
||||
|
||||
let sizes = rows.map { row in
|
||||
row.map { subview in
|
||||
subview.sizeThatFits(.unspecified)
|
||||
}
|
||||
}
|
||||
|
||||
let maxHeight = sizes.joined().reduce(0) { partialResult, size in
|
||||
max(partialResult, size.height)
|
||||
}
|
||||
|
||||
for (i, row) in sizes.enumerated() {
|
||||
var rowX: CGFloat = bounds.minX
|
||||
let verticalSpacing = i == 0 ? 0 : rowSpacing
|
||||
for (j, subview) in row.enumerated() {
|
||||
let size = subview.sizeThatFits(.unspecified)
|
||||
for (j, size) in row.enumerated() {
|
||||
let subview = rows[i][j]
|
||||
let horizontalSpacing = j == 0 ? 0 : itemSpacing
|
||||
let point = CGPoint(x: rowX + horizontalSpacing, y: rowY + verticalSpacing + (size.height / 2))
|
||||
subview.place(at: point, anchor: .leading, proposal: ProposedViewSize(size))
|
||||
rowHeight = max(rowHeight, size.height)
|
||||
let point = CGPoint(x: rowX + horizontalSpacing, y: rowY + verticalSpacing + (maxHeight / 2))
|
||||
subview.place(at: point, anchor: .leading, proposal: ProposedViewSize(CGSize(width: size.width, height: maxHeight)))
|
||||
rowHeight = max(rowHeight, maxHeight)
|
||||
rowX += size.width + horizontalSpacing
|
||||
}
|
||||
rowY += rowHeight + verticalSpacing
|
||||
}
|
||||
}
|
||||
|
||||
/// Group the subviews by type using `ReactionLayoutItemType`
|
||||
/// - Parameter subviews: A flat list of all the subviews
|
||||
/// - Returns: The subviews organised by type
|
||||
private func getSubviewsByItemType(subviews: [FlowLayoutSubview]) -> ReactionSubviews? {
|
||||
var collapseButton: FlowLayoutSubview?
|
||||
var addMoreButton: FlowLayoutSubview?
|
||||
var reactions: [FlowLayoutSubview] = []
|
||||
for subview in subviews {
|
||||
switch subview[ReactionLayoutItemType.self] {
|
||||
case .reaction:
|
||||
reactions.append(subview)
|
||||
case .expandCollapse:
|
||||
collapseButton = subview
|
||||
case .addMore:
|
||||
addMoreButton = subview
|
||||
}
|
||||
}
|
||||
guard let collapseButton, let addMoreButton, reactions.count > 0 else {
|
||||
return nil
|
||||
}
|
||||
return ReactionSubviews(reactions: reactions, collapseButton: collapseButton, addMoreButton: addMoreButton)
|
||||
}
|
||||
}
|
||||
|
||||
struct ReactionSubviews {
|
||||
var reactions: [FlowLayoutSubview]
|
||||
var collapseButton: FlowLayoutSubview
|
||||
var addMoreButton: FlowLayoutSubview
|
||||
}
|
||||
|
||||
/// A protocol representing subviews so that we can inject mocks in unit tests.
|
||||
@@ -198,6 +267,23 @@ extension LayoutSubviews: FlowLayoutSubviews { }
|
||||
protocol FlowLayoutSubview {
|
||||
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize
|
||||
func place(at position: CGPoint, anchor: UnitPoint, proposal: ProposedViewSize)
|
||||
subscript<K>(key: K.Type) -> K.Value where K: LayoutValueKey { get }
|
||||
}
|
||||
|
||||
extension LayoutSubview: FlowLayoutSubview { }
|
||||
|
||||
enum ReactionLayoutItem {
|
||||
case reaction
|
||||
case expandCollapse
|
||||
case addMore
|
||||
}
|
||||
|
||||
struct ReactionLayoutItemType: LayoutValueKey {
|
||||
static let defaultValue: ReactionLayoutItem = .reaction
|
||||
}
|
||||
|
||||
extension View {
|
||||
func reactionLayoutItem(_ value: ReactionLayoutItem) -> some View {
|
||||
layoutValue(key: ReactionLayoutItemType.self, value: value)
|
||||
}
|
||||
}
|
||||
+45
-17
@@ -24,25 +24,36 @@ struct TimelineReactionsView: View {
|
||||
private static let verticalSpacing: CGFloat = 4
|
||||
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
||||
@Environment(\.layoutDirection) private var layoutDirection: LayoutDirection
|
||||
@Namespace private var animation
|
||||
|
||||
let itemID: TimelineItemIdentifier
|
||||
let reactions: [AggregatedReaction]
|
||||
@Binding var collapsed: Bool
|
||||
|
||||
var body: some View {
|
||||
CollapsibleFlowLayout(itemSpacing: 4, rowSpacing: 4, collapsed: collapsed, rowsBeforeCollapsible: 2) {
|
||||
CollapsibleReactionLayout(itemSpacing: 4, rowSpacing: 4, collapsed: collapsed, rowsBeforeCollapsible: 2) {
|
||||
ForEach(reactions, id: \.self) { reaction in
|
||||
TimelineReactionButton(itemID: itemID, reaction: reaction) { key in
|
||||
TimelineReactionButton(reaction: reaction) { key in
|
||||
context.send(viewAction: .toggleReaction(key: key, itemID: itemID))
|
||||
} showReactionSummary: { key in
|
||||
context.send(viewAction: .reactionSummary(itemID: itemID, key: key))
|
||||
}
|
||||
.reactionLayoutItem(.reaction)
|
||||
}
|
||||
Button {
|
||||
collapsed.toggle()
|
||||
} label: {
|
||||
TimelineCollapseButtonLabel(collapsed: collapsed)
|
||||
}
|
||||
.reactionLayoutItem(.expandCollapse)
|
||||
.animation(.easeOut, value: collapsed)
|
||||
Button {
|
||||
context.send(viewAction: .displayEmojiPicker(itemID: itemID))
|
||||
} label: {
|
||||
TimelineReactionAddMoreButtonLabel()
|
||||
}
|
||||
.animation(.easeOut, value: collapsed)
|
||||
.reactionLayoutItem(.addMore)
|
||||
}
|
||||
.coordinateSpace(name: Self.flowCoordinateSpace)
|
||||
}
|
||||
@@ -54,15 +65,11 @@ struct TimelineReactionButtonLabel<Content: View>: View {
|
||||
@ViewBuilder var content: () -> Content
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 4) {
|
||||
content()
|
||||
}
|
||||
.padding(.vertical, 6)
|
||||
.padding(.horizontal, 8)
|
||||
.background(backgroundShape.inset(by: 1).fill(overlayBackgroundColor))
|
||||
.overlay(backgroundShape.inset(by: 2.0).strokeBorder(overlayBorderColor))
|
||||
.overlay(backgroundShape.strokeBorder(Color.compound.bgCanvasDefault, lineWidth: 2))
|
||||
.accessibilityElement(children: .combine)
|
||||
content()
|
||||
.background(backgroundShape.inset(by: 1).fill(overlayBackgroundColor))
|
||||
.overlay(backgroundShape.inset(by: 2.0).strokeBorder(overlayBorderColor))
|
||||
.overlay(backgroundShape.strokeBorder(Color.compound.bgCanvasDefault, lineWidth: 2))
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
|
||||
var backgroundShape: some InsettableShape {
|
||||
@@ -84,6 +91,8 @@ struct TimelineCollapseButtonLabel: View {
|
||||
var body: some View {
|
||||
TimelineReactionButtonLabel {
|
||||
Text(collapsed ? L10n.screenRoomReactionsShowMore : L10n.screenRoomReactionsShowLess)
|
||||
.padding(.vertical, 6)
|
||||
.padding(.horizontal, 8)
|
||||
.layoutPriority(1)
|
||||
.drawingGroup()
|
||||
.font(.compound.bodyMD)
|
||||
@@ -93,7 +102,6 @@ struct TimelineCollapseButtonLabel: View {
|
||||
}
|
||||
|
||||
struct TimelineReactionButton: View {
|
||||
let itemID: TimelineItemIdentifier
|
||||
let reaction: AggregatedReaction
|
||||
let toggleReaction: (String) -> Void
|
||||
let showReactionSummary: (String) -> Void
|
||||
@@ -110,13 +118,17 @@ struct TimelineReactionButton: View {
|
||||
|
||||
var label: some View {
|
||||
TimelineReactionButtonLabel(isHighlighted: reaction.isHighlighted) {
|
||||
Text(reaction.key)
|
||||
.font(.compound.bodyMD)
|
||||
if reaction.count > 1 {
|
||||
Text(String(reaction.count))
|
||||
HStack(spacing: 4) {
|
||||
Text(reaction.key)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundColor(textColor)
|
||||
if reaction.count > 1 {
|
||||
Text(String(reaction.count))
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 6)
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +137,22 @@ struct TimelineReactionButton: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct TimelineReactionAddMoreButtonLabel: View {
|
||||
@ScaledMetric private var addMoreButtonIconSize = 16
|
||||
|
||||
var body: some View {
|
||||
TimelineReactionButtonLabel {
|
||||
Image(asset: Asset.Images.timelineReactionAddMore)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TimelineReactionViewPreviewsContainer: View {
|
||||
@State private var collapseState1 = false
|
||||
@State private var collapseState2 = true
|
||||
|
||||
Reference in New Issue
Block a user