mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-12 23:31:02 +00:00
208 lines
10 KiB
Swift
208 lines
10 KiB
Swift
import SwiftUI
|
|
|
|
/// The travel group: who comes along, the latest messages in the trip chat
|
|
/// as bubbles, and the open poll with its votes as bars.
|
|
struct CollabContent: View {
|
|
let snapshot: WidgetSnapshot?
|
|
let family: TrekFamily
|
|
@Environment(\.colorScheme) private var scheme
|
|
|
|
var body: some View {
|
|
let collab = snapshot?.collab
|
|
let labels = snapshot?.labels
|
|
if let collab, let labels {
|
|
switch family {
|
|
case .small:
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
Text(labels.group ?? "").trekEyebrow(scheme)
|
|
AvatarStack(members: collab.members, size: 30, scheme: scheme).padding(.top, 8)
|
|
Spacer(minLength: 0)
|
|
if let poll = collab.poll {
|
|
Label(labels.poll ?? "", systemImage: "chart.bar.fill")
|
|
.font(.system(size: 10.5, weight: .heavy))
|
|
.foregroundStyle(Trek.info)
|
|
Text(poll.question).font(.system(size: 13.5, weight: .bold)).foregroundStyle(Trek.ink(scheme)).lineLimit(2)
|
|
} else if let last = collab.messages.last {
|
|
Text(last.author).font(.system(size: 10.5, weight: .heavy)).foregroundStyle(Trek.muted(scheme))
|
|
Text(last.text).font(.system(size: 13, weight: .semibold)).foregroundStyle(Trek.ink(scheme)).lineLimit(3)
|
|
} else {
|
|
Text(labels.noMessages ?? "").font(.system(size: 12.5, weight: .semibold)).foregroundStyle(Trek.muted(scheme))
|
|
}
|
|
}
|
|
case .medium:
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack {
|
|
Text(snapshot?.trip?.title ?? labels.group ?? "").trekEyebrow(scheme)
|
|
Spacer(minLength: 4)
|
|
AvatarStack(members: collab.members, size: 22, scheme: scheme)
|
|
}
|
|
bubbles(Array(collab.messages.suffix(2)), labels)
|
|
Spacer(minLength: 0)
|
|
}
|
|
case .large:
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
HStack(alignment: .center) {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(labels.group ?? "").trekEyebrow(scheme)
|
|
if let trip = snapshot?.trip {
|
|
Text(trip.title).font(.system(size: 16, weight: .heavy)).foregroundStyle(Trek.ink(scheme)).lineLimit(1)
|
|
}
|
|
}
|
|
Spacer(minLength: 4)
|
|
AvatarStack(members: collab.members, size: 28, scheme: scheme)
|
|
}
|
|
bubbles(Array(collab.messages.suffix(3)), labels)
|
|
Spacer(minLength: 0)
|
|
if let poll = collab.poll { PollCard(poll: poll, labels: labels, scheme: scheme) }
|
|
}
|
|
default:
|
|
accessory(collab, labels)
|
|
}
|
|
} else {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text(labels?.group ?? "").trekEyebrow(scheme)
|
|
Spacer(minLength: 0)
|
|
Image(systemName: "person.3.fill").font(.system(size: 28)).foregroundStyle(Trek.muted(scheme).opacity(0.5))
|
|
Text(labels?.noMessages ?? "").font(.system(size: 13, weight: .semibold)).foregroundStyle(Trek.muted(scheme))
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func bubbles(_ messages: [WidgetSnapshot.Collab.Message], _ labels: WidgetSnapshot.Labels) -> some View {
|
|
if messages.isEmpty {
|
|
Text(labels.noMessages ?? "").font(.system(size: 13, weight: .semibold)).foregroundStyle(Trek.muted(scheme))
|
|
} else {
|
|
VStack(spacing: 6) {
|
|
ForEach(Array(messages.enumerated()), id: \.offset) { _, message in
|
|
ChatBubble(message: message, scheme: scheme)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func accessory(_ collab: WidgetSnapshot.Collab, _ labels: WidgetSnapshot.Labels) -> some View {
|
|
if let last = collab.messages.last {
|
|
VStack(alignment: .leading, spacing: 1) {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "bubble.left.and.bubble.right.fill").font(.system(size: 11, weight: .bold))
|
|
Text(last.author).font(.system(size: 12, weight: .bold)).lineLimit(1)
|
|
}
|
|
Text(last.text).font(.system(size: 13, weight: .semibold)).lineLimit(2)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
} else {
|
|
Text(labels.noMessages ?? "").font(.system(size: 12, weight: .medium)).frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Trek {
|
|
/// A colour of a person's own, from their name.
|
|
static func personColor(_ name: String) -> Color {
|
|
let palette: [UInt32] = [0x4A7DDB, 0x2FA37A, 0xE8A13A, 0x8E6CD9, 0xD6408B, 0x2BA6A6, 0xD6673B, 0x5A6CDE]
|
|
return Color(hex: palette[Int(seed(name) % UInt64(palette.count))])
|
|
}
|
|
}
|
|
|
|
/// Overlapping initials in each person's colour, with a count for the rest.
|
|
struct AvatarStack: View {
|
|
let members: [WidgetSnapshot.Collab.Member]
|
|
let size: CGFloat
|
|
let scheme: ColorScheme
|
|
|
|
var body: some View {
|
|
HStack(spacing: -size * 0.18) {
|
|
ForEach(Array(members.prefix(4).enumerated()), id: \.offset) { _, member in
|
|
Circle()
|
|
.fill(LinearGradient(colors: [Trek.personColor(member.name), Trek.personColor(member.name).opacity(0.75)], startPoint: .top, endPoint: .bottom))
|
|
.frame(width: size, height: size)
|
|
.overlay(Text(member.initials).font(.system(size: size * 0.34, weight: .heavy)).minimumScaleFactor(0.7).lineLimit(1).foregroundStyle(.white).padding(.horizontal, size * 0.1))
|
|
.overlay(Circle().stroke(scheme == .dark ? Color(hex: 0x111115) : Color(hex: 0xF4F2EE), lineWidth: 2))
|
|
}
|
|
if members.count > 4 {
|
|
Circle()
|
|
.fill(Trek.card(scheme))
|
|
.frame(width: size, height: size)
|
|
.overlay(Text("+\(members.count - 4)").font(.system(size: size * 0.34, weight: .heavy)).foregroundStyle(Trek.muted(scheme)))
|
|
.overlay(Circle().stroke(scheme == .dark ? Color(hex: 0x111115) : Color(hex: 0xF4F2EE), lineWidth: 2))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A chat message: the others on the left with their initials, mine on the right in ink.
|
|
struct ChatBubble: View {
|
|
let message: WidgetSnapshot.Collab.Message
|
|
let scheme: ColorScheme
|
|
|
|
var body: some View {
|
|
HStack(alignment: .bottom, spacing: 6) {
|
|
if message.mine { Spacer(minLength: 36) }
|
|
if !message.mine {
|
|
Circle()
|
|
.fill(Trek.personColor(message.author))
|
|
.frame(width: 20, height: 20)
|
|
.overlay(Text(message.initials).font(.system(size: 8, weight: .heavy)).foregroundStyle(.white))
|
|
}
|
|
Text(message.text)
|
|
.font(.system(size: 12.5, weight: .medium))
|
|
.foregroundStyle(message.mine ? (scheme == .dark ? Color(hex: 0x101013) : .white) : Trek.ink(scheme))
|
|
.lineLimit(2)
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 6)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
.fill(message.mine ? Trek.ink(scheme) : Trek.card(scheme))
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
.stroke(message.mine ? Color.clear : Trek.cardBorder(scheme), lineWidth: 0.7)
|
|
)
|
|
if !message.mine { Spacer(minLength: 36) }
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The open poll: the question and each option as a bar of its votes.
|
|
struct PollCard: View {
|
|
let poll: WidgetSnapshot.Collab.Poll
|
|
let labels: WidgetSnapshot.Labels
|
|
let scheme: ColorScheme
|
|
|
|
var body: some View {
|
|
let most = max(poll.options.map(\.votes).max() ?? 1, 1)
|
|
VStack(alignment: .leading, spacing: 7) {
|
|
HStack(spacing: 5) {
|
|
Image(systemName: "chart.bar.fill").font(.system(size: 10, weight: .bold)).foregroundStyle(Trek.info)
|
|
Text(labels.poll ?? "").font(.system(size: 10.5, weight: .heavy)).textCase(.uppercase).foregroundStyle(Trek.info)
|
|
Spacer(minLength: 0)
|
|
Text("\(poll.voters) \(labels.votes ?? "")").font(.system(size: 10.5, weight: .bold)).foregroundStyle(Trek.muted(scheme))
|
|
}
|
|
Text(poll.question).font(.system(size: 13.5, weight: .bold)).foregroundStyle(Trek.ink(scheme)).lineLimit(1)
|
|
ForEach(Array(poll.options.prefix(3).enumerated()), id: \.offset) { _, option in
|
|
let leading = option.votes == most && option.votes > 0
|
|
ZStack(alignment: .leading) {
|
|
GeometryReader { geo in
|
|
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
.fill(leading ? Trek.info.opacity(scheme == .dark ? 0.35 : 0.22) : Trek.track(scheme))
|
|
.frame(width: max(8, geo.size.width * CGFloat(option.votes) / CGFloat(most)))
|
|
}
|
|
HStack {
|
|
Text(option.label).font(.system(size: 12, weight: leading ? .heavy : .semibold)).foregroundStyle(Trek.ink(scheme)).lineLimit(1)
|
|
Spacer(minLength: 4)
|
|
Text("\(option.votes)").font(Trek.number(12, .heavy)).foregroundStyle(Trek.muted(scheme))
|
|
}
|
|
.padding(.horizontal, 9)
|
|
}
|
|
.frame(height: 24)
|
|
}
|
|
}
|
|
.padding(10)
|
|
.background(RoundedRectangle(cornerRadius: 15, style: .continuous).fill(Trek.card(scheme)))
|
|
.overlay(RoundedRectangle(cornerRadius: 15, style: .continuous).stroke(Trek.cardBorder(scheme), lineWidth: 0.7))
|
|
}
|
|
}
|