mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-11-01 07:53:26 +00:00
Review fixes: pin the screenshot routing table
The geohash warn path lived only in AppRuntime with no coverage. The decision is now a pure nonisolated table (AppRuntime.resolveScreenshotResponse) pinned by tests: location sheet or geohash timeline → local privacy alert (nothing sent), App Info → nothing, mesh/DM → chat layer (where public channels stay silent). Mesh deliberately gets no local alert: a mesh screenshot reveals no place and triggers no send, so there is nothing actionable to warn about — alerting on every screenshot would train people to dismiss the one alert that matters. Rationale now documented on the response enum. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
435fa945d6
commit
c9c1ebcbad
@ -424,26 +424,24 @@ private extension AppRuntime {
|
||||
}
|
||||
|
||||
func handleScreenshotCaptured() {
|
||||
if appChromeModel.isLocationChannelsSheetPresented {
|
||||
let isLocationChannelActive: Bool = {
|
||||
if case .location = chatViewModel.activeChannel { return true }
|
||||
return false
|
||||
}()
|
||||
|
||||
switch Self.resolveScreenshotResponse(
|
||||
isLocationChannelsSheetPresented: appChromeModel.isLocationChannelsSheetPresented,
|
||||
isAppInfoPresented: appChromeModel.isAppInfoPresented,
|
||||
hasPrivateChatOpen: chatViewModel.selectedPrivateChatPeer != nil,
|
||||
isLocationChannelActive: isLocationChannelActive
|
||||
) {
|
||||
case .warnLocally:
|
||||
appChromeModel.triggerScreenshotPrivacyWarning()
|
||||
return
|
||||
case .ignore:
|
||||
break
|
||||
case .forwardToChat:
|
||||
chatViewModel.handleScreenshotCaptured()
|
||||
}
|
||||
|
||||
if appChromeModel.isAppInfoPresented {
|
||||
return
|
||||
}
|
||||
|
||||
// Screenshots are never announced to public channels (see
|
||||
// ChatLifecycleCoordinator.handleScreenshotCaptured). A geohash
|
||||
// timeline screenshot still reveals a place, so warn the person
|
||||
// taking it — locally, the same alert the channel sheet uses.
|
||||
if chatViewModel.selectedPrivateChatPeer == nil,
|
||||
case .location = chatViewModel.activeChannel {
|
||||
appChromeModel.triggerScreenshotPrivacyWarning()
|
||||
return
|
||||
}
|
||||
|
||||
chatViewModel.handleScreenshotCaptured()
|
||||
}
|
||||
|
||||
func openExternalURL(_ url: URL) {
|
||||
@ -460,3 +458,40 @@ private extension AppRuntime {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Screenshot routing
|
||||
|
||||
extension AppRuntime {
|
||||
/// What a screenshot triggers. Nothing on this table sends anything to
|
||||
/// a public channel (see `ChatLifecycleCoordinator.handleScreenshotCaptured`).
|
||||
enum ScreenshotCaptureResponse: Equatable {
|
||||
/// Show the local location-privacy alert; nothing is sent anywhere.
|
||||
case warnLocally
|
||||
/// Do nothing. App Info holds no conversation or location content.
|
||||
case ignore
|
||||
/// Hand to the chat layer: a DM notice goes to the peer when a
|
||||
/// secure session exists; public timelines stay silent. Mesh
|
||||
/// deliberately gets no local alert either — a mesh screenshot
|
||||
/// reveals no place and triggers no send, so there is nothing to
|
||||
/// warn about, and alerting on every screenshot would train people
|
||||
/// to dismiss the one alert that matters (the location one).
|
||||
case forwardToChat
|
||||
}
|
||||
|
||||
/// Pure decision table so the screenshot routing is testable without a
|
||||
/// runtime.
|
||||
nonisolated static func resolveScreenshotResponse(
|
||||
isLocationChannelsSheetPresented: Bool,
|
||||
isAppInfoPresented: Bool,
|
||||
hasPrivateChatOpen: Bool,
|
||||
isLocationChannelActive: Bool
|
||||
) -> ScreenshotCaptureResponse {
|
||||
if isLocationChannelsSheetPresented { return .warnLocally }
|
||||
if isAppInfoPresented { return .ignore }
|
||||
// A geohash timeline screenshot still reveals a place — warn the
|
||||
// person taking it, locally, with the same alert the channel sheet
|
||||
// uses.
|
||||
if !hasPrivateChatOpen, isLocationChannelActive { return .warnLocally }
|
||||
return .forwardToChat
|
||||
}
|
||||
}
|
||||
|
||||
@ -350,3 +350,59 @@ struct ChatLifecycleCoordinatorContextTests {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Screenshot Routing
|
||||
|
||||
/// Pins `AppRuntime`'s screenshot decision table: the geohash warn path,
|
||||
/// the App Info exemption, and the deliberate mesh silence. Combined with
|
||||
/// `handleScreenshotCaptured_publicChannel_staysSilent` above, this proves
|
||||
/// location + screenshot → local privacy alert, and nothing sent anywhere.
|
||||
struct ScreenshotCaptureRoutingTests {
|
||||
|
||||
@Test("Location channels warn locally; the sheet warns everywhere")
|
||||
func locationScreenshotsWarnLocally() {
|
||||
// Geohash timeline, no DM open: warn the person, tell no one.
|
||||
#expect(AppRuntime.resolveScreenshotResponse(
|
||||
isLocationChannelsSheetPresented: false,
|
||||
isAppInfoPresented: false,
|
||||
hasPrivateChatOpen: false,
|
||||
isLocationChannelActive: true
|
||||
) == .warnLocally)
|
||||
|
||||
// The channel sheet reveals location regardless of active channel.
|
||||
#expect(AppRuntime.resolveScreenshotResponse(
|
||||
isLocationChannelsSheetPresented: true,
|
||||
isAppInfoPresented: false,
|
||||
hasPrivateChatOpen: true,
|
||||
isLocationChannelActive: false
|
||||
) == .warnLocally)
|
||||
}
|
||||
|
||||
@Test("App Info is exempt; mesh and DMs forward to the chat layer")
|
||||
func nonLocationScreenshotsForwardOrIgnore() {
|
||||
#expect(AppRuntime.resolveScreenshotResponse(
|
||||
isLocationChannelsSheetPresented: false,
|
||||
isAppInfoPresented: true,
|
||||
hasPrivateChatOpen: false,
|
||||
isLocationChannelActive: true
|
||||
) == .ignore)
|
||||
|
||||
// Mesh timeline: forwarded, where the coordinator stays silent for
|
||||
// public channels — no local alert either (nothing is sent and no
|
||||
// place is revealed; see ScreenshotCaptureResponse docs).
|
||||
#expect(AppRuntime.resolveScreenshotResponse(
|
||||
isLocationChannelsSheetPresented: false,
|
||||
isAppInfoPresented: false,
|
||||
hasPrivateChatOpen: false,
|
||||
isLocationChannelActive: false
|
||||
) == .forwardToChat)
|
||||
|
||||
// An open DM keeps its peer notice even from a location channel.
|
||||
#expect(AppRuntime.resolveScreenshotResponse(
|
||||
isLocationChannelsSheetPresented: false,
|
||||
isAppInfoPresented: false,
|
||||
hasPrivateChatOpen: true,
|
||||
isLocationChannelActive: true
|
||||
) == .forwardToChat)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user