mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 15:05:20 +00:00
Feature/peer colors (#476)
* Feature: assign stable per-peer colors (non-self) across mesh/geohash/DM; keep self orange and mentions-to-me orange; cache colors * Fix compile warnings: remove unused primaryColor in formatMessageAsText; remove unused CryptoKit import; mark peerColor/formatMessageAsText @MainActor to call main-actor helpers * Fix: re-import CryptoKit for SHA256 usage in ChatViewModel * Peer lists: apply same per-peer colors as chat (self orange); suffix uses lighter variant; geohash uses Nostr pubkey, mesh uses Noise key * Fix warning: remove unused peerNicknames in MeshPeerList * UI: add 'mention' action in message actions to prefill input with @nick#abcd and focus input * UX: long-press a message to prefill @mention with sender's full nick#abcd and focus input * UX: remove long-press mention; add context menu 'Copy message' that copies only the message body (no nick/timestamp) * Geo teleported: robust tag detection (accept 't', 'teleport', and boolean-like values); mark self on send when tagging; should fix peer list icons * Logs: add GeoTeleport diagnostics (incoming tags, self/peer marking, counts) to debug peer list dashed icon behavior * Teleport persistence: store per-geohash teleported state in UserDefaults; initialize on startup; OR with location-derived status; sheet writes persisted flag on select/teleport --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
@@ -21,6 +21,7 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa
|
|||||||
private var lastLocation: CLLocation?
|
private var lastLocation: CLLocation?
|
||||||
private var refreshTimer: Timer?
|
private var refreshTimer: Timer?
|
||||||
private let userDefaultsKey = "locationChannel.selected"
|
private let userDefaultsKey = "locationChannel.selected"
|
||||||
|
private let teleportedStoreKey = "locationChannel.teleportedSet"
|
||||||
private var isGeocoding: Bool = false
|
private var isGeocoding: Bool = false
|
||||||
|
|
||||||
// Published state for UI bindings
|
// Published state for UI bindings
|
||||||
@@ -31,6 +32,9 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa
|
|||||||
@Published var teleported: Bool = false
|
@Published var teleported: Bool = false
|
||||||
@Published private(set) var locationNames: [GeohashChannelLevel: String] = [:]
|
@Published private(set) var locationNames: [GeohashChannelLevel: String] = [:]
|
||||||
|
|
||||||
|
// Persisted set of geohashes that were selected via teleport
|
||||||
|
private var teleportedSet: Set<String> = []
|
||||||
|
|
||||||
private override init() {
|
private override init() {
|
||||||
super.init()
|
super.init()
|
||||||
cl.delegate = self
|
cl.delegate = self
|
||||||
@@ -41,6 +45,15 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa
|
|||||||
let channel = try? JSONDecoder().decode(ChannelID.self, from: data) {
|
let channel = try? JSONDecoder().decode(ChannelID.self, from: data) {
|
||||||
selectedChannel = channel
|
selectedChannel = channel
|
||||||
}
|
}
|
||||||
|
// Load persisted teleported set
|
||||||
|
if let data = UserDefaults.standard.data(forKey: teleportedStoreKey),
|
||||||
|
let arr = try? JSONDecoder().decode([String].self, from: data) {
|
||||||
|
teleportedSet = Set(arr)
|
||||||
|
}
|
||||||
|
// Initialize teleported flag from persisted state if a location channel is selected
|
||||||
|
if case .location(let ch) = selectedChannel {
|
||||||
|
teleported = teleportedSet.contains(ch.geohash)
|
||||||
|
}
|
||||||
let status: CLAuthorizationStatus
|
let status: CLAuthorizationStatus
|
||||||
if #available(iOS 14.0, *) {
|
if #available(iOS 14.0, *) {
|
||||||
status = cl.authorizationStatus
|
status = cl.authorizationStatus
|
||||||
@@ -104,6 +117,24 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa
|
|||||||
if let data = try? JSONEncoder().encode(channel) {
|
if let data = try? JSONEncoder().encode(channel) {
|
||||||
UserDefaults.standard.set(data, forKey: self.userDefaultsKey)
|
UserDefaults.standard.set(data, forKey: self.userDefaultsKey)
|
||||||
}
|
}
|
||||||
|
// Update teleported flag based on persisted state for immediate UI behavior
|
||||||
|
switch channel {
|
||||||
|
case .mesh:
|
||||||
|
self.teleported = false
|
||||||
|
case .location(let ch):
|
||||||
|
self.teleported = self.teleportedSet.contains(ch.geohash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark or unmark a geohash as teleported in persistence and update current flag if relevant
|
||||||
|
func markTeleported(for geohash: String, _ flag: Bool) {
|
||||||
|
if flag { teleportedSet.insert(geohash) } else { teleportedSet.remove(geohash) }
|
||||||
|
if let data = try? JSONEncoder().encode(Array(teleportedSet)) {
|
||||||
|
UserDefaults.standard.set(data, forKey: teleportedStoreKey)
|
||||||
|
}
|
||||||
|
if case .location(let ch) = selectedChannel, ch.geohash == geohash {
|
||||||
|
Task { @MainActor in self.teleported = flag }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,13 +195,14 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa
|
|||||||
}
|
}
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self.availableChannels = result
|
self.availableChannels = result
|
||||||
// Recompute teleported status based on current location vs selected channel
|
// Recompute teleported status based on persisted state OR current location vs selected channel
|
||||||
switch self.selectedChannel {
|
switch self.selectedChannel {
|
||||||
case .mesh:
|
case .mesh:
|
||||||
self.teleported = false
|
self.teleported = false
|
||||||
case .location(let ch):
|
case .location(let ch):
|
||||||
|
let persisted = self.teleportedSet.contains(ch.geohash)
|
||||||
let currentGH = Geohash.encode(latitude: coord.latitude, longitude: coord.longitude, precision: ch.level.precision)
|
let currentGH = Geohash.encode(latitude: coord.latitude, longitude: coord.longitude, precision: ch.level.precision)
|
||||||
self.teleported = (currentGH != ch.geohash)
|
self.teleported = persisted || (currentGH != ch.geohash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,8 +79,8 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Combine
|
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
import Combine
|
||||||
import CommonCrypto
|
import CommonCrypto
|
||||||
import CoreBluetooth
|
import CoreBluetooth
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@@ -1126,6 +1126,15 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
NostrRelayManager.shared.sendEvent(event)
|
NostrRelayManager.shared.sendEvent(event)
|
||||||
// Track ourselves as active participant
|
// Track ourselves as active participant
|
||||||
self.recordGeoParticipant(pubkeyHex: identity.publicKeyHex)
|
self.recordGeoParticipant(pubkeyHex: identity.publicKeyHex)
|
||||||
|
SecureLogger.log("GeoTeleport: sent geo message pub=\(identity.publicKeyHex.prefix(8))… teleported=\(LocationChannelManager.shared.teleported)",
|
||||||
|
category: SecureLogger.session, level: .debug)
|
||||||
|
// If we tagged this as teleported, also mark our pubkey in teleportedGeo for UI
|
||||||
|
if LocationChannelManager.shared.teleported {
|
||||||
|
let key = identity.publicKeyHex.lowercased()
|
||||||
|
self.teleportedGeo = self.teleportedGeo.union([key])
|
||||||
|
SecureLogger.log("GeoTeleport: mark self teleported key=\(key.prefix(8))… total=\(self.teleportedGeo.count)",
|
||||||
|
category: SecureLogger.session, level: .info)
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.log("❌ Failed to send geohash message: \(error)", category: SecureLogger.session, level: .error)
|
SecureLogger.log("❌ Failed to send geohash message: \(error)", category: SecureLogger.session, level: .error)
|
||||||
self.addSystemMessage("failed to send to location channel")
|
self.addSystemMessage("failed to send to location channel")
|
||||||
@@ -1181,6 +1190,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if LocationChannelManager.shared.teleported {
|
if LocationChannelManager.shared.teleported {
|
||||||
let key = id.publicKeyHex.lowercased()
|
let key = id.publicKeyHex.lowercased()
|
||||||
teleportedGeo = teleportedGeo.union([key])
|
teleportedGeo = teleportedGeo.union([key])
|
||||||
|
SecureLogger.log("GeoTeleport: channel switch mark self teleported key=\(key.prefix(8))… total=\(teleportedGeo.count)",
|
||||||
|
category: SecureLogger.session, level: .info)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -1195,11 +1206,30 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Deduplicate
|
// Deduplicate
|
||||||
if self.processedNostrEvents.contains(event.id) { return }
|
if self.processedNostrEvents.contains(event.id) { return }
|
||||||
self.recordProcessedEvent(event.id)
|
self.recordProcessedEvent(event.id)
|
||||||
|
// Log incoming tags for diagnostics
|
||||||
|
let tagSummary = event.tags.map { "[" + $0.joined(separator: ",") + "]" }.joined(separator: ",")
|
||||||
|
SecureLogger.log("GeoTeleport: recv pub=\(event.pubkey.prefix(8))… tags=\(tagSummary)",
|
||||||
|
category: SecureLogger.session, level: .debug)
|
||||||
// Track teleport tag for participants
|
// Track teleport tag for participants
|
||||||
if let teleTag = event.tags.first(where: { $0.first == "t" }), teleTag.count >= 2, teleTag[1] == "teleport" {
|
// Detect teleport tag robustly: accept ["t","teleport"], ["t"], ["teleport"], or boolean-like values
|
||||||
|
let hasTeleportTag: Bool = {
|
||||||
|
for tag in event.tags {
|
||||||
|
guard let key = tag.first?.lowercased() else { continue }
|
||||||
|
if key == "t" || key == "teleport" { return true }
|
||||||
|
// Some clients may encode as ["t","1"] or ["t","true"]
|
||||||
|
if key == "t", tag.dropFirst().contains(where: { v in
|
||||||
|
let lv = v.lowercased()
|
||||||
|
return lv == "teleport" || lv == "1" || lv == "true"
|
||||||
|
}) { return true }
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}()
|
||||||
|
if hasTeleportTag {
|
||||||
let key = event.pubkey.lowercased()
|
let key = event.pubkey.lowercased()
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self.teleportedGeo = self.teleportedGeo.union([key])
|
self.teleportedGeo = self.teleportedGeo.union([key])
|
||||||
|
SecureLogger.log("GeoTeleport: mark peer teleported key=\(key.prefix(8))… total=\(self.teleportedGeo.count)",
|
||||||
|
category: SecureLogger.session, level: .info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Skip our own events (we already locally echoed)
|
// Skip our own events (we already locally echoed)
|
||||||
@@ -2679,6 +2709,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
return processedContent
|
return processedContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func formatMessageAsText(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
|
func formatMessageAsText(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
|
||||||
// Determine if this message was sent by self (mesh, geo, or DM)
|
// Determine if this message was sent by self (mesh, geo, or DM)
|
||||||
let isSelf: Bool = {
|
let isSelf: Bool = {
|
||||||
@@ -2706,8 +2737,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Not cached, format the message
|
// Not cached, format the message
|
||||||
var result = AttributedString()
|
var result = AttributedString()
|
||||||
|
|
||||||
let primaryColor = isDark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
let baseColor: Color = isSelf ? .orange : peerColor(for: message, isDark: isDark)
|
||||||
let baseColor: Color = isSelf ? .orange : primaryColor
|
|
||||||
|
|
||||||
if message.sender != "system" {
|
if message.sender != "system" {
|
||||||
// Sender (at the beginning) with light-gray suffix styling if present
|
// Sender (at the beginning) with light-gray suffix styling if present
|
||||||
@@ -2812,7 +2842,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}()
|
}()
|
||||||
var mentionStyle = AttributeContainer()
|
var mentionStyle = AttributeContainer()
|
||||||
mentionStyle.font = .system(size: 14, weight: isSelf ? .bold : .semibold, design: .monospaced)
|
mentionStyle.font = .system(size: 14, weight: isSelf ? .bold : .semibold, design: .monospaced)
|
||||||
let mentionColor: Color = isMentionToMe ? .orange : primaryColor
|
let mentionColor: Color = isMentionToMe ? .orange : baseColor
|
||||||
mentionStyle.foregroundColor = mentionColor
|
mentionStyle.foregroundColor = mentionColor
|
||||||
// Emit '@'
|
// Emit '@'
|
||||||
result.append(AttributedString("@").mergingAttributes(mentionStyle))
|
result.append(AttributedString("@").mergingAttributes(mentionStyle))
|
||||||
@@ -3135,6 +3165,62 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Per-Peer Colors
|
||||||
|
private var peerColorCache: [String: Color] = [:]
|
||||||
|
|
||||||
|
private func djb2(_ s: String) -> UInt64 {
|
||||||
|
var hash: UInt64 = 5381
|
||||||
|
for b in s.utf8 { hash = ((hash << 5) &+ hash) &+ UInt64(b) }
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func colorForPeerSeed(_ seed: String, isDark: Bool) -> Color {
|
||||||
|
if let cached = peerColorCache[seed] { return cached }
|
||||||
|
var hue = Double(djb2(seed) % 360) / 360.0
|
||||||
|
// Avoid orange (~30°) reserved for self
|
||||||
|
let orange = 30.0 / 360.0
|
||||||
|
if abs(hue - orange) < 0.05 { hue = fmod(hue + 0.12, 1.0) }
|
||||||
|
let saturation: Double = isDark ? 0.80 : 0.70
|
||||||
|
let brightness: Double = isDark ? 0.75 : 0.45
|
||||||
|
let c = Color(hue: hue, saturation: saturation, brightness: brightness)
|
||||||
|
peerColorCache[seed] = c
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func peerColor(for message: BitchatMessage, isDark: Bool) -> Color {
|
||||||
|
var seed: String
|
||||||
|
if let spid = message.senderPeerID {
|
||||||
|
if spid.hasPrefix("nostr:") || spid.hasPrefix("nostr_") {
|
||||||
|
let full = nostrKeyMapping[spid]?.lowercased() ?? spid.lowercased()
|
||||||
|
seed = "nostr:" + full
|
||||||
|
} else if spid.count == 16, let full = getNoiseKeyForShortID(spid)?.lowercased() {
|
||||||
|
seed = "noise:" + full
|
||||||
|
} else {
|
||||||
|
seed = spid.lowercased()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
seed = message.sender.lowercased()
|
||||||
|
}
|
||||||
|
return colorForPeerSeed(seed, isDark: isDark)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public helpers for views to color peers consistently in lists
|
||||||
|
@MainActor
|
||||||
|
func colorForNostrPubkey(_ pubkeyHexLowercased: String, isDark: Bool) -> Color {
|
||||||
|
return colorForPeerSeed("nostr:" + pubkeyHexLowercased.lowercased(), isDark: isDark)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func colorForMeshPeer(id peerID: String, isDark: Bool) -> Color {
|
||||||
|
if let peer = unifiedPeerService.getPeer(by: peerID) {
|
||||||
|
let full = peer.noisePublicKey.hexEncodedString().lowercased()
|
||||||
|
return colorForPeerSeed("noise:" + full, isDark: isDark)
|
||||||
|
}
|
||||||
|
return colorForPeerSeed(peerID.lowercased(), isDark: isDark)
|
||||||
|
}
|
||||||
|
|
||||||
private func trimMeshTimelineIfNeeded() {
|
private func trimMeshTimelineIfNeeded() {
|
||||||
if meshTimeline.count > meshTimelineCap {
|
if meshTimeline.count > meshTimelineCap {
|
||||||
let removeCount = meshTimeline.count - meshTimelineCap
|
let removeCount = meshTimeline.count - meshTimelineCap
|
||||||
|
|||||||
@@ -204,6 +204,14 @@ struct ContentView: View {
|
|||||||
isPresented: $showMessageActions,
|
isPresented: $showMessageActions,
|
||||||
titleVisibility: .visible
|
titleVisibility: .visible
|
||||||
) {
|
) {
|
||||||
|
Button("mention") {
|
||||||
|
if let sender = selectedMessageSender {
|
||||||
|
// Pre-fill the input with an @mention and focus the field
|
||||||
|
messageText = "@\(sender) "
|
||||||
|
isTextFieldFocused = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Button("private message") {
|
Button("private message") {
|
||||||
if let peerID = selectedMessageSenderID {
|
if let peerID = selectedMessageSenderID {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@@ -346,6 +354,17 @@ struct ContentView: View {
|
|||||||
showMessageActions = true
|
showMessageActions = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.contextMenu {
|
||||||
|
Button("Copy message") {
|
||||||
|
#if os(iOS)
|
||||||
|
UIPasteboard.general.string = message.content
|
||||||
|
#else
|
||||||
|
let pb = NSPasteboard.general
|
||||||
|
pb.clearContents()
|
||||||
|
pb.setString(message.content, forType: .string)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ struct GeohashPeopleList: View {
|
|||||||
let textColor: Color
|
let textColor: Color
|
||||||
let secondaryTextColor: Color
|
let secondaryTextColor: Color
|
||||||
let onTapPerson: () -> Void
|
let onTapPerson: () -> Void
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
@@ -51,14 +52,15 @@ struct GeohashPeopleList: View {
|
|||||||
}
|
}
|
||||||
let (base, suffix) = splitSuffix(from: person.displayName)
|
let (base, suffix) = splitSuffix(from: person.displayName)
|
||||||
let isMe = person.id == myHex
|
let isMe = person.id == myHex
|
||||||
|
let assignedColor = viewModel.colorForNostrPubkey(person.id, isDark: colorScheme == .dark)
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
let rowColor: Color = isMe ? .orange : textColor
|
let rowColor: Color = isMe ? .orange : assignedColor
|
||||||
Text(base)
|
Text(base)
|
||||||
.font(.system(size: 14, design: .monospaced))
|
.font(.system(size: 14, design: .monospaced))
|
||||||
.fontWeight(isMe ? .bold : .regular)
|
.fontWeight(isMe ? .bold : .regular)
|
||||||
.foregroundColor(rowColor)
|
.foregroundColor(rowColor)
|
||||||
if !suffix.isEmpty {
|
if !suffix.isEmpty {
|
||||||
let suffixColor = isMe ? Color.orange.opacity(0.6) : textColor.opacity(0.6)
|
let suffixColor = isMe ? Color.orange.opacity(0.6) : rowColor.opacity(0.6)
|
||||||
Text(suffix)
|
Text(suffix)
|
||||||
.font(.system(size: 14, design: .monospaced))
|
.font(.system(size: 14, design: .monospaced))
|
||||||
.foregroundColor(suffixColor)
|
.foregroundColor(suffixColor)
|
||||||
|
|||||||
@@ -107,8 +107,8 @@ struct LocationChannelsSheet: View {
|
|||||||
let subtitlePrefix = "#\(channel.geohash) • \(coverage)"
|
let subtitlePrefix = "#\(channel.geohash) • \(coverage)"
|
||||||
let highlight = viewModel.geohashParticipantCount(for: channel.geohash) > 0
|
let highlight = viewModel.geohashParticipantCount(for: channel.geohash) > 0
|
||||||
channelRow(title: geohashTitleWithCount(for: channel), subtitlePrefix: subtitlePrefix, subtitleName: namePart, isSelected: isSelected(channel), titleBold: highlight) {
|
channelRow(title: geohashTitleWithCount(for: channel), subtitlePrefix: subtitlePrefix, subtitleName: namePart, isSelected: isSelected(channel), titleBold: highlight) {
|
||||||
// Selecting a suggested nearby channel is not a teleport
|
// Selecting a suggested nearby channel is not a teleport. Persist this.
|
||||||
manager.teleported = false
|
manager.markTeleported(for: channel.geohash, false)
|
||||||
manager.select(ChannelID.location(channel))
|
manager.select(ChannelID.location(channel))
|
||||||
isPresented = false
|
isPresented = false
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ struct LocationChannelsSheet: View {
|
|||||||
let level = levelForLength(gh.count)
|
let level = levelForLength(gh.count)
|
||||||
let ch = GeohashChannel(level: level, geohash: gh)
|
let ch = GeohashChannel(level: level, geohash: gh)
|
||||||
// Mark this selection as a manual teleport
|
// Mark this selection as a manual teleport
|
||||||
manager.teleported = true
|
manager.markTeleported(for: ch.geohash, true)
|
||||||
manager.select(ChannelID.location(ch))
|
manager.select(ChannelID.location(ch))
|
||||||
isPresented = false
|
isPresented = false
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ struct MeshPeerList: View {
|
|||||||
let onTapPeer: (String) -> Void
|
let onTapPeer: (String) -> Void
|
||||||
let onToggleFavorite: (String) -> Void
|
let onToggleFavorite: (String) -> Void
|
||||||
let onShowFingerprint: (String) -> Void
|
let onShowFingerprint: (String) -> Void
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
@@ -17,7 +18,6 @@ struct MeshPeerList: View {
|
|||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.padding(.top, 12)
|
.padding(.top, 12)
|
||||||
} else {
|
} else {
|
||||||
let peerNicknames = viewModel.meshService.getPeerNicknames()
|
|
||||||
let myPeerID = viewModel.meshService.myPeerID
|
let myPeerID = viewModel.meshService.myPeerID
|
||||||
let mapped: [(peer: BitchatPeer, isMe: Bool, hasUnread: Bool, enc: EncryptionStatus)] = viewModel.allPeers.map { peer in
|
let mapped: [(peer: BitchatPeer, isMe: Bool, hasUnread: Bool, enc: EncryptionStatus)] = viewModel.allPeers.map { peer in
|
||||||
let isMe = peer.id == myPeerID
|
let isMe = peer.id == myPeerID
|
||||||
@@ -61,7 +61,8 @@ struct MeshPeerList: View {
|
|||||||
|
|
||||||
let displayName = isMe ? viewModel.nickname : peer.nickname
|
let displayName = isMe ? viewModel.nickname : peer.nickname
|
||||||
let (base, suffix) = splitSuffix(from: displayName)
|
let (base, suffix) = splitSuffix(from: displayName)
|
||||||
let baseColor = ((peer.favoriteStatus?.isFavorite ?? false) || peerNicknames[peer.id] != nil) ? textColor : secondaryTextColor
|
let assigned = viewModel.colorForMeshPeer(id: peer.id, isDark: colorScheme == .dark)
|
||||||
|
let baseColor = isMe ? Color.orange : assigned
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Text(base)
|
Text(base)
|
||||||
.font(.system(size: 14, design: .monospaced))
|
.font(.system(size: 14, design: .monospaced))
|
||||||
|
|||||||
Reference in New Issue
Block a user