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
20 changes: 14 additions & 6 deletions SwiftUI-WorkoutApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
67551C362AEC338600084A35 /* SWAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67551C352AEC338600084A35 /* SWAddress.swift */; };
6758463F2965B7F2000BA5E0 /* PDFViewRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6758463E2965B7F2000BA5E0 /* PDFViewRepresentable.swift */; };
675EC64F2814126800C2E229 /* TextEntryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 675EC64E2814126800C2E229 /* TextEntryScreen.swift */; };
675EC6572815433600C2E229 /* UsersListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 675EC6562815433600C2E229 /* UsersListScreen.swift */; };
675EC6572815433600C2E229 /* FriendsListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 675EC6562815433600C2E229 /* FriendsListScreen.swift */; };
675EC65F2815532800C2E229 /* EventFormScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 675EC65E2815532800C2E229 /* EventFormScreen.swift */; };
675EC6632815AA4A00C2E229 /* MapSnapshotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 675EC6622815AA4A00C2E229 /* MapSnapshotView.swift */; };
675FB8DB2ADDB87200C9671F /* ParksMapScreen+LocationSettingReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 675FB8DA2ADDB87200C9671F /* ParksMapScreen+LocationSettingReminderView.swift */; };
Expand All @@ -48,6 +48,8 @@
67795FB22D1C05D90087132F /* SWModels in Frameworks */ = {isa = PBXBuildFile; productRef = 67795FB12D1C05D90087132F /* SWModels */; };
67795FB42D1C05D90087132F /* SWNetworkClient in Frameworks */ = {isa = PBXBuildFile; productRef = 67795FB32D1C05D90087132F /* SWNetworkClient */; };
67891E38283E947B00B10802 /* ParkFormScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67891E37283E947B00B10802 /* ParkFormScreen.swift */; };
679781892D5732D0004D0629 /* ParticipantsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 679781882D5732D0004D0629 /* ParticipantsScreen.swift */; };
6797818B2D574C0E004D0629 /* MainUserFriendsListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6797818A2D574C0E004D0629 /* MainUserFriendsListScreen.swift */; };
6798AA3E280AEDC900DB76F1 /* SwiftUI_WorkoutAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6798AA3D280AEDC900DB76F1 /* SwiftUI_WorkoutAppApp.swift */; };
6798AA40280AEDC900DB76F1 /* RootScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6798AA3F280AEDC900DB76F1 /* RootScreen.swift */; };
6798AA42280AEDCA00DB76F1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6798AA41280AEDCA00DB76F1 /* Assets.xcassets */; };
Expand Down Expand Up @@ -119,7 +121,7 @@
6758463E2965B7F2000BA5E0 /* PDFViewRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFViewRepresentable.swift; sourceTree = "<group>"; };
675EC64E2814126800C2E229 /* TextEntryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEntryScreen.swift; sourceTree = "<group>"; };
675EC65528153B8200C2E229 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
675EC6562815433600C2E229 /* UsersListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersListScreen.swift; sourceTree = "<group>"; };
675EC6562815433600C2E229 /* FriendsListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendsListScreen.swift; sourceTree = "<group>"; };
675EC65E2815532800C2E229 /* EventFormScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventFormScreen.swift; sourceTree = "<group>"; };
675EC6622815AA4A00C2E229 /* MapSnapshotView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapSnapshotView.swift; sourceTree = "<group>"; };
675FB8DA2ADDB87200C9671F /* ParksMapScreen+LocationSettingReminderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParksMapScreen+LocationSettingReminderView.swift"; sourceTree = "<group>"; };
Expand All @@ -137,6 +139,8 @@
677717162B36D87200ED90BD /* SwiftUI-WorkoutApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "SwiftUI-WorkoutApp.xctestplan"; sourceTree = "<group>"; };
67891E37283E947B00B10802 /* ParkFormScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParkFormScreen.swift; sourceTree = "<group>"; };
678CE36A2D1C510900F060C6 /* SWNetwork */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SWNetwork; sourceTree = "<group>"; };
679781882D5732D0004D0629 /* ParticipantsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantsScreen.swift; sourceTree = "<group>"; };
6797818A2D574C0E004D0629 /* MainUserFriendsListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainUserFriendsListScreen.swift; sourceTree = "<group>"; };
6798AA3A280AEDC900DB76F1 /* WorkoutApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WorkoutApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
6798AA3D280AEDC900DB76F1 /* SwiftUI_WorkoutAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUI_WorkoutAppApp.swift; sourceTree = "<group>"; };
6798AA3F280AEDC900DB76F1 /* RootScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootScreen.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -411,6 +415,7 @@
674D0622282A9896007E75C6 /* SearchUsersScreen.swift */,
6765B2572D4544C8006164AB /* MainUserProfileScreen.swift */,
6740003F2D55E97900E5CB06 /* BlackListScreen.swift */,
6797818A2D574C0E004D0629 /* MainUserFriendsListScreen.swift */,
);
path = Profile;
sourceTree = "<group>";
Expand All @@ -426,7 +431,8 @@
67C87FD4284388EF00D6377D /* PhotoSection */,
67D9169528396C1E0098D3CB /* SendMessageScreen.swift */,
675EC64E2814126800C2E229 /* TextEntryScreen.swift */,
675EC6562815433600C2E229 /* UsersListScreen.swift */,
675EC6562815433600C2E229 /* FriendsListScreen.swift */,
679781882D5732D0004D0629 /* ParticipantsScreen.swift */,
);
path = Common;
sourceTree = "<group>";
Expand Down Expand Up @@ -624,6 +630,7 @@
671B4AEB2D4F683E00286996 /* ImagePickerViews.swift in Sources */,
67BD2D012AF7D21B00F44064 /* ParksManager.swift in Sources */,
6705E7EE283B703400DABCC8 /* JournalSettingsScreen.swift in Sources */,
6797818B2D574C0E004D0629 /* MainUserFriendsListScreen.swift in Sources */,
6798AA40280AEDC900DB76F1 /* RootScreen.swift in Sources */,
671B4AE92D4F623100286996 /* ModernPickedImagesGrid.swift in Sources */,
675EC64F2814126800C2E229 /* TextEntryScreen.swift in Sources */,
Expand All @@ -637,6 +644,7 @@
676D5A612D48BF0700EE5E9E /* String+localized.swift in Sources */,
6765B2562D451771006164AB /* UIImage+toMediaFile.swift in Sources */,
674DF03E2B11254D00828016 /* Binding+.swift in Sources */,
679781892D5732D0004D0629 /* ParticipantsScreen.swift in Sources */,
6766A036284603CA0033F1E8 /* TabViewModel.swift in Sources */,
67D916862838F0DD0098D3CB /* DialogScreen.swift in Sources */,
67627750283A3A54009C203F /* JournalsListScreen.swift in Sources */,
Expand All @@ -655,7 +663,7 @@
67D9169628396C1E0098D3CB /* SendMessageScreen.swift in Sources */,
6764D6382D52009F00699007 /* DialogsViewModel.swift in Sources */,
6747575928128603002F0A24 /* ParkDetailScreen.swift in Sources */,
675EC6572815433600C2E229 /* UsersListScreen.swift in Sources */,
675EC6572815433600C2E229 /* FriendsListScreen.swift in Sources */,
675EC65F2815532800C2E229 /* EventFormScreen.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -860,7 +868,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 12;
CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_ASSET_PATHS = "SwiftUI-WorkoutApp/Preview\\ Content/PreviewContent.swift SwiftUI-WorkoutApp/Preview\\ Content";
DEVELOPMENT_TEAM = CR68PP2Z3F;
ENABLE_PREVIEWS = YES;
Expand Down Expand Up @@ -911,7 +919,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 12;
CURRENT_PROJECT_VERSION = 13;
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 @@ -28,12 +28,12 @@ enum Endpoint {
case deleteUser

// MARK: Получить профиль пользователя
/// **GET** ${API}/users/<id>
/// **GET** ${API}/users/<user_id>
/// `id` - идентификатор пользователя, чей профиль нужно получить
case getUser(id: Int)

// MARK: Получить список друзей пользователя
/// **GET** ${API}/users/<id>/friends
/// **GET** ${API}/users/<user_id>/friends
/// `id` - идентификатор пользователя, чьих друзей нужно получить
case getFriendsForUser(id: Int)

Expand All @@ -42,7 +42,7 @@ enum Endpoint {
case getFriendRequests

// MARK: Принять заявку на добавление в друзья
/// **POST** ${API}/friends/<id>/accept
/// **POST** ${API}/friends/<user_id>/accept
case acceptFriendRequest(from: Int)

// MARK: Отклонить заявку на добавление в друзья
Expand Down Expand Up @@ -70,7 +70,7 @@ enum Endpoint {
case deleteFromBlacklist(_ userID: Int)

// MARK: Найти пользователей по логину
/// **GET** ${API}/users/search?name=<user>
/// **GET** ${API}/users/search?name=<user_login>
case findUsers(with: String)

// MARK: Получить список стран/городов
Expand All @@ -88,23 +88,23 @@ enum Endpoint {
case getUpdatedParks(from: String)

// MARK: Получить выбранную площадку:
/// **GET** ${API}/areas/<id>
/// **GET** ${API}/areas/<area_id>
///
/// - Работает и с аутентификацией, и без
/// - Для авторизованного пользователя нужно делать запрос с токеном,
/// чтобы получить корректные данные (тренируется ли на площадке)
case getPark(id: Int)

// MARK: Добавить новую спортплощадку
// MARK: Добавить новую площадку
/// **POST** ${API}/areas
case createPark(form: ParkForm)

// MARK: Изменить выбранную спортплощадку
/// **POST** ${API}/areas/<id>
// MARK: Изменить выбранную площадку
/// **POST** ${API}/areas/<area_id>
case editPark(id: Int, form: ParkForm)

// MARK: Удалить площадку
/// **DELETE** ${API}/areas/<id>
/// **DELETE** ${API}/areas/<area_id>
case deletePark(_ parkID: Int)

// MARK: Добавить комментарий для площадки
Expand Down Expand Up @@ -156,7 +156,7 @@ enum Endpoint {
case createEvent(form: EventForm)

// MARK: Изменить существующее мероприятие
/// **POST** ${API}/trainings/<id>
/// **POST** ${API}/trainings/<event_id>
case editEvent(id: Int, form: EventForm)

// MARK: Сообщить, что пользователь пойдет на мероприятие
Expand All @@ -176,7 +176,7 @@ enum Endpoint {
case deleteEventComment(_ eventID: Int, commentID: Int)

// MARK: Изменить свой комментарий для мероприятия
/// **POST** ${API}/trainings/<training_id>/comments/<comment_id>
/// **POST** ${API}/trainings/<event_id>/comments/<comment_id>
case editEventComment(eventID: Int, commentID: Int, newComment: String)

// MARK: Удалить мероприятие
Expand Down Expand Up @@ -228,11 +228,11 @@ enum Endpoint {
case saveJournalEntry(userID: Int, journalID: Int, message: String)

// MARK: Изменить запись в дневнике пользователя
/// **PUT** ${API}/users/<user_id>/journals/<journal_id>/messages/<id>
/// **PUT** ${API}/users/<user_id>/journals/<journal_id>/messages/<entry_id>
case editEntry(userID: Int, journalID: Int, entryID: Int, newEntryText: String)

// MARK: Удалить запись в дневнике пользователя
/// **DELETE** ${API}/users/<user_id>/journals/<journal_id>/messages/<id>
/// **DELETE** ${API}/users/<user_id>/journals/<journal_id>/messages/<entry_id>
case deleteEntry(userID: Int, journalID: Int, entryID: Int)

// MARK: Удалить дневник пользователя
Expand Down
139 changes: 139 additions & 0 deletions SwiftUI-WorkoutApp/Screens/Common/FriendsListScreen.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import SWDesignSystem
import SwiftUI
import SWModels
import SWNetworkClient
import SWUtils

/// Экран со списком друзей
struct FriendsListScreen: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var defaults: DefaultsService
@State private var friends = [UserResponse]()
@State private var isLoading = false
@State private var messagingModel = MessagingModel()
@State private var sendMessageTask: Task<Void, Never>?
private var client: SWClient { SWClient(with: defaults) }
let mode: Mode

var body: some View {
ScrollView {
LazyVStack(spacing: 12) {
ForEach(friends) { user in
listItem(for: user)
.disabled(user.id == defaults.mainUserInfo?.id)
}
}
.animation(.default, value: friends)
.padding()
}
.sheet(
item: $messagingModel.recipient,
onDismiss: { endMessaging() },
content: messageSheet
)
.loadingOverlay(if: isLoading)
.background(Color.swBackground)
.task { await askForUsers() }
.refreshable { await askForUsers(refresh: true) }
.navigationTitle("Друзья")
.navigationBarTitleDisplayMode(.inline)
}
}

extension FriendsListScreen {
enum Mode {
/// Друзья пользователя с указанным `id`
///
/// При нажатии на друга откроется его профиль
case user(id: Int)
/// Друзья пользователя с указанным `id` для чата
///
/// При нажатии на друга откроется окно отправки сообщения
case chat(userID: Int)
}
}

private extension FriendsListScreen {
@ViewBuilder
func listItem(for model: UserResponse) -> some View {
switch mode {
case .chat:
Button {
messagingModel.recipient = model
} label: {
userRowView(with: model)
}
case .user:
NavigationLink {
UserDetailsScreen(for: model)
.navigationBarTitleDisplayMode(.inline)
} label: {
userRowView(with: model)
}
}
}

func userRowView(with model: UserResponse) -> some View {
UserRowView(
mode: .regular(
.init(
imageURL: model.avatarURL,
name: model.userName ?? "",
address: SWAddress(model.countryID, model.cityID)?.address ?? ""
)
)
)
}

func messageSheet(for recipient: UserResponse) -> some View {
SendMessageScreen(
header: .init(recipient.messageFor),
text: $messagingModel.message,
isLoading: messagingModel.isLoading,
isSendButtonDisabled: !messagingModel.canSendMessage,
sendAction: { sendMessage(to: recipient.id) }
)
}

func sendMessage(to userID: Int) {
messagingModel.isLoading = true
sendMessageTask = Task {
do {
let isSuccess = try await client.sendMessage(messagingModel.message, to: userID)
endMessaging(isSuccess: isSuccess)
} catch {
SWAlert.shared.presentDefaultUIKit(error)
}
messagingModel.isLoading = false
}
}

func endMessaging(isSuccess: Bool = true) {
if isSuccess {
messagingModel.message = ""
messagingModel.recipient = nil
}
}

func askForUsers(refresh: Bool = false) async {
guard !isLoading else { return }
do {
switch mode {
case let .user(id), let .chat(id):
if !friends.isEmpty, !refresh { return }
if !refresh { isLoading = true }
friends = try await client.getFriendsForUser(id: id)
}
} catch {
SWAlert.shared.presentDefaultUIKit(error)
}
isLoading = false
}
}

#if DEBUG
#Preview {
FriendsListScreen(mode: .user(id: .previewUserID))
.environmentObject(DefaultsService())
}
#endif
Loading