Refactor code, ensure websocket connection (and @ObservableObject updates) occur on main thread
This commit is contained in:
		@@ -13,7 +13,7 @@ struct SoyuzApp: App {
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    @State var currentIcon = "move.3d"
 | 
					    @State var currentIcon = "move.3d"
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    @ObservedObject var printerManager = PrinterRequestManager()
 | 
					    @ObservedObject static var printerManager = PrinterRequestManager()
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    var body: some Scene {
 | 
					    var body: some Scene {
 | 
				
			||||||
//        WindowGroup(id: "floating-stats") {
 | 
					//        WindowGroup(id: "floating-stats") {
 | 
				
			||||||
@@ -22,13 +22,13 @@ struct SoyuzApp: App {
 | 
				
			|||||||
//        }
 | 
					//        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        WindowGroup("Configuration", id: "soyuz_cfg", content: {
 | 
					        WindowGroup("Configuration", id: "soyuz_cfg", content: {
 | 
				
			||||||
            PrinterConfigView(printerManager: printerManager)
 | 
					            PrinterConfigView(printerManager: SoyuzApp.printerManager)
 | 
				
			||||||
                //.frame(minWidth: 300, maxWidth: 600, minHeight: 60, maxHeight: 100)
 | 
					                //.frame(minWidth: 300, maxWidth: 600, minHeight: 60, maxHeight: 100)
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .windowResizability(.contentSize)
 | 
					        .windowResizability(.contentSize)
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        MenuBarExtra("Soyuz", systemImage: currentIcon) {
 | 
					        MenuBarExtra("Soyuz", systemImage: currentIcon) {
 | 
				
			||||||
            SoyuzMenuBarExtraView(printerManager: printerManager, currentMenuBarIcon: $currentIcon)
 | 
					            SoyuzMenuBarExtraView(printerManager: SoyuzApp.printerManager, currentMenuBarIcon: $currentIcon)
 | 
				
			||||||
                .padding([.top, .leading, .trailing], 8)
 | 
					                .padding([.top, .leading, .trailing], 8)
 | 
				
			||||||
                .padding([.bottom], 6)
 | 
					                .padding([.bottom], 6)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,8 +10,9 @@ import Network
 | 
				
			|||||||
import AppKit
 | 
					import AppKit
 | 
				
			||||||
import Starscream
 | 
					import Starscream
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// MARK: Bonjour Protocol
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Protocol defining minimal API for network discovery
 | 
				
			||||||
 | 
					// MARK: Net Discovery Protocol
 | 
				
			||||||
protocol NetworkDiscoveryEngine {
 | 
					protocol NetworkDiscoveryEngine {
 | 
				
			||||||
    func startScan(queue: DispatchQueue)
 | 
					    func startScan(queue: DispatchQueue)
 | 
				
			||||||
    func setBrowseResultsChangedHandler(_ handler: @escaping ((Set<NWBrowser.Result>, Set<NWBrowser.Result.Change>) -> Void))
 | 
					    func setBrowseResultsChangedHandler(_ handler: @escaping ((Set<NWBrowser.Result>, Set<NWBrowser.Result.Change>) -> Void))
 | 
				
			||||||
@@ -32,8 +33,9 @@ extension NWBrowser: NetworkDiscoveryEngine {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// MARK: Starscream Protocol
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Properly formatted JSON-RPC Request for use with Starscream
 | 
				
			||||||
 | 
					// MARK: JSON-RPC Request Codable
 | 
				
			||||||
struct JsonRpcRequest: Codable {
 | 
					struct JsonRpcRequest: Codable {
 | 
				
			||||||
    var jsonrpc = "2.0"
 | 
					    var jsonrpc = "2.0"
 | 
				
			||||||
    let method: String
 | 
					    let method: String
 | 
				
			||||||
@@ -49,15 +51,17 @@ struct JsonRpcRequest: Codable {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MARK: PrinterRequestManager
 | 
				
			||||||
//@MainActor
 | 
					//@MainActor
 | 
				
			||||||
class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
					class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
				
			||||||
    let WEBSOCKET_TIMEOUT_INTERVAL: TimeInterval = 60.0
 | 
					    let WEBSOCKET_TIMEOUT_INTERVAL: TimeInterval = 60.0
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Debug stuff
 | 
					    // Debug timestamp stuff
 | 
				
			||||||
    let startDate = Date()
 | 
					    let startDate = Date()
 | 
				
			||||||
    let startDateString: String
 | 
					    let startDateString: String
 | 
				
			||||||
    let filename: URL
 | 
					    let filename: URL
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    // Debug file writing stuff
 | 
				
			||||||
    func writeToDebugLog(_ output: String) {
 | 
					    func writeToDebugLog(_ output: String) {
 | 
				
			||||||
        do {
 | 
					        do {
 | 
				
			||||||
            let fileHandle = try FileHandle(forWritingTo: filename)
 | 
					            let fileHandle = try FileHandle(forWritingTo: filename)
 | 
				
			||||||
@@ -91,38 +95,12 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
				
			|||||||
    @Published var socketPort: String
 | 
					    @Published var socketPort: String
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    let nwBrowser: NetworkDiscoveryEngine
 | 
					    let nwBrowser: NetworkDiscoveryEngine
 | 
				
			||||||
    //let nwBrowser = NWBrowser(for: .bonjourWithTXTRecord(type: "_moonraker._tcp", domain: "local."), using: .tcp)
 | 
					 | 
				
			||||||
    var connection: NWConnection!
 | 
					    var connection: NWConnection!
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    var socket: WebSocket?
 | 
					    var socket: WebSocket?
 | 
				
			||||||
    var lastPingDate = Date()
 | 
					    var lastPingDate = Date()
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Parse a JSON-RPC query-response message
 | 
					    // MARK: PRM init()
 | 
				
			||||||
    func parse_response(_ response: jsonRpcResponse) {
 | 
					 | 
				
			||||||
        state = response.result.status.print_stats?.state ?? ""
 | 
					 | 
				
			||||||
        progress = response.result.status.virtual_sdcard?.progress ?? 0.0
 | 
					 | 
				
			||||||
        extruderTemperature = response.result.status.extruder?.temperature ?? 0.0
 | 
					 | 
				
			||||||
        bedTemperature = response.result.status.heater_bed?.temperature ?? 0.0
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        print(response)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // Parse a JSON-RPC update message
 | 
					 | 
				
			||||||
    func parse_update(_ update: jsonRpcUpdate) {
 | 
					 | 
				
			||||||
        if let newState = update.params.status?.print_stats?.state {
 | 
					 | 
				
			||||||
            state = newState
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if let newProgress = update.params.status?.virtual_sdcard?.progress  {
 | 
					 | 
				
			||||||
            progress = newProgress
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if let newExtruderTemp = update.params.status?.extruder?.temperature  {
 | 
					 | 
				
			||||||
            extruderTemperature = newExtruderTemp
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if let newBedTemp = update.params.status?.heater_bed?.temperature  {
 | 
					 | 
				
			||||||
            bedTemperature = newBedTemp
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    init(browser: NetworkDiscoveryEngine = NWBrowser(for: .bonjourWithTXTRecord(type: "_moonraker._tcp", domain: "local."), using: .tcp)) {
 | 
					    init(browser: NetworkDiscoveryEngine = NWBrowser(for: .bonjourWithTXTRecord(type: "_moonraker._tcp", domain: "local."), using: .tcp)) {
 | 
				
			||||||
        state = ""
 | 
					        state = ""
 | 
				
			||||||
        progress = 0.0
 | 
					        progress = 0.0
 | 
				
			||||||
@@ -131,9 +109,8 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
				
			|||||||
        socketHost = ""
 | 
					        socketHost = ""
 | 
				
			||||||
        socketPort = ""
 | 
					        socketPort = ""
 | 
				
			||||||
        nwBrowser = browser
 | 
					        nwBrowser = browser
 | 
				
			||||||
        //reconnectionTimer = nil
 | 
					 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // MARK: Debug stuff
 | 
					        // Debug output-to-file functionality
 | 
				
			||||||
        startDateString = "\(startDate)\n\n"
 | 
					        startDateString = "\(startDate)\n\n"
 | 
				
			||||||
        filename = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("klippermon-debug-\(startDateString).txt")
 | 
					        filename = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("klippermon-debug-\(startDateString).txt")
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
@@ -143,7 +120,7 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
				
			|||||||
            print("[error] Couldn't write to \(filename) - \(error)")
 | 
					            print("[error] Couldn't write to \(filename) - \(error)")
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // MARK: Bonjour browser initialization at instantiation
 | 
					        // Bonjour browser results changed handler
 | 
				
			||||||
        nwBrowser.setBrowseResultsChangedHandler({ (newResults, changes) in
 | 
					        nwBrowser.setBrowseResultsChangedHandler({ (newResults, changes) in
 | 
				
			||||||
            print("[update] Results changed.")
 | 
					            print("[update] Results changed.")
 | 
				
			||||||
            newResults.forEach { result in
 | 
					            newResults.forEach { result in
 | 
				
			||||||
@@ -164,12 +141,8 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
				
			|||||||
                break
 | 
					                break
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        // Start up the bonjour browser, get results and process them in the update handler
 | 
					 | 
				
			||||||
        nwBrowser.startScan(queue: DispatchQueue.main)
 | 
					 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // Screen sleep functionality
 | 
					        // Set up sleep/wake notification observers
 | 
				
			||||||
//        NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(screenDidSleep(_:)), name: NSWorkspace.screensDidSleepNotification, object: nil)
 | 
					 | 
				
			||||||
//        NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(screenDidWake(_:)), name: NSWorkspace.screensDidWakeNotification, object: nil)
 | 
					 | 
				
			||||||
        let center = NSWorkspace.shared.notificationCenter;
 | 
					        let center = NSWorkspace.shared.notificationCenter;
 | 
				
			||||||
        let mainQueue = OperationQueue.main
 | 
					        let mainQueue = OperationQueue.main
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
@@ -180,30 +153,15 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
				
			|||||||
        center.addObserver(forName: NSWorkspace.screensDidSleepNotification, object: nil, queue: mainQueue) { notification in
 | 
					        center.addObserver(forName: NSWorkspace.screensDidSleepNotification, object: nil, queue: mainQueue) { notification in
 | 
				
			||||||
            self.screenChangedSleepState(notification)
 | 
					            self.screenChangedSleepState(notification)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
    func screenChangedSleepState(_ notification: Notification) {
 | 
					        // Start up the bonjour browser, get results and process them in the update handler
 | 
				
			||||||
        switch(notification.name) {
 | 
					        nwBrowser.startScan(queue: DispatchQueue.main)
 | 
				
			||||||
        case NSWorkspace.screensDidSleepNotification:
 | 
					 | 
				
			||||||
            socket?.disconnect()
 | 
					 | 
				
			||||||
        case NSWorkspace.screensDidWakeNotification:
 | 
					 | 
				
			||||||
            self.openWebsocket()
 | 
					 | 
				
			||||||
        default:
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    func screenDidWake(_ notification: Notification) {
 | 
					 | 
				
			||||||
        print("Screen woke: \(notification.name)")
 | 
					 | 
				
			||||||
        if socket != nil {
 | 
					 | 
				
			||||||
            self.openWebsocket()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Called from the UI with an endpoint.
 | 
					    // Called from the UI with an endpoint.
 | 
				
			||||||
    // Momentarily connect/disconnects from the endpoint to retrieve the host/port
 | 
					    // Momentarily connect/disconnects from the endpoint to retrieve the host/port
 | 
				
			||||||
    // calls private function openWebsocket to process the host/port
 | 
					    // calls private function openWebsocket to process the host/port
 | 
				
			||||||
    func resolveBonjourHost(_ endpoint: NWEndpoint) {
 | 
					    func connectToBonjourEndpoint(_ endpoint: NWEndpoint) {
 | 
				
			||||||
        // Debug stuff
 | 
					        // Debug stuff
 | 
				
			||||||
        endpoint.txtRecord?.forEach({ (key: String, value: NWTXTRecord.Entry) in
 | 
					        endpoint.txtRecord?.forEach({ (key: String, value: NWTXTRecord.Entry) in
 | 
				
			||||||
            print("\(key): \(value)")
 | 
					            print("\(key): \(value)")
 | 
				
			||||||
@@ -226,10 +184,13 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
				
			|||||||
                    
 | 
					                    
 | 
				
			||||||
                    print("[sanitized] Resolved \(sanitizedHost):\(port)")
 | 
					                    print("[sanitized] Resolved \(sanitizedHost):\(port)")
 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
                    socketHost = sanitizedHost
 | 
					 | 
				
			||||||
                    socketPort = "\(port)"
 | 
					 | 
				
			||||||
                    connection.cancel()
 | 
					                    connection.cancel()
 | 
				
			||||||
                    self.openWebsocket()
 | 
					                    
 | 
				
			||||||
 | 
					                    DispatchQueue.main.async {
 | 
				
			||||||
 | 
					                        self.socketHost = sanitizedHost
 | 
				
			||||||
 | 
					                        self.socketPort = "\(port)"
 | 
				
			||||||
 | 
					                        self.openWebsocket()
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            default:
 | 
					            default:
 | 
				
			||||||
                break
 | 
					                break
 | 
				
			||||||
@@ -238,18 +199,10 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
				
			|||||||
        connection.start(queue: .global())
 | 
					        connection.start(queue: .global())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    func reconnectWebsocket() {
 | 
					 | 
				
			||||||
        if socket == nil {
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
        socket!.disconnect()
 | 
					    // MARK: Private functions
 | 
				
			||||||
        self.openWebsocket()
 | 
					 | 
				
			||||||
        //socket!.write(ping: "PING!".data(using: .utf8)!)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Opens the websocket connection
 | 
					    // Opens the websocket connection
 | 
				
			||||||
    // TODO: host and port should be function arguments probably maybe
 | 
					 | 
				
			||||||
    private func openWebsocket() {
 | 
					    private func openWebsocket() {
 | 
				
			||||||
        //let fullUrlString = "http://\(socketHost):\(socketPort)/websocket"
 | 
					        //let fullUrlString = "http://\(socketHost):\(socketPort)/websocket"
 | 
				
			||||||
        var request = URLRequest(url: URL(string: "http://\(socketHost):\(socketPort)/websocket")!)
 | 
					        var request = URLRequest(url: URL(string: "http://\(socketHost):\(socketPort)/websocket")!)
 | 
				
			||||||
@@ -257,14 +210,29 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
				
			|||||||
        socket = WebSocket(request: request)
 | 
					        socket = WebSocket(request: request)
 | 
				
			||||||
        socket!.delegate = self
 | 
					        socket!.delegate = self
 | 
				
			||||||
        socket!.connect()
 | 
					        socket!.connect()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
        // TODO: Check that this keeps the connection alive properly
 | 
					    private func reconnectWebsocket() {
 | 
				
			||||||
        Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [self] timer in
 | 
					        if socket == nil {
 | 
				
			||||||
            //self.checkWebsocketIsAlive()
 | 
					            return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        socket!.disconnect()
 | 
				
			||||||
 | 
					        self.openWebsocket()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // MARK: Callsbacks
 | 
				
			||||||
 | 
					    func screenChangedSleepState(_ notification: Notification) {
 | 
				
			||||||
 | 
					        switch(notification.name) {
 | 
				
			||||||
 | 
					        case NSWorkspace.screensDidSleepNotification:
 | 
				
			||||||
 | 
					            socket?.disconnect()
 | 
				
			||||||
 | 
					        case NSWorkspace.screensDidWakeNotification:
 | 
				
			||||||
 | 
					            self.openWebsocket()
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // MARK: delegate callback for Starscream WebSocketClient
 | 
					 | 
				
			||||||
    func didReceive(event: Starscream.WebSocketEvent, client: Starscream.WebSocket) {
 | 
					    func didReceive(event: Starscream.WebSocketEvent, client: Starscream.WebSocket) {
 | 
				
			||||||
        switch event {
 | 
					        switch event {
 | 
				
			||||||
        case .connected(let headers):
 | 
					        case .connected(let headers):
 | 
				
			||||||
@@ -325,4 +293,30 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    // MARK: JSON-RPC Parsing
 | 
				
			||||||
 | 
					    // Parse a JSON-RPC query-response message
 | 
				
			||||||
 | 
					    func parse_response(_ response: jsonRpcResponse) {
 | 
				
			||||||
 | 
					        state = response.result.status.print_stats?.state ?? ""
 | 
				
			||||||
 | 
					        progress = response.result.status.virtual_sdcard?.progress ?? 0.0
 | 
				
			||||||
 | 
					        extruderTemperature = response.result.status.extruder?.temperature ?? 0.0
 | 
				
			||||||
 | 
					        bedTemperature = response.result.status.heater_bed?.temperature ?? 0.0
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        print(response)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Parse a JSON-RPC update message
 | 
				
			||||||
 | 
					    func parse_update(_ update: jsonRpcUpdate) {
 | 
				
			||||||
 | 
					        if let newState = update.params.status?.print_stats?.state {
 | 
				
			||||||
 | 
					            state = newState
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let newProgress = update.params.status?.virtual_sdcard?.progress  {
 | 
				
			||||||
 | 
					            progress = newProgress
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let newExtruderTemp = update.params.status?.extruder?.temperature  {
 | 
				
			||||||
 | 
					            extruderTemperature = newExtruderTemp
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let newBedTemp = update.params.status?.heater_bed?.temperature  {
 | 
				
			||||||
 | 
					            bedTemperature = newBedTemp
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,7 +33,7 @@ struct PrinterConfigView: View {
 | 
				
			|||||||
                        HStack {
 | 
					                        HStack {
 | 
				
			||||||
                            Text(result.endpoint.toFriendlyString())
 | 
					                            Text(result.endpoint.toFriendlyString())
 | 
				
			||||||
                            Button {
 | 
					                            Button {
 | 
				
			||||||
                                printerManager.resolveBonjourHost(result.endpoint)
 | 
					                                printerManager.connectToBonjourEndpoint(result.endpoint)
 | 
				
			||||||
                            } label: {
 | 
					                            } label: {
 | 
				
			||||||
                                Text("Connect")
 | 
					                                Text("Connect")
 | 
				
			||||||
                                    .foregroundColor(.white)
 | 
					                                    .foregroundColor(.white)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user