gitbay/Views/Shared/ReactionBar.swift

v1.14.0
gitbay-ios/gitbay/Views/Shared/ReactionBar.swift history · blame · raw

77 lines · 3448 bytes

 1import SwiftUI
 2
 3/// The reactions on an issue, a merge request or a comment: one chip per
 4/// reaction given, tinted when it is the account's own, and a menu of the
 5/// whole vocabulary. Tapping a chip toggles the account's reaction; the
 6/// command decides, and the screen reloads from what it says.
 7struct ReactionBar: View {
 8
 9    let reactions: [ReactionCount]
10    let disabled: Bool
11    /// Names the bar's controls: `<idPrefix>-<reaction>` and `<idPrefix>-add`.
12    let idPrefix: String
13    let react: (String) -> Void
14
15    var body: some View {
16        HStack(spacing: 6) {
17            ForEach(reactions) { given in
18                Button {
19                    react(given.reaction)
20                } label: {
21                    HStack(spacing: 3) {
22                        Text(Reaction.emoji(for: given.reaction))
23                        Text("\(given.count)")
24                            .monospacedDigit()
25                            .foregroundStyle(given.me ? Color.gbAccent : .secondary)
26                    }
27                    .font(.gbSans(.caption))
28                    .padding(.horizontal, 7)
29                    .padding(.vertical, 3)
30                    .background((given.me ? Color.gbAccent : .secondary).opacity(0.07), in: gbChipShape)
31                    .overlay(gbChipShape.stroke((given.me ? Color.gbAccent : .secondary).opacity(0.35), lineWidth: 1))
32                    .frame(minHeight: 44)
33                    .contentShape(Rectangle())
34                }
35                .buttonStyle(.borderless)
36                .disabled(disabled)
37                .accessibilityLabel(label(for: given))
38                .accessibilityIdentifier("\(idPrefix)-\(given.reaction)")
39            }
40            Menu {
41                ForEach(Reaction.all) { reaction in
42                    Button {
43                        react(reaction.name)
44                    } label: {
45                        // The account's own reaction carries the menu's checkmark;
46                        // picking it again takes it back.
47                        if Reaction.remove(reaction.name, given: reactions) {
48                            Label("\(reaction.emoji)  \(reaction.name)", systemImage: "checkmark")
49                        } else {
50                            Text("\(reaction.emoji)  \(reaction.name)")
51                        }
52                    }
53                    .accessibilityLabel(Reaction.spoken(for: reaction.name))
54                    .accessibilityAddTraits(Reaction.remove(reaction.name, given: reactions) ? .isSelected : [])
55                }
56            } label: {
57                Image(systemName: "face.smiling")
58                    .foregroundStyle(.secondary)
59                    .frame(minWidth: 44, minHeight: 44)
60                    .contentShape(Rectangle())
61            }
62            .menuStyle(.borderlessButton)
63            // A menu in a list row takes the accent; the icon is a quiet
64            // affordance beside the chips, not a link.
65            .tint(.secondary)
66            .disabled(disabled)
67            .accessibilityLabel("Add reaction")
68            .accessibilityIdentifier("\(idPrefix)-add")
69            Spacer(minLength: 0)
70        }
71    }
72
73    private func label(for given: ReactionCount) -> String {
74        let count = given.count == 1 ? "1 reaction" : "\(given.count) reactions"
75        return "\(Reaction.spoken(for: given.reaction)), \(count)\(given.me ? ", including yours" : "")"
76    }
77}