Refactor room commands and improve UX

- Replace /list with /rooms showing all discovered rooms with join status
- Remove /discover command (merged into /rooms)
- Rename /favorite to /save for clarity
- Make room-specific commands (/transfer, /pass, /save) only appear in room context
- Remove voice notes from app info (feature doesn't exist)
- Remove Commands line from app info screen
- Show checkmark (✓) for joined rooms in /rooms output
This commit is contained in:
jack
2025-07-05 19:35:37 +02:00
parent 506c2bc7cb
commit 41c2899008
4 changed files with 72 additions and 92 deletions
+5 -4
View File
@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 54; objectVersion = 63;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -76,10 +76,10 @@
6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = "<group>"; }; 6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = "<group>"; };
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = "<group>"; }; 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = "<group>"; };
7EEBDA723E1CFD88758DA4AC /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7EEBDA723E1CFD88758DA4AC /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
822AA698BDB7BB939B62A010 /* BloomFilterTests.swift.disabled */ = {isa = PBXFileReference; path = BloomFilterTests.swift.disabled; sourceTree = "<group>"; }; 822AA698BDB7BB939B62A010 /* BloomFilterTests.swift.disabled */ = {isa = PBXFileReference; lastKnownFileType = text; path = BloomFilterTests.swift.disabled; sourceTree = "<group>"; };
8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePaddingTests.swift; sourceTree = "<group>"; }; 8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePaddingTests.swift; sourceTree = "<group>"; };
96CDF6D0AF0F2052A6C7E634 /* LoggingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingService.swift; sourceTree = "<group>"; }; 96CDF6D0AF0F2052A6C7E634 /* LoggingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingService.swift; sourceTree = "<group>"; };
997D512074C64904D75DDD40 /* bitchat.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; 997D512074C64904D75DDD40 /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; }; A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; };
AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRetryService.swift; sourceTree = "<group>"; }; AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRetryService.swift; sourceTree = "<group>"; };
@@ -284,7 +284,6 @@
); );
mainGroup = 18198ED912AAF495D8AF7763; mainGroup = 18198ED912AAF495D8AF7763;
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 54;
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
targets = ( targets = (
@@ -476,6 +475,7 @@
CODE_SIGNING_REQUIRED = YES; CODE_SIGNING_REQUIRED = YES;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = L3N5LHJD5Y;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_FILE = bitchat/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
@@ -521,6 +521,7 @@
CODE_SIGNING_REQUIRED = YES; CODE_SIGNING_REQUIRED = YES;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = L3N5LHJD5Y;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_FILE = bitchat/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+44 -68
View File
@@ -1445,25 +1445,54 @@ extension ChatViewModel: BitchatDelegate {
) )
messages.append(systemMessage) messages.append(systemMessage)
} }
case "/list": case "/rooms":
if joinedRooms.isEmpty { // Discover all rooms (both joined and not joined)
var allRooms: Set<String> = Set()
// Add joined rooms
allRooms.formUnion(joinedRooms)
// Find rooms from messages we've seen
for msg in messages {
if let room = msg.room {
allRooms.insert(room)
}
}
// Also check room messages we've cached
for (room, _) in roomMessages {
allRooms.insert(room)
}
// Add password protected rooms we know about
allRooms.formUnion(passwordProtectedRooms)
if allRooms.isEmpty {
let systemMessage = BitchatMessage( let systemMessage = BitchatMessage(
sender: "system", sender: "system",
content: "you haven't joined any rooms yet. use /create or /join to get started.", content: "no rooms discovered yet. rooms appear as people use them.",
timestamp: Date(), timestamp: Date(),
isRelay: false isRelay: false
) )
messages.append(systemMessage) messages.append(systemMessage)
} else { } else {
let roomList = joinedRooms.sorted().map { room in let roomList = allRooms.sorted().map { room in
let isProtected = passwordProtectedRooms.contains(room) ? " 🔒" : "" var status = ""
let isCreator = roomCreators[room] == meshService.myPeerID ? " (owner)" : "" if joinedRooms.contains(room) {
return "\(room)\(isProtected)\(isCreator)" status += ""
}
if passwordProtectedRooms.contains(room) {
status += " 🔒"
}
if roomCreators[room] == meshService.myPeerID {
status += " (owner)"
}
return "\(room)\(status)"
}.joined(separator: "\n") }.joined(separator: "\n")
let systemMessage = BitchatMessage( let systemMessage = BitchatMessage(
sender: "system", sender: "system",
content: "your rooms:\n\(roomList)", content: "discovered rooms:\n\(roomList)\n\n✓ = joined",
timestamp: Date(), timestamp: Date(),
isRelay: false isRelay: false
) )
@@ -1492,59 +1521,6 @@ extension ChatViewModel: BitchatDelegate {
) )
messages.append(systemMessage) messages.append(systemMessage)
} }
case "/discover":
// Discover public rooms
var publicRooms: [String] = []
var protectedRooms: [String] = []
// Find rooms from messages we've seen
for msg in messages {
if let room = msg.room {
if passwordProtectedRooms.contains(room) {
if !protectedRooms.contains(room) {
protectedRooms.append(room)
}
} else {
if !publicRooms.contains(room) {
publicRooms.append(room)
}
}
}
}
// Also check room messages we've cached
for (room, _) in roomMessages {
if passwordProtectedRooms.contains(room) {
if !protectedRooms.contains(room) {
protectedRooms.append(room)
}
} else {
if !publicRooms.contains(room) {
publicRooms.append(room)
}
}
}
var discoveryMessage = ""
if publicRooms.isEmpty && protectedRooms.isEmpty {
discoveryMessage = "no rooms discovered yet. rooms appear as people use them."
} else {
if !publicRooms.isEmpty {
discoveryMessage += "public rooms:\n" + publicRooms.sorted().joined(separator: ", ")
}
if !protectedRooms.isEmpty {
if !discoveryMessage.isEmpty { discoveryMessage += "\n\n" }
discoveryMessage += "protected rooms:\n" + protectedRooms.sorted().map { "\($0) 🔒" }.joined(separator: ", ")
}
}
let systemMessage = BitchatMessage(
sender: "system",
content: discoveryMessage,
timestamp: Date(),
isRelay: false
)
messages.append(systemMessage)
case "/transfer": case "/transfer":
// Transfer room ownership // Transfer room ownership
let parts = command.split(separator: " ", maxSplits: 1).map(String.init) let parts = command.split(separator: " ", maxSplits: 1).map(String.init)
@@ -1598,12 +1574,12 @@ extension ChatViewModel: BitchatDelegate {
messages.removeAll() messages.removeAll()
bitchatLog("cleared main chat", category: "chat") bitchatLog("cleared main chat", category: "chat")
} }
case "/favorite", "/fav": case "/save":
// Toggle favorite status for current room // Toggle save status for current room
guard let room = currentRoom else { guard let room = currentRoom else {
let systemMessage = BitchatMessage( let systemMessage = BitchatMessage(
sender: "system", sender: "system",
content: "you must be in a room to mark it as favorite.", content: "you must be in a room to save it.",
timestamp: Date(), timestamp: Date(),
isRelay: false isRelay: false
) )
@@ -1612,7 +1588,7 @@ extension ChatViewModel: BitchatDelegate {
} }
let isFavorite = MessageRetentionService.shared.toggleFavoriteRoom(room) let isFavorite = MessageRetentionService.shared.toggleFavoriteRoom(room)
let status = isFavorite ? "added to" : "removed from" let status = isFavorite ? "saved" : "unsaved"
// If just marked as favorite, load any previously saved messages // If just marked as favorite, load any previously saved messages
if isFavorite { if isFavorite {
@@ -1634,7 +1610,7 @@ extension ChatViewModel: BitchatDelegate {
let systemMessage = BitchatMessage( let systemMessage = BitchatMessage(
sender: "system", sender: "system",
content: "room \(room) \(status) favorites. loaded \(savedMessages.count) saved messages.", content: "room \(room) \(status). loaded \(savedMessages.count) saved messages.",
timestamp: Date(), timestamp: Date(),
isRelay: false isRelay: false
) )
@@ -1642,7 +1618,7 @@ extension ChatViewModel: BitchatDelegate {
} else { } else {
let systemMessage = BitchatMessage( let systemMessage = BitchatMessage(
sender: "system", sender: "system",
content: "room \(room) \(status) favorites.", content: "room \(room) \(status).",
timestamp: Date(), timestamp: Date(),
isRelay: false isRelay: false
) )
@@ -1651,7 +1627,7 @@ extension ChatViewModel: BitchatDelegate {
} else { } else {
let systemMessage = BitchatMessage( let systemMessage = BitchatMessage(
sender: "system", sender: "system",
content: "room \(room) \(status) favorites.", content: "room \(room) \(status).",
timestamp: Date(), timestamp: Date(),
isRelay: false isRelay: false
) )
-8
View File
@@ -70,9 +70,6 @@ struct AppInfoView: View {
FeatureRow(icon: "lock.fill", title: "Password Rooms", FeatureRow(icon: "lock.fill", title: "Password Rooms",
description: "Secure rooms with passwords and AES encryption") description: "Secure rooms with passwords and AES encryption")
FeatureRow(icon: "mic.fill", title: "Voice Notes",
description: "Push-to-talk voice messages with compression")
} }
// Privacy // Privacy
@@ -117,7 +114,6 @@ struct AppInfoView: View {
Text("Battery: Adaptive scanning based on level") Text("Battery: Adaptive scanning based on level")
Text("Platform: Universal (iOS, iPadOS, macOS)") Text("Platform: Universal (iOS, iPadOS, macOS)")
Text("Rooms: Password-protected with key commitments") Text("Rooms: Password-protected with key commitments")
Text("Commands: /j /m /w /list /pass /favorite /discover")
Text("Storage: Keychain for passwords, encrypted retention") Text("Storage: Keychain for passwords, encrypted retention")
} }
.font(.system(size: 14, design: .monospaced)) .font(.system(size: 14, design: .monospaced))
@@ -203,9 +199,6 @@ struct AppInfoView: View {
FeatureRow(icon: "lock.fill", title: "Password Rooms", FeatureRow(icon: "lock.fill", title: "Password Rooms",
description: "Secure rooms with passwords and AES encryption") description: "Secure rooms with passwords and AES encryption")
FeatureRow(icon: "mic.fill", title: "Voice Notes",
description: "Push-to-talk voice messages with compression")
} }
// Privacy // Privacy
@@ -250,7 +243,6 @@ struct AppInfoView: View {
Text("Battery: Adaptive scanning based on level") Text("Battery: Adaptive scanning based on level")
Text("Platform: Universal (iOS, iPadOS, macOS)") Text("Platform: Universal (iOS, iPadOS, macOS)")
Text("Rooms: Password-protected with key commitments") Text("Rooms: Password-protected with key commitments")
Text("Commands: /j /m /w /list /pass /favorite /discover")
Text("Storage: Keychain for passwords, encrypted retention") Text("Storage: Keychain for passwords, encrypted retention")
} }
.font(.system(size: 14, design: .monospaced)) .font(.system(size: 14, design: .monospaced))
+23 -12
View File
@@ -506,18 +506,24 @@ struct ContentView: View {
// Command suggestions // Command suggestions
if showCommandSuggestions && !commandSuggestions.isEmpty { if showCommandSuggestions && !commandSuggestions.isEmpty {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
let commandDescriptions = [ let baseCommands: [String: String] = [
"/j": "join or create a room", "/j": "join or create a room",
"/list": "show your joined rooms", "/rooms": "show all discovered rooms",
"/w": "see who's online", "/w": "see who's online",
"/m": "send private message", "/m": "send private message",
"/clear": "clear chat messages", "/clear": "clear chat messages"
]
let roomCommands: [String: String] = [
"/transfer": "transfer room ownership", "/transfer": "transfer room ownership",
"/pass": "change room password", "/pass": "change room password",
"/favorite": "toggle room retention", "/save": "save room messages locally"
"/discover": "find active rooms"
] ]
let commandDescriptions = viewModel.currentRoom != nil
? baseCommands.merging(roomCommands) { (_, new) in new }
: baseCommands
ForEach(commandSuggestions, id: \.self) { command in ForEach(commandSuggestions, id: \.self) { command in
Button(action: { Button(action: {
// Replace current text with selected command // Replace current text with selected command
@@ -590,17 +596,22 @@ struct ContentView: View {
// Check for command autocomplete // Check for command autocomplete
if newValue.hasPrefix("/") && newValue.count >= 1 { if newValue.hasPrefix("/") && newValue.count >= 1 {
let commandDescriptions = [ // Build context-aware command list
var commandDescriptions = [
("/j", "join or create a room"), ("/j", "join or create a room"),
("/list", "show your joined rooms"), ("/rooms", "show all discovered rooms"),
("/w", "see who's online"), ("/w", "see who's online"),
("/m", "send private message"), ("/m", "send private message"),
("/clear", "clear chat messages"), ("/clear", "clear chat messages")
("/transfer", "transfer room ownership"),
("/pass", "change room password"),
("/favorite", "toggle room retention"),
("/discover", "find active rooms")
] ]
// Add room-specific commands if in a room
if viewModel.currentRoom != nil {
commandDescriptions.append(("/transfer", "transfer room ownership"))
commandDescriptions.append(("/pass", "change room password"))
commandDescriptions.append(("/save", "save room messages locally"))
}
let input = newValue.lowercased() let input = newValue.lowercased()
commandSuggestions = commandDescriptions commandSuggestions = commandDescriptions
.filter { $0.0.starts(with: input) } .filter { $0.0.starts(with: input) }