diff --git a/yobble.xcodeproj/project.pbxproj b/yobble.xcodeproj/project.pbxproj index 9b056ec..c6e365e 100644 --- a/yobble.xcodeproj/project.pbxproj +++ b/yobble.xcodeproj/project.pbxproj @@ -395,7 +395,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = V22H44W47J; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -435,7 +435,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = V22H44W47J; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; diff --git a/yobble/Network/SearchModels.swift b/yobble/Network/SearchModels.swift index 2ed1fc2..d137296 100644 --- a/yobble/Network/SearchModels.swift +++ b/yobble/Network/SearchModels.swift @@ -26,6 +26,18 @@ struct SearchProfile: Decodable { } extension UserSearchResult { + var officialFullName: String? { + trimmed(fullName) ?? trimmed(profile?.fullName) + } + + var preferredCustomName: String? { + trimmed(customName) ?? trimmed(profile?.customName) + } + + var loginHandle: String { + "@\(login)" + } + var displayName: String { if let official = officialFullName { return official @@ -42,50 +54,6 @@ extension UserSearchResult { return loginHandle } - var secondaryText: String? { - // Отдельное поле для совместимости с существующими вызовами -// if let official = officialFullName, official != displayName { -// return official -// } - if let profileLogin = trimmed(profile?.login), profileLogin != login { - return "@\(profileLogin)" - } - return nil - } - - var officialFullName: String? { - trimmed(fullName) ?? trimmed(profile?.fullName) - } - - var preferredCustomName: String? { - trimmed(customName) ?? trimmed(profile?.customName) - } - - var loginHandle: String { - "@\(login)" - } - - var secondaryLabelForSearch: String? { - if let official = officialFullName { -// if let custom = preferredCustomName, custom != official { -// return custom -// } - if official != loginHandle { - return loginHandle - } - } - - if let secondary = secondaryText, secondary != displayName { - return secondary - } - - if displayName != loginHandle { - return loginHandle - } - - return nil - } - var avatarInitial: String { let source = officialFullName ?? preferredCustomName diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index 8a42c7b..b224649 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -29,6 +29,9 @@ }, "Chat ID:" : { + }, + "Companion ID" : { + "comment" : "Search placeholder companion title" }, "Companion ID:" : { @@ -338,6 +341,9 @@ } } }, + "Здесь появится информация о собеседнике и существующих чатах." : { + "comment" : "Search placeholder description" + }, "Идеи" : { "extractionState" : "stale", "localizations" : { @@ -1082,6 +1088,9 @@ } } }, + "Профиль в разработке" : { + "comment" : "Search placeholder title" + }, "Публичная информация" : { }, @@ -1204,6 +1213,9 @@ } } }, + "Скопировать" : { + "comment" : "Search placeholder copy" + }, "Скоро появится мини-игра, где можно заработать очки для кастомизации профиля. Следите за обновлениями!" : { "comment" : "Concept tab placeholder description" }, diff --git a/yobble/Views/Tab/ChatsTab.swift b/yobble/Views/Tab/ChatsTab.swift index 461b14b..263e85a 100644 --- a/yobble/Views/Tab/ChatsTab.swift +++ b/yobble/Views/Tab/ChatsTab.swift @@ -23,6 +23,7 @@ struct ChatsTab: View { @State private var isGlobalSearchLoading: Bool = false @State private var globalSearchError: String? @State private var globalSearchTask: Task? = nil + @State private var selectedSearchUserId: UUID? private let searchRevealDistance: CGFloat = 90 @@ -361,43 +362,58 @@ struct ChatsTab: View { } private func globalSearchRow(for user: UserSearchResult) -> some View { - HStack(spacing: 12) { - Circle() - .fill(user.isOfficial ? Color.accentColor.opacity(0.85) : Color.accentColor.opacity(0.15)) - .frame(width: 44, height: 44) - .overlay( - Text(user.avatarInitial) - .font(.headline) - .foregroundColor(user.isOfficial ? .white : Color.accentColor) - ) + Button { + selectedSearchUserId = user.userId + } label: { + HStack(spacing: 12) { + Circle() + .fill(user.isOfficial ? Color.accentColor.opacity(0.85) : Color.accentColor.opacity(0.15)) + .frame(width: 44, height: 44) + .overlay( + Text(user.avatarInitial) + .font(.headline) + .foregroundColor(user.isOfficial ? .white : Color.accentColor) + ) - VStack(alignment: .leading, spacing: 4) { - HStack(spacing: 6) { - Text(user.displayName) - .fontWeight(user.isOfficial ? .semibold : .regular) - .foregroundColor(.primary) - .lineLimit(1) - .truncationMode(.tail) + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 6) { + Text(user.displayName) + .fontWeight(user.isOfficial ? .semibold : .regular) + .foregroundColor(.primary) + .lineLimit(1) + .truncationMode(.tail) - if user.isOfficial { - Image(systemName: "checkmark.seal.fill") - .foregroundColor(Color.accentColor) - .font(.caption) + if user.isOfficial { + Image(systemName: "checkmark.seal.fill") + .foregroundColor(Color.accentColor) + .font(.caption) + } + } + + if let secondary = secondaryLine(for: user) { + Text(secondary) + .font(.footnote) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.tail) } } - - if let secondary = user.secondaryLabelForSearch { - Text(secondary) - .font(.footnote) - .foregroundColor(.secondary) - .lineLimit(1) - .truncationMode(.tail) - } + .frame(maxWidth: .infinity, alignment: .leading) } - .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 8) } - .padding(.vertical, 8) + .buttonStyle(.plain) .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) + .background( + NavigationLink( + destination: SearchResultPlaceholderView(userId: user.userId), + tag: user.userId, + selection: $selectedSearchUserId + ) { + EmptyView() + } + .hidden() + ) } } @@ -423,6 +439,7 @@ private extension ChatsTab { globalSearchResults = [] globalSearchError = nil isGlobalSearchLoading = false + selectedSearchUserId = nil return } @@ -441,6 +458,7 @@ private extension ChatsTab { isGlobalSearchLoading = false globalSearchError = nil globalSearchTask = nil + selectedSearchUserId = nil } } catch is CancellationError { // Ignore cancellation @@ -451,6 +469,7 @@ private extension ChatsTab { isGlobalSearchLoading = false globalSearchError = friendlyErrorMessage(for: error) globalSearchTask = nil + selectedSearchUserId = nil } } } @@ -462,8 +481,37 @@ private extension ChatsTab { globalSearchResults = [] globalSearchError = nil isGlobalSearchLoading = false + selectedSearchUserId = nil } + func secondaryLine(for user: UserSearchResult) -> String? { + if let official = user.officialFullName { + if let custom = user.preferredCustomName, custom != official { + return "\(user.loginHandle) (\(custom))" + } +// if official != user.loginHandle { +// return user.loginHandle +// } + } + else if let custom = user.preferredCustomName, custom != user.displayName { + return custom + } + + if let profileLogin = user.profile?.login?.trimmingCharacters(in: .whitespacesAndNewlines), !profileLogin.isEmpty { + let handle = "@\(profileLogin)" + if handle != user.loginHandle { + return handle + } + } + + if user.displayName != user.loginHandle { + return user.loginHandle + } + + return nil + } + + func friendlyErrorMessage(for error: Error) -> String { if let searchError = error as? SearchServiceError { return searchError.errorDescription ?? NSLocalizedString("Не удалось выполнить поиск.", comment: "Search error fallback") @@ -500,6 +548,52 @@ private struct ScrollDismissesKeyboardModifier: ViewModifier { } } +private struct SearchResultPlaceholderView: View { + let userId: UUID + + var body: some View { + VStack(spacing: 16) { + Text(NSLocalizedString("Профиль в разработке", comment: "Search placeholder title")) + .font(.headline) + + VStack(alignment: .leading, spacing: 8) { + Text(NSLocalizedString("Companion ID", comment: "Search placeholder companion title")) + .font(.subheadline) + .foregroundColor(.secondary) + Text(userId.uuidString) + .font(.body.monospaced()) + .contextMenu { + Button(action: { +#if canImport(UIKit) + UIPasteboard.general.string = userId.uuidString +#endif + }) { + Label(NSLocalizedString("Скопировать", comment: "Search placeholder copy"), systemImage: "doc.on.doc") + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color(UIColor.secondarySystemBackground)) + ) + + Text(NSLocalizedString("Здесь появится информация о собеседнике и существующих чатах.", comment: "Search placeholder description")) + .font(.footnote) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal) + + Spacer() + } + .padding(.top, 40) + .padding(.horizontal, 24) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .background(Color(UIColor.systemBackground)) + } +} + private struct ChatRowView: View { let chat: PrivateChatListItem let currentUserId: String?