Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions SwiftUI-WorkoutApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
67627750283A3A54009C203F /* JournalsListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6762774F283A3A54009C203F /* JournalsListScreen.swift */; };
67627755283A4C77009C203F /* JournalEntriesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67627754283A4C77009C203F /* JournalEntriesScreen.swift */; };
6762775B283A87AD009C203F /* JournalCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6762775A283A87AD009C203F /* JournalCell.swift */; };
6764D6382D52009F00699007 /* DialogsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6764D6372D52009F00699007 /* DialogsViewModel.swift */; };
6765B2562D451771006164AB /* UIImage+toMediaFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6765B2552D451771006164AB /* UIImage+toMediaFile.swift */; };
6765B2582D4544C8006164AB /* MainUserProfileScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6765B2572D4544C8006164AB /* MainUserProfileScreen.swift */; };
6765B25B2D455D5C006164AB /* ProfileViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6765B25A2D455D5C006164AB /* ProfileViews.swift */; };
Expand Down Expand Up @@ -123,6 +124,7 @@
6762774F283A3A54009C203F /* JournalsListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JournalsListScreen.swift; sourceTree = "<group>"; };
67627754283A4C77009C203F /* JournalEntriesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JournalEntriesScreen.swift; sourceTree = "<group>"; };
6762775A283A87AD009C203F /* JournalCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JournalCell.swift; sourceTree = "<group>"; };
6764D6372D52009F00699007 /* DialogsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogsViewModel.swift; sourceTree = "<group>"; };
6765B2552D451771006164AB /* UIImage+toMediaFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+toMediaFile.swift"; sourceTree = "<group>"; };
6765B2572D4544C8006164AB /* MainUserProfileScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainUserProfileScreen.swift; sourceTree = "<group>"; };
6765B25A2D455D5C006164AB /* ProfileViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViews.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -227,6 +229,7 @@
67419AD5282E8E7C004F5339 /* Messages */ = {
isa = PBXGroup;
children = (
6764D6372D52009F00699007 /* DialogsViewModel.swift */,
67D916802838E2460098D3CB /* DialogsListScreen.swift */,
67D916852838F0DD0098D3CB /* DialogScreen.swift */,
);
Expand Down Expand Up @@ -646,6 +649,7 @@
6798AA84280C0F7D00DB76F1 /* EditProfileScreen.swift in Sources */,
6798AA73280B43FE00DB76F1 /* LoginScreen.swift in Sources */,
67D9169628396C1E0098D3CB /* SendMessageScreen.swift in Sources */,
6764D6382D52009F00699007 /* DialogsViewModel.swift in Sources */,
6747575928128603002F0A24 /* ParkDetailScreen.swift in Sources */,
675EC6572815433600C2E229 /* UsersListScreen.swift in Sources */,
675EC65F2815532800C2E229 /* EventFormScreen.swift in Sources */,
Expand Down Expand Up @@ -852,7 +856,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 7;
CURRENT_PROJECT_VERSION = 8;
DEVELOPMENT_ASSET_PATHS = "SwiftUI-WorkoutApp/Preview\\ Content/PreviewContent.swift SwiftUI-WorkoutApp/Preview\\ Content";
DEVELOPMENT_TEAM = CR68PP2Z3F;
ENABLE_PREVIEWS = YES;
Expand Down Expand Up @@ -903,7 +907,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 7;
CURRENT_PROJECT_VERSION = 8;
DEVELOPMENT_ASSET_PATHS = "SwiftUI-WorkoutApp/Preview\\ Content/PreviewContent.swift SwiftUI-WorkoutApp/Preview\\ Content";
DEVELOPMENT_TEAM = CR68PP2Z3F;
ENABLE_PREVIEWS = YES;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ public struct SWClient: Sendable {
/// - Parameters:
/// - userID: `id` пользователя
/// - Returns: вся информация о пользователе
@discardableResult
public func getUserByID(_ userID: Int) async throws -> UserResponse {
let endpoint = Endpoint.getUser(id: userID)
return try await makeResult(for: endpoint)
Expand Down
77 changes: 21 additions & 56 deletions SwiftUI-WorkoutApp/Screens/Messages/DialogsListScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ import SWUtils
struct DialogsListScreen: View {
@Environment(\.isNetworkConnected) private var isNetworkConnected
@EnvironmentObject private var defaults: DefaultsService
@State private var dialogs = [DialogResponse]()
@EnvironmentObject private var viewModel: DialogsViewModel
@State private var selectedDialog: DialogResponse?
@State private var isLoading = false
@State private var indexToDelete: Int?
@State private var openFriendList = false
@State private var showDeleteConfirmation = false
Expand All @@ -34,22 +33,22 @@ struct DialogsListScreen: View {
.navigationTitle("Сообщения")
}
.navigationViewStyle(.stack)
.task { await askForDialogs() }
.onChange(of: defaults.isAuthorized, perform: viewModel.clearDialogsOnLogout)
.task(id: defaults.isAuthorized) { await askForDialogs() }
}
}

private extension DialogsListScreen {
var authorizedContentView: some View {
dialogList
.overlay { emptyContentView }
.loadingOverlay(if: isLoading)
.loadingOverlay(if: viewModel.isLoading)
.background(Color.swBackground)
.confirmationDialog(
.init(Constants.Alert.deleteDialog),
isPresented: $showDeleteConfirmation,
titleVisibility: .visible
) { deleteDialogButton }
.refreshable { await askForDialogs(refresh: true) }
.toolbar {
ToolbarItem(placement: .topBarLeading) {
refreshButton
Expand All @@ -58,9 +57,6 @@ private extension DialogsListScreen {
friendListButton
}
}
.onDisappear {
[refreshTask, deleteDialogTask].forEach { $0?.cancel() }
}
}

var refreshButton: some View {
Expand All @@ -71,8 +67,8 @@ private extension DialogsListScreen {
} label: {
Icons.Regular.refresh.view
}
.opacity(showEmptyView || !DeviceOSVersionChecker.iOS16Available ? 1 : 0)
.disabled(isLoading)
.opacity(viewModel.showEmptyView ? 1 : 0)
.disabled(viewModel.isLoading || !isNetworkConnected)
}

var friendListButton: some View {
Expand All @@ -86,28 +82,24 @@ private extension DialogsListScreen {
Icons.Regular.plus.view
.symbolVariant(.circle)
}
.opacity(hasFriends || !dialogs.isEmpty ? 1 : 0)
.opacity(hasFriends || viewModel.hasDialogs ? 1 : 0)
.disabled(!isNetworkConnected)
}

var emptyContentView: some View {
EmptyContentView(
mode: .dialogs,
action: emptyViewAction
action: { openFriendList.toggle() }
)
.opacity(showEmptyView ? 1 : 0)
}

var showEmptyView: Bool {
dialogs.isEmpty && !isLoading
.opacity(viewModel.showEmptyView ? 1 : 0)
}

@ViewBuilder
var dialogList: some View {
ZStack {
Color.swBackground
List {
ForEach(dialogs) { model in
ForEach(viewModel.dialogs) { model in
dialogListItem(model)
.listRowInsets(.init(top: 12, leading: 16, bottom: 12, trailing: 16))
.listRowBackground(Color.swBackground)
Expand All @@ -116,9 +108,10 @@ private extension DialogsListScreen {
.onDelete { initiateDeletion(at: $0) }
}
.listStyle(.plain)
.opacity(dialogs.isEmpty ? 0 : 1)
.opacity(viewModel.hasDialogs ? 1 : 0)
.refreshable { await askForDialogs(refresh: true) }
}
.animation(.default, value: dialogs.count)
.animation(.default, value: viewModel.dialogs.count)
.background(
NavigationLink(
destination: lazyDestination,
Expand All @@ -130,7 +123,12 @@ private extension DialogsListScreen {
@ViewBuilder
var lazyDestination: some View {
if let selectedDialog {
DialogScreen(dialog: selectedDialog) { markAsRead($0) }
DialogScreen(
dialog: selectedDialog,
markedAsReadClbk: { dialog in
viewModel.markAsRead(dialog, defaults: defaults)
}
)
}
}

Expand Down Expand Up @@ -162,39 +160,12 @@ private extension DialogsListScreen {
defaults.hasFriends
}

func emptyViewAction() {
openFriendList.toggle()
}

func markAsRead(_ dialog: DialogResponse) {
dialogs = dialogs.map { item in
if item.id == dialog.id {
var updatedDialog = dialog
updatedDialog.unreadMessagesCount = 0
return updatedDialog
} else {
return item
}
}
guard dialog.unreadMessagesCount > 0,
defaults.unreadMessagesCount >= dialog.unreadMessagesCount
else { return }
let newValue = defaults.unreadMessagesCount - dialog.unreadMessagesCount
defaults.saveUnreadMessagesCount(newValue)
}

func askForDialogs(refresh: Bool = false) async {
guard defaults.isAuthorized else { return }
if isLoading || (!dialogs.isEmpty && !refresh) { return }
if !refresh { isLoading = true }
do {
dialogs = try await client.getDialogs()
let unreadMessagesCount = dialogs.map(\.unreadMessagesCount).reduce(0, +)
defaults.saveUnreadMessagesCount(unreadMessagesCount)
try await viewModel.askForDialogs(refresh: refresh, defaults: defaults)
} catch {
SWAlert.shared.presentDefaultUIKit(message: error.localizedDescription)
}
isLoading = false
}

func initiateDeletion(at indexSet: IndexSet) {
Expand All @@ -204,17 +175,11 @@ private extension DialogsListScreen {

func deleteAction(at index: Int?) {
deleteDialogTask = Task {
guard let index, !isLoading else { return }
isLoading = true
do {
let dialogID = dialogs[index].id
if try await client.deleteDialog(dialogID) {
dialogs.remove(at: index)
}
try await viewModel.deleteDialog(at: index, defaults: defaults)
} catch {
SWAlert.shared.presentDefaultUIKit(message: error.localizedDescription)
}
isLoading = false
}
}
}
Expand Down
60 changes: 60 additions & 0 deletions SwiftUI-WorkoutApp/Screens/Messages/DialogsViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Foundation
import SWModels
import SWNetworkClient
import SWUtils

final class DialogsViewModel: ObservableObject {
@Published private(set) var dialogs = [DialogResponse]()
@Published private(set) var isLoading = false
var hasDialogs: Bool { !dialogs.isEmpty }
var showEmptyView: Bool { !hasDialogs && !isLoading }

@MainActor
func askForDialogs(
refresh: Bool = false,
defaults: DefaultsService
) async throws {
guard defaults.isAuthorized else { return }
if isLoading || (!dialogs.isEmpty && !refresh) { return }
if !refresh || dialogs.isEmpty { isLoading = true }
dialogs = try await SWClient(with: defaults).getDialogs()
let unreadMessagesCount = dialogs.map(\.unreadMessagesCount).reduce(0, +)
defaults.saveUnreadMessagesCount(unreadMessagesCount)
isLoading = false
}

@MainActor
func deleteDialog(at index: Int?, defaults: DefaultsService) async throws {
guard let index, !isLoading else { return }
isLoading = true
let dialogID = dialogs[index].id
if try await SWClient(with: defaults).deleteDialog(dialogID) {
dialogs.remove(at: index)
}
isLoading = false
}

@MainActor
func markAsRead(_ dialog: DialogResponse, defaults: DefaultsService) {
dialogs = dialogs.map { item in
if item.id == dialog.id {
var updatedDialog = dialog
updatedDialog.unreadMessagesCount = 0
return updatedDialog
} else {
return item
}
}
guard dialog.unreadMessagesCount > 0,
defaults.unreadMessagesCount >= dialog.unreadMessagesCount
else { return }
let newValue = defaults.unreadMessagesCount - dialog.unreadMessagesCount
defaults.saveUnreadMessagesCount(newValue)
}

@MainActor
func clearDialogsOnLogout(isAuthorized: Bool) {
guard !isAuthorized else { return }
dialogs.removeAll()
}
}
22 changes: 18 additions & 4 deletions SwiftUI-WorkoutApp/Screens/Root/RootScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,37 @@ import SwiftUI
struct RootScreen: View {
@Environment(\.userFlags) private var userFlags
@Binding var selectedTab: TabViewModel.Tab
let unreadCount: Int

var body: some View {
TabView(selection: $selectedTab) {
ForEach(TabViewModel.Tab.allCases, id: \.rawValue) { tab in
tab.screen
.tabItem { tab.tabItemLabel }
.tag(tab)
.badge(tab == .messages ? unreadCount : 0)
}
}
.navigationViewStyle(.stack)
}
}

#if DEBUG
#Preview {
RootScreen(selectedTab: .constant(.map))
.environmentObject(ParksManager())
.environmentObject(DefaultsService())
#Preview("Есть бейдж для чатов") {
RootScreen(
selectedTab: .constant(.map),
unreadCount: 1
)
.environmentObject(ParksManager())
.environmentObject(DefaultsService())
}

#Preview("Нет бейджа") {
RootScreen(
selectedTab: .constant(.map),
unreadCount: 0
)
.environmentObject(ParksManager())
.environmentObject(DefaultsService())
}
#endif
Loading