Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 29 additions & 17 deletions whitenoise-mac/Core/MarmotMapping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ extension ChatItem {
directPeer: ChatPeerProfile? = nil,
groupAvatarURL: String? = nil
) {
let groupName = row.groupName.trimmingCharacters(in: .whitespacesAndNewlines)
let peerName = directPeer?.displayName?.trimmingCharacters(in: .whitespacesAndNewlines)
let projectedTitle = row.title.trimmingCharacters(in: .whitespacesAndNewlines)
let groupName = PeerDisplayText.sanitize(row.groupName) ?? ""
let peerName = PeerDisplayText.sanitize(directPeer?.displayName)
let projectedTitle = PeerDisplayText.sanitize(row.title) ?? ""
let title: String
if let peerName, !peerName.isEmpty {
title = peerName
Expand Down Expand Up @@ -115,13 +115,13 @@ extension ChatItem {
}
guard presentation.isChatBubble,
preview.sender != activeAccountIdHex,
let senderName = preview.senderDisplayName?.trimmingCharacters(in: .whitespacesAndNewlines),
let senderName = PeerDisplayText.sanitize(preview.senderDisplayName),
!senderName.isEmpty
else {
return body
}

return "\(senderName): \(body)"
return "\(PeerDisplayText.templateFragment(senderName)): \(body)"
}
}

Expand Down Expand Up @@ -401,7 +401,7 @@ nonisolated extension MessageItem {
break
}

return nonBlank(event.text) ?? groupSystemFallback(event.systemType)
return PeerDisplayText.sanitize(event.text) ?? groupSystemFallback(event.systemType)
}

private static func memberAddedText(
Expand Down Expand Up @@ -595,25 +595,35 @@ nonisolated extension MessageItem {
activeAccountIdHex: String?,
senderProfiles: [String: ChatPeerProfile]
) -> String? {
guard let name = nonBlank(event.name) else { return nil }
guard let name = PeerDisplayText.sanitize(event.name) else { return nil }
let actorName = systemAccountName(
event.actorAccountIdHex,
activeAccountIdHex: activeAccountIdHex,
senderProfiles: senderProfiles,
position: .subject
)
let oldName = nonBlank(event.oldName)
let oldName = PeerDisplayText.sanitize(event.oldName)
let isolatedName = PeerDisplayText.templateFragment(name)

if let actorName, let oldName {
return String(format: L10n.string("%@ renamed the group from \"%@\" to \"%@\""), actorName, oldName, name)
return String(
format: L10n.string("%@ renamed the group from \"%@\" to \"%@\""),
actorName,
PeerDisplayText.templateFragment(oldName),
isolatedName
)
}
if let actorName {
return String(format: L10n.string("%@ renamed the group to \"%@\""), actorName, name)
return String(format: L10n.string("%@ renamed the group to \"%@\""), actorName, isolatedName)
}
if let oldName {
return String(format: L10n.string("The group was renamed from \"%@\" to \"%@\""), oldName, name)
return String(
format: L10n.string("The group was renamed from \"%@\" to \"%@\""),
PeerDisplayText.templateFragment(oldName),
isolatedName
)
}
return String(format: L10n.string("The group was renamed to \"%@\""), name)
return String(format: L10n.string("The group was renamed to \"%@\""), isolatedName)
}

private static func groupAvatarChangedText(
Expand Down Expand Up @@ -685,7 +695,9 @@ nonisolated extension MessageItem {
return L10n.string("you")
}
}
return displayName(for: accountIdHex, profile: senderProfiles[accountIdHex])
return PeerDisplayText.templateFragment(
displayName(for: accountIdHex, profile: senderProfiles[accountIdHex])
)
}

private enum SystemAccountNamePosition {
Expand Down Expand Up @@ -834,11 +846,11 @@ nonisolated extension MessageItem {
return L10n.string("Agent operation")
case .groupSystem:
let payload = TimelinePayload.decode(from: body)
if let text = firstNonBlank([payload?.text]) {
if let text = PeerDisplayText.sanitize(payload?.text) {
return text
}
if payload == nil, !body.isEmpty {
return body
if payload == nil, let text = PeerDisplayText.sanitize(body) {
return text
}
return groupSystemFallback(payload?.systemType ?? tagValue("system", in: tags))
case .unsupported:
Expand Down Expand Up @@ -887,7 +899,7 @@ nonisolated extension MessageItem {
}

private static func displayName(for sender: String, profile: ChatPeerProfile?) -> String {
firstNonBlank([profile?.displayName]) ?? DisplayText.short(sender)
PeerDisplayText.sanitize(profile?.displayName) ?? DisplayText.short(sender)
}

private static func tagValue(_ name: String, in tags: [MessageTagFfi]) -> String? {
Expand Down
23 changes: 23 additions & 0 deletions whitenoise-mac/Core/PeerDisplayText.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Foundation

/// Sanitization for peer-controlled strings shown as display names or group titles.
nonisolated enum PeerDisplayText {
private static let firstStrongIsolate = "\u{2068}"
private static let popDirectionalIsolate = "\u{2069}"

/// Strips Unicode format controls (`Cf`), including bidi embedding/override/isolate
/// scalars (U+202A–U+202E, U+2066–U+2069), then treats an all-blank result as absent.
static func sanitize(_ text: String?) -> String? {
guard let text else { return nil }
let filtered = String(
String.UnicodeScalarView(text.unicodeScalars.filter { $0.properties.generalCategory != .format })
)
return filtered.nilIfBlank
}

/// Sanitized text wrapped in first-strong isolate and PDI for safe `%@` interpolation.
static func templateFragment(_ text: String) -> String {
guard let core = sanitize(text) else { return "" }
return firstStrongIsolate + core + popDirectionalIsolate
}
}
6 changes: 3 additions & 3 deletions whitenoise-mac/Core/WorkspaceState+NewChat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ extension WorkspaceState {
)
}
let displayName = firstNonBlank([
resolved?.profileDisplayName,
resolved?.profileName,
resolved?.directoryDisplayName,
PeerDisplayText.sanitize(resolved?.profileDisplayName),
PeerDisplayText.sanitize(resolved?.profileName),
PeerDisplayText.sanitize(resolved?.directoryDisplayName),
])
return NewChatRecipient(
sourceQuery: query,
Expand Down
17 changes: 9 additions & 8 deletions whitenoise-mac/Core/WorkspaceState+Notifications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,11 @@ extension WorkspaceState {

func localNotificationRequest(for update: NotificationUpdateFfi) -> LocalNotificationRequest {
let senderName =
firstNonBlank([
update.sender.displayName,
update.sender.accountIdHex,
]) ?? L10n.string("Someone")
PeerDisplayText.sanitize(update.sender.displayName)
?? PeerDisplayText.sanitize(update.sender.accountIdHex)
?? L10n.string("Someone")
let senderTemplateName = PeerDisplayText.templateFragment(senderName)
let groupName = PeerDisplayText.sanitize(update.groupName)
let previewText = firstNonBlank([update.previewText]) ?? L10n.string("New message")

// For an E2EE messenger, notification content is rendered as banners,
Expand All @@ -233,7 +234,7 @@ extension WorkspaceState {
body = L10n.string("New group invite")
} else {
title = L10n.string("Group invite")
body = firstNonBlank([update.groupName, senderName]) ?? L10n.string("New group invite")
body = groupName ?? senderName
}
case .newMessage:
switch previewMode {
Expand All @@ -242,15 +243,15 @@ extension WorkspaceState {
title = senderName
body = previewText
} else {
title = firstNonBlank([update.groupName]) ?? L10n.string("New message")
body = "\(senderName): \(previewText)"
title = groupName ?? L10n.string("New message")
body = "\(senderTemplateName): \(previewText)"
}
case .senderOnly:
if update.isDm {
title = senderName
body = genericBody
} else {
title = firstNonBlank([update.groupName]) ?? L10n.string("New message")
title = groupName ?? L10n.string("New message")
body = senderName
}
case .hidden:
Expand Down
12 changes: 8 additions & 4 deletions whitenoise-mac/Core/WorkspaceState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1660,12 +1660,16 @@ final class WorkspaceState {
let members = details.members
.map { member in
let action = actionByMemberId[member.memberIdHex]
let displayName =
firstNonBlank([
PeerDisplayText.sanitize(member.displayName),
PeerDisplayText.sanitize(member.account),
]) ?? DisplayText.short(member.npub, head: 12, tail: 8)
return GroupMemberItem(
id: member.memberIdHex,
displayName: firstNonBlank([member.displayName, member.account])
?? DisplayText.short(member.npub, head: 12, tail: 8),
displayName: displayName,
npub: member.npub,
accountLabel: member.account,
accountLabel: PeerDisplayText.sanitize(member.account),
isLocal: member.local,
isAdmin: member.isAdmin,
isSelf: member.isSelf,
Expand All @@ -1684,7 +1688,7 @@ final class WorkspaceState {
return GroupDetailsSnapshot(
groupIdHex: details.group.groupIdHex,
endpoint: details.group.endpoint,
name: firstNonBlank([details.group.name]) ?? L10n.string("Unnamed group"),
name: PeerDisplayText.sanitize(details.group.name) ?? L10n.string("Unnamed group"),
description: details.group.description,
avatarURL: avatarURL,
sanitizedAvatarURL: RemoteImageURLPolicy.sanitizedURL(from: avatarURL),
Expand Down
10 changes: 3 additions & 7 deletions whitenoise-mac/Models/MessengerModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ struct GroupMemberItem: Identifiable, Hashable {
if isSelf {
return L10n.string("You")
}
if let accountLabel, !accountLabel.isEmpty {
if let accountLabel = PeerDisplayText.sanitize(accountLabel) {
return accountLabel
}
return DisplayText.short(npub, head: 12, tail: 8)
Expand Down Expand Up @@ -2204,17 +2204,13 @@ struct NewChatRecipient: Equatable {
self.memberRef = memberRef
self.accountIdHex = accountIdHex
self.npub = npub
self.displayName = displayName
self.displayName = PeerDisplayText.sanitize(displayName)
self.pictureURL = pictureURL
self.sanitizedPictureURL = RemoteImageURLPolicy.sanitizedURL(from: pictureURL)
}

var title: String {
guard let displayName = displayName?.trimmingCharacters(in: .whitespacesAndNewlines),
!displayName.isEmpty
else { return DisplayText.short(accountIdHex) }

return displayName
displayName ?? DisplayText.short(accountIdHex)
}

var subtitle: String {
Expand Down
2 changes: 1 addition & 1 deletion whitenoise-mac/Views/GroupViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ struct GroupMemberRow: View {
.disabled(workspace.hasInFlightGroupDetailsMutation)
Button("Cancel", role: .cancel) {}
} message: {
Text("This removes \(member.displayName) from the group.")
Text("This removes \(PeerDisplayText.templateFragment(member.displayName)) from the group.")
}
}
}
Expand Down
Loading
Loading