mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 03:45:20 +00:00
The autocomplete panel is the only in-app surface for discovering slash
commands, but several suggestions do not match what CommandProcessor
accepts, so tapping them inserts a command that returns "unknown
command":
- CommandInfo suggests /dm, /favorite, /unfavorite, but the processor
only handles /m, /msg, /fav, /unfav. Aliases are aligned to the
accepted spellings (msg, fav, unfav).
- Favorites are suggested only in geohash contexts (isGeoPublic ||
isGeoDM) — exactly where the processor rejects them ("favorites are
only for mesh peers"). The gating is inverted so they appear in mesh,
where they work.
Also, small related fixes to the discovery surface:
- /help is now handled (the ChatViewModel command docstring already
claimed it existed); it prints a local system line listing the valid
commands, and the unknown-command error points at it.
- The suggestion panel keeps the matched command's usage row (e.g.
"/msg <nickname>") visible while arguments are typed, instead of
vanishing at the first space; in that mode the row is informational
and no longer overwrites the draft on tap.
New string is added source-language (en) only. The CommandInfo contract
test is updated to the corrected metadata.
108 lines
3.8 KiB
Swift
108 lines
3.8 KiB
Swift
//
|
|
// CommandSuggestionsView.swift
|
|
// bitchat
|
|
//
|
|
// Created by Islam on 29/10/2025.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct CommandSuggestionsView: View {
|
|
@EnvironmentObject private var privateConversationModel: PrivateConversationModel
|
|
@EnvironmentObject private var locationChannelsModel: LocationChannelsModel
|
|
@ThemedPalette private var palette
|
|
|
|
@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[..<spaceIndex]).lowercased()
|
|
}
|
|
|
|
private var filteredCommands: [CommandInfo] {
|
|
guard messageText.hasPrefix("/") else { return [] }
|
|
let isGeoPublic = locationChannelsModel.selectedChannel.isLocation
|
|
let isGeoDM = privateConversationModel.selectedPeerID?.isGeoDM == true
|
|
let commands = CommandInfo.all(isGeoPublic: isGeoPublic, isGeoDM: isGeoDM)
|
|
// While arguments are being typed, keep the matched command's usage
|
|
// row visible instead of vanishing at the first space.
|
|
if let typed = typedCommandAlias {
|
|
return commands.filter { $0.alias == typed && $0.placeholder != nil }
|
|
}
|
|
return commands.filter { command in
|
|
command.alias.starts(with: messageText.lowercased())
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
// Render nothing when there are no matches: a zero-height view would
|
|
// still receive the composer VStack's spacing and push the input row
|
|
// off-center.
|
|
if !filteredCommands.isEmpty {
|
|
let isUsageReminder = typedCommandAlias != nil
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
ForEach(filteredCommands) { command in
|
|
Button {
|
|
// In usage-reminder mode the row is informational; an
|
|
// insert here would wipe the arguments being typed.
|
|
guard !isUsageReminder else { return }
|
|
messageText = command.alias + " "
|
|
} label: {
|
|
buttonRow(for: command)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.background(Color.gray.opacity(0.1))
|
|
}
|
|
}
|
|
.themedOverlayPanel()
|
|
}
|
|
}
|
|
|
|
private func buttonRow(for command: CommandInfo) -> some View {
|
|
HStack {
|
|
Text(command.alias)
|
|
.bitchatFont(size: 11)
|
|
.foregroundColor(palette.primary)
|
|
.fontWeight(.medium)
|
|
|
|
if let placeholder = command.placeholder {
|
|
Text(placeholder)
|
|
.bitchatFont(size: 10)
|
|
.foregroundColor(palette.secondary.opacity(0.8))
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Text(command.description)
|
|
.bitchatFont(size: 10)
|
|
.foregroundColor(palette.secondary)
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 3)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
|
|
@available(iOS 17, macOS 14, *)
|
|
#Preview {
|
|
@Previewable @State var messageText: String = "/"
|
|
let keychain = KeychainManager()
|
|
let viewModel = ChatViewModel(
|
|
keychain: keychain,
|
|
idBridge: NostrIdentityBridge(),
|
|
identityManager: SecureIdentityStateManager(keychain)
|
|
)
|
|
let privateConversationModel = PrivateConversationModel(
|
|
chatViewModel: viewModel,
|
|
conversations: viewModel.conversations
|
|
)
|
|
let locationChannelsModel = LocationChannelsModel()
|
|
|
|
CommandSuggestionsView(messageText: $messageText)
|
|
.environmentObject(privateConversationModel)
|
|
.environmentObject(locationChannelsModel)
|
|
}
|