-
-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathServerUrlSelector.swift
211 lines (192 loc) · 7.68 KB
/
ServerUrlSelector.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
//
// ServerUrlSelector.swift
// Revolt
//
// Created by pythcon on 12/7/24.
//
import SwiftUI
import Types
struct ServerUrlSelector: View {
@EnvironmentObject var viewState: ViewState
@Environment(\.colorScheme) var colorScheme
private let officialServer = "https://api.revolt.chat"
@State private var showCustomServer = false
@State private var customDomain: String = ""
@State private var isValidating = false
@State private var validationError: String? = nil
@State private var validationSuccess: String? = nil
@State private var connectionStatus: ConnectionStatus = .untested
@FocusState private var isTextFieldFocused: Bool
enum ConnectionStatus {
case untested
case testing
case success
case failed
}
private func validateAndUpdateApiInfo(_ domain: String) {
if domain.isEmpty { return }
isValidating = true
connectionStatus = .testing
validationError = nil
validationSuccess = nil
let baseUrl: String
if domain == officialServer {
baseUrl = officialServer
} else {
// First try the domain as-is
let cleanDomain = domain.hasSuffix("/") ? String(domain.dropLast()) : domain
baseUrl = (domain.starts(with: "http://") || domain.starts(with: "https://")) ? cleanDomain : "https://" + cleanDomain
}
// Function to try validation with a URL
func tryValidation(url: String) async -> Bool {
let tempHttp = HTTPClient(token: nil, baseURL: url)
do {
let fetchedApiInfo = try await tempHttp.fetchApiInfo().get()
// Validate that this is actually a Revolt API
guard
!fetchedApiInfo.revolt.isEmpty, // Check revolt version exists
fetchedApiInfo.features.january != nil, // Check for required features
fetchedApiInfo.features.autumn != nil, // Check for required features
!fetchedApiInfo.ws.isEmpty, // Check websocket URL exists
!fetchedApiInfo.app.isEmpty // Check app URL exists
else {
return false
}
await MainActor.run {
viewState.apiInfo = fetchedApiInfo
viewState.userSettingsStore.store.serverUrl = url
if url != baseUrl {
customDomain = url // Update textbox if we added /api
}
isValidating = false
validationSuccess = "Successfully connected to server"
connectionStatus = .success
}
return true
} catch {
return false
}
}
Task {
// Try original URL first
if await tryValidation(url: baseUrl) {
return
}
// If not official server and first attempt failed, try with /api
if domain != officialServer {
let apiUrl = baseUrl + "/api"
if await tryValidation(url: apiUrl) {
return
}
}
// If both attempts failed, show error
await MainActor.run {
isValidating = false
validationError = "Unable to connect to server"
connectionStatus = .failed
}
}
}
private var statusIcon: some View {
Group {
switch connectionStatus {
case .untested:
Image(systemName: "link.circle.fill")
.foregroundStyle(.gray)
case .testing:
ProgressView()
.controlSize(.small)
case .success:
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
case .failed:
Image(systemName: "x.circle.fill")
.foregroundStyle(.red)
}
}
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("Server")
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
Button(action: {
withAnimation {
showCustomServer.toggle()
if !showCustomServer {
validateAndUpdateApiInfo(officialServer)
viewState.userSettingsStore.store.serverUrl = officialServer
}
}
}) {
Text(showCustomServer ? "Use Official" : "Use Custom")
.font(.caption)
.foregroundStyle(viewState.theme.accent)
}
}
if showCustomServer {
HStack {
TextField(
"Domain (e.g. example.com)",
text: $customDomain
)
.textContentType(.URL)
.keyboardType(.URL)
.disabled(isValidating)
.focused($isTextFieldFocused)
.onChange(of: isTextFieldFocused) { oldValue, newValue in
if !newValue {
validateAndUpdateApiInfo(customDomain)
}
}
.onChange(of: customDomain) { oldValue, newValue in
print("onChange: \(oldValue) -> \(newValue)")
print("connectionStatus: \(connectionStatus)")
print("validationSuccess: \(validationSuccess)")
if oldValue != newValue {
connectionStatus = .untested
validationError = nil
validationSuccess = nil
}
}
Button(action: {
i
';f !customDomain.isEmpty {
validateAndUpdateApiInfo(customDomain)
}
}) {
statusIcon
}
.disabled(connectionStatus == .testing || customDomain.isEmpty)
}
.padding()
.background((colorScheme == .light) ? Color(white: 0.851) : Color(white: 0.2))
.clipShape(.rect(cornerRadius: 5))
if let error = validationError {
Text(error)
.font(.caption)
.foregroundStyle(.red)
} else if let success = validationSuccess {
Text(success)
.font(.caption)
.foregroundStyle(.green)
}
} else {
Text("Official Revolt Server")
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background((colorScheme == .light) ? Color(white: 0.851) : Color(white: 0.2))
.clipShape(.rect(cornerRadius: 5))
}
}
.padding(.bottom)
.onAppear {
if viewState.userSettingsStore.store.serverUrl.isEmpty {
viewState.userSettingsStore.store.serverUrl = officialServer
validateAndUpdateApiInfo(officialServer)
}
}
}
}