Compare commits
	
		
			2 Commits
		
	
	
		
			198b51bd91
			...
			8acacdb8c1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8acacdb8c1 | |||
| 1c9f249289 | 
@ -398,6 +398,15 @@
 | 
			
		||||
    },
 | 
			
		||||
    "Если не нашли ответ, напишите нам своё предложение или проблему." : {
 | 
			
		||||
      "comment" : "FAQ: contact developers footer"
 | 
			
		||||
    },
 | 
			
		||||
    "Заблокированные" : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Заблокированные пользователи" : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Заблокируйте аккаунт, чтобы скрыть его сообщения и взаимодействия" : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Заглушка: Push-уведомления" : {
 | 
			
		||||
 | 
			
		||||
@ -1814,6 +1823,9 @@
 | 
			
		||||
    },
 | 
			
		||||
    "Публичная информация" : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Разблокировать" : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Разрешить пересылку сообщений" : {
 | 
			
		||||
      "localizations" : {
 | 
			
		||||
@ -2177,6 +2189,9 @@
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "У вас нет заблокированных пользователей" : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Уведомления" : {
 | 
			
		||||
      "localizations" : {
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,7 @@ struct ChatsTab: View {
 | 
			
		||||
    @State private var isPendingChatActive: Bool = false
 | 
			
		||||
 | 
			
		||||
    private let searchRevealDistance: CGFloat = 90
 | 
			
		||||
    private let scrollToTopAnchorId = "ChatsListTopAnchor"
 | 
			
		||||
 | 
			
		||||
    private var currentUserId: String? {
 | 
			
		||||
        let userId = loginViewModel.userId
 | 
			
		||||
@ -109,8 +110,9 @@ struct ChatsTab: View {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var chatList: some View {
 | 
			
		||||
        ZStack {
 | 
			
		||||
            List {
 | 
			
		||||
        ScrollViewReader { proxy in
 | 
			
		||||
            ZStack {
 | 
			
		||||
                List {
 | 
			
		||||
//            VStack(spacing: 0) {
 | 
			
		||||
//                searchBar
 | 
			
		||||
//                    .padding(.horizontal, 16)
 | 
			
		||||
@ -118,63 +120,70 @@ struct ChatsTab: View {
 | 
			
		||||
//            }
 | 
			
		||||
//            .background(Color(UIColor.systemBackground))
 | 
			
		||||
 | 
			
		||||
                if let message = viewModel.errorMessage {
 | 
			
		||||
                    Section {
 | 
			
		||||
                        HStack(alignment: .top, spacing: 8) {
 | 
			
		||||
                            Image(systemName: "exclamationmark.triangle.fill")
 | 
			
		||||
                                .foregroundColor(.orange)
 | 
			
		||||
                            Text(message)
 | 
			
		||||
                                .font(.subheadline)
 | 
			
		||||
                                .foregroundColor(.orange)
 | 
			
		||||
                            Spacer(minLength: 0)
 | 
			
		||||
                            Button(action: triggerChatsReload) {
 | 
			
		||||
                                Text(NSLocalizedString("Обновить", comment: ""))
 | 
			
		||||
                    if let message = viewModel.errorMessage {
 | 
			
		||||
                        Section {
 | 
			
		||||
                            HStack(alignment: .top, spacing: 8) {
 | 
			
		||||
                                Image(systemName: "exclamationmark.triangle.fill")
 | 
			
		||||
                                    .foregroundColor(.orange)
 | 
			
		||||
                                Text(message)
 | 
			
		||||
                                    .font(.subheadline)
 | 
			
		||||
                                    .foregroundColor(.orange)
 | 
			
		||||
                                Spacer(minLength: 0)
 | 
			
		||||
                                Button(action: triggerChatsReload) {
 | 
			
		||||
                                    Text(NSLocalizedString("Обновить", comment: ""))
 | 
			
		||||
                                        .font(.subheadline)
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            .padding(.vertical, 4)
 | 
			
		||||
                        }
 | 
			
		||||
                        .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)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    Section(header: globalSearchHeader) {
 | 
			
		||||
                        globalSearchContent
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    if isSearching {
 | 
			
		||||
                        Section(header: localSearchHeader) {
 | 
			
		||||
                            if localSearchResults.isEmpty {
 | 
			
		||||
                                emptySearchResultView
 | 
			
		||||
                                    .listRowInsets(EdgeInsets(top: 24, leading: 16, bottom: 24, trailing: 16))
 | 
			
		||||
                                    .listRowSeparator(.hidden)
 | 
			
		||||
                            } else {
 | 
			
		||||
                                let firstLocalChatId = localSearchResults.first?.chatId
 | 
			
		||||
                                ForEach(localSearchResults) { chat in
 | 
			
		||||
                                    chatRowItem(for: chat)
 | 
			
		||||
                                        .id(chat.chatId == firstLocalChatId ? scrollToTopAnchorId : chat.chatId)
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        Section(header: globalSearchHeader) {
 | 
			
		||||
                            globalSearchContent
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
//                    if let message = viewModel.errorMessage, viewModel.chats.isEmpty {
 | 
			
		||||
//                        errorState(message: message)
 | 
			
		||||
//                    } else
 | 
			
		||||
                    if viewModel.chats.isEmpty {
 | 
			
		||||
                        emptyState
 | 
			
		||||
                    } else {
 | 
			
		||||
                        if viewModel.chats.isEmpty {
 | 
			
		||||
                            emptyState
 | 
			
		||||
                        } else {
 | 
			
		||||
 | 
			
		||||
                        ForEach(viewModel.chats) { chat in
 | 
			
		||||
                            chatRowItem(for: chat)
 | 
			
		||||
                        }
 | 
			
		||||
                            let firstChatId = viewModel.chats.first?.chatId
 | 
			
		||||
                            ForEach(viewModel.chats) { chat in
 | 
			
		||||
                                chatRowItem(for: chat)
 | 
			
		||||
                                    .id(chat.chatId == firstChatId ? scrollToTopAnchorId : chat.chatId)
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                        if viewModel.isLoadingMore {
 | 
			
		||||
                            loadingMoreRow
 | 
			
		||||
                            if viewModel.isLoadingMore {
 | 
			
		||||
                                loadingMoreRow
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .listStyle(.plain)
 | 
			
		||||
            .modifier(ScrollDismissesKeyboardModifier())
 | 
			
		||||
            .simultaneousGesture(searchBarGesture)
 | 
			
		||||
            .simultaneousGesture(tapToDismissKeyboardGesture)
 | 
			
		||||
                .listStyle(.plain)
 | 
			
		||||
                .modifier(ScrollDismissesKeyboardModifier())
 | 
			
		||||
                .simultaneousGesture(searchBarGesture)
 | 
			
		||||
                .simultaneousGesture(tapToDismissKeyboardGesture)
 | 
			
		||||
                .onReceive(NotificationCenter.default.publisher(for: .chatsShouldScrollToTop)) { _ in
 | 
			
		||||
                    scrollChatsToTop(using: proxy)
 | 
			
		||||
                }
 | 
			
		||||
//            .safeAreaInset(edge: .top) {
 | 
			
		||||
//                VStack(spacing: 0) {
 | 
			
		||||
//                    searchBar
 | 
			
		||||
@ -186,7 +195,8 @@ struct ChatsTab: View {
 | 
			
		||||
//                .background(Color(UIColor.systemBackground))
 | 
			
		||||
//            }
 | 
			
		||||
 | 
			
		||||
            pendingChatNavigationLink
 | 
			
		||||
                pendingChatNavigationLink
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -217,6 +227,14 @@ struct ChatsTab: View {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func scrollChatsToTop(using proxy: ScrollViewProxy) {
 | 
			
		||||
        DispatchQueue.main.async {
 | 
			
		||||
            withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) {
 | 
			
		||||
                proxy.scrollTo(scrollToTopAnchorId, anchor: .top)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var searchBarGesture: some Gesture {
 | 
			
		||||
        DragGesture(minimumDistance: 10, coordinateSpace: .local)
 | 
			
		||||
            .onChanged { value in
 | 
			
		||||
@ -319,8 +337,10 @@ struct ChatsTab: View {
 | 
			
		||||
        if globalSearchResults.isEmpty {
 | 
			
		||||
            globalSearchEmptyRow
 | 
			
		||||
        } else {
 | 
			
		||||
            let firstGlobalUserId = globalSearchResults.first?.id
 | 
			
		||||
            ForEach(globalSearchResults) { user in
 | 
			
		||||
                globalSearchRow(for: user)
 | 
			
		||||
                    .id(user.id == firstGlobalUserId ? AnyHashable(scrollToTopAnchorId) : AnyHashable(user.id))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -1181,4 +1201,5 @@ extension Notification.Name {
 | 
			
		||||
    static let debugRefreshChats = Notification.Name("debugRefreshChats")
 | 
			
		||||
    static let chatsShouldRefresh = Notification.Name("chatsShouldRefresh")
 | 
			
		||||
    static let chatsReloadCompleted = Notification.Name("chatsReloadCompleted")
 | 
			
		||||
    static let chatsShouldScrollToTop = Notification.Name("chatsShouldScrollToTop")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@ struct CustomTabBar: View {
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                TabBarButton(systemName: "bubble.left.and.bubble.right.fill", text: NSLocalizedString("Чаты", comment: ""), isSelected: selectedTab == 2) {
 | 
			
		||||
                    selectedTab = 2
 | 
			
		||||
                    handleChatsTabTap()
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                TabBarButton(systemName: "gearshape.fill", text: NSLocalizedString("Настройки", comment: ""), isSelected: selectedTab == 5) {
 | 
			
		||||
@ -34,7 +34,7 @@ struct CustomTabBar: View {
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                TabBarButton(systemName: "bubble.left.and.bubble.right.fill", text: NSLocalizedString("Чаты", comment: ""), isSelected: selectedTab == 2) {
 | 
			
		||||
                    selectedTab = 2
 | 
			
		||||
                    handleChatsTabTap()
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                TabBarButton(systemName: "person.crop.square", text: NSLocalizedString("Лицо", comment: ""), isSelected: selectedTab == 3) {
 | 
			
		||||
@ -93,3 +93,13 @@ struct CreateButton: View {
 | 
			
		||||
        .offset(y: -3)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private extension CustomTabBar {
 | 
			
		||||
    func handleChatsTabTap() {
 | 
			
		||||
        if selectedTab == 2 {
 | 
			
		||||
            NotificationCenter.default.post(name: .chatsShouldScrollToTop, object: nil)
 | 
			
		||||
        } else {
 | 
			
		||||
            selectedTab = 2
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										103
									
								
								yobble/Views/Tab/Settings/BlockedUsersView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								yobble/Views/Tab/Settings/BlockedUsersView.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,103 @@
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
struct BlockedUsersView: View {
 | 
			
		||||
    @State private var blockedUsers: [BlockedUser] = []
 | 
			
		||||
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        List {
 | 
			
		||||
            if blockedUsers.isEmpty {
 | 
			
		||||
                emptyState
 | 
			
		||||
            } else {
 | 
			
		||||
                Section(header: Text(NSLocalizedString("Заблокированные", comment: ""))) {
 | 
			
		||||
                    ForEach(blockedUsers) { user in
 | 
			
		||||
                        HStack(spacing: 12) {
 | 
			
		||||
                            Circle()
 | 
			
		||||
                                .fill(Color.accentColor.opacity(0.15))
 | 
			
		||||
                                .frame(width: 44, height: 44)
 | 
			
		||||
                                .overlay(
 | 
			
		||||
                                    Text(user.initials)
 | 
			
		||||
                                        .font(.headline)
 | 
			
		||||
                                        .foregroundColor(.accentColor)
 | 
			
		||||
                                )
 | 
			
		||||
                            VStack(alignment: .leading, spacing: 4) {
 | 
			
		||||
                                Text(user.displayName)
 | 
			
		||||
                                    .font(.body)
 | 
			
		||||
                                if let handle = user.handle {
 | 
			
		||||
                                    Text(handle)
 | 
			
		||||
                                        .font(.caption)
 | 
			
		||||
                                        .foregroundColor(.secondary)
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            Spacer()
 | 
			
		||||
                            Button(role: .destructive) {
 | 
			
		||||
                                unblock(user)
 | 
			
		||||
                            } label: {
 | 
			
		||||
                                Text(NSLocalizedString("Разблокировать", comment: ""))
 | 
			
		||||
                            }
 | 
			
		||||
                            .buttonStyle(.borderless)
 | 
			
		||||
                        }
 | 
			
		||||
                        .padding(.vertical, 4)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .navigationTitle(NSLocalizedString("Заблокированные", comment: ""))
 | 
			
		||||
        .navigationBarTitleDisplayMode(.inline)
 | 
			
		||||
        .task {
 | 
			
		||||
            await loadBlockedUsers()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var emptyState: some View {
 | 
			
		||||
        VStack(spacing: 12) {
 | 
			
		||||
            Image(systemName: "hand.raised")
 | 
			
		||||
                .font(.system(size: 48))
 | 
			
		||||
                .foregroundColor(.secondary)
 | 
			
		||||
            Text(NSLocalizedString("У вас нет заблокированных пользователей", comment: ""))
 | 
			
		||||
                .font(.headline)
 | 
			
		||||
                .multilineTextAlignment(.center)
 | 
			
		||||
//            Text(NSLocalizedString("Заблокируйте аккаунт, чтобы скрыть его сообщения и взаимодействия", comment: ""))
 | 
			
		||||
//                .font(.subheadline)
 | 
			
		||||
//                .foregroundColor(.secondary)
 | 
			
		||||
//                .multilineTextAlignment(.center)
 | 
			
		||||
        }
 | 
			
		||||
        .frame(maxWidth: .infinity, alignment: .center)
 | 
			
		||||
        .padding(.vertical, 32)
 | 
			
		||||
        .listRowInsets(EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16))
 | 
			
		||||
        .listRowSeparator(.hidden)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func loadBlockedUsers() async {
 | 
			
		||||
        // TODO: integrate with real data source once available
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func unblock(_ user: BlockedUser) {
 | 
			
		||||
        // TODO: implement unblock logic when backend is ready
 | 
			
		||||
        blockedUsers.removeAll { $0.id == user.id }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private struct BlockedUser: Identifiable, Equatable {
 | 
			
		||||
    let id: UUID
 | 
			
		||||
    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()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let handle {
 | 
			
		||||
            let filtered = handle.filter { $0.isLetter }.prefix(2)
 | 
			
		||||
            if !filtered.isEmpty {
 | 
			
		||||
                return filtered.uppercased()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return "??"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -17,10 +17,14 @@ struct SettingsView: View {
 | 
			
		||||
//                NavigationLink(destination: EditProfileView()) {
 | 
			
		||||
//                    Label("Мой профиль", systemImage: "person.crop.circle")
 | 
			
		||||
//                }
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                NavigationLink(destination: EditPrivacyView()) {
 | 
			
		||||
                    Label(NSLocalizedString("Конфиденциальность", comment: ""), systemImage: "lock.fill")
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                NavigationLink(destination: BlockedUsersView()) {
 | 
			
		||||
                    Label(NSLocalizedString("Заблокированные пользователи", comment: ""), systemImage: "hand.raised.fill")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // MARK: - Безопасность
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user