mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 03:05:19 +00:00
Small encrypted crews (hard cap 16) between public broadcast and 1:1 DMs: Protocol - MessageType.groupMessage = 0x25: broadcast packets with a cleartext 16-byte group ID + epoch, ChaCha20-Poly1305 ciphertext (epoch bound as AEAD AAD), inner Ed25519 sender signature over "bitchat-group-msg-v1"|groupID|messageID|timestamp|content - NoisePayloadType.groupInvite = 0x06 / .groupKeyUpdate = 0x07: creator-signed group state (key, epoch, roster) 1:1 over Noise; signature over "bitchat-group-v1"|groupID|epoch|key-hash|roster-hash and the Noise session peer must BE the creator - SyncTypeFlags bit 10 (groupMessage): variable-length LE bitfield widens 1 -> 2 bytes inside the length-prefixed REQUEST_SYNC TLV; old clients ignore unknown bits and answer with types they know - PeerCapabilities.localSupported now advertises .groups Storage - GroupStore: symmetric keys in the keychain, roster/name/epoch as protected JSON in Application Support; wiped in panicClearAllData() Behavior - Non-members relay 0x25 like any broadcast but cannot read it; group messages join gossip-sync backfill with the public-message window - Receivers drop wrong-epoch envelopes, bad sender signatures, and senders missing from the creator-signed roster - Fire-and-flood delivery (no per-member acks in v1) UI - Groups open as chat windows through the private-chat sheet (virtual "group_" peer IDs); groups section in the people sheet; /group create/invite/remove/leave/list commands; invitees get a system message + notification and the group appears in their people sheet Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
87 lines
3.9 KiB
Swift
87 lines
3.9 KiB
Swift
import BitFoundation
|
|
import SwiftUI
|
|
|
|
/// Compact "groups" section for the people sheet: one row per private group
|
|
/// this device belongs to, tappable to open the group chat window.
|
|
struct GroupChatList: View {
|
|
@ThemedPalette private var palette
|
|
|
|
let groups: [GroupChatRow]
|
|
let onTapGroup: (PeerID) -> Void
|
|
|
|
private enum Strings {
|
|
static let header = String(localized: "groups.section.header", comment: "Section header above the private groups list")
|
|
static let creator = String(localized: "groups.state.creator", comment: "State label for a group the user created")
|
|
static let unread = String(localized: "mesh_peers.state.unread", comment: "State label for a peer with unread private messages")
|
|
static let newMessagesTooltip = String(localized: "mesh_peers.tooltip.new_messages", comment: "Tooltip for the unread messages indicator")
|
|
static let openGroupHint = String(localized: "groups.accessibility.open_group_hint", comment: "Accessibility hint on a group row explaining activation opens the group chat")
|
|
static let memberCountFormat = String(localized: "groups.member_count %@", comment: "Member count shown next to a group name; placeholder is the count")
|
|
}
|
|
|
|
var body: some View {
|
|
if !groups.isEmpty {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
Text(Strings.header)
|
|
.bitchatFont(size: 11, weight: .medium)
|
|
.foregroundColor(palette.secondary)
|
|
.padding(.horizontal)
|
|
.padding(.top, 10)
|
|
.padding(.bottom, 2)
|
|
.accessibilityAddTraits(.isHeader)
|
|
|
|
ForEach(groups) { group in
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "person.3.fill")
|
|
.font(.bitchatSystem(size: 10))
|
|
.foregroundColor(palette.primary)
|
|
|
|
Text("#\(group.name)")
|
|
.bitchatFont(size: 14)
|
|
.foregroundColor(palette.primary)
|
|
.lineLimit(1)
|
|
.truncationMode(.tail)
|
|
|
|
Text(String(format: Strings.memberCountFormat, locale: .current, "\(group.memberCount)"))
|
|
.bitchatFont(size: 12)
|
|
.foregroundColor(palette.secondary)
|
|
|
|
if group.isCreator {
|
|
Image(systemName: "crown.fill")
|
|
.font(.bitchatSystem(size: 9))
|
|
.foregroundColor(.yellow)
|
|
.help(Strings.creator)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
if group.hasUnread {
|
|
Image(systemName: "envelope.fill")
|
|
.font(.bitchatSystem(size: 10))
|
|
.foregroundColor(.orange)
|
|
.help(Strings.newMessagesTooltip)
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.vertical, 6)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture { onTapGroup(group.peerID) }
|
|
.accessibilityElement(children: .ignore)
|
|
.accessibilityLabel(accessibilityDescription(for: group))
|
|
.accessibilityAddTraits(.isButton)
|
|
.accessibilityHint(Strings.openGroupHint)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func accessibilityDescription(for group: GroupChatRow) -> String {
|
|
var parts: [String] = [
|
|
group.name,
|
|
String(format: Strings.memberCountFormat, locale: .current, "\(group.memberCount)")
|
|
]
|
|
if group.isCreator { parts.append(Strings.creator) }
|
|
if group.hasUnread { parts.append(Strings.unread) }
|
|
return parts.joined(separator: ", ")
|
|
}
|
|
}
|