mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 02:25:20 +00:00
Add localizable strings in show commands view (#828)
* Add localizable strings in show commands view n refactor code to remove duplicate string declarations. * Modify struct to enum in separate file to Models/CommandsInfo. * Add corrections code in refactory and enum. * Correct code in command enum and ContentView. * Dedicated CommandSuggestionsView to extract code * Simplify CommandInfo + minor optimization * Fix preview --------- Co-authored-by: islam <2553451+qalandarov@users.noreply.github.com>
This commit is contained in:
co-authored by
islam
parent
79bd4af912
commit
97fc21c23f
@@ -0,0 +1,58 @@
|
|||||||
|
//
|
||||||
|
// CommandsInfo.swift
|
||||||
|
// bitchat
|
||||||
|
//
|
||||||
|
// This is free and unencumbered software released into the public domain.
|
||||||
|
// For more information, see <https://unlicense.org>
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - CommandInfo Enum
|
||||||
|
|
||||||
|
enum CommandInfo: String, Identifiable {
|
||||||
|
case block
|
||||||
|
case clear
|
||||||
|
case hug
|
||||||
|
case message = "dm"
|
||||||
|
case slap
|
||||||
|
case unblock
|
||||||
|
case who
|
||||||
|
case favorite
|
||||||
|
case unfavorite
|
||||||
|
|
||||||
|
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:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .block: String(localized: "content.commands.block")
|
||||||
|
case .clear: String(localized: "content.commands.clear")
|
||||||
|
case .hug: String(localized: "content.commands.hug")
|
||||||
|
case .message: String(localized: "content.commands.message")
|
||||||
|
case .slap: String(localized: "content.commands.slap")
|
||||||
|
case .unblock: String(localized: "content.commands.unblock")
|
||||||
|
case .who: String(localized: "content.commands.who")
|
||||||
|
case .favorite: String(localized: "content.commands.favorite")
|
||||||
|
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]
|
||||||
|
if isGeoPublic || isGeoDM {
|
||||||
|
return baseCommands + [.favorite, .unfavorite]
|
||||||
|
}
|
||||||
|
return baseCommands
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -116,4 +116,18 @@ enum ChannelID: Equatable, Codable {
|
|||||||
case .location(let ch): return ch.geohash
|
case .location(let ch): return ch.geohash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isMesh: Bool {
|
||||||
|
switch self {
|
||||||
|
case .mesh: true
|
||||||
|
case .location: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isLocation: Bool {
|
||||||
|
switch self {
|
||||||
|
case .mesh: false
|
||||||
|
case .location: true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
//
|
||||||
|
// CommandSuggestionsView.swift
|
||||||
|
// bitchat
|
||||||
|
//
|
||||||
|
// Created by Islam on 29/10/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CommandSuggestionsView: View {
|
||||||
|
@EnvironmentObject private var viewModel: ChatViewModel
|
||||||
|
@ObservedObject private var locationManager = LocationChannelManager.shared
|
||||||
|
|
||||||
|
@Binding var messageText: String
|
||||||
|
|
||||||
|
let textColor: Color
|
||||||
|
let backgroundColor: Color
|
||||||
|
let secondaryTextColor: Color
|
||||||
|
|
||||||
|
private var filteredCommands: [CommandInfo] {
|
||||||
|
guard messageText.hasPrefix("/") && !messageText.contains(" ") else { return [] }
|
||||||
|
let isGeoPublic = locationManager.selectedChannel.isLocation
|
||||||
|
let isGeoDM = viewModel.selectedPrivateChatPeer?.isGeoDM == true
|
||||||
|
return CommandInfo.all(isGeoPublic: isGeoPublic, isGeoDM: isGeoDM).filter { command in
|
||||||
|
command.alias.starts(with: messageText.lowercased())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
ForEach(filteredCommands) { command in
|
||||||
|
Button {
|
||||||
|
messageText = command.alias + " "
|
||||||
|
} label: {
|
||||||
|
buttonRow(for: command)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.background(Color.gray.opacity(0.1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(backgroundColor)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.stroke(secondaryTextColor.opacity(0.3), lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buttonRow(for command: CommandInfo) -> some View {
|
||||||
|
HStack {
|
||||||
|
Text(command.alias)
|
||||||
|
.font(.bitchatSystem(size: 11, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
|
||||||
|
if let placeholder = command.placeholder {
|
||||||
|
Text(placeholder)
|
||||||
|
.font(.bitchatSystem(size: 10, design: .monospaced))
|
||||||
|
.foregroundColor(secondaryTextColor.opacity(0.8))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text(command.description)
|
||||||
|
.font(.bitchatSystem(size: 10, design: .monospaced))
|
||||||
|
.foregroundColor(secondaryTextColor)
|
||||||
|
}
|
||||||
|
.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)
|
||||||
|
)
|
||||||
|
|
||||||
|
CommandSuggestionsView(
|
||||||
|
messageText: $messageText,
|
||||||
|
textColor: .green,
|
||||||
|
backgroundColor: .primary,
|
||||||
|
secondaryTextColor: .secondary
|
||||||
|
)
|
||||||
|
.environmentObject(viewModel)
|
||||||
|
}
|
||||||
@@ -43,8 +43,6 @@ struct ContentView: View {
|
|||||||
@State private var showPeerList = false
|
@State private var showPeerList = false
|
||||||
@State private var showSidebar = false
|
@State private var showSidebar = false
|
||||||
@State private var showAppInfo = false
|
@State private var showAppInfo = false
|
||||||
@State private var showCommandSuggestions = false
|
|
||||||
@State private var commandSuggestions: [String] = []
|
|
||||||
@State private var showMessageActions = false
|
@State private var showMessageActions = false
|
||||||
@State private var selectedMessageSender: String?
|
@State private var selectedMessageSender: String?
|
||||||
@State private var selectedMessageSenderID: PeerID?
|
@State private var selectedMessageSenderID: PeerID?
|
||||||
@@ -605,77 +603,12 @@ struct ContentView: View {
|
|||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command suggestions
|
CommandSuggestionsView(
|
||||||
if showCommandSuggestions && !commandSuggestions.isEmpty {
|
messageText: $messageText,
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
textColor: textColor,
|
||||||
// Define commands with aliases and syntax
|
backgroundColor: backgroundColor,
|
||||||
let baseInfo: [(commands: [String], syntax: String?, description: String)] = [
|
secondaryTextColor: secondaryTextColor
|
||||||
(["/block"], "[nickname]", "block or list blocked peers"),
|
)
|
||||||
(["/clear"], nil, "clear chat messages"),
|
|
||||||
(["/hug"], "<nickname>", "send someone a warm hug"),
|
|
||||||
(["/m", "/msg"], "<nickname> [message]", "send private message"),
|
|
||||||
(["/slap"], "<nickname>", "slap someone with a trout"),
|
|
||||||
(["/unblock"], "<nickname>", "unblock a peer"),
|
|
||||||
(["/w"], nil, "see who's online")
|
|
||||||
]
|
|
||||||
let isGeoPublic: Bool = { if case .location = locationManager.selectedChannel { return true }; return false }()
|
|
||||||
let isGeoDM = viewModel.selectedPrivateChatPeer?.isGeoDM == true
|
|
||||||
let favInfo: [(commands: [String], syntax: String?, description: String)] = [
|
|
||||||
(["/fav"], "<nickname>", "add to favorites"),
|
|
||||||
(["/unfav"], "<nickname>", "remove from favorites")
|
|
||||||
]
|
|
||||||
let commandInfo = baseInfo + ((isGeoPublic || isGeoDM) ? [] : favInfo)
|
|
||||||
|
|
||||||
// Build the display
|
|
||||||
let allCommands = commandInfo
|
|
||||||
|
|
||||||
// Show matching commands
|
|
||||||
ForEach(commandSuggestions, id: \.self) { command in
|
|
||||||
// Find the command info for this suggestion
|
|
||||||
if let info = allCommands.first(where: { $0.commands.contains(command) }) {
|
|
||||||
Button(action: {
|
|
||||||
// Replace current text with selected command
|
|
||||||
messageText = command + " "
|
|
||||||
showCommandSuggestions = false
|
|
||||||
commandSuggestions = []
|
|
||||||
}) {
|
|
||||||
HStack {
|
|
||||||
// Show all aliases together
|
|
||||||
Text(info.commands.joined(separator: ", "))
|
|
||||||
.font(.bitchatSystem(size: 11, design: .monospaced))
|
|
||||||
.foregroundColor(textColor)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
|
|
||||||
// Show syntax if any
|
|
||||||
if let syntax = info.syntax {
|
|
||||||
Text(syntax)
|
|
||||||
.font(.bitchatSystem(size: 10, design: .monospaced))
|
|
||||||
.foregroundColor(secondaryTextColor.opacity(0.8))
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// Show description
|
|
||||||
Text(info.description)
|
|
||||||
.font(.bitchatSystem(size: 10, design: .monospaced))
|
|
||||||
.foregroundColor(secondaryTextColor)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 3)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.background(Color.gray.opacity(0.1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(backgroundColor)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 4)
|
|
||||||
.stroke(secondaryTextColor.opacity(0.3), lineWidth: 1)
|
|
||||||
)
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recording indicator
|
// Recording indicator
|
||||||
if isPreparingVoiceNote || isRecordingVoiceNote {
|
if isPreparingVoiceNote || isRecordingVoiceNote {
|
||||||
@@ -709,68 +642,11 @@ struct ContentView: View {
|
|||||||
)
|
)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.onChange(of: messageText) { newValue in
|
.onChange(of: messageText) { newValue in
|
||||||
// Cancel previous debounce timer
|
|
||||||
autocompleteDebounceTimer?.invalidate()
|
autocompleteDebounceTimer?.invalidate()
|
||||||
|
|
||||||
// Debounce autocomplete updates to reduce calls during rapid typing
|
|
||||||
autocompleteDebounceTimer = Timer.scheduledTimer(withTimeInterval: 0.15, repeats: false) { _ in
|
autocompleteDebounceTimer = Timer.scheduledTimer(withTimeInterval: 0.15, repeats: false) { _ in
|
||||||
// Get cursor position (approximate - end of text for now)
|
|
||||||
let cursorPosition = newValue.count
|
let cursorPosition = newValue.count
|
||||||
viewModel.updateAutocomplete(for: newValue, cursorPosition: cursorPosition)
|
viewModel.updateAutocomplete(for: newValue, cursorPosition: cursorPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for command autocomplete (instant, no debounce needed)
|
|
||||||
if newValue.hasPrefix("/") && newValue.count >= 1 {
|
|
||||||
// Build context-aware command list
|
|
||||||
let isGeoPublic: Bool = {
|
|
||||||
if case .location = locationManager.selectedChannel { return true }
|
|
||||||
return false
|
|
||||||
}()
|
|
||||||
let isGeoDM = viewModel.selectedPrivateChatPeer?.isGeoDM == true
|
|
||||||
var commandDescriptions = [
|
|
||||||
("/block", String(localized: "content.commands.block", comment: "Description for /block command")),
|
|
||||||
("/clear", String(localized: "content.commands.clear", comment: "Description for /clear command")),
|
|
||||||
("/hug", String(localized: "content.commands.hug", comment: "Description for /hug command")),
|
|
||||||
("/m", String(localized: "content.commands.message", comment: "Description for /m command")),
|
|
||||||
("/slap", String(localized: "content.commands.slap", comment: "Description for /slap command")),
|
|
||||||
("/unblock", String(localized: "content.commands.unblock", comment: "Description for /unblock command")),
|
|
||||||
("/w", String(localized: "content.commands.who", comment: "Description for /w command"))
|
|
||||||
]
|
|
||||||
// Only show favorites commands when not in geohash context
|
|
||||||
if !(isGeoPublic || isGeoDM) {
|
|
||||||
commandDescriptions.append(("/fav", String(localized: "content.commands.favorite", comment: "Description for /fav command")))
|
|
||||||
commandDescriptions.append(("/unfav", String(localized: "content.commands.unfavorite", comment: "Description for /unfav command")))
|
|
||||||
}
|
|
||||||
|
|
||||||
let input = newValue.lowercased()
|
|
||||||
|
|
||||||
// Map of aliases to primary commands
|
|
||||||
let aliases: [String: String] = [
|
|
||||||
"/join": "/j",
|
|
||||||
"/msg": "/m"
|
|
||||||
]
|
|
||||||
|
|
||||||
// Filter commands, but convert aliases to primary
|
|
||||||
commandSuggestions = commandDescriptions
|
|
||||||
.filter { $0.0.starts(with: input) }
|
|
||||||
.map { $0.0 }
|
|
||||||
|
|
||||||
// Also check if input matches an alias
|
|
||||||
for (alias, primary) in aliases {
|
|
||||||
if alias.starts(with: input) && !commandSuggestions.contains(primary) {
|
|
||||||
if commandDescriptions.contains(where: { $0.0 == primary }) {
|
|
||||||
commandSuggestions.append(primary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove duplicates and sort
|
|
||||||
commandSuggestions = Array(Set(commandSuggestions)).sorted()
|
|
||||||
showCommandSuggestions = !commandSuggestions.isEmpty
|
|
||||||
} else {
|
|
||||||
showCommandSuggestions = false
|
|
||||||
commandSuggestions = []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack(alignment: .center, spacing: 4) {
|
HStack(alignment: .center, spacing: 4) {
|
||||||
|
|||||||
Reference in New Issue
Block a user