eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1import SwiftUI
2
3// Presence, shared by iOS + Mac. The pure rules mirror web/static/ui.js
4// (presenceLabel / onlineFilterKeeps / chatCategory) so a member's status
5// MEANS the same everywhere. Presentation splits by platform: web + Mac show
6// the dot + word status line (there's room); iOS/mobile shows online as a
7// green ring on the DM header avatar and nothing at all for away/offline
8// (see ios/LChat/Views/Avatar.swift `onlineRing`).
9
10/// ui.js presenceLabel: "Online" / "Away" / "Offline"; nil when the state is
11/// unknown (a keyless invitee, a server without presence yet) — show nothing.
12public func presenceText(_ presence: String?) -> String? {
13 switch presence {
14 case "online": return "Online"
15 case "away": return "Away"
16 case "offline": return "Offline"
17 default: return nil
18 }
19}
20
21/// Roster facts about one member of a chat (the viewer excluded) — the input
22/// to the filter rules below. Built by ThreadStore.memberPresence(of:).
23public struct MemberPresence {
24 public let agent: Bool
25 public let presence: String?
26 public init(agent: Bool, presence: String?) {
27 self.agent = agent
28 self.presence = presence
29 }
30}
31
32/// ui.js onlineFilterKeeps: whether "Online only" keeps a chat. People decide
33/// in any room with people (an always-on bot must not make every mixed room
34/// read active, and Away doesn't count — the filter answers "who can I reach
35/// right now"); a chat with only bots falls back to the bots' own status,
36/// i.e. is the sidecar up. No other members (Notes, apps) → dropped.
37public func onlineFilterKeeps(_ others: [MemberPresence]) -> Bool {
38 let people = others.filter { !$0.agent }
39 if !people.isEmpty { return people.contains { $0.presence == "online" } }
40 return others.contains { $0.agent && $0.presence == "online" }
41}
42
43/// ui.js chatCategory: which filter-menu checkbox governs a chat — apps
44/// (mini-app threads), bots (every other member is an agent), else people
45/// (anything with another person in it, plus your own Notes).
46public enum ChatFilterCategory {
47 case people, bots, apps
48}
49
50public func chatCategory(isApp: Bool, others: [MemberPresence]) -> ChatFilterCategory {
51 if isApp { return .apps }
52 if !others.isEmpty, others.allSatisfy({ $0.agent }) { return .bots }
53 return .people
54}
55
56/// The inline chip's words for an active funnel filter — the plain-language
57/// answer to "why is a chat missing from my list?". Reads like a person would
58/// say it ("Online only", "Hiding bots & apps", "Online only · hiding apps"),
59/// not a flag dump; nil when nothing is filtering. Shared so the iOS and Mac
60/// chips can't drift on copy.
61public func chatFilterSummary(onlineOnly: Bool, people: Bool, bots: Bool, apps: Bool) -> String? {
62 var hidden: [String] = []
63 if !people { hidden.append("people") }
64 if !bots { hidden.append("bots") }
65 if !apps { hidden.append("apps") }
66 // "a, b & c" — the & keeps the last pair conversational at any length.
67 let list = hidden.count > 1
68 ? hidden.dropLast().joined(separator: ", ") + " & " + hidden[hidden.count - 1]
69 : hidden.first
70 switch (onlineOnly, list) {
71 case (true, nil): return "Online only"
72 case (true, let l?): return "Online only · hiding \(l)"
73 case (false, let l?): return "Hiding \(l)"
74 case (false, nil): return nil
75 }
76}
77
78/// The status line under a name: a coloured dot + the word, the native mirror
79/// of the web's .presence-line (green online / yellow away / red offline).
80/// Renders nothing when the state is unknown. Text font/colour follow the
81/// caller's modifiers; only the dot's colour is fixed. Web + Mac only —
82/// iOS/mobile has no room for it, so there online is a green ring on the DM
83/// header avatar and away/offline show nothing.
84public struct PresenceStatusLine: View {
85 let state: String?
86
87 public init(_ state: String?) {
88 self.state = state
89 }
90
91 public var body: some View {
92 if let label = presenceText(state) {
93 HStack(spacing: 5) {
94 Circle().fill(dotColor).frame(width: 7, height: 7)
95 Text(label)
96 }
97 .accessibilityElement(children: .combine)
98 }
99 }
100
101 private var dotColor: Color {
102 switch state {
103 case "online": Color(red: 0x34 / 255, green: 0xC7 / 255, blue: 0x59 / 255)
104 case "away": Color(red: 0xF5 / 255, green: 0xB4 / 255, blue: 0x00 / 255)
105 default: Color(red: 0xFF / 255, green: 0x3B / 255, blue: 0x30 / 255)
106 }
107 }
108}
109