Compare commits
2 Commits
198b51bd91
...
8acacdb8c1
| Author | SHA1 | Date | |
|---|---|---|---|
| 8acacdb8c1 | |||
| 1c9f249289 |
@ -398,6 +398,15 @@
|
|||||||
},
|
},
|
||||||
"Если не нашли ответ, напишите нам своё предложение или проблему." : {
|
"Если не нашли ответ, напишите нам своё предложение или проблему." : {
|
||||||
"comment" : "FAQ: contact developers footer"
|
"comment" : "FAQ: contact developers footer"
|
||||||
|
},
|
||||||
|
"Заблокированные" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Заблокированные пользователи" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Заблокируйте аккаунт, чтобы скрыть его сообщения и взаимодействия" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Заглушка: Push-уведомления" : {
|
"Заглушка: Push-уведомления" : {
|
||||||
|
|
||||||
@ -1814,6 +1823,9 @@
|
|||||||
},
|
},
|
||||||
"Публичная информация" : {
|
"Публичная информация" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Разблокировать" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Разрешить пересылку сообщений" : {
|
"Разрешить пересылку сообщений" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -2177,6 +2189,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"У вас нет заблокированных пользователей" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Уведомления" : {
|
"Уведомления" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@ -32,6 +32,7 @@ struct ChatsTab: View {
|
|||||||
@State private var isPendingChatActive: Bool = false
|
@State private var isPendingChatActive: Bool = false
|
||||||
|
|
||||||
private let searchRevealDistance: CGFloat = 90
|
private let searchRevealDistance: CGFloat = 90
|
||||||
|
private let scrollToTopAnchorId = "ChatsListTopAnchor"
|
||||||
|
|
||||||
private var currentUserId: String? {
|
private var currentUserId: String? {
|
||||||
let userId = loginViewModel.userId
|
let userId = loginViewModel.userId
|
||||||
@ -109,8 +110,9 @@ struct ChatsTab: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var chatList: some View {
|
private var chatList: some View {
|
||||||
ZStack {
|
ScrollViewReader { proxy in
|
||||||
List {
|
ZStack {
|
||||||
|
List {
|
||||||
// VStack(spacing: 0) {
|
// VStack(spacing: 0) {
|
||||||
// searchBar
|
// searchBar
|
||||||
// .padding(.horizontal, 16)
|
// .padding(.horizontal, 16)
|
||||||
@ -118,63 +120,70 @@ 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: triggerChatsReload) {
|
|
||||||
Text(NSLocalizedString("Обновить", comment: ""))
|
|
||||||
.font(.subheadline)
|
.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))
|
||||||
}
|
|
||||||
.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: globalSearchHeader) {
|
if isSearching {
|
||||||
globalSearchContent
|
Section(header: localSearchHeader) {
|
||||||
}
|
if localSearchResults.isEmpty {
|
||||||
} else {
|
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 {
|
// if let message = viewModel.errorMessage, viewModel.chats.isEmpty {
|
||||||
// errorState(message: message)
|
// errorState(message: message)
|
||||||
// } else
|
// } else
|
||||||
if viewModel.chats.isEmpty {
|
if viewModel.chats.isEmpty {
|
||||||
emptyState
|
emptyState
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
ForEach(viewModel.chats) { chat in
|
let firstChatId = viewModel.chats.first?.chatId
|
||||||
chatRowItem(for: chat)
|
ForEach(viewModel.chats) { chat in
|
||||||
}
|
chatRowItem(for: chat)
|
||||||
|
.id(chat.chatId == firstChatId ? scrollToTopAnchorId : chat.chatId)
|
||||||
|
}
|
||||||
|
|
||||||
if viewModel.isLoadingMore {
|
if viewModel.isLoadingMore {
|
||||||
loadingMoreRow
|
loadingMoreRow
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.listStyle(.plain)
|
||||||
.listStyle(.plain)
|
.modifier(ScrollDismissesKeyboardModifier())
|
||||||
.modifier(ScrollDismissesKeyboardModifier())
|
.simultaneousGesture(searchBarGesture)
|
||||||
.simultaneousGesture(searchBarGesture)
|
.simultaneousGesture(tapToDismissKeyboardGesture)
|
||||||
.simultaneousGesture(tapToDismissKeyboardGesture)
|
.onReceive(NotificationCenter.default.publisher(for: .chatsShouldScrollToTop)) { _ in
|
||||||
|
scrollChatsToTop(using: proxy)
|
||||||
|
}
|
||||||
// .safeAreaInset(edge: .top) {
|
// .safeAreaInset(edge: .top) {
|
||||||
// VStack(spacing: 0) {
|
// VStack(spacing: 0) {
|
||||||
// searchBar
|
// searchBar
|
||||||
@ -186,7 +195,8 @@ struct ChatsTab: View {
|
|||||||
// .background(Color(UIColor.systemBackground))
|
// .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 {
|
private var searchBarGesture: some Gesture {
|
||||||
DragGesture(minimumDistance: 10, coordinateSpace: .local)
|
DragGesture(minimumDistance: 10, coordinateSpace: .local)
|
||||||
.onChanged { value in
|
.onChanged { value in
|
||||||
@ -319,8 +337,10 @@ struct ChatsTab: View {
|
|||||||
if globalSearchResults.isEmpty {
|
if globalSearchResults.isEmpty {
|
||||||
globalSearchEmptyRow
|
globalSearchEmptyRow
|
||||||
} else {
|
} else {
|
||||||
|
let firstGlobalUserId = globalSearchResults.first?.id
|
||||||
ForEach(globalSearchResults) { user in
|
ForEach(globalSearchResults) { user in
|
||||||
globalSearchRow(for: user)
|
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 debugRefreshChats = Notification.Name("debugRefreshChats")
|
||||||
static let chatsShouldRefresh = Notification.Name("chatsShouldRefresh")
|
static let chatsShouldRefresh = Notification.Name("chatsShouldRefresh")
|
||||||
static let chatsReloadCompleted = Notification.Name("chatsReloadCompleted")
|
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) {
|
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) {
|
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) {
|
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) {
|
TabBarButton(systemName: "person.crop.square", text: NSLocalizedString("Лицо", comment: ""), isSelected: selectedTab == 3) {
|
||||||
@ -93,3 +93,13 @@ struct CreateButton: View {
|
|||||||
.offset(y: -3)
|
.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()) {
|
// NavigationLink(destination: EditProfileView()) {
|
||||||
// Label("Мой профиль", systemImage: "person.crop.circle")
|
// Label("Мой профиль", systemImage: "person.crop.circle")
|
||||||
// }
|
// }
|
||||||
|
|
||||||
NavigationLink(destination: EditPrivacyView()) {
|
NavigationLink(destination: EditPrivacyView()) {
|
||||||
Label(NSLocalizedString("Конфиденциальность", comment: ""), systemImage: "lock.fill")
|
Label(NSLocalizedString("Конфиденциальность", comment: ""), systemImage: "lock.fill")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavigationLink(destination: BlockedUsersView()) {
|
||||||
|
Label(NSLocalizedString("Заблокированные пользователи", comment: ""), systemImage: "hand.raised.fill")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Безопасность
|
// MARK: - Безопасность
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user