gitbay/Views/Shared/ReactionBar.swift
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}