diff --git a/bitchat/Localizable.xcstrings b/bitchat/Localizable.xcstrings index be1bba06..d7c842cd 100644 --- a/bitchat/Localizable.xcstrings +++ b/bitchat/Localizable.xcstrings @@ -14707,6 +14707,18 @@ } } }, + "content.commands.help" : { + "comment" : "Description of the /help command in the suggestions panel", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "show available commands" + } + } + } + }, "content.commands.hug" : { "extractionState" : "manual", "localizations" : { diff --git a/bitchat/Models/CommandInfo.swift b/bitchat/Models/CommandInfo.swift index 0bf25537..1f8245be 100644 --- a/bitchat/Models/CommandInfo.swift +++ b/bitchat/Models/CommandInfo.swift @@ -11,33 +11,38 @@ import Foundation // MARK: - CommandInfo Enum enum CommandInfo: String, Identifiable { + // Raw values must match the aliases CommandProcessor actually accepts — + // the suggestion panel is the app's only command-discovery surface, and + // suggesting a spelling the processor rejects teaches users dead ends. case block case clear + case help case hug - case message = "dm" + case message = "msg" case slap case unblock case who - case favorite - case unfavorite - + case favorite = "fav" + case unfavorite = "unfav" + var id: String { rawValue } - + var alias: String { "/" + rawValue } - + var placeholder: String? { switch self { case .block, .hug, .message, .slap, .unblock, .favorite, .unfavorite: return "<" + String(localized: "content.input.nickname_placeholder") + ">" - case .clear, .who: + case .clear, .help, .who: return nil } } - + var description: String { switch self { case .block: String(localized: "content.commands.block") case .clear: String(localized: "content.commands.clear") + case .help: String(localized: "content.commands.help") case .hug: String(localized: "content.commands.hug") case .message: String(localized: "content.commands.message") case .slap: String(localized: "content.commands.slap") @@ -47,12 +52,14 @@ enum CommandInfo: String, Identifiable { case .unfavorite: String(localized: "content.commands.unfavorite") } } - + static func all(isGeoPublic: Bool, isGeoDM: Bool) -> [CommandInfo] { - let baseCommands: [CommandInfo] = [.block, .unblock, .clear, .hug, .message, .slap, .who] + let baseCommands: [CommandInfo] = [.block, .unblock, .clear, .help, .hug, .message, .slap, .who] + // The processor rejects favorites in geohash contexts, so only + // suggest them where they actually work: mesh. if isGeoPublic || isGeoDM { - return baseCommands + [.favorite, .unfavorite] + return baseCommands } - return baseCommands + return baseCommands + [.favorite, .unfavorite] } } diff --git a/bitchat/Services/CommandProcessor.swift b/bitchat/Services/CommandProcessor.swift index e9e81654..680c20f8 100644 --- a/bitchat/Services/CommandProcessor.swift +++ b/bitchat/Services/CommandProcessor.swift @@ -105,11 +105,28 @@ final class CommandProcessor { case "/unfav": if inGeoPublic || inGeoDM { return .error(message: "favorites are only for mesh peers in #mesh") } return handleFavorite(args, add: false) + case "/help": + return .success(message: Self.helpText) default: - return .error(message: "unknown command: \(cmd)") + return .error(message: "unknown command: \(cmd) — type /help for commands") } } + /// Local-only command reference, printed as a system message. The + /// suggestion panel hides once arguments are typed, and typos used to + /// dead-end in a bare "unknown command" — this is the way out. + static let helpText = """ + commands: + /msg @name [message] — start a private chat + /who — list who's here + /clear — clear this chat + /hug @name — send a hug + /slap @name — slap with a large trout + /block @name · /unblock @name + /fav @name · /unfav @name — favorites (mesh only) + /help — this list + """ + // MARK: - Command Handlers private func handleMessage(_ args: String) -> CommandResult { diff --git a/bitchat/Views/Components/CommandSuggestionsView.swift b/bitchat/Views/Components/CommandSuggestionsView.swift index 786a3caf..ca357771 100644 --- a/bitchat/Views/Components/CommandSuggestionsView.swift +++ b/bitchat/Views/Components/CommandSuggestionsView.swift @@ -14,23 +14,41 @@ struct CommandSuggestionsView: View { @Binding var messageText: String + /// The command already typed in full, once arguments have begun. + private var typedCommandAlias: String? { + guard messageText.hasPrefix("/"), + let spaceIndex = messageText.firstIndex(of: " ") + else { return nil } + return String(messageText[..