mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 19:05:20 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebfceb21bb | ||
|
|
b2685e5156 |
@@ -382,6 +382,11 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
@Published var showBluetoothAlert = false
|
@Published var showBluetoothAlert = false
|
||||||
@Published var bluetoothAlertMessage = ""
|
@Published var bluetoothAlertMessage = ""
|
||||||
@Published var bluetoothState: CBManagerState = .unknown
|
@Published var bluetoothState: CBManagerState = .unknown
|
||||||
|
|
||||||
|
// Presentation state for privacy gating
|
||||||
|
@Published var isLocationChannelsSheetPresented: Bool = false
|
||||||
|
@Published var isAppInfoPresented: Bool = false
|
||||||
|
@Published var showScreenshotPrivacyWarning: Bool = false
|
||||||
|
|
||||||
// Messages are naturally ephemeral - no persistent storage
|
// Messages are naturally ephemeral - no persistent storage
|
||||||
// Persist mesh public timeline across channel switches
|
// Persist mesh public timeline across channel switches
|
||||||
@@ -1407,6 +1412,11 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
switch channel {
|
switch channel {
|
||||||
case .mesh:
|
case .mesh:
|
||||||
messages = meshTimeline
|
messages = meshTimeline
|
||||||
|
// Debug: log if any empty messages are present
|
||||||
|
let emptyMesh = messages.filter { $0.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }.count
|
||||||
|
if emptyMesh > 0 {
|
||||||
|
SecureLogger.log("RenderGuard: mesh timeline contains \(emptyMesh) empty messages", category: SecureLogger.session, level: .debug)
|
||||||
|
}
|
||||||
stopGeoParticipantsTimer()
|
stopGeoParticipantsTimer()
|
||||||
geohashPeople = []
|
geohashPeople = []
|
||||||
teleportedGeo.removeAll()
|
teleportedGeo.removeAll()
|
||||||
@@ -1414,13 +1424,26 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Sanitize existing timeline (filter any prior empty-content entries)
|
// Sanitize existing timeline (filter any prior empty-content entries)
|
||||||
var arr = geoTimelines[ch.geohash] ?? []
|
var arr = geoTimelines[ch.geohash] ?? []
|
||||||
arr.removeAll { $0.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
arr.removeAll { $0.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
||||||
// Ensure chronological order when returning to a geohash
|
// Deduplicate by ID while preserving order (from oldest to newest)
|
||||||
if arr.count > 1 {
|
if arr.count > 1 {
|
||||||
arr.sort { $0.timestamp < $1.timestamp }
|
var seen = Set<String>()
|
||||||
|
var dedup: [BitchatMessage] = []
|
||||||
|
for m in arr.sorted(by: { $0.timestamp < $1.timestamp }) {
|
||||||
|
if !seen.contains(m.id) {
|
||||||
|
dedup.append(m)
|
||||||
|
seen.insert(m.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
arr = dedup
|
||||||
}
|
}
|
||||||
// Persist the cleaned/sorted timeline for this geohash
|
// Persist the cleaned/sorted timeline for this geohash
|
||||||
geoTimelines[ch.geohash] = arr
|
geoTimelines[ch.geohash] = arr
|
||||||
messages = arr
|
messages = arr
|
||||||
|
// Debug: log if any empty messages are present post-sanitize
|
||||||
|
let emptyGeo = messages.filter { $0.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }.count
|
||||||
|
if emptyGeo > 0 {
|
||||||
|
SecureLogger.log("RenderGuard: geohash \(ch.geohash) timeline has \(emptyGeo) empty messages after sanitize", category: SecureLogger.session, level: .debug)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Unsubscribe previous
|
// Unsubscribe previous
|
||||||
if let sub = geoSubscriptionID {
|
if let sub = geoSubscriptionID {
|
||||||
@@ -2588,6 +2611,17 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@objc private func userDidTakeScreenshot() {
|
@objc private func userDidTakeScreenshot() {
|
||||||
|
// Respect privacy: do not broadcast screenshots taken from non-chat sheets
|
||||||
|
if isLocationChannelsSheetPresented {
|
||||||
|
// Show a warning about sharing location screenshots publicly
|
||||||
|
showScreenshotPrivacyWarning = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isAppInfoPresented {
|
||||||
|
// Silently ignore screenshots of app info
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Send screenshot notification based on current context
|
// Send screenshot notification based on current context
|
||||||
let screenshotMessage = "* \(nickname) took a screenshot *"
|
let screenshotMessage = "* \(nickname) took a screenshot *"
|
||||||
|
|
||||||
@@ -2607,7 +2641,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show local notification immediately as system message
|
// Show local notification immediately as system message (only in chat)
|
||||||
let localNotification = BitchatMessage(
|
let localNotification = BitchatMessage(
|
||||||
sender: "system",
|
sender: "system",
|
||||||
content: "you took a screenshot",
|
content: "you took a screenshot",
|
||||||
@@ -2658,7 +2692,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Show local notification immediately as system message
|
// Show local notification immediately as system message (only in chat)
|
||||||
let localNotification = BitchatMessage(
|
let localNotification = BitchatMessage(
|
||||||
sender: "system",
|
sender: "system",
|
||||||
content: "you took a screenshot",
|
content: "you took a screenshot",
|
||||||
@@ -5781,9 +5815,12 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if isGeo && finalMessage.sender != "system" {
|
if isGeo && finalMessage.sender != "system" {
|
||||||
if let gh = currentGeohash {
|
if let gh = currentGeohash {
|
||||||
var arr = geoTimelines[gh] ?? []
|
var arr = geoTimelines[gh] ?? []
|
||||||
arr.append(finalMessage)
|
// Dedup by message ID before appending to per-geohash timeline
|
||||||
if arr.count > geoTimelineCap { arr = Array(arr.suffix(geoTimelineCap)) }
|
if !arr.contains(where: { $0.id == finalMessage.id }) {
|
||||||
geoTimelines[gh] = arr
|
arr.append(finalMessage)
|
||||||
|
if arr.count > geoTimelineCap { arr = Array(arr.suffix(geoTimelineCap)) }
|
||||||
|
geoTimelines[gh] = arr
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.sheet(isPresented: $showAppInfo) {
|
.sheet(isPresented: $showAppInfo) {
|
||||||
AppInfoView()
|
AppInfoView()
|
||||||
|
.onAppear { viewModel.isAppInfoPresented = true }
|
||||||
|
.onDisappear { viewModel.isAppInfoPresented = false }
|
||||||
}
|
}
|
||||||
.sheet(isPresented: Binding(
|
.sheet(isPresented: Binding(
|
||||||
get: { viewModel.showingFingerprintFor != nil },
|
get: { viewModel.showingFingerprintFor != nil },
|
||||||
@@ -279,8 +281,10 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
let items = windowedMessages.map { (uiID: "\(contextKey)|\($0.id)", message: $0) }
|
let items = windowedMessages.map { (uiID: "\(contextKey)|\($0.id)", message: $0) }
|
||||||
|
// Filter out empty/whitespace-only messages to avoid blank rows
|
||||||
ForEach(items, id: \.uiID) { item in
|
let filteredItems = items.filter { !$0.message.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
||||||
|
|
||||||
|
ForEach(filteredItems, id: \.uiID) { item in
|
||||||
let message = item.message
|
let message = item.message
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
// Check if current user is mentioned
|
// Check if current user is mentioned
|
||||||
@@ -1182,6 +1186,13 @@ struct ContentView: View {
|
|||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.sheet(isPresented: $showLocationChannelsSheet) {
|
.sheet(isPresented: $showLocationChannelsSheet) {
|
||||||
LocationChannelsSheet(isPresented: $showLocationChannelsSheet)
|
LocationChannelsSheet(isPresented: $showLocationChannelsSheet)
|
||||||
|
.onAppear { viewModel.isLocationChannelsSheetPresented = true }
|
||||||
|
.onDisappear { viewModel.isLocationChannelsSheetPresented = false }
|
||||||
|
}
|
||||||
|
.alert("heads up", isPresented: $viewModel.showScreenshotPrivacyWarning) {
|
||||||
|
Button("ok", role: .cancel) {}
|
||||||
|
} message: {
|
||||||
|
Text("screenshots of location channels will reveal your location. think before sharing publicly.")
|
||||||
}
|
}
|
||||||
.background(backgroundColor.opacity(0.95))
|
.background(backgroundColor.opacity(0.95))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -304,22 +304,17 @@ struct LocationChannelsSheet: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HStack(spacing: 0) {
|
let subtitleFull: String = {
|
||||||
Text(subtitlePrefix)
|
if let name = subtitleName, !name.isEmpty {
|
||||||
.font(.system(size: 12, design: .monospaced))
|
return subtitlePrefix + " • " + name
|
||||||
.foregroundColor(.secondary)
|
|
||||||
if let name = subtitleName {
|
|
||||||
Text(" • ")
|
|
||||||
.font(.system(size: 12, design: .monospaced))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Text(name)
|
|
||||||
.font(.system(size: 12, design: .monospaced))
|
|
||||||
.fontWeight(subtitleNameBold ? .bold : .regular)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
.truncationMode(.tail)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return subtitlePrefix
|
||||||
|
}()
|
||||||
|
Text(subtitleFull)
|
||||||
|
.font(.system(size: 12, design: .monospaced))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
if isSelected {
|
if isSelected {
|
||||||
|
|||||||
Reference in New Issue
Block a user