add creation chat
This commit is contained in:
parent
21d120cb3d
commit
a937db4385
@ -395,7 +395,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements;
|
CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = V22H44W47J;
|
DEVELOPMENT_TEAM = V22H44W47J;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@ -435,7 +435,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements;
|
CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = V22H44W47J;
|
DEVELOPMENT_TEAM = V22H44W47J;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
|||||||
@ -10,6 +10,28 @@ struct PrivateChatHistoryData: Decodable {
|
|||||||
let hasMore: Bool
|
let hasMore: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PrivateChatCreateData: Decodable {
|
||||||
|
let chatId: String
|
||||||
|
let chatType: PrivateChatListItem.ChatType
|
||||||
|
let status: String
|
||||||
|
let message: String
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case chatId
|
||||||
|
case chatType
|
||||||
|
case status
|
||||||
|
case message
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.chatId = try container.decodeFlexibleString(forKey: .chatId)
|
||||||
|
self.chatType = try container.decode(PrivateChatListItem.ChatType.self, forKey: .chatType)
|
||||||
|
self.status = try container.decode(String.self, forKey: .status)
|
||||||
|
self.message = try container.decode(String.self, forKey: .message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct PrivateMessageSendData: Decodable {
|
struct PrivateMessageSendData: Decodable {
|
||||||
let messageId: String
|
let messageId: String
|
||||||
let chatId: String
|
let chatId: String
|
||||||
@ -180,6 +202,34 @@ struct ChatProfile: Decodable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ChatProfile {
|
||||||
|
init(
|
||||||
|
userId: String,
|
||||||
|
login: String?,
|
||||||
|
fullName: String?,
|
||||||
|
customName: String?,
|
||||||
|
bio: String? = nil,
|
||||||
|
lastSeen: Int? = nil,
|
||||||
|
createdAt: Date? = nil,
|
||||||
|
stories: [JSONValue] = [],
|
||||||
|
permissions: ChatPermissions? = nil,
|
||||||
|
profilePermissions: ChatProfilePermissions? = nil,
|
||||||
|
relationship: RelationshipStatus? = nil
|
||||||
|
) {
|
||||||
|
self.userId = userId
|
||||||
|
self.login = login
|
||||||
|
self.fullName = fullName
|
||||||
|
self.customName = customName
|
||||||
|
self.bio = bio
|
||||||
|
self.lastSeen = lastSeen
|
||||||
|
self.createdAt = createdAt
|
||||||
|
self.stories = stories
|
||||||
|
self.permissions = permissions
|
||||||
|
self.profilePermissions = profilePermissions
|
||||||
|
self.relationship = relationship
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ChatPermissions: Decodable {
|
struct ChatPermissions: Decodable {
|
||||||
let youCanSendMessage: Bool
|
let youCanSendMessage: Bool
|
||||||
let youCanPublicInvitePermission: Bool
|
let youCanPublicInvitePermission: Bool
|
||||||
|
|||||||
@ -111,6 +111,43 @@ final class ChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createOrFindPrivateChat(
|
||||||
|
targetUserId: String,
|
||||||
|
completion: @escaping (Result<PrivateChatCreateData, Error>) -> Void
|
||||||
|
) {
|
||||||
|
let query: [String: String?] = [
|
||||||
|
"target_user_id": targetUserId
|
||||||
|
]
|
||||||
|
|
||||||
|
client.request(
|
||||||
|
path: "/v1/chat/private/create",
|
||||||
|
method: .post,
|
||||||
|
query: query,
|
||||||
|
requiresAuth: true
|
||||||
|
) { [decoder] result in
|
||||||
|
switch result {
|
||||||
|
case .success(let response):
|
||||||
|
do {
|
||||||
|
let apiResponse = try decoder.decode(APIResponse<PrivateChatCreateData>.self, from: response.data)
|
||||||
|
guard apiResponse.status == "fine" else {
|
||||||
|
let message = apiResponse.detail ?? NSLocalizedString("Не удалось создать чат.", comment: "")
|
||||||
|
completion(.failure(ChatServiceError.unexpectedStatus(message)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion(.success(apiResponse.data))
|
||||||
|
} catch {
|
||||||
|
let debugMessage = Self.describeDecodingError(error: error, data: response.data)
|
||||||
|
if AppConfig.DEBUG {
|
||||||
|
print("[ChatService] create private chat decode failed: \(debugMessage)")
|
||||||
|
}
|
||||||
|
completion(.failure(ChatServiceError.decoding(debugDescription: debugMessage)))
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func sendPrivateMessage(
|
func sendPrivateMessage(
|
||||||
chatId: String,
|
chatId: String,
|
||||||
content: String,
|
content: String,
|
||||||
|
|||||||
@ -939,6 +939,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Не удалось открыть чат" : {
|
||||||
|
"comment" : "Chat creation error title"
|
||||||
|
},
|
||||||
|
"Не удалось открыть чат." : {
|
||||||
|
"comment" : "Chat creation fallback"
|
||||||
|
},
|
||||||
"Не удалось отправить сообщение." : {
|
"Не удалось отправить сообщение." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -954,6 +960,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Не удалось создать чат." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Не удалось сохранить изменения профиля." : {
|
"Не удалось сохранить изменения профиля." : {
|
||||||
"comment" : "Profile update unexpected status"
|
"comment" : "Profile update unexpected status"
|
||||||
@ -1303,6 +1312,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Ошибка сервера (%@)." : {
|
"Ошибка сервера (%@)." : {
|
||||||
|
"comment" : "Chat creation server status",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -1323,6 +1333,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Ошибка сети: %@" : {
|
"Ошибка сети: %@" : {
|
||||||
|
"comment" : "Chat creation network error",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -1333,7 +1344,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Ошибка соединения с сервером." : {
|
"Ошибка соединения с сервером." : {
|
||||||
"comment" : "Search error connection",
|
"comment" : "Chat creation connection\nSearch error connection",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -1565,6 +1576,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Понятно" : {
|
||||||
|
"comment" : "Chat creation error acknowledgment"
|
||||||
|
},
|
||||||
"Попробуйте изменить запрос поиска." : {
|
"Попробуйте изменить запрос поиска." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -1715,6 +1729,9 @@
|
|||||||
"Произошла неизвестная ошибка." : {
|
"Произошла неизвестная ошибка." : {
|
||||||
"comment" : "Search unknown error"
|
"comment" : "Search unknown error"
|
||||||
},
|
},
|
||||||
|
"Произошла неизвестная ошибка. Попробуйте позже." : {
|
||||||
|
"comment" : "Chat creation unknown error"
|
||||||
|
},
|
||||||
"Произошла ошибка." : {
|
"Произошла ошибка." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -1911,7 +1928,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Сессия истекла. Войдите снова." : {
|
"Сессия истекла. Войдите снова." : {
|
||||||
"comment" : "Search error unauthorized",
|
"comment" : "Chat creation unauthorized\nSearch error unauthorized",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ struct ChatsTab: View {
|
|||||||
@Binding var searchRevealProgress: CGFloat
|
@Binding var searchRevealProgress: CGFloat
|
||||||
@Binding var searchText: String
|
@Binding var searchText: String
|
||||||
private let searchService = SearchService()
|
private let searchService = SearchService()
|
||||||
|
private let chatService = ChatService()
|
||||||
@AppStorage("chatRowMessageLineLimit") private var messageLineLimitSetting: Int = 2
|
@AppStorage("chatRowMessageLineLimit") private var messageLineLimitSetting: Int = 2
|
||||||
@StateObject private var viewModel = PrivateChatsViewModel()
|
@StateObject private var viewModel = PrivateChatsViewModel()
|
||||||
@State private var selectedChatId: String?
|
@State private var selectedChatId: String?
|
||||||
@ -24,7 +25,11 @@ struct ChatsTab: View {
|
|||||||
@State private var isGlobalSearchLoading: Bool = false
|
@State private var isGlobalSearchLoading: Bool = false
|
||||||
@State private var globalSearchError: String?
|
@State private var globalSearchError: String?
|
||||||
@State private var globalSearchTask: Task<Void, Never>? = nil
|
@State private var globalSearchTask: Task<Void, Never>? = nil
|
||||||
@State private var selectedSearchUserId: UUID?
|
@State private var creatingChatForUserId: UUID?
|
||||||
|
@State private var chatCreationError: String?
|
||||||
|
@State private var isChatCreationAlertPresented: Bool = false
|
||||||
|
@State private var pendingChatItem: PrivateChatListItem?
|
||||||
|
@State private var isPendingChatActive: Bool = false
|
||||||
|
|
||||||
private let searchRevealDistance: CGFloat = 90
|
private let searchRevealDistance: CGFloat = 90
|
||||||
|
|
||||||
@ -53,6 +58,23 @@ struct ChatsTab: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.alert(
|
||||||
|
NSLocalizedString("Не удалось открыть чат", comment: "Chat creation error title"),
|
||||||
|
isPresented: Binding(
|
||||||
|
get: { isChatCreationAlertPresented && chatCreationError != nil },
|
||||||
|
set: { newValue in
|
||||||
|
isChatCreationAlertPresented = newValue
|
||||||
|
if !newValue {
|
||||||
|
chatCreationError = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
presenting: chatCreationError
|
||||||
|
) { _ in
|
||||||
|
Button(NSLocalizedString("Понятно", comment: "Chat creation error acknowledgment"), role: .cancel) { }
|
||||||
|
} message: { error in
|
||||||
|
Text(error)
|
||||||
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
globalSearchTask?.cancel()
|
globalSearchTask?.cancel()
|
||||||
globalSearchTask = nil
|
globalSearchTask = nil
|
||||||
@ -69,7 +91,8 @@ struct ChatsTab: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var chatList: some View {
|
private var chatList: some View {
|
||||||
List {
|
ZStack {
|
||||||
|
List {
|
||||||
// VStack(spacing: 0) {
|
// VStack(spacing: 0) {
|
||||||
// searchBar
|
// searchBar
|
||||||
// .padding(.horizontal, 16)
|
// .padding(.horizontal, 16)
|
||||||
@ -77,72 +100,102 @@ struct ChatsTab: View {
|
|||||||
// }
|
// }
|
||||||
// .background(Color(UIColor.systemBackground))
|
// .background(Color(UIColor.systemBackground))
|
||||||
|
|
||||||
if let message = viewModel.errorMessage {
|
if let message = viewModel.errorMessage {
|
||||||
Section {
|
Section {
|
||||||
HStack(alignment: .top, spacing: 8) {
|
HStack(alignment: .top, spacing: 8) {
|
||||||
Image(systemName: "exclamationmark.triangle.fill")
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
.foregroundColor(.orange)
|
.foregroundColor(.orange)
|
||||||
Text(message)
|
Text(message)
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.orange)
|
|
||||||
Spacer(minLength: 0)
|
|
||||||
Button(action: { viewModel.refresh() }) {
|
|
||||||
Text(NSLocalizedString("Обновить", comment: ""))
|
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
Button(action: { viewModel.refresh() }) {
|
||||||
|
Text(NSLocalizedString("Обновить", comment: ""))
|
||||||
|
.font(.subheadline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSearching {
|
||||||
|
Section(header: localSearchHeader) {
|
||||||
|
if localSearchResults.isEmpty {
|
||||||
|
emptySearchResultView
|
||||||
|
.listRowInsets(EdgeInsets(top: 24, leading: 16, bottom: 24, trailing: 16))
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
} else {
|
||||||
|
ForEach(localSearchResults) { chat in
|
||||||
|
chatRowItem(for: chat)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
|
||||||
}
|
|
||||||
|
|
||||||
if isSearching {
|
Section(header: globalSearchHeader) {
|
||||||
Section(header: localSearchHeader) {
|
globalSearchContent
|
||||||
if localSearchResults.isEmpty {
|
}
|
||||||
emptySearchResultView
|
} else {
|
||||||
.listRowInsets(EdgeInsets(top: 24, leading: 16, bottom: 24, trailing: 16))
|
if let message = viewModel.errorMessage, viewModel.chats.isEmpty {
|
||||||
.listRowSeparator(.hidden)
|
errorState(message: message)
|
||||||
|
} else if viewModel.chats.isEmpty {
|
||||||
|
emptyState
|
||||||
} else {
|
} else {
|
||||||
ForEach(localSearchResults) { chat in
|
|
||||||
|
ForEach(viewModel.chats) { chat in
|
||||||
chatRowItem(for: chat)
|
chatRowItem(for: chat)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: globalSearchHeader) {
|
if viewModel.isLoadingMore {
|
||||||
globalSearchContent
|
loadingMoreRow
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if let message = viewModel.errorMessage, viewModel.chats.isEmpty {
|
|
||||||
errorState(message: message)
|
|
||||||
} else if viewModel.chats.isEmpty {
|
|
||||||
emptyState
|
|
||||||
} else {
|
|
||||||
|
|
||||||
ForEach(viewModel.chats) { chat in
|
|
||||||
chatRowItem(for: chat)
|
|
||||||
}
|
|
||||||
|
|
||||||
if viewModel.isLoadingMore {
|
|
||||||
loadingMoreRow
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.listStyle(.plain)
|
||||||
.listStyle(.plain)
|
.modifier(ScrollDismissesKeyboardModifier())
|
||||||
.modifier(ScrollDismissesKeyboardModifier())
|
.simultaneousGesture(searchBarGesture)
|
||||||
.simultaneousGesture(searchBarGesture)
|
.simultaneousGesture(tapToDismissKeyboardGesture)
|
||||||
.simultaneousGesture(tapToDismissKeyboardGesture)
|
// .safeAreaInset(edge: .top) {
|
||||||
// .safeAreaInset(edge: .top) {
|
// VStack(spacing: 0) {
|
||||||
// VStack(spacing: 0) {
|
// searchBar
|
||||||
// searchBar
|
// .padding(.horizontal, 16)
|
||||||
// .padding(.horizontal, 16)
|
// .padding(.top, 8)
|
||||||
// .padding(.top, 8)
|
// .padding(.bottom, 8)
|
||||||
// .padding(.bottom, 8)
|
// Divider()
|
||||||
// Divider()
|
// }
|
||||||
|
// .background(Color(UIColor.systemBackground))
|
||||||
// }
|
// }
|
||||||
// .background(Color(UIColor.systemBackground))
|
|
||||||
// }
|
pendingChatNavigationLink
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var pendingChatNavigationLink: some View {
|
||||||
|
NavigationLink(
|
||||||
|
destination: pendingChatDestination,
|
||||||
|
isActive: Binding(
|
||||||
|
get: { isPendingChatActive && pendingChatItem != nil },
|
||||||
|
set: { newValue in
|
||||||
|
if !newValue {
|
||||||
|
isPendingChatActive = false
|
||||||
|
pendingChatItem = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.hidden()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var pendingChatDestination: some View {
|
||||||
|
if let pendingChatItem {
|
||||||
|
PrivateChatView(chat: pendingChatItem, currentUserId: currentUserId)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var searchBarGesture: some Gesture {
|
private var searchBarGesture: some Gesture {
|
||||||
@ -404,7 +457,7 @@ struct ChatsTab: View {
|
|||||||
|
|
||||||
private func globalSearchRow(for user: UserSearchResult) -> some View {
|
private func globalSearchRow(for user: UserSearchResult) -> some View {
|
||||||
Button {
|
Button {
|
||||||
selectedSearchUserId = user.userId
|
openPrivateChat(for: user)
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Circle()
|
Circle()
|
||||||
@ -440,21 +493,17 @@ struct ChatsTab: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
|
if creatingChatForUserId == user.userId {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
.disabled(creatingChatForUserId != nil)
|
||||||
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
||||||
.background(
|
|
||||||
NavigationLink(
|
|
||||||
destination: SearchResultPlaceholderView(userId: user.userId),
|
|
||||||
tag: user.userId,
|
|
||||||
selection: $selectedSearchUserId
|
|
||||||
) {
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
.hidden()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -480,7 +529,6 @@ private extension ChatsTab {
|
|||||||
globalSearchResults = []
|
globalSearchResults = []
|
||||||
globalSearchError = nil
|
globalSearchError = nil
|
||||||
isGlobalSearchLoading = false
|
isGlobalSearchLoading = false
|
||||||
selectedSearchUserId = nil
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -499,7 +547,6 @@ private extension ChatsTab {
|
|||||||
isGlobalSearchLoading = false
|
isGlobalSearchLoading = false
|
||||||
globalSearchError = nil
|
globalSearchError = nil
|
||||||
globalSearchTask = nil
|
globalSearchTask = nil
|
||||||
selectedSearchUserId = nil
|
|
||||||
}
|
}
|
||||||
} catch is CancellationError {
|
} catch is CancellationError {
|
||||||
// Ignore cancellation
|
// Ignore cancellation
|
||||||
@ -510,7 +557,6 @@ private extension ChatsTab {
|
|||||||
isGlobalSearchLoading = false
|
isGlobalSearchLoading = false
|
||||||
globalSearchError = friendlyErrorMessage(for: error)
|
globalSearchError = friendlyErrorMessage(for: error)
|
||||||
globalSearchTask = nil
|
globalSearchTask = nil
|
||||||
selectedSearchUserId = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -522,7 +568,6 @@ private extension ChatsTab {
|
|||||||
globalSearchResults = []
|
globalSearchResults = []
|
||||||
globalSearchError = nil
|
globalSearchError = nil
|
||||||
isGlobalSearchLoading = false
|
isGlobalSearchLoading = false
|
||||||
selectedSearchUserId = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func secondaryLine(for user: UserSearchResult) -> String? {
|
func secondaryLine(for user: UserSearchResult) -> String? {
|
||||||
@ -552,6 +597,91 @@ private extension ChatsTab {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func openPrivateChat(for user: UserSearchResult) {
|
||||||
|
guard creatingChatForUserId == nil else { return }
|
||||||
|
|
||||||
|
dismissKeyboard()
|
||||||
|
creatingChatForUserId = user.userId
|
||||||
|
chatCreationError = nil
|
||||||
|
|
||||||
|
chatService.createOrFindPrivateChat(targetUserId: user.userId.uuidString) { result in
|
||||||
|
creatingChatForUserId = nil
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success(let data):
|
||||||
|
let chatItem = PrivateChatListItem(
|
||||||
|
chatId: data.chatId,
|
||||||
|
chatType: data.chatType,
|
||||||
|
chatData: chatProfile(from: user),
|
||||||
|
lastMessage: nil,
|
||||||
|
createdAt: nil,
|
||||||
|
unreadCount: 0
|
||||||
|
)
|
||||||
|
|
||||||
|
pendingChatItem = chatItem
|
||||||
|
isPendingChatActive = true
|
||||||
|
viewModel.refresh()
|
||||||
|
case .failure(let error):
|
||||||
|
chatCreationError = friendlyChatCreationMessage(for: error)
|
||||||
|
isChatCreationAlertPresented = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func chatProfile(from user: UserSearchResult) -> ChatProfile {
|
||||||
|
let profile = user.profile
|
||||||
|
let login = profile?.login ?? user.login
|
||||||
|
let fullName = user.officialFullName ?? user.fullName ?? profile?.fullName
|
||||||
|
let customName = user.preferredCustomName ?? user.customName ?? profile?.customName
|
||||||
|
|
||||||
|
return ChatProfile(
|
||||||
|
userId: user.userId.uuidString,
|
||||||
|
login: login,
|
||||||
|
fullName: fullName,
|
||||||
|
customName: customName,
|
||||||
|
bio: profile?.bio,
|
||||||
|
lastSeen: profile?.lastSeen,
|
||||||
|
createdAt: profile?.createdAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func friendlyChatCreationMessage(for error: Error) -> String {
|
||||||
|
if let chatError = error as? ChatServiceError {
|
||||||
|
return chatError.errorDescription ?? NSLocalizedString("Не удалось открыть чат.", comment: "Chat creation fallback")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let networkError = error as? NetworkError {
|
||||||
|
switch networkError {
|
||||||
|
case .unauthorized:
|
||||||
|
return NSLocalizedString("Сессия истекла. Войдите снова.", comment: "Chat creation unauthorized")
|
||||||
|
case .invalidURL, .noResponse:
|
||||||
|
return NSLocalizedString("Ошибка соединения с сервером.", comment: "Chat creation connection")
|
||||||
|
case .network(let underlying):
|
||||||
|
return String(format: NSLocalizedString("Ошибка сети: %@", comment: "Chat creation network error"), underlying.localizedDescription)
|
||||||
|
case .server(let statusCode, let data):
|
||||||
|
if let data {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
if let payload = try? decoder.decode(ErrorResponse.self, from: data) {
|
||||||
|
if let detail = payload.detail?.trimmingCharacters(in: .whitespacesAndNewlines), !detail.isEmpty {
|
||||||
|
return detail
|
||||||
|
}
|
||||||
|
if let message = payload.data?.message?.trimmingCharacters(in: .whitespacesAndNewlines), !message.isEmpty {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let raw = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String(format: NSLocalizedString("Ошибка сервера (%@).", comment: "Chat creation server status"), "\(statusCode)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NSLocalizedString("Произошла неизвестная ошибка. Попробуйте позже.", comment: "Chat creation unknown error")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func friendlyErrorMessage(for error: Error) -> String {
|
func friendlyErrorMessage(for error: Error) -> String {
|
||||||
if let searchError = error as? SearchServiceError {
|
if let searchError = error as? SearchServiceError {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user