patch search
This commit is contained in:
		
							parent
							
								
									7ce0fb3cd3
								
							
						
					
					
						commit
						f6ca117b8e
					
				@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,7 @@ struct ChatsTab: View {
 | 
			
		||||
    @State private var isGlobalSearchLoading: Bool = false
 | 
			
		||||
    @State private var globalSearchError: String?
 | 
			
		||||
    @State private var globalSearchTask: Task<Void, Never>? = 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?
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user