mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 06:45:18 +00:00
Fix/geohash blocks (#483)
* fix(geo-block): enable block/unblock for geohash users and enforce blocks\n\n- Add persisted set of blocked Nostr pubkeys in SecureIdentityStateManager\n- Check Nostr blocks for incoming messages (public + geo DMs)\n- Prevent sending geo DMs to blocked users\n- Extend /block and /unblock to resolve geohash display names to pubkeys and act accordingly\n- Improve /block list to show geohash blocks (visible names or #suffix) * fix(geo-block): enforce blocks on geohash public and DM receive; add block/unblock actions to geohash people list * fix: mark handlePublicMessage as @MainActor to call isMessageBlocked safely * ui(block): show blocked indicator (nosign icon) next to blocked peers in mesh and geohash lists * fix(geo-block): early-drop blocked pubkeys on geohash receive; filter blocked users from participants list * fix(geo-block): purge existing geohash messages/DMs on block; block directly from chat using Nostr sender ID when available --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
@@ -149,6 +149,9 @@ struct IdentityCache: Codable {
|
|||||||
// Last interaction timestamps (privacy: optional)
|
// Last interaction timestamps (privacy: optional)
|
||||||
var lastInteractions: [String: Date] = [:]
|
var lastInteractions: [String: Date] = [:]
|
||||||
|
|
||||||
|
// Blocked Nostr pubkeys (lowercased hex) for geohash chats
|
||||||
|
var blockedNostrPubkeys: Set<String> = []
|
||||||
|
|
||||||
// Schema version for future migrations
|
// Schema version for future migrations
|
||||||
var version: Int = 1
|
var version: Int = 1
|
||||||
}
|
}
|
||||||
@@ -222,4 +225,4 @@ enum ConnectionQuality {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Migration Support
|
// MARK: - Migration Support
|
||||||
// Removed LegacyFavorite - no longer needed
|
// Removed LegacyFavorite - no longer needed
|
||||||
|
|||||||
@@ -335,6 +335,30 @@ class SecureIdentityStateManager {
|
|||||||
self.saveIdentityCache()
|
self.saveIdentityCache()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Geohash (Nostr) Blocking
|
||||||
|
|
||||||
|
func isNostrBlocked(pubkeyHexLowercased: String) -> Bool {
|
||||||
|
queue.sync {
|
||||||
|
return cache.blockedNostrPubkeys.contains(pubkeyHexLowercased.lowercased())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setNostrBlocked(_ pubkeyHexLowercased: String, isBlocked: Bool) {
|
||||||
|
let key = pubkeyHexLowercased.lowercased()
|
||||||
|
queue.async(flags: .barrier) {
|
||||||
|
if isBlocked {
|
||||||
|
self.cache.blockedNostrPubkeys.insert(key)
|
||||||
|
} else {
|
||||||
|
self.cache.blockedNostrPubkeys.remove(key)
|
||||||
|
}
|
||||||
|
self.saveIdentityCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBlockedNostrPubkeys() -> Set<String> {
|
||||||
|
queue.sync { cache.blockedNostrPubkeys }
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Ephemeral Session Management
|
// MARK: - Ephemeral Session Management
|
||||||
|
|
||||||
@@ -464,4 +488,4 @@ class SecureIdentityStateManager {
|
|||||||
return cache.verifiedFingerprints
|
return cache.verifiedFingerprints
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,58 +149,75 @@ class CommandProcessor {
|
|||||||
let targetName = args.trimmingCharacters(in: .whitespaces)
|
let targetName = args.trimmingCharacters(in: .whitespaces)
|
||||||
|
|
||||||
if targetName.isEmpty {
|
if targetName.isEmpty {
|
||||||
// List blocked users
|
// List blocked users (mesh) and geohash (Nostr) blocks
|
||||||
guard let blockedUsers = chatViewModel?.blockedUsers, !blockedUsers.isEmpty else {
|
let meshBlocked = chatViewModel?.blockedUsers ?? []
|
||||||
return .success(message: "no blocked peers")
|
|
||||||
}
|
|
||||||
|
|
||||||
var blockedNicknames: [String] = []
|
var blockedNicknames: [String] = []
|
||||||
if let peers = meshService?.getPeerNicknames() {
|
if let peers = meshService?.getPeerNicknames() {
|
||||||
for (peerID, nickname) in peers {
|
for (peerID, nickname) in peers {
|
||||||
if let fingerprint = meshService?.getFingerprint(for: peerID),
|
if let fingerprint = meshService?.getFingerprint(for: peerID),
|
||||||
blockedUsers.contains(fingerprint) {
|
meshBlocked.contains(fingerprint) {
|
||||||
blockedNicknames.append(nickname)
|
blockedNicknames.append(nickname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let list = blockedNicknames.isEmpty ? "blocked peers (not currently online)"
|
// Geohash blocked names (prefer visible display names; fallback to #suffix)
|
||||||
: blockedNicknames.sorted().joined(separator: ", ")
|
let geoBlocked = Array(SecureIdentityStateManager.shared.getBlockedNostrPubkeys())
|
||||||
return .success(message: "blocked peers: \(list)")
|
var geoNames: [String] = []
|
||||||
|
if let vm = chatViewModel {
|
||||||
|
let visible = vm.visibleGeohashPeople()
|
||||||
|
let visibleIndex = Dictionary(uniqueKeysWithValues: visible.map { ($0.id.lowercased(), $0.displayName) })
|
||||||
|
for pk in geoBlocked {
|
||||||
|
if let name = visibleIndex[pk.lowercased()] {
|
||||||
|
geoNames.append(name)
|
||||||
|
} else {
|
||||||
|
let suffix = String(pk.suffix(4))
|
||||||
|
geoNames.append("anon#\(suffix)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let meshList = blockedNicknames.isEmpty ? "none" : blockedNicknames.sorted().joined(separator: ", ")
|
||||||
|
let geoList = geoNames.isEmpty ? "none" : geoNames.sorted().joined(separator: ", ")
|
||||||
|
return .success(message: "blocked peers: \(meshList) | geohash blocks: \(geoList)")
|
||||||
}
|
}
|
||||||
|
|
||||||
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
|
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
|
||||||
|
|
||||||
guard let peerID = chatViewModel?.getPeerIDForNickname(nickname),
|
if let peerID = chatViewModel?.getPeerIDForNickname(nickname),
|
||||||
let fingerprint = meshService?.getFingerprint(for: peerID) else {
|
let fingerprint = meshService?.getFingerprint(for: peerID) {
|
||||||
return .error(message: "cannot block \(nickname): not found or unable to verify identity")
|
if SecureIdentityStateManager.shared.isBlocked(fingerprint: fingerprint) {
|
||||||
|
return .success(message: "\(nickname) is already blocked")
|
||||||
|
}
|
||||||
|
// Block the user (mesh/noise identity)
|
||||||
|
if var identity = SecureIdentityStateManager.shared.getSocialIdentity(for: fingerprint) {
|
||||||
|
identity.isBlocked = true
|
||||||
|
identity.isFavorite = false
|
||||||
|
SecureIdentityStateManager.shared.updateSocialIdentity(identity)
|
||||||
|
} else {
|
||||||
|
let blockedIdentity = SocialIdentity(
|
||||||
|
fingerprint: fingerprint,
|
||||||
|
localPetname: nil,
|
||||||
|
claimedNickname: nickname,
|
||||||
|
trustLevel: .unknown,
|
||||||
|
isFavorite: false,
|
||||||
|
isBlocked: true,
|
||||||
|
notes: nil
|
||||||
|
)
|
||||||
|
SecureIdentityStateManager.shared.updateSocialIdentity(blockedIdentity)
|
||||||
|
}
|
||||||
|
return .success(message: "blocked \(nickname). you will no longer receive messages from them")
|
||||||
|
}
|
||||||
|
// Mesh lookup failed; try geohash (Nostr) participant by display name
|
||||||
|
if let pub = chatViewModel?.nostrPubkeyForDisplayName(nickname) {
|
||||||
|
if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: pub) {
|
||||||
|
return .success(message: "\(nickname) is already blocked")
|
||||||
|
}
|
||||||
|
SecureIdentityStateManager.shared.setNostrBlocked(pub, isBlocked: true)
|
||||||
|
return .success(message: "blocked \(nickname) in geohash chats")
|
||||||
}
|
}
|
||||||
|
|
||||||
if SecureIdentityStateManager.shared.isBlocked(fingerprint: fingerprint) {
|
return .error(message: "cannot block \(nickname): not found or unable to verify identity")
|
||||||
return .success(message: "\(nickname) is already blocked")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block the user
|
|
||||||
if var identity = SecureIdentityStateManager.shared.getSocialIdentity(for: fingerprint) {
|
|
||||||
identity.isBlocked = true
|
|
||||||
identity.isFavorite = false
|
|
||||||
SecureIdentityStateManager.shared.updateSocialIdentity(identity)
|
|
||||||
} else {
|
|
||||||
let blockedIdentity = SocialIdentity(
|
|
||||||
fingerprint: fingerprint,
|
|
||||||
localPetname: nil,
|
|
||||||
claimedNickname: nickname,
|
|
||||||
trustLevel: .unknown,
|
|
||||||
isFavorite: false,
|
|
||||||
isBlocked: true,
|
|
||||||
notes: nil
|
|
||||||
)
|
|
||||||
SecureIdentityStateManager.shared.updateSocialIdentity(blockedIdentity)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The peerStateManager and SecureIdentityStateManager handle the blocking state
|
|
||||||
|
|
||||||
return .success(message: "blocked \(nickname). you will no longer receive messages from them")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleUnblock(_ args: String) -> CommandResult {
|
private func handleUnblock(_ args: String) -> CommandResult {
|
||||||
@@ -211,19 +228,23 @@ class CommandProcessor {
|
|||||||
|
|
||||||
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
|
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
|
||||||
|
|
||||||
guard let peerID = chatViewModel?.getPeerIDForNickname(nickname),
|
if let peerID = chatViewModel?.getPeerIDForNickname(nickname),
|
||||||
let fingerprint = meshService?.getFingerprint(for: peerID) else {
|
let fingerprint = meshService?.getFingerprint(for: peerID) {
|
||||||
return .error(message: "cannot unblock \(nickname): not found")
|
if !SecureIdentityStateManager.shared.isBlocked(fingerprint: fingerprint) {
|
||||||
|
return .success(message: "\(nickname) is not blocked")
|
||||||
|
}
|
||||||
|
SecureIdentityStateManager.shared.setBlocked(fingerprint, isBlocked: false)
|
||||||
|
return .success(message: "unblocked \(nickname)")
|
||||||
}
|
}
|
||||||
|
// Try geohash unblock
|
||||||
if !SecureIdentityStateManager.shared.isBlocked(fingerprint: fingerprint) {
|
if let pub = chatViewModel?.nostrPubkeyForDisplayName(nickname) {
|
||||||
return .success(message: "\(nickname) is not blocked")
|
if !SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: pub) {
|
||||||
|
return .success(message: "\(nickname) is not blocked")
|
||||||
|
}
|
||||||
|
SecureIdentityStateManager.shared.setNostrBlocked(pub, isBlocked: false)
|
||||||
|
return .success(message: "unblocked \(nickname) in geohash chats")
|
||||||
}
|
}
|
||||||
|
return .error(message: "cannot unblock \(nickname): not found")
|
||||||
SecureIdentityStateManager.shared.setBlocked(fingerprint, isBlocked: false)
|
|
||||||
// The SecureIdentityStateManager handles the unblocking state
|
|
||||||
|
|
||||||
return .success(message: "unblocked \(nickname)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleFavorite(_ args: String, add: Bool) -> CommandResult {
|
private func handleFavorite(_ args: String, add: Bool) -> CommandResult {
|
||||||
|
|||||||
@@ -654,19 +654,23 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let wasReadBefore = self.sentReadReceipts.contains(messageId)
|
let wasReadBefore = self.sentReadReceipts.contains(messageId)
|
||||||
let isRecentMessage = Date().timeIntervalSince(messageTimestamp) < 30
|
let isRecentMessage = Date().timeIntervalSince(messageTimestamp) < 30
|
||||||
let shouldMarkUnread = !wasReadBefore && !isViewing && isRecentMessage
|
let shouldMarkUnread = !wasReadBefore && !isViewing && isRecentMessage
|
||||||
let msg = BitchatMessage(
|
let msg = BitchatMessage(
|
||||||
id: messageId,
|
id: messageId,
|
||||||
sender: senderName,
|
sender: senderName,
|
||||||
content: pm.content,
|
content: pm.content,
|
||||||
timestamp: messageTimestamp,
|
timestamp: messageTimestamp,
|
||||||
isRelay: false,
|
isRelay: false,
|
||||||
originalSender: nil,
|
originalSender: nil,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
recipientNickname: self.nickname,
|
recipientNickname: self.nickname,
|
||||||
senderPeerID: convKey,
|
senderPeerID: convKey,
|
||||||
mentions: nil,
|
mentions: nil,
|
||||||
deliveryStatus: .delivered(to: self.nickname, at: Date())
|
deliveryStatus: .delivered(to: self.nickname, at: Date())
|
||||||
)
|
)
|
||||||
|
// Respect geohash blocks
|
||||||
|
if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: senderPubkey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if self.privateChats[convKey] == nil { self.privateChats[convKey] = [] }
|
if self.privateChats[convKey] == nil { self.privateChats[convKey] = [] }
|
||||||
self.privateChats[convKey]?.append(msg)
|
self.privateChats[convKey]?.append(msg)
|
||||||
self.trimPrivateChatMessagesIfNeeded(for: convKey)
|
self.trimPrivateChatMessagesIfNeeded(for: convKey)
|
||||||
@@ -1243,6 +1247,10 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let nick = nickTag[1]
|
let nick = nickTag[1]
|
||||||
self.geoNicknames[event.pubkey.lowercased()] = nick
|
self.geoNicknames[event.pubkey.lowercased()] = nick
|
||||||
}
|
}
|
||||||
|
// If this pubkey is blocked, skip mapping, participants, and timeline
|
||||||
|
if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: event.pubkey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
// Store mapping for geohash DM initiation
|
// Store mapping for geohash DM initiation
|
||||||
let key16 = "nostr_" + String(event.pubkey.prefix(16))
|
let key16 = "nostr_" + String(event.pubkey.prefix(16))
|
||||||
self.nostrKeyMapping[key16] = event.pubkey
|
self.nostrKeyMapping[key16] = event.pubkey
|
||||||
@@ -1446,6 +1454,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
var map = geoParticipants[gh] ?? [:]
|
var map = geoParticipants[gh] ?? [:]
|
||||||
// Prune expired entries
|
// Prune expired entries
|
||||||
map = map.filter { $0.value >= cutoff }
|
map = map.filter { $0.value >= cutoff }
|
||||||
|
// Remove blocked Nostr pubkeys
|
||||||
|
map = map.filter { !SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: $0.key) }
|
||||||
geoParticipants[gh] = map
|
geoParticipants[gh] = map
|
||||||
// Build display list
|
// Build display list
|
||||||
let people = map
|
let people = map
|
||||||
@@ -1478,7 +1488,9 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
func visibleGeohashPeople() -> [GeoPerson] {
|
func visibleGeohashPeople() -> [GeoPerson] {
|
||||||
guard let gh = currentGeohash else { return [] }
|
guard let gh = currentGeohash else { return [] }
|
||||||
let cutoff = Date().addingTimeInterval(-5 * 60)
|
let cutoff = Date().addingTimeInterval(-5 * 60)
|
||||||
let map = (geoParticipants[gh] ?? [:]).filter { $0.value >= cutoff }
|
let map = (geoParticipants[gh] ?? [:])
|
||||||
|
.filter { $0.value >= cutoff }
|
||||||
|
.filter { !SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: $0.key) }
|
||||||
let people = map
|
let people = map
|
||||||
.map { (pub, seen) in GeoPerson(id: pub, displayName: displayNameForNostrPubkey(pub), lastSeen: seen) }
|
.map { (pub, seen) in GeoPerson(id: pub, displayName: displayNameForNostrPubkey(pub), lastSeen: seen) }
|
||||||
.sorted { $0.lastSeen > $1.lastSeen }
|
.sorted { $0.lastSeen > $1.lastSeen }
|
||||||
@@ -1492,6 +1504,66 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
return map.values.filter { $0 >= cutoff }.count
|
return map.values.filter { $0 >= cutoff }.count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Geohash block helpers
|
||||||
|
@MainActor
|
||||||
|
func isGeohashUserBlocked(pubkeyHexLowercased: String) -> Bool {
|
||||||
|
return SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: pubkeyHexLowercased)
|
||||||
|
}
|
||||||
|
@MainActor
|
||||||
|
func blockGeohashUser(pubkeyHexLowercased: String, displayName: String) {
|
||||||
|
let hex = pubkeyHexLowercased.lowercased()
|
||||||
|
SecureIdentityStateManager.shared.setNostrBlocked(hex, isBlocked: true)
|
||||||
|
|
||||||
|
// Remove from participants for all geohashes
|
||||||
|
for (gh, var map) in geoParticipants {
|
||||||
|
map.removeValue(forKey: hex)
|
||||||
|
geoParticipants[gh] = map
|
||||||
|
}
|
||||||
|
refreshGeohashPeople()
|
||||||
|
|
||||||
|
// Remove their public messages from current geohash timeline and visible list
|
||||||
|
if let gh = currentGeohash {
|
||||||
|
if var arr = geoTimelines[gh] {
|
||||||
|
arr.removeAll { msg in
|
||||||
|
if let spid = msg.senderPeerID, spid.hasPrefix("nostr") {
|
||||||
|
if let full = nostrKeyMapping[spid]?.lowercased() { return full == hex }
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
geoTimelines[gh] = arr
|
||||||
|
}
|
||||||
|
// Also filter currently bound messages if we are in geohash channel
|
||||||
|
switch activeChannel {
|
||||||
|
case .location:
|
||||||
|
messages.removeAll { msg in
|
||||||
|
if let spid = msg.senderPeerID, spid.hasPrefix("nostr") {
|
||||||
|
if let full = nostrKeyMapping[spid]?.lowercased() { return full == hex }
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove geohash DM conversation if exists
|
||||||
|
let convKey = "nostr_" + String(hex.prefix(16))
|
||||||
|
if privateChats[convKey] != nil {
|
||||||
|
privateChats.removeValue(forKey: convKey)
|
||||||
|
unreadPrivateMessages.remove(convKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove mapping keys pointing to this pubkey to avoid accidental resolution
|
||||||
|
for (k, v) in nostrKeyMapping where v.lowercased() == hex { nostrKeyMapping.removeValue(forKey: k) }
|
||||||
|
|
||||||
|
addSystemMessage("blocked \(displayName) in geohash chats")
|
||||||
|
}
|
||||||
|
@MainActor
|
||||||
|
func unblockGeohashUser(pubkeyHexLowercased: String, displayName: String) {
|
||||||
|
SecureIdentityStateManager.shared.setNostrBlocked(pubkeyHexLowercased, isBlocked: false)
|
||||||
|
addSystemMessage("unblocked \(displayName) in geohash chats")
|
||||||
|
}
|
||||||
|
|
||||||
/// Begin sampling multiple geohashes (used by channel sheet) without changing active channel.
|
/// Begin sampling multiple geohashes (used by channel sheet) without changing active channel.
|
||||||
@MainActor
|
@MainActor
|
||||||
func beginGeohashSampling(for geohashes: [String]) {
|
func beginGeohashSampling(for geohashes: [String]) {
|
||||||
@@ -1618,6 +1690,14 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Respect geohash blocks
|
||||||
|
if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: recipientHex) {
|
||||||
|
if let msgIdx = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) {
|
||||||
|
privateChats[peerID]?[msgIdx].deliveryStatus = .failed(reason: "user is blocked")
|
||||||
|
}
|
||||||
|
addSystemMessage("cannot send message: user is blocked.")
|
||||||
|
return
|
||||||
|
}
|
||||||
// Send via Nostr using per-geohash identity
|
// Send via Nostr using per-geohash identity
|
||||||
do {
|
do {
|
||||||
let id = try NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash)
|
let id = try NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash)
|
||||||
@@ -4607,10 +4687,28 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
@MainActor
|
@MainActor
|
||||||
private func isMessageBlocked(_ message: BitchatMessage) -> Bool {
|
private func isMessageBlocked(_ message: BitchatMessage) -> Bool {
|
||||||
if let peerID = message.senderPeerID ?? getPeerIDForNickname(message.sender) {
|
if let peerID = message.senderPeerID ?? getPeerIDForNickname(message.sender) {
|
||||||
return isPeerBlocked(peerID)
|
// Check mesh/known peers first
|
||||||
|
if isPeerBlocked(peerID) { return true }
|
||||||
|
// Check geohash (Nostr) blocks using mapping to full pubkey
|
||||||
|
if peerID.hasPrefix("nostr") {
|
||||||
|
if let full = nostrKeyMapping[peerID]?.lowercased() {
|
||||||
|
if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: full) { return true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Geohash Nickname Resolution (for /block in geohash)
|
||||||
|
@MainActor
|
||||||
|
func nostrPubkeyForDisplayName(_ name: String) -> String? {
|
||||||
|
// Look up current visible geohash participants for an exact displayName match
|
||||||
|
for p in visibleGeohashPeople() {
|
||||||
|
if p.displayName == name { return p.id }
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
/// Process action messages (hugs, slaps) into system messages
|
/// Process action messages (hugs, slaps) into system messages
|
||||||
private func processActionMessage(_ message: BitchatMessage) -> BitchatMessage {
|
private func processActionMessage(_ message: BitchatMessage) -> BitchatMessage {
|
||||||
@@ -4878,9 +4976,13 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handle incoming public message
|
/// Handle incoming public message
|
||||||
|
@MainActor
|
||||||
private func handlePublicMessage(_ message: BitchatMessage) {
|
private func handlePublicMessage(_ message: BitchatMessage) {
|
||||||
let finalMessage = processActionMessage(message)
|
let finalMessage = processActionMessage(message)
|
||||||
|
|
||||||
|
// Drop if sender is blocked (covers geohash via Nostr pubkey mapping)
|
||||||
|
if isMessageBlocked(finalMessage) { return }
|
||||||
|
|
||||||
// Classify origin: geochat if senderPeerID starts with 'nostr:', else mesh (or system)
|
// Classify origin: geochat if senderPeerID starts with 'nostr:', else mesh (or system)
|
||||||
let isGeo = finalMessage.senderPeerID?.hasPrefix("nostr:") == true
|
let isGeo = finalMessage.senderPeerID?.hasPrefix("nostr:") == true
|
||||||
|
|
||||||
|
|||||||
@@ -244,9 +244,18 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Button("BLOCK", role: .destructive) {
|
Button("BLOCK", role: .destructive) {
|
||||||
if let sender = selectedMessageSender {
|
// Prefer direct geohash block when we have a Nostr sender ID
|
||||||
|
#if os(iOS)
|
||||||
|
if let peerID = selectedMessageSenderID, peerID.hasPrefix("nostr:"),
|
||||||
|
let full = viewModel.fullNostrHex(forSenderPeerID: peerID),
|
||||||
|
let sender = selectedMessageSender {
|
||||||
|
viewModel.blockGeohashUser(pubkeyHexLowercased: full, displayName: sender)
|
||||||
|
} else if let sender = selectedMessageSender {
|
||||||
viewModel.sendMessage("/block \(sender)")
|
viewModel.sendMessage("/block \(sender)")
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
if let sender = selectedMessageSender { viewModel.sendMessage("/block \(sender)") }
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
Button("cancel", role: .cancel) {}
|
Button("cancel", role: .cancel) {}
|
||||||
|
|||||||
@@ -71,6 +71,15 @@ struct GeohashPeopleList: View {
|
|||||||
.foregroundColor(rowColor)
|
.foregroundColor(rowColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Blocked indicator for geohash users
|
||||||
|
if let me = myHex, person.id != me {
|
||||||
|
if viewModel.isGeohashUserBlocked(pubkeyHexLowercased: person.id) {
|
||||||
|
Image(systemName: "nosign")
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.help("Blocked in geochash")
|
||||||
|
}
|
||||||
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
@@ -83,6 +92,18 @@ struct GeohashPeopleList: View {
|
|||||||
onTapPerson()
|
onTapPerson()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.contextMenu {
|
||||||
|
if let me = myHex, person.id == me {
|
||||||
|
EmptyView()
|
||||||
|
} else {
|
||||||
|
let blocked = viewModel.isGeohashUserBlocked(pubkeyHexLowercased: person.id)
|
||||||
|
if blocked {
|
||||||
|
Button("Unblock") { viewModel.unblockGeohashUser(pubkeyHexLowercased: person.id, displayName: person.displayName) }
|
||||||
|
} else {
|
||||||
|
Button("Block") { viewModel.blockGeohashUser(pubkeyHexLowercased: person.id, displayName: person.displayName) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,14 @@ struct MeshPeerList: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Blocked indicator
|
||||||
|
if !isMe, viewModel.isPeerBlocked(peer.id) {
|
||||||
|
Image(systemName: "nosign")
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.help("Blocked")
|
||||||
|
}
|
||||||
|
|
||||||
if let icon = item.enc.icon, !isMe {
|
if let icon = item.enc.icon, !isMe {
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.font(.system(size: 10))
|
.font(.system(size: 10))
|
||||||
|
|||||||
Reference in New Issue
Block a user