Compare commits
2 Commits
813795aece
...
aac0a25c4d
| Author | SHA1 | Date | |
|---|---|---|---|
| aac0a25c4d | |||
| e79cbd7ea4 |
@ -11,6 +11,7 @@ struct TopBarView: View {
|
|||||||
// var viewModel: LoginViewModel
|
// var viewModel: LoginViewModel
|
||||||
@ObservedObject var viewModel: LoginViewModel
|
@ObservedObject var viewModel: LoginViewModel
|
||||||
@Binding var isSettingsPresented: Bool
|
@Binding var isSettingsPresented: Bool
|
||||||
|
@Binding var isQrPresented: Bool
|
||||||
|
|
||||||
// Привязка для управления боковым меню
|
// Привязка для управления боковым меню
|
||||||
@Binding var isSideMenuPresented: Bool
|
@Binding var isSideMenuPresented: Bool
|
||||||
@ -29,6 +30,10 @@ struct TopBarView: View {
|
|||||||
return title == "Profile"
|
return title == "Profile"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isContactsTab: Bool {
|
||||||
|
return title == "Contacts"
|
||||||
|
}
|
||||||
|
|
||||||
private var statusMessage: String? {
|
private var statusMessage: String? {
|
||||||
if viewModel.chatLoadingState == .loading {
|
if viewModel.chatLoadingState == .loading {
|
||||||
return NSLocalizedString("Загрузка чатов", comment: "")
|
return NSLocalizedString("Загрузка чатов", comment: "")
|
||||||
@ -112,6 +117,14 @@ struct TopBarView: View {
|
|||||||
.imageScale(.large)
|
.imageScale(.large)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
}
|
}
|
||||||
|
} else if isContactsTab {
|
||||||
|
NavigationLink(isActive: $isQrPresented) {
|
||||||
|
QrView()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "qrcode.viewfinder")
|
||||||
|
.imageScale(.large)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// else if isChatsTab {
|
// else if isChatsTab {
|
||||||
@ -220,6 +233,7 @@ struct TopBarView_Previews: PreviewProvider {
|
|||||||
@StateObject private var viewModel = LoginViewModel()
|
@StateObject private var viewModel = LoginViewModel()
|
||||||
@State private var searchText: String = ""
|
@State private var searchText: String = ""
|
||||||
@State private var isSettingsPresented = false
|
@State private var isSettingsPresented = false
|
||||||
|
@State private var isQrPresented = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TopBarView(
|
TopBarView(
|
||||||
@ -229,6 +243,7 @@ struct TopBarView_Previews: PreviewProvider {
|
|||||||
accounts: [selectedAccount],
|
accounts: [selectedAccount],
|
||||||
viewModel: viewModel,
|
viewModel: viewModel,
|
||||||
isSettingsPresented: $isSettingsPresented,
|
isSettingsPresented: $isSettingsPresented,
|
||||||
|
isQrPresented: $isSettingsPresented,
|
||||||
isSideMenuPresented: $isSideMenuPresented,
|
isSideMenuPresented: $isSideMenuPresented,
|
||||||
chatSearchRevealProgress: $revealProgress,
|
chatSearchRevealProgress: $revealProgress,
|
||||||
chatSearchText: $searchText,
|
chatSearchText: $searchText,
|
||||||
|
|||||||
173
yobble/Network/ContactsService.swift
Normal file
173
yobble/Network/ContactsService.swift
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum ContactsServiceError: LocalizedError {
|
||||||
|
case unexpectedStatus(String)
|
||||||
|
case decoding(debugDescription: String)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .unexpectedStatus(let message):
|
||||||
|
return message
|
||||||
|
case .decoding(let debugDescription):
|
||||||
|
return AppConfig.DEBUG
|
||||||
|
? debugDescription
|
||||||
|
: NSLocalizedString("Не удалось загрузить контакты.", comment: "Contacts service decoding error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContactPayload: Decodable {
|
||||||
|
let userId: UUID
|
||||||
|
let login: String
|
||||||
|
let fullName: String?
|
||||||
|
let customName: String?
|
||||||
|
let friendCode: Bool
|
||||||
|
let createdAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ContactsService {
|
||||||
|
private let client: NetworkClient
|
||||||
|
private let decoder: JSONDecoder
|
||||||
|
|
||||||
|
init(client: NetworkClient = .shared) {
|
||||||
|
self.client = client
|
||||||
|
self.decoder = JSONDecoder()
|
||||||
|
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
self.decoder.dateDecodingStrategy = .custom(Self.decodeDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchContacts(completion: @escaping (Result<[ContactPayload], Error>) -> Void) {
|
||||||
|
client.request(
|
||||||
|
path: "/v1/user/contact/list",
|
||||||
|
method: .get,
|
||||||
|
requiresAuth: true
|
||||||
|
) { [decoder] result in
|
||||||
|
switch result {
|
||||||
|
case .success(let response):
|
||||||
|
do {
|
||||||
|
let apiResponse = try decoder.decode(APIResponse<[ContactPayload]>.self, from: response.data)
|
||||||
|
guard apiResponse.status == "fine" else {
|
||||||
|
let message = apiResponse.detail ?? NSLocalizedString("Не удалось загрузить контакты.", comment: "Contacts service unexpected status")
|
||||||
|
completion(.failure(ContactsServiceError.unexpectedStatus(message)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion(.success(apiResponse.data))
|
||||||
|
} catch {
|
||||||
|
let debugMessage = Self.describeDecodingError(error: error, data: response.data)
|
||||||
|
if AppConfig.DEBUG {
|
||||||
|
print("[ContactsService] decode contacts failed: \(debugMessage)")
|
||||||
|
}
|
||||||
|
completion(.failure(ContactsServiceError.decoding(debugDescription: debugMessage)))
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
if case let NetworkError.server(_, data) = error,
|
||||||
|
let data,
|
||||||
|
let message = Self.errorMessage(from: data) {
|
||||||
|
completion(.failure(ContactsServiceError.unexpectedStatus(message)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchContacts() async throws -> [ContactPayload] {
|
||||||
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
|
fetchContacts { result in
|
||||||
|
continuation.resume(with: result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func decodeDate(from decoder: Decoder) throws -> Date {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
let string = try container.decode(String.self)
|
||||||
|
if let date = iso8601WithFractionalSeconds.date(from: string) {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
if let date = iso8601Simple.date(from: string) {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
throw DecodingError.dataCorruptedError(
|
||||||
|
in: container,
|
||||||
|
debugDescription: "Невозможно декодировать дату: \(string)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func describeDecodingError(error: Error, data: Data) -> String {
|
||||||
|
var parts: [String] = []
|
||||||
|
|
||||||
|
if let decodingError = error as? DecodingError {
|
||||||
|
parts.append(decodingDescription(from: decodingError))
|
||||||
|
} else {
|
||||||
|
parts.append(error.localizedDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let payload = truncatedPayload(from: data) {
|
||||||
|
parts.append("payload=\(payload)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.joined(separator: "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func decodingDescription(from error: DecodingError) -> String {
|
||||||
|
switch error {
|
||||||
|
case .typeMismatch(let type, let context):
|
||||||
|
return "Type mismatch for \(type) at \(codingPath(from: context)): \(context.debugDescription)"
|
||||||
|
case .valueNotFound(let type, let context):
|
||||||
|
return "Value not found for \(type) at \(codingPath(from: context)): \(context.debugDescription)"
|
||||||
|
case .keyNotFound(let key, let context):
|
||||||
|
return "Missing key '\(key.stringValue)' at \(codingPath(from: context)): \(context.debugDescription)"
|
||||||
|
case .dataCorrupted(let context):
|
||||||
|
return "Corrupted data at \(codingPath(from: context)): \(context.debugDescription)"
|
||||||
|
@unknown default:
|
||||||
|
return error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func codingPath(from context: DecodingError.Context) -> String {
|
||||||
|
let path = context.codingPath.map { $0.stringValue }.filter { !$0.isEmpty }
|
||||||
|
return path.isEmpty ? "root" : path.joined(separator: ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func truncatedPayload(from data: Data, limit: Int = 512) -> String? {
|
||||||
|
guard let string = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
|
!string.isEmpty else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if string.count <= limit {
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = string.index(string.startIndex, offsetBy: limit)
|
||||||
|
return String(string[string.startIndex..<index]) + "…"
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func errorMessage(from data: Data) -> String? {
|
||||||
|
if let apiError = try? JSONDecoder().decode(ErrorResponse.self, from: data) {
|
||||||
|
if let detail = apiError.detail, !detail.isEmpty {
|
||||||
|
return detail
|
||||||
|
}
|
||||||
|
if let message = apiError.data?.message, !message.isEmpty {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let string = String(data: data, encoding: .utf8), !string.isEmpty {
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let iso8601WithFractionalSeconds: ISO8601DateFormatter = {
|
||||||
|
let formatter = ISO8601DateFormatter()
|
||||||
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
private static let iso8601Simple: ISO8601DateFormatter = {
|
||||||
|
let formatter = ISO8601DateFormatter()
|
||||||
|
formatter.formatOptions = [.withInternetDateTime]
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
}
|
||||||
@ -158,6 +158,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Qr" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Yobble" : {
|
"Yobble" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -389,6 +392,9 @@
|
|||||||
"Добавление новых блокировок появится позже." : {
|
"Добавление новых блокировок появится позже." : {
|
||||||
"comment" : "Add blocked user placeholder message"
|
"comment" : "Add blocked user placeholder message"
|
||||||
},
|
},
|
||||||
|
"Добавьте контакты, чтобы быстрее выходить на связь." : {
|
||||||
|
"comment" : "Contacts empty state subtitle"
|
||||||
|
},
|
||||||
"Другое" : {
|
"Другое" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -596,6 +602,12 @@
|
|||||||
"Кликер в разработке" : {
|
"Кликер в разработке" : {
|
||||||
"comment" : "Concept tab placeholder title"
|
"comment" : "Concept tab placeholder title"
|
||||||
},
|
},
|
||||||
|
"Код дружбы" : {
|
||||||
|
"comment" : "Friend code badge"
|
||||||
|
},
|
||||||
|
"Контактов пока нет" : {
|
||||||
|
"comment" : "Contacts empty state title"
|
||||||
|
},
|
||||||
"Контакты" : {
|
"Контакты" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -919,6 +931,9 @@
|
|||||||
},
|
},
|
||||||
"Не удалось загрузить историю чата." : {
|
"Не удалось загрузить историю чата." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Не удалось загрузить контакты." : {
|
||||||
|
"comment" : "Contacts service decoding error\nContacts service unexpected status"
|
||||||
},
|
},
|
||||||
"Не удалось загрузить профиль." : {
|
"Не удалось загрузить профиль." : {
|
||||||
"comment" : "Profile service decoding error\nProfile unexpected status"
|
"comment" : "Profile service decoding error\nProfile unexpected status"
|
||||||
@ -1326,7 +1341,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Ошибка" : {
|
"Ошибка" : {
|
||||||
"comment" : "Common error title\nProfile update error title",
|
"comment" : "Common error title\nContacts load error title\nProfile update error title",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|||||||
@ -1,24 +1,213 @@
|
|||||||
//
|
|
||||||
// ContactsTab.swift
|
|
||||||
// yobble
|
|
||||||
//
|
|
||||||
// Created by cheykrym on 23.10.2025.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ContactsTab: View {
|
struct ContactsTab: View {
|
||||||
|
@State private var contacts: [Contact] = []
|
||||||
|
@State private var isLoading = false
|
||||||
|
@State private var loadError: String?
|
||||||
|
@State private var activeAlert: ContactsAlert?
|
||||||
|
|
||||||
|
private let contactsService = ContactsService()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
List {
|
||||||
VStack {
|
if isLoading && contacts.isEmpty {
|
||||||
Text("Здесь не будут чаты")
|
loadingState
|
||||||
.font(.title)
|
} else if let loadError, contacts.isEmpty {
|
||||||
.foregroundColor(.gray)
|
errorState(loadError)
|
||||||
|
} else if contacts.isEmpty {
|
||||||
|
emptyState
|
||||||
|
} else {
|
||||||
|
Section(header: Text(NSLocalizedString("Контакты", comment: ""))) {
|
||||||
|
ForEach(contacts) { contact in
|
||||||
|
ContactRow(contact: contact)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
.background(Color(.systemGroupedBackground))
|
||||||
|
.task {
|
||||||
|
await loadContacts()
|
||||||
|
}
|
||||||
|
.refreshable {
|
||||||
|
await loadContacts()
|
||||||
|
}
|
||||||
|
.alert(item: $activeAlert) { alert in
|
||||||
|
switch alert {
|
||||||
|
case .error(_, let message):
|
||||||
|
return Alert(
|
||||||
|
title: Text(NSLocalizedString("Ошибка", comment: "Contacts load error title")),
|
||||||
|
message: Text(message),
|
||||||
|
dismissButton: .default(Text(NSLocalizedString("OK", comment: "Common OK")))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var loadingState: some View {
|
||||||
|
Section {
|
||||||
|
ProgressView()
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func errorState(_ message: String) -> some View {
|
||||||
|
Section {
|
||||||
|
Text(message)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var emptyState: some View {
|
||||||
|
Section {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
Image(systemName: "person.crop.circle.badge.questionmark")
|
||||||
|
.font(.system(size: 48))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text(NSLocalizedString("Контактов пока нет", comment: "Contacts empty state title"))
|
||||||
|
.font(.headline)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
Text(NSLocalizedString("Добавьте контакты, чтобы быстрее выходить на связь.", comment: "Contacts empty state subtitle"))
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func loadContacts() async {
|
||||||
|
if isLoading {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
loadError = nil
|
||||||
|
|
||||||
|
do {
|
||||||
|
let payloads = try await contactsService.fetchContacts()
|
||||||
|
contacts = payloads.map(Contact.init)
|
||||||
|
} catch {
|
||||||
|
loadError = error.localizedDescription
|
||||||
|
activeAlert = .error(message: error.localizedDescription)
|
||||||
|
if AppConfig.DEBUG { print("[ContactsTab] load contacts failed: \(error)") }
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ContactRow: View {
|
||||||
|
let contact: Contact
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.accentColor.opacity(0.15))
|
||||||
|
.frame(width: 44, height: 44)
|
||||||
|
.overlay(
|
||||||
|
Text(contact.initials)
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(contact.displayName)
|
||||||
|
.font(.body)
|
||||||
|
if let handle = contact.handle {
|
||||||
|
Text(handle)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
if contact.friendCode {
|
||||||
|
Label(NSLocalizedString("Код дружбы", comment: "Friend code badge"), systemImage: "heart.circle")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
Text(contact.formattedCreatedAt)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
.padding(.vertical, 4)
|
||||||
// .background(Color(.secondarySystemBackground)) // Фон для всей вкладки
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Contact: Identifiable, Equatable {
|
||||||
|
let id: UUID
|
||||||
|
let login: String
|
||||||
|
let fullName: String?
|
||||||
|
let customName: String?
|
||||||
|
let friendCode: Bool
|
||||||
|
let createdAt: Date
|
||||||
|
|
||||||
|
let displayName: String
|
||||||
|
let handle: String?
|
||||||
|
|
||||||
|
var initials: String {
|
||||||
|
let components = displayName.split(separator: " ")
|
||||||
|
let nameInitials = components.prefix(2).compactMap { $0.first }
|
||||||
|
if !nameInitials.isEmpty {
|
||||||
|
return nameInitials
|
||||||
|
.map { String($0).uppercased() }
|
||||||
|
.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
let filtered = login.filter { $0.isLetter }.prefix(2)
|
||||||
|
if !filtered.isEmpty {
|
||||||
|
return filtered.uppercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
return "??"
|
||||||
|
}
|
||||||
|
|
||||||
|
var formattedCreatedAt: String {
|
||||||
|
Self.relativeFormatter.localizedString(for: createdAt, relativeTo: Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
init(payload: ContactPayload) {
|
||||||
|
self.id = payload.userId
|
||||||
|
self.login = payload.login
|
||||||
|
self.fullName = payload.fullName
|
||||||
|
self.customName = payload.customName
|
||||||
|
self.friendCode = payload.friendCode
|
||||||
|
self.createdAt = payload.createdAt
|
||||||
|
|
||||||
|
if let customName = payload.customName, !customName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
self.displayName = customName
|
||||||
|
} else if let fullName = payload.fullName, !fullName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
self.displayName = fullName
|
||||||
|
} else {
|
||||||
|
self.displayName = payload.login
|
||||||
|
}
|
||||||
|
|
||||||
|
if !payload.login.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
self.handle = "@\(payload.login)"
|
||||||
|
} else {
|
||||||
|
self.handle = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let relativeFormatter: RelativeDateTimeFormatter = {
|
||||||
|
let formatter = RelativeDateTimeFormatter()
|
||||||
|
formatter.unitsStyle = .short
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ContactsAlert: Identifiable {
|
||||||
|
case error(id: UUID = UUID(), message: String)
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .error(let id, _):
|
||||||
|
return id.uuidString
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ struct MainView: View {
|
|||||||
@State private var chatSearchRevealProgress: CGFloat = 0
|
@State private var chatSearchRevealProgress: CGFloat = 0
|
||||||
@State private var chatSearchText: String = ""
|
@State private var chatSearchText: String = ""
|
||||||
@State private var isSettingsPresented = false
|
@State private var isSettingsPresented = false
|
||||||
|
@State private var isQrPresented = false
|
||||||
@State private var deepLinkChatItem: PrivateChatListItem?
|
@State private var deepLinkChatItem: PrivateChatListItem?
|
||||||
@State private var isDeepLinkChatActive = false
|
@State private var isDeepLinkChatActive = false
|
||||||
|
|
||||||
@ -50,6 +51,7 @@ struct MainView: View {
|
|||||||
accounts: accounts,
|
accounts: accounts,
|
||||||
viewModel: viewModel,
|
viewModel: viewModel,
|
||||||
isSettingsPresented: $isSettingsPresented,
|
isSettingsPresented: $isSettingsPresented,
|
||||||
|
isQrPresented: $isQrPresented,
|
||||||
isSideMenuPresented: $isSideMenuPresented,
|
isSideMenuPresented: $isSideMenuPresented,
|
||||||
chatSearchRevealProgress: $chatSearchRevealProgress,
|
chatSearchRevealProgress: $chatSearchRevealProgress,
|
||||||
chatSearchText: $chatSearchText
|
chatSearchText: $chatSearchText
|
||||||
|
|||||||
11
yobble/Views/Tab/QrView.swift
Normal file
11
yobble/Views/Tab/QrView.swift
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct QrView: View {
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
|
||||||
|
}
|
||||||
|
.navigationTitle("Qr")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user