mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 21:05:20 +00:00
Add @ autocomplete functionality
- Added autocomplete state tracking in ChatViewModel - Implemented updateAutocomplete() to parse @ mentions and show suggestions - Added completeNickname() to handle nickname completion - Created autocomplete UI overlay above input field - Shows list of matching nicknames when typing @ - Highlights selected suggestion - Clicking suggestion completes the mention
This commit is contained in:
@@ -191,6 +191,7 @@ extension BitchatMessage {
|
|||||||
if originalSender != nil { flags |= 0x04 }
|
if originalSender != nil { flags |= 0x04 }
|
||||||
if recipientNickname != nil { flags |= 0x08 }
|
if recipientNickname != nil { flags |= 0x08 }
|
||||||
if senderPeerID != nil { flags |= 0x10 }
|
if senderPeerID != nil { flags |= 0x10 }
|
||||||
|
if mentions != nil && !mentions!.isEmpty { flags |= 0x20 }
|
||||||
|
|
||||||
data.append(flags)
|
data.append(flags)
|
||||||
|
|
||||||
@@ -244,6 +245,19 @@ extension BitchatMessage {
|
|||||||
data.append(peerData.prefix(255))
|
data.append(peerData.prefix(255))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mentions array
|
||||||
|
if let mentions = mentions {
|
||||||
|
data.append(UInt8(min(mentions.count, 255))) // Number of mentions
|
||||||
|
for mention in mentions.prefix(255) {
|
||||||
|
if let mentionData = mention.data(using: .utf8) {
|
||||||
|
data.append(UInt8(min(mentionData.count, 255)))
|
||||||
|
data.append(mentionData.prefix(255))
|
||||||
|
} else {
|
||||||
|
data.append(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +282,7 @@ extension BitchatMessage {
|
|||||||
let hasOriginalSender = (flags & 0x04) != 0
|
let hasOriginalSender = (flags & 0x04) != 0
|
||||||
let hasRecipientNickname = (flags & 0x08) != 0
|
let hasRecipientNickname = (flags & 0x08) != 0
|
||||||
let hasSenderPeerID = (flags & 0x10) != 0
|
let hasSenderPeerID = (flags & 0x10) != 0
|
||||||
|
let hasMentions = (flags & 0x20) != 0
|
||||||
|
|
||||||
// Timestamp
|
// Timestamp
|
||||||
guard offset + 8 <= dataCopy.count else {
|
guard offset + 8 <= dataCopy.count else {
|
||||||
@@ -345,6 +360,26 @@ extension BitchatMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mentions array
|
||||||
|
var mentions: [String]?
|
||||||
|
if hasMentions && offset < dataCopy.count {
|
||||||
|
let mentionCount = Int(dataCopy[offset]); offset += 1
|
||||||
|
if mentionCount > 0 {
|
||||||
|
mentions = []
|
||||||
|
for _ in 0..<mentionCount {
|
||||||
|
if offset < dataCopy.count {
|
||||||
|
let length = Int(dataCopy[offset]); offset += 1
|
||||||
|
if offset + length <= dataCopy.count {
|
||||||
|
if let mention = String(data: dataCopy[offset..<offset+length], encoding: .utf8) {
|
||||||
|
mentions?.append(mention)
|
||||||
|
}
|
||||||
|
offset += length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let message = BitchatMessage(
|
let message = BitchatMessage(
|
||||||
sender: sender,
|
sender: sender,
|
||||||
content: content,
|
content: content,
|
||||||
@@ -353,7 +388,8 @@ extension BitchatMessage {
|
|||||||
originalSender: originalSender,
|
originalSender: originalSender,
|
||||||
isPrivate: isPrivate,
|
isPrivate: isPrivate,
|
||||||
recipientNickname: recipientNickname,
|
recipientNickname: recipientNickname,
|
||||||
senderPeerID: senderPeerID
|
senderPeerID: senderPeerID,
|
||||||
|
mentions: mentions
|
||||||
)
|
)
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ struct BitchatMessage: Codable, Equatable {
|
|||||||
let isPrivate: Bool
|
let isPrivate: Bool
|
||||||
let recipientNickname: String?
|
let recipientNickname: String?
|
||||||
let senderPeerID: String?
|
let senderPeerID: String?
|
||||||
|
let mentions: [String]? // Array of mentioned nicknames
|
||||||
|
|
||||||
init(sender: String, content: String, timestamp: Date, isRelay: Bool, originalSender: String? = nil, isPrivate: Bool = false, recipientNickname: String? = nil, senderPeerID: String? = nil) {
|
init(sender: String, content: String, timestamp: Date, isRelay: Bool, originalSender: String? = nil, isPrivate: Bool = false, recipientNickname: String? = nil, senderPeerID: String? = nil, mentions: [String]? = nil) {
|
||||||
self.id = UUID().uuidString
|
self.id = UUID().uuidString
|
||||||
self.sender = sender
|
self.sender = sender
|
||||||
self.content = content
|
self.content = content
|
||||||
@@ -79,6 +80,7 @@ struct BitchatMessage: Codable, Equatable {
|
|||||||
self.isPrivate = isPrivate
|
self.isPrivate = isPrivate
|
||||||
self.recipientNickname = recipientNickname
|
self.recipientNickname = recipientNickname
|
||||||
self.senderPeerID = senderPeerID
|
self.senderPeerID = senderPeerID
|
||||||
|
self.mentions = mentions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
self.characteristic = characteristic
|
self.characteristic = characteristic
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendMessage(_ content: String, to recipientID: String? = nil) {
|
func sendMessage(_ content: String, mentions: [String] = [], to recipientID: String? = nil) {
|
||||||
messageQueue.async { [weak self] in
|
messageQueue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
@@ -162,7 +162,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
content: content,
|
content: content,
|
||||||
timestamp: Date(),
|
timestamp: Date(),
|
||||||
isRelay: false,
|
isRelay: false,
|
||||||
originalSender: nil
|
originalSender: nil,
|
||||||
|
mentions: mentions.isEmpty ? nil : mentions
|
||||||
)
|
)
|
||||||
|
|
||||||
if let messageData = message.toBinaryPayload() {
|
if let messageData = message.toBinaryPayload() {
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ class ChatViewModel: ObservableObject {
|
|||||||
@Published var privateChats: [String: [BitchatMessage]] = [:] // peerID -> messages
|
@Published var privateChats: [String: [BitchatMessage]] = [:] // peerID -> messages
|
||||||
@Published var selectedPrivateChatPeer: String? = nil
|
@Published var selectedPrivateChatPeer: String? = nil
|
||||||
@Published var unreadPrivateMessages: Set<String> = []
|
@Published var unreadPrivateMessages: Set<String> = []
|
||||||
|
@Published var autocompleteSuggestions: [String] = []
|
||||||
|
@Published var showAutocomplete: Bool = false
|
||||||
|
@Published var autocompleteRange: NSRange? = nil
|
||||||
|
@Published var selectedAutocompleteIndex: Int = 0
|
||||||
@Published var privateMessageNotification: (sender: String, message: String)? = nil
|
@Published var privateMessageNotification: (sender: String, message: String)? = nil
|
||||||
|
|
||||||
let meshService = BluetoothMeshService()
|
let meshService = BluetoothMeshService()
|
||||||
@@ -57,18 +61,22 @@ class ChatViewModel: ObservableObject {
|
|||||||
// Send as private message
|
// Send as private message
|
||||||
sendPrivateMessage(content, to: selectedPeer)
|
sendPrivateMessage(content, to: selectedPeer)
|
||||||
} else {
|
} else {
|
||||||
|
// Parse mentions from the content
|
||||||
|
let mentions = parseMentions(from: content)
|
||||||
|
|
||||||
// Add message to local display
|
// Add message to local display
|
||||||
let message = BitchatMessage(
|
let message = BitchatMessage(
|
||||||
sender: nickname,
|
sender: nickname,
|
||||||
content: content,
|
content: content,
|
||||||
timestamp: Date(),
|
timestamp: Date(),
|
||||||
isRelay: false,
|
isRelay: false,
|
||||||
originalSender: nil
|
originalSender: nil,
|
||||||
|
mentions: mentions.isEmpty ? nil : mentions
|
||||||
)
|
)
|
||||||
messages.append(message)
|
messages.append(message)
|
||||||
|
|
||||||
// Send via mesh
|
// Send via mesh with mentions
|
||||||
meshService.sendMessage(content)
|
meshService.sendMessage(content, mentions: mentions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +173,72 @@ class ChatViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateAutocomplete(for text: String, cursorPosition: Int) {
|
||||||
|
// Find @ symbol before cursor
|
||||||
|
let beforeCursor = String(text.prefix(cursorPosition))
|
||||||
|
|
||||||
|
// Look for @ pattern
|
||||||
|
let pattern = "@([a-zA-Z0-9_]*)$"
|
||||||
|
guard let regex = try? NSRegularExpression(pattern: pattern, options: []),
|
||||||
|
let match = regex.firstMatch(in: beforeCursor, options: [], range: NSRange(location: 0, length: beforeCursor.count)) else {
|
||||||
|
showAutocomplete = false
|
||||||
|
autocompleteSuggestions = []
|
||||||
|
autocompleteRange = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the partial nickname
|
||||||
|
let partialRange = match.range(at: 1)
|
||||||
|
guard let range = Range(partialRange, in: beforeCursor) else {
|
||||||
|
showAutocomplete = false
|
||||||
|
autocompleteSuggestions = []
|
||||||
|
autocompleteRange = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let partial = String(beforeCursor[range]).lowercased()
|
||||||
|
|
||||||
|
// Get all available nicknames
|
||||||
|
let peerNicknames = meshService.getPeerNicknames()
|
||||||
|
var allNicknames = Array(peerNicknames.values)
|
||||||
|
allNicknames.append(nickname) // Include self
|
||||||
|
|
||||||
|
// Filter suggestions
|
||||||
|
let suggestions = allNicknames.filter { nick in
|
||||||
|
nick.lowercased().hasPrefix(partial)
|
||||||
|
}.sorted()
|
||||||
|
|
||||||
|
if !suggestions.isEmpty {
|
||||||
|
autocompleteSuggestions = suggestions
|
||||||
|
showAutocomplete = true
|
||||||
|
autocompleteRange = match.range(at: 0) // Store full @mention range
|
||||||
|
selectedAutocompleteIndex = 0
|
||||||
|
} else {
|
||||||
|
showAutocomplete = false
|
||||||
|
autocompleteSuggestions = []
|
||||||
|
autocompleteRange = nil
|
||||||
|
selectedAutocompleteIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func completeNickname(_ nickname: String, in text: inout String) -> Int {
|
||||||
|
guard let range = autocompleteRange else { return text.count }
|
||||||
|
|
||||||
|
// Replace the @partial with @nickname
|
||||||
|
let nsText = text as NSString
|
||||||
|
let newText = nsText.replacingCharacters(in: range, with: "@\(nickname) ")
|
||||||
|
text = newText
|
||||||
|
|
||||||
|
// Hide autocomplete
|
||||||
|
showAutocomplete = false
|
||||||
|
autocompleteSuggestions = []
|
||||||
|
autocompleteRange = nil
|
||||||
|
selectedAutocompleteIndex = 0
|
||||||
|
|
||||||
|
// Return new cursor position (after the space)
|
||||||
|
return range.location + nickname.count + 2
|
||||||
|
}
|
||||||
|
|
||||||
func formatMessage(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
|
func formatMessage(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
|
||||||
var result = AttributedString()
|
var result = AttributedString()
|
||||||
|
|
||||||
@@ -203,11 +277,49 @@ class ChatViewModel: ObservableObject {
|
|||||||
senderStyle.font = .system(size: 12, weight: .medium, design: .monospaced)
|
senderStyle.font = .system(size: 12, weight: .medium, design: .monospaced)
|
||||||
result.append(sender.mergingAttributes(senderStyle))
|
result.append(sender.mergingAttributes(senderStyle))
|
||||||
|
|
||||||
let content = AttributedString(message.content)
|
// Process content to highlight mentions
|
||||||
var contentStyle = AttributeContainer()
|
let contentText = message.content
|
||||||
contentStyle.font = .system(size: 14, design: .monospaced)
|
var processedContent = AttributedString()
|
||||||
contentStyle.foregroundColor = isDark ? Color.white : Color.black
|
|
||||||
result.append(content.mergingAttributes(contentStyle))
|
// Regular expression to find @mentions
|
||||||
|
let pattern = "@([a-zA-Z0-9_]+)"
|
||||||
|
let regex = try? NSRegularExpression(pattern: pattern, options: [])
|
||||||
|
let matches = regex?.matches(in: contentText, options: [], range: NSRange(location: 0, length: contentText.count)) ?? []
|
||||||
|
|
||||||
|
var lastEndIndex = contentText.startIndex
|
||||||
|
|
||||||
|
for match in matches {
|
||||||
|
// Add text before the mention
|
||||||
|
if let range = Range(match.range(at: 0), in: contentText) {
|
||||||
|
let beforeText = String(contentText[lastEndIndex..<range.lowerBound])
|
||||||
|
if !beforeText.isEmpty {
|
||||||
|
var normalStyle = AttributeContainer()
|
||||||
|
normalStyle.font = .system(size: 14, design: .monospaced)
|
||||||
|
normalStyle.foregroundColor = isDark ? Color.white : Color.black
|
||||||
|
processedContent.append(AttributedString(beforeText).mergingAttributes(normalStyle))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the mention with highlight
|
||||||
|
let mentionText = String(contentText[range])
|
||||||
|
var mentionStyle = AttributeContainer()
|
||||||
|
mentionStyle.font = .system(size: 14, weight: .semibold, design: .monospaced)
|
||||||
|
mentionStyle.foregroundColor = Color.orange
|
||||||
|
processedContent.append(AttributedString(mentionText).mergingAttributes(mentionStyle))
|
||||||
|
|
||||||
|
lastEndIndex = range.upperBound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining text
|
||||||
|
if lastEndIndex < contentText.endIndex {
|
||||||
|
let remainingText = String(contentText[lastEndIndex...])
|
||||||
|
var normalStyle = AttributeContainer()
|
||||||
|
normalStyle.font = .system(size: 14, design: .monospaced)
|
||||||
|
normalStyle.foregroundColor = isDark ? Color.white : Color.black
|
||||||
|
processedContent.append(AttributedString(remainingText).mergingAttributes(normalStyle))
|
||||||
|
}
|
||||||
|
|
||||||
|
result.append(processedContent)
|
||||||
|
|
||||||
if message.isRelay, let originalSender = message.originalSender {
|
if message.isRelay, let originalSender = message.originalSender {
|
||||||
let relay = AttributedString(" (via \(originalSender))")
|
let relay = AttributedString(" (via \(originalSender))")
|
||||||
@@ -259,8 +371,23 @@ extension ChatViewModel: BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
// Different haptic feedback for private vs public messages
|
// Check if we're mentioned
|
||||||
if message.isPrivate && message.sender != nickname {
|
let isMentioned = message.mentions?.contains(nickname) ?? false
|
||||||
|
|
||||||
|
// Different haptic feedback for mentions, private messages, and regular messages
|
||||||
|
if isMentioned && message.sender != nickname {
|
||||||
|
// Very prominent haptic for @mentions - triple tap with heavy impact
|
||||||
|
let impactFeedback = UIImpactFeedbackGenerator(style: .heavy)
|
||||||
|
impactFeedback.prepare()
|
||||||
|
impactFeedback.impactOccurred()
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
|
impactFeedback.impactOccurred()
|
||||||
|
}
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||||
|
impactFeedback.impactOccurred()
|
||||||
|
}
|
||||||
|
} else if message.isPrivate && message.sender != nickname {
|
||||||
// Heavy haptic for private messages - more pronounced
|
// Heavy haptic for private messages - more pronounced
|
||||||
let impactFeedback = UIImpactFeedbackGenerator(style: .heavy)
|
let impactFeedback = UIImpactFeedbackGenerator(style: .heavy)
|
||||||
impactFeedback.prepare()
|
impactFeedback.prepare()
|
||||||
@@ -322,4 +449,26 @@ extension ChatViewModel: BitchatDelegate {
|
|||||||
endPrivateChat()
|
endPrivateChat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func parseMentions(from content: String) -> [String] {
|
||||||
|
let pattern = "@([a-zA-Z0-9_]+)"
|
||||||
|
let regex = try? NSRegularExpression(pattern: pattern, options: [])
|
||||||
|
let matches = regex?.matches(in: content, options: [], range: NSRange(location: 0, length: content.count)) ?? []
|
||||||
|
|
||||||
|
var mentions: [String] = []
|
||||||
|
let peerNicknames = meshService.getPeerNicknames()
|
||||||
|
let allNicknames = Set(peerNicknames.values).union([nickname]) // Include self
|
||||||
|
|
||||||
|
for match in matches {
|
||||||
|
if let range = Range(match.range(at: 1), in: content) {
|
||||||
|
let mentionedName = String(content[range])
|
||||||
|
// Only include if it's a valid nickname
|
||||||
|
if allNicknames.contains(mentionedName) {
|
||||||
|
mentions.append(mentionedName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array(Set(mentions)) // Remove duplicates
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import SwiftUI
|
|||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@EnvironmentObject var viewModel: ChatViewModel
|
@EnvironmentObject var viewModel: ChatViewModel
|
||||||
@State private var messageText = ""
|
@State private var messageText = ""
|
||||||
|
@State private var textFieldSelection: NSRange? = nil
|
||||||
@FocusState private var isTextFieldFocused: Bool
|
@FocusState private var isTextFieldFocused: Bool
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
@State private var showPeerList = false
|
@State private var showPeerList = false
|
||||||
@@ -224,8 +225,12 @@ struct ContentView: View {
|
|||||||
|
|
||||||
ForEach(Array(messages.enumerated()), id: \.offset) { index, message in
|
ForEach(Array(messages.enumerated()), id: \.offset) { index, message in
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
// Check if current user is mentioned
|
||||||
|
let isMentioned = message.mentions?.contains(viewModel.nickname) ?? false
|
||||||
|
|
||||||
Text(viewModel.formatMessage(message, colorScheme: colorScheme))
|
Text(viewModel.formatMessage(message, colorScheme: colorScheme))
|
||||||
.font(.system(size: 14, design: .monospaced))
|
.font(.system(size: 14, design: .monospaced))
|
||||||
|
.fontWeight(isMentioned ? .bold : .regular)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
@@ -258,6 +263,41 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var inputView: some View {
|
private var inputView: some View {
|
||||||
|
ZStack(alignment: .bottom) {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
// Autocomplete suggestions overlay
|
||||||
|
if viewModel.showAutocomplete && !viewModel.autocompleteSuggestions.isEmpty {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
ForEach(Array(viewModel.autocompleteSuggestions.enumerated()), id: \.element) { index, suggestion in
|
||||||
|
Button(action: {
|
||||||
|
_ = viewModel.completeNickname(suggestion, in: &messageText)
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Text("@\(suggestion)")
|
||||||
|
.font(.system(size: 12, design: .monospaced))
|
||||||
|
.foregroundColor(index == viewModel.selectedAutocompleteIndex ? backgroundColor : textColor)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(index == viewModel.selectedAutocompleteIndex ? textColor : Color.clear)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(backgroundColor)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.stroke(secondaryTextColor.opacity(0.5), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.frame(maxWidth: 200, alignment: .leading)
|
||||||
|
.padding(.leading, 100) // Align with input field
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Text("[\(viewModel.formatTimestamp(Date()))]")
|
Text("[\(viewModel.formatTimestamp(Date()))]")
|
||||||
.font(.system(size: 12, design: .monospaced))
|
.font(.system(size: 12, design: .monospaced))
|
||||||
@@ -285,6 +325,11 @@ struct ContentView: View {
|
|||||||
.font(.system(size: 14, design: .monospaced))
|
.font(.system(size: 14, design: .monospaced))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
.focused($isTextFieldFocused)
|
.focused($isTextFieldFocused)
|
||||||
|
.onChange(of: messageText) { newValue in
|
||||||
|
// Get cursor position (approximate - end of text for now)
|
||||||
|
let cursorPosition = newValue.count
|
||||||
|
viewModel.updateAutocomplete(for: newValue, cursorPosition: cursorPosition)
|
||||||
|
}
|
||||||
.onSubmit {
|
.onSubmit {
|
||||||
sendMessage()
|
sendMessage()
|
||||||
}
|
}
|
||||||
@@ -299,6 +344,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
.background(backgroundColor.opacity(0.95))
|
.background(backgroundColor.opacity(0.95))
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
isTextFieldFocused = true
|
isTextFieldFocused = true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user