From f542091d5784be2966acc86d403f6425e03424c2 Mon Sep 17 00:00:00 2001 From: Mikhail Golovko Date: Thu, 30 Oct 2025 12:30:12 +0300 Subject: [PATCH 1/9] IOS-5387 Test --- .../SpaceHub/SpaceHubCoordinatorView.swift | 172 +++++++++--------- 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/Anytype/Sources/PresentationLayer/Flows/SpaceHub/SpaceHubCoordinatorView.swift b/Anytype/Sources/PresentationLayer/Flows/SpaceHub/SpaceHubCoordinatorView.swift index e64d9fad43..c3b277cfd1 100644 --- a/Anytype/Sources/PresentationLayer/Flows/SpaceHub/SpaceHubCoordinatorView.swift +++ b/Anytype/Sources/PresentationLayer/Flows/SpaceHub/SpaceHubCoordinatorView.swift @@ -27,92 +27,92 @@ struct SpaceHubCoordinatorView: View { .updateShortcuts(spaceId: model.fallbackSpaceId) .snackbar(toastBarData: $model.toastBarData) - .sheet(item: $model.showGalleryImport) { data in - GalleryInstallationCoordinatorView(data: data) - } - .sheet(isPresented: $model.showSpaceManager) { - SpacesManagerView() - } - .sheet(item: $model.membershipTierId) { tierId in - MembershipCoordinator(initialTierId: tierId.value) - } - .sheet(item: $model.membershipNameFinalizationData) { - MembershipNameFinalizationView(tier: $0) - } - .sheet(item: $model.showGlobalSearchData) { - GlobalSearchView(data: $0) - } - .sheet(item: $model.typeSearchForObjectCreationSpaceId) { - model.typeSearchForObjectCreationModule(spaceId: $0.value) - } - .anytypeSheet(item: $model.spaceJoinData) { - SpaceJoinView(data: $0, onManageSpaces: { - model.onManageSpacesSelected() - }) - } - .anytypeSheet(item: $model.userWarningAlert, dismissOnBackgroundView: false) { - UserWarningAlertCoordinatorView(alert: $0) - } - .anytypeSheet(isPresented: $model.showObjectIsNotAvailableAlert) { - ObjectIsNotAvailableAlert() - } - .sheet(item: $model.showSpaceShareData) { - SpaceShareCoordinatorView(data: $0) - .pageNavigation(model.pageNavigation) - } - .sheet(item: $model.showSpaceMembersData) { - SpaceMembersView(data: $0) - .pageNavigation(model.pageNavigation) - } - .anytypeSheet(item: $model.profileData) { - ProfileView(info: $0) - .pageNavigation(model.pageNavigation) - } - .anytypeSheet(item: $model.spaceProfileData) { - SpaceProfileView(info: $0) - } - .safariBookmarkObject($model.bookmarkScreenData) { - model.onOpenBookmarkAsObject($0) - } - .sheet(item: $model.spaceCreateData) { - SpaceCreateCoordinatorView(data: $0) - } - .sheet(isPresented: $model.showSpaceTypeForCreate) { - SpaceCreateTypePickerView(onSelectSpaceType: { type in - model.onSpaceTypeSelected(type) - }, onSelectQrCodeScan: { - model.onSelectQrCodeScan() - }) - .navigationZoomTransition(sourceID: "SpaceCreateTypePickerView", in: namespace) - } - .qrCodeScanner(shouldScan: $model.shouldScanQrCode) - .sheet(isPresented: $model.showSharingExtension) { - SharingExtensionCoordinatorView() - } - .sheet(isPresented: $model.showAppSettings) { - SettingsCoordinatorView() - .pageNavigation(model.pageNavigation) - } - - // load photos - .photosPicker(isPresented: $model.showPhotosPicker, selection: $model.photosItems) - .onChange(of: model.photosItems) { - model.photosPickerFinished() - } - - // load from camera - .cameraAccessFullScreenCover(item: $model.cameraData) { - SimpleCameraView(data: $0) - } - - // load files - .fileImporter( - isPresented: $model.showFilesPicker, - allowedContentTypes: [.data], - allowsMultipleSelection: true - ) { result in - model.fileImporterFinished(result: result) - } +// .sheet(item: $model.showGalleryImport) { data in +// GalleryInstallationCoordinatorView(data: data) +// } +// .sheet(isPresented: $model.showSpaceManager) { +// SpacesManagerView() +// } +// .sheet(item: $model.membershipTierId) { tierId in +// MembershipCoordinator(initialTierId: tierId.value) +// } +// .sheet(item: $model.membershipNameFinalizationData) { +// MembershipNameFinalizationView(tier: $0) +// } +// .sheet(item: $model.showGlobalSearchData) { +// GlobalSearchView(data: $0) +// } +// .sheet(item: $model.typeSearchForObjectCreationSpaceId) { +// model.typeSearchForObjectCreationModule(spaceId: $0.value) +// } +// .anytypeSheet(item: $model.spaceJoinData) { +// SpaceJoinView(data: $0, onManageSpaces: { +// model.onManageSpacesSelected() +// }) +// } +// .anytypeSheet(item: $model.userWarningAlert, dismissOnBackgroundView: false) { +// UserWarningAlertCoordinatorView(alert: $0) +// } +// .anytypeSheet(isPresented: $model.showObjectIsNotAvailableAlert) { +// ObjectIsNotAvailableAlert() +// } +// .sheet(item: $model.showSpaceShareData) { +// SpaceShareCoordinatorView(data: $0) +// .pageNavigation(model.pageNavigation) +// } +// .sheet(item: $model.showSpaceMembersData) { +// SpaceMembersView(data: $0) +// .pageNavigation(model.pageNavigation) +// } +// .anytypeSheet(item: $model.profileData) { +// ProfileView(info: $0) +// .pageNavigation(model.pageNavigation) +// } +// .anytypeSheet(item: $model.spaceProfileData) { +// SpaceProfileView(info: $0) +// } +// .safariBookmarkObject($model.bookmarkScreenData) { +// model.onOpenBookmarkAsObject($0) +// } +// .sheet(item: $model.spaceCreateData) { +// SpaceCreateCoordinatorView(data: $0) +// } +// .sheet(isPresented: $model.showSpaceTypeForCreate) { +// SpaceCreateTypePickerView(onSelectSpaceType: { type in +// model.onSpaceTypeSelected(type) +// }, onSelectQrCodeScan: { +// model.onSelectQrCodeScan() +// }) +// .navigationZoomTransition(sourceID: "SpaceCreateTypePickerView", in: namespace) +// } +// .qrCodeScanner(shouldScan: $model.shouldScanQrCode) +// .sheet(isPresented: $model.showSharingExtension) { +// SharingExtensionCoordinatorView() +// } +// .sheet(isPresented: $model.showAppSettings) { +// SettingsCoordinatorView() +// .pageNavigation(model.pageNavigation) +// } +// +// // load photos +// .photosPicker(isPresented: $model.showPhotosPicker, selection: $model.photosItems) +// .onChange(of: model.photosItems) { +// model.photosPickerFinished() +// } +// +// // load from camera +// .cameraAccessFullScreenCover(item: $model.cameraData) { +// SimpleCameraView(data: $0) +// } +// +// // load files +// .fileImporter( +// isPresented: $model.showFilesPicker, +// allowedContentTypes: [.data], +// allowsMultipleSelection: true +// ) { result in +// model.fileImporterFinished(result: result) +// } } private var content: some View { From ee0d3e5670657e6fba13057398456ac71f21df69 Mon Sep 17 00:00:00 2001 From: Mikhail Golovko Date: Thu, 30 Oct 2025 13:12:22 +0300 Subject: [PATCH 2/9] IOS-5387 Separate space list --- .../SpaceHub/SpaceHubCoordinatorView.swift | 3 +- .../Modules/SpaceHub/SpaceHubView.swift | 94 +++---------------- .../Modules/SpaceHub/SpaceHubViewModel.swift | 5 +- .../Modules/SpaceHub/Subviews/SpaceCard.swift | 3 +- .../SpaceHub/Subviews/SpaceHubList.swift | 90 ++++++++++++++++++ 5 files changed, 112 insertions(+), 83 deletions(-) create mode 100644 Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceHubList.swift diff --git a/Anytype/Sources/PresentationLayer/Flows/SpaceHub/SpaceHubCoordinatorView.swift b/Anytype/Sources/PresentationLayer/Flows/SpaceHub/SpaceHubCoordinatorView.swift index c3b277cfd1..a28585270a 100644 --- a/Anytype/Sources/PresentationLayer/Flows/SpaceHub/SpaceHubCoordinatorView.swift +++ b/Anytype/Sources/PresentationLayer/Flows/SpaceHub/SpaceHubCoordinatorView.swift @@ -12,7 +12,8 @@ struct SpaceHubCoordinatorView: View { @Namespace private var namespace var body: some View { - content + let _ = Self._printChanges() + return content .onAppear { model.keyboardDismiss = keyboardDismiss model.dismissAllPresented = dismissAllPresented diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubView.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubView.swift index f75e69e531..2efde86985 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubView.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubView.swift @@ -5,9 +5,6 @@ import DesignKit struct SpaceHubView: View { @State private var model: SpaceHubViewModel - @State private var draggedSpace: ParticipantSpaceViewDataWithPreview? - @State private var draggedInitialIndex: Int? - @State private var vaultBackToRootsToggle = FeatureFlags.vaultBackToRoots private var namespace: Namespace.ID @@ -17,7 +14,8 @@ struct SpaceHubView: View { } var body: some View { - content + let _ = Self._printChanges() + return content .onAppear { model.onAppear() } .taskWithMemoryScope { await model.startSubscriptions() } .task(item: model.spaceMuteData) { data in @@ -34,8 +32,8 @@ struct SpaceHubView: View { @ViewBuilder private var content: some View { Group { - if let spaces = model.spaces { - spacesView(spaces) + if model.dataLoaded { + spacesView() } else { EmptyView() // Do not show empty state view if we do not receive data yet } @@ -43,20 +41,20 @@ struct SpaceHubView: View { Spacer() } .ignoresSafeArea(edges: .bottom) - .animation(.default, value: model.spaces) } - private func spacesView(_ spaces: [ParticipantSpaceViewDataWithPreview]) -> some View { + private func spacesView() -> some View { NavigationStack { - Group { - if spaces.isEmpty { - emptyStateView - } else if model.filteredSpaces.isNotEmpty { - scrollView - } else { - SpaceHubSearchEmptySpaceView() - } - } + SpaceHubList(model: model) +// Group { +// if model.filteredSpaces.isEmpty && model.searchText.isEmpty { +// emptyStateView +// } else if model.filteredSpaces.isNotEmpty { +// scrollView +// } else { +// SpaceHubSearchEmptySpaceView() +// } +// } .navigationTitle(Loc.myChannels) .navigationBarTitleDisplayMode(.inline) .toolbar { toolbarItems } @@ -67,20 +65,6 @@ struct SpaceHubView: View { }.tint(Color.Text.secondary) } - private var scrollView: some View { - ScrollView { - VStack(spacing: vaultBackToRootsToggle ? 8 : 0) { - HomeUpdateSubmoduleView().padding(8) - - ForEach(model.filteredSpaces) { - spaceCard($0) - } - - Spacer.fixedHeight(40) - } - } - } - private var toolbarItems: some ToolbarContent { SpaceHubToolbar( showLoading: model.showLoading, @@ -95,54 +79,6 @@ struct SpaceHubView: View { } ) } - - private var emptyStateView: some View { - SpaceHubEmptyStateView { - model.onTapCreateSpace() - } - } - - private func spaceCard(_ space: ParticipantSpaceViewDataWithPreview) -> some View { - SpaceCard( - spaceData: space, - wallpaper: model.wallpapers[space.spaceView.targetSpaceId] ?? .default, - draggedSpace: $draggedSpace, - onTap: { - model.onSpaceTap(spaceId: space.spaceView.targetSpaceId) - }, - onTapCopy: { - model.copySpaceInfo(spaceView: space.spaceView) - }, - onTapMute: { - model.muteSpace(spaceView: space.spaceView) - }, - onTapPin: { - try await model.pin(spaceView: space.spaceView) - }, - onTapUnpin: { - try await model.unpin(spaceView: space.spaceView) - }, - onTapSettings: { - model.openSpaceSettings(spaceId: space.spaceView.targetSpaceId) - }, - onTapDelete: { - model.onDeleteSpace(spaceId: space.spaceView.targetSpaceId) - } - ) - .equatable() - .padding(.horizontal, vaultBackToRootsToggle ? 16 : 0) - .if(space.spaceView.isPinned) { - $0.onDrop( - of: [.text], - delegate: SpaceHubDropDelegate( - destinationItem: space, - allSpaces: $model.spaces, - draggedItem: $draggedSpace, - initialIndex: $draggedInitialIndex - ) - ) - } - } } #Preview { diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubViewModel.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubViewModel.swift index 7c3f6e9be7..c04704f453 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubViewModel.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubViewModel.swift @@ -8,9 +8,10 @@ import Loc @MainActor @Observable final class SpaceHubViewModel { + var spaces: [ParticipantSpaceViewDataWithPreview]? + var dataLoaded = false var searchText: String = "" - var filteredSpaces: [ParticipantSpaceViewDataWithPreview] = [] var wallpapers: [String: SpaceWallpaperType] = [:] @@ -23,7 +24,6 @@ final class SpaceHubViewModel { @ObservationIgnored private weak var output: (any SpaceHubModuleOutput)? - @Injected(\.userDefaultsStorage) @ObservationIgnored private var userDefaults: any UserDefaultsStorageProtocol @@ -127,6 +127,7 @@ final class SpaceHubViewModel { private func subscribeOnSpaces() async { for await spaces in await spaceHubSpacesStorage.spacesStream { self.spaces = spaces.sorted(by: sortSpacesForPinnedFeature) + self.dataLoaded = spaces.isNotEmpty showLoading = spaces.contains { $0.spaceView.isLoading } updateFilteredSpaces() } diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard.swift index 2e9c91c5fe..092180d1ad 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard.swift @@ -15,7 +15,8 @@ struct SpaceCard: View, @preconcurrency Equatable { let onTapDelete: () -> Void var body: some View { - Button { + let _ = Self._printChanges() + return Button { onTap() } label: { if !FeatureFlags.vaultBackToRoots { diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceHubList.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceHubList.swift new file mode 100644 index 0000000000..9a2b68b927 --- /dev/null +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceHubList.swift @@ -0,0 +1,90 @@ +import SwiftUI +import AnytypeCore + +struct SpaceHubList: View { + + @Bindable var model: SpaceHubViewModel + + @State private var draggedSpace: ParticipantSpaceViewDataWithPreview? + @State private var draggedInitialIndex: Int? + @State private var vaultBackToRootsToggle = FeatureFlags.vaultBackToRoots + + var body: some View { + let _ = Self._printChanges() + return content + } + + @ViewBuilder + private var content: some View { + if model.filteredSpaces.isEmpty && model.searchText.isEmpty { + emptyStateView + } else if model.filteredSpaces.isNotEmpty { + scrollView + } else { + SpaceHubSearchEmptySpaceView() + } + } + + private var scrollView: some View { + ScrollView { + VStack(spacing: vaultBackToRootsToggle ? 8 : 0) { + HomeUpdateSubmoduleView().padding(8) + + ForEach(model.filteredSpaces) { + spaceCard($0) + } + + Spacer.fixedHeight(40) + } + } + .animation(.default, value: model.filteredSpaces) + } + + private var emptyStateView: some View { + SpaceHubEmptyStateView { + model.onTapCreateSpace() + } + } + + private func spaceCard(_ space: ParticipantSpaceViewDataWithPreview) -> some View { + SpaceCard( + spaceData: space, + wallpaper: model.wallpapers[space.spaceView.targetSpaceId] ?? .default, + draggedSpace: $draggedSpace, + onTap: { + model.onSpaceTap(spaceId: space.spaceView.targetSpaceId) + }, + onTapCopy: { + model.copySpaceInfo(spaceView: space.spaceView) + }, + onTapMute: { + model.muteSpace(spaceView: space.spaceView) + }, + onTapPin: { + try await model.pin(spaceView: space.spaceView) + }, + onTapUnpin: { + try await model.unpin(spaceView: space.spaceView) + }, + onTapSettings: { + model.openSpaceSettings(spaceId: space.spaceView.targetSpaceId) + }, + onTapDelete: { + model.onDeleteSpace(spaceId: space.spaceView.targetSpaceId) + } + ) + .equatable() + .padding(.horizontal, vaultBackToRootsToggle ? 16 : 0) + .if(space.spaceView.isPinned) { + $0.onDrop( + of: [.text], + delegate: SpaceHubDropDelegate( + destinationItem: space, + allSpaces: $model.spaces, + draggedItem: $draggedSpace, + initialIndex: $draggedInitialIndex + ) + ) + } + } +} From 9b68d188c1da26d8ae2ec5405f14760de96cc491 Mon Sep 17 00:00:00 2001 From: Mikhail Golovko Date: Thu, 30 Oct 2025 15:29:54 +0300 Subject: [PATCH 3/9] IOS-5387 Fixes --- .../SpaceHub/SpaceHubCoordinatorView.swift | 3 +- .../SpaceHub/SpaceHubDropDelegate.swift | 18 +- .../Modules/SpaceHub/SpaceHubView.swift | 3 +- .../Modules/SpaceHub/SpaceHubViewModel.swift | 53 +++-- .../SpaceHub/Subviews/NewSpaceCardLabel.swift | 203 ------------------ .../SpaceCard/NewSpaceCardLabel.swift | 138 ++++++++++++ .../NewSpaceCardLastMessageView.swift | 60 ++++++ .../Subviews/{ => SpaceCard}/SpaceCard.swift | 53 ++--- .../Subviews/SpaceCard/SpaceCardLabel.swift | 102 +++++++++ .../SpaceCard/SpaceCardLastMessageModel.swift | 17 ++ .../SpaceCard/SpaceCardLastMessageView.swift | 47 ++++ .../Subviews/SpaceCard/SpaceCardModel.swift | 24 +++ .../SpaceCard/SpaceCardModelBuilder.swift | 77 +++++++ .../SpaceHub/Subviews/SpaceCardLabel.swift | 155 ------------- .../SpaceHub/Subviews/SpaceHubList.swift | 40 ++-- 15 files changed, 546 insertions(+), 447 deletions(-) delete mode 100644 Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/NewSpaceCardLabel.swift create mode 100644 Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/NewSpaceCardLabel.swift create mode 100644 Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/NewSpaceCardLastMessageView.swift rename Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/{ => SpaceCard}/SpaceCard.swift (65%) create mode 100644 Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLabel.swift create mode 100644 Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLastMessageModel.swift create mode 100644 Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLastMessageView.swift create mode 100644 Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModel.swift create mode 100644 Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModelBuilder.swift delete mode 100644 Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCardLabel.swift diff --git a/Anytype/Sources/PresentationLayer/Flows/SpaceHub/SpaceHubCoordinatorView.swift b/Anytype/Sources/PresentationLayer/Flows/SpaceHub/SpaceHubCoordinatorView.swift index a28585270a..c3b277cfd1 100644 --- a/Anytype/Sources/PresentationLayer/Flows/SpaceHub/SpaceHubCoordinatorView.swift +++ b/Anytype/Sources/PresentationLayer/Flows/SpaceHub/SpaceHubCoordinatorView.swift @@ -12,8 +12,7 @@ struct SpaceHubCoordinatorView: View { @Namespace private var namespace var body: some View { - let _ = Self._printChanges() - return content + content .onAppear { model.keyboardDismiss = keyboardDismiss model.dismissAllPresented = dismissAllPresented diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubDropDelegate.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubDropDelegate.swift index 585859bcaa..1bd4e174c2 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubDropDelegate.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubDropDelegate.swift @@ -5,9 +5,9 @@ import AnytypeCore struct SpaceHubDropDelegate: DropDelegate { - let destinationItem: ParticipantSpaceViewDataWithPreview + let destinationSpaceViewId: String? @Binding var allSpaces: [ParticipantSpaceViewDataWithPreview]? - @Binding var draggedItem: ParticipantSpaceViewDataWithPreview? + @Binding var draggedSpaceViewId: String? @Binding var initialIndex: Int? func dropUpdated(info: DropInfo) -> DropProposal? { @@ -15,20 +15,20 @@ struct SpaceHubDropDelegate: DropDelegate { } func performDrop(info: DropInfo) -> Bool { - guard let allSpaces, draggedItem.isNotNil, let initialIndex else { return false } + guard let allSpaces, draggedSpaceViewId.isNotNil, let initialIndex else { return false } - guard let finalIndex = allSpaces.firstIndex(of: destinationItem) else { return false } + guard let finalIndex = allSpaces.firstIndex(where: { $0.spaceView.id == destinationSpaceViewId }) else { return false } guard finalIndex != initialIndex else { return false } - self.draggedItem = nil + self.draggedSpaceViewId = nil self.initialIndex = nil return true } func dropEntered(info: DropInfo) { - guard var allSpaces, let draggedItem else { return } - guard let fromIndex = allSpaces.firstIndex(where: { $0.space.id == draggedItem.space.id } ) else { return } - guard let toIndex = allSpaces.firstIndex(where: { $0.space.id == destinationItem.space.id } ) else { return } + guard var allSpaces, let draggedSpaceViewId else { return } + guard let fromIndex = allSpaces.firstIndex(where: { $0.space.spaceView.id == draggedSpaceViewId } ) else { return } + guard let toIndex = allSpaces.firstIndex(where: { $0.space.spaceView.id == destinationSpaceViewId } ) else { return } guard fromIndex != toIndex else { return } @@ -52,7 +52,7 @@ struct SpaceHubDropDelegate: DropDelegate { let spaceOrderService = Container.shared.spaceOrderService() try await spaceOrderService.setOrder( - spaceViewIdMoved: draggedItem.spaceView.id, newOrder: newOrder + spaceViewIdMoved: draggedSpaceViewId, newOrder: newOrder ) AnytypeAnalytics.instance().logReorderSpace() } diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubView.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubView.swift index 2efde86985..4ebf434500 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubView.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubView.swift @@ -14,8 +14,7 @@ struct SpaceHubView: View { } var body: some View { - let _ = Self._printChanges() - return content + content .onAppear { model.onAppear() } .taskWithMemoryScope { await model.startSubscriptions() } .task(item: model.spaceMuteData) { data in diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubViewModel.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubViewModel.swift index c04704f453..42d769ea83 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubViewModel.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubViewModel.swift @@ -12,7 +12,7 @@ final class SpaceHubViewModel { var spaces: [ParticipantSpaceViewDataWithPreview]? var dataLoaded = false var searchText: String = "" - var filteredSpaces: [ParticipantSpaceViewDataWithPreview] = [] + var filteredSpaces: [SpaceCardModel] = [] var wallpapers: [String: SpaceWallpaperType] = [:] @@ -39,6 +39,8 @@ final class SpaceHubViewModel { private var pushNotificationsSystemSettingsBroadcaster: any PushNotificationsSystemSettingsBroadcasterProtocol @Injected(\.workspaceService) @ObservationIgnored private var workspaceService: any WorkspaceServiceProtocol + @Injected(\.spaceCardModelBuilder)@ObservationIgnored + private var spaceCardModelBuilder: any SpaceCardModelBuilderProtocol init(output: (any SpaceHubModuleOutput)?) { self.output = output @@ -62,11 +64,13 @@ final class SpaceHubViewModel { } - func copySpaceInfo(spaceView: SpaceView) { + func copySpaceInfo(spaceViewId: String) { + guard let spaceView = spaces?.first(where: { $0.spaceView.id == spaceViewId })?.spaceView else { return } UIPasteboard.general.string = String(describing: spaceView) } - func muteSpace(spaceView: SpaceView) { + func muteSpace(spaceViewId: String) { + guard let spaceView = spaces?.first(where: { $0.spaceView.id == spaceViewId })?.spaceView else { return } let isUnmutedAll = spaceView.pushNotificationMode.isUnmutedAll spaceMuteData = SpaceMuteData( spaceId: spaceView.targetSpaceId, @@ -74,19 +78,19 @@ final class SpaceHubViewModel { ) } - func pin(spaceView: SpaceView) async throws { + func pin(spaceViewId: String) async throws { guard let spaces else { return } let pinnedSpaces = spaces.filter { $0.spaceView.isPinned } + + var newOrder = pinnedSpaces.filter { $0.spaceView.id != spaceViewId }.map(\.spaceView.id) + newOrder.insert(spaceViewId, at: 0) - var newOrder = pinnedSpaces.filter { $0.spaceView.id != spaceView.id }.map(\.spaceView.id) - newOrder.insert(spaceView.id, at: 0) - - try await spaceOrderService.setOrder(spaceViewIdMoved: spaceView.id, newOrder: newOrder) + try await spaceOrderService.setOrder(spaceViewIdMoved: spaceViewId, newOrder: newOrder) AnytypeAnalytics.instance().logPinSpace() } - func unpin(spaceView: SpaceView) async throws { - try await spaceOrderService.unsetOrder(spaceViewId: spaceView.id) + func unpin(spaceViewId: String) async throws { + try await spaceOrderService.unsetOrder(spaceViewId: spaceViewId) AnytypeAnalytics.instance().logUnpinSpace() } @@ -209,19 +213,22 @@ final class SpaceHubViewModel { } private func updateFilteredSpaces() { - - guard let spaces else { - filteredSpaces = [] - return - } - - guard !searchText.isEmpty else { - filteredSpaces = spaces - return - } - - filteredSpaces = spaces.filter { space in - space.spaceView.name.localizedCaseInsensitiveContains(searchText) + Task { + guard let spaces else { + filteredSpaces = [] + return + } + + let spacesToFilter: [ParticipantSpaceViewDataWithPreview] + if searchText.isEmpty { + spacesToFilter = spaces + } else { + spacesToFilter = spaces.filter { space in + space.spaceView.name.localizedCaseInsensitiveContains(searchText) + } + } + + self.filteredSpaces = await spaceCardModelBuilder.build(from: spacesToFilter, wallpapers: wallpapers) } } } diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/NewSpaceCardLabel.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/NewSpaceCardLabel.swift deleted file mode 100644 index 8681065564..0000000000 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/NewSpaceCardLabel.swift +++ /dev/null @@ -1,203 +0,0 @@ -import SwiftUI -import AnytypeCore - -// SpaceCardLabel and SpaceCard are splitted for better SwiftUI diff. -struct NewSpaceCardLabel: View { - - let spaceData: ParticipantSpaceViewDataWithPreview - let wallpaper: SpaceWallpaperType - private let dateFormatter = ChatPreviewDateFormatter() - @Binding var draggedSpace: ParticipantSpaceViewDataWithPreview? - - @Namespace private var namespace - - var body: some View { - content - .if(spaceData.spaceView.isPinned) { - $0.onDrag { - draggedSpace = spaceData - return NSItemProvider() - } preview: { - EmptyView() - } - } - } - - private var content: some View { - HStack(alignment: .center, spacing: 12) { - IconView(icon: spaceData.spaceView.objectIconImage) - .frame(width: 56, height: 56) - - Group { - if let message = spaceData.preview.lastMessage { - mainContentWithMessage(message) - } else { - mainContentWithoutMessage - } - } - // Fixing the animation when the cell is moved and updated inside - // Optimization - create a data model for SpaceCard and map to in in SpaceHubViewModel on background thread - .id(spaceData.hashValue) - .matchedGeometryEffect(id: "content", in: namespace, properties: .position, anchor: .topLeading) - } - .padding(.horizontal, 16) - .padding(.vertical, 17) - // Optimization for fast sizeThatFits - .frame(height: 98) - - .cornerRadius(20, style: .continuous) - .background(DashboardWallpaper( - mode: .spaceHub, - wallpaper: wallpaper, - spaceIcon: spaceData.spaceView.objectIconImage - )) - .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) - } - - private func mainContentWithMessage(_ message: LastMessagePreview) -> some View { - VStack(alignment: .leading, spacing: 0) { - HStack(alignment: .bottom) { - HStack(alignment: .center) { - AnytypeText(spaceData.spaceView.name.withPlaceholder, style: .bodySemibold) - .lineLimit(1) - .foregroundColor(Color.Text.primary) - if isMuted { - Spacer.fixedWidth(4) - Image(asset: .X18.muted).foregroundColor(.Control.transparentSecondary) - } - } - - Spacer(minLength: 8) - - VStack(spacing: 0) { - lastMessageDate - Spacer.fixedHeight(2) - } - } - - HStack(alignment: .top) { - lastMessagePreview(message) - Spacer() - decoration - } - - Spacer(minLength: 0) - } - } - - private var mainContentWithoutMessage: some View { - VStack(alignment: .leading, spacing: 0) { - HStack { - AnytypeText(spaceData.spaceView.name.withPlaceholder, style: .bodySemibold) - .lineLimit(1) - .foregroundColor(Color.Text.primary) - if isMuted { - Spacer.fixedWidth(8) - Image(asset: .X18.muted).foregroundColor(.Control.transparentSecondary) - } - Spacer() - pin - } - } - } - - @ViewBuilder - func lastMessagePreview(_ message: LastMessagePreview) -> some View { - Group { - if message.attachments.isNotEmpty { - messageWithAttachements(message) - } else if message.text.isNotEmpty { - messageWithoutAttachements(message) - } else { - AnytypeText(message.creator?.title ?? Loc.Chat.newMessages, style: .chatPreviewMedium) - .foregroundColor(.Text.transparentSecondary) - .lineLimit(1) - } - } - .multilineTextAlignment(.leading) - } - - func messageWithoutAttachements(_ message: LastMessagePreview) -> some View { - Group { - if let creator = message.creator { - VStack(alignment: .leading, spacing: 2) { - Text(creator.title).anytypeFontStyle(.chatPreviewMedium) - Text(message.text).anytypeFontStyle(.chatPreviewRegular) - } - .lineLimit(1) - } else { - Text(message.text).anytypeFontStyle(.chatPreviewRegular) - .lineLimit(2) - } - } - .foregroundColor(.Text.transparentSecondary) - .anytypeLineHeightStyle(.chatPreviewRegular) - } - - @ViewBuilder - func messageWithAttachements(_ message: LastMessagePreview) -> some View { - VStack(alignment: .leading, spacing: 2) { - - if let creator = message.creator { - AnytypeText(creator.title, style: .chatPreviewMedium) - .foregroundColor(.Text.transparentSecondary) - .lineLimit(1) - } - - HStack(spacing: 2) { - ForEach(message.attachments.prefix(3)) { - IconView(icon: $0.objectIconImage).frame(width: 18, height: 18) - } - - Spacer.fixedWidth(2) - AnytypeText(message.localizedAttachmentsText, style: .chatPreviewRegular) - .foregroundColor(.Text.transparentSecondary) - .lineLimit(1) - } - } - } - - @ViewBuilder - private var lastMessageDate: some View { - if let lastMessage = spaceData.preview.lastMessage { - AnytypeText(dateFormatter.localizedDateString(for: lastMessage.createdAt, showTodayTime: true), style: .relation2Regular) - .foregroundColor(.Control.transparentSecondary) - } - } - - @ViewBuilder - private var decoration: some View { - if spaceData.preview.hasCounters { - unreadCounters - } else { - pin - } - } - - private var unreadCounters: some View { - HStack(spacing: 4) { - if spaceData.preview.mentionCounter > 0 { - MentionBadge(style: isMuted ? .muted : .highlighted) - } - if spaceData.preview.unreadCounter > 0 { - CounterView( - count: spaceData.preview.unreadCounter, - style: isMuted ? .muted : .highlighted - ) - } - } - } - - @ViewBuilder - private var pin: some View { - if spaceData.spaceView.isPinned { - Image(asset: .X18.pin) - .foregroundColor(Color.Control.transparentSecondary) - .frame(width: 18, height: 18) - } - } - - private var isMuted: Bool { - FeatureFlags.muteSpacePossibility && !spaceData.spaceView.pushNotificationMode.isUnmutedAll - } -} diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/NewSpaceCardLabel.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/NewSpaceCardLabel.swift new file mode 100644 index 0000000000..9da045530d --- /dev/null +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/NewSpaceCardLabel.swift @@ -0,0 +1,138 @@ +import SwiftUI +import AnytypeCore + +// SpaceCardLabel and SpaceCard are splitted for better SwiftUI diff. +struct NewSpaceCardLabel: View { + + let model: SpaceCardModel + @Binding var draggedSpaceViewId: String? + + @Namespace private var namespace + + var body: some View { + content + .if(model.isPinned) { + $0.onDrag { + draggedSpaceViewId = model.spaceViewId + return NSItemProvider() + } preview: { + EmptyView() + } + } + } + + private var content: some View { + HStack(alignment: .center, spacing: 12) { + IconView(icon: model.objectIconImage) + .frame(width: 56, height: 56) + + Group { + if let message = model.lastMessage { + mainContentWithMessage(message) + } else { + mainContentWithoutMessage + } + } + .matchedGeometryEffect(id: "content", in: namespace, properties: .position, anchor: .topLeading) + } + .padding(.horizontal, 16) + .padding(.vertical, 17) + // Optimization for fast sizeThatFits + .frame(height: 98) + + .cornerRadius(20, style: .continuous) + .background(DashboardWallpaper( + mode: .spaceHub, + wallpaper: model.wallpaper, + spaceIcon: model.objectIconImage + )) + .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) + } + + private func mainContentWithMessage(_ message: SpaceCardLastMessageModel) -> some View { + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .bottom) { + HStack(alignment: .center) { + AnytypeText(model.name.withPlaceholder, style: .bodySemibold) + .lineLimit(1) + .foregroundColor(Color.Text.primary) + if model.isMuted { + Spacer.fixedWidth(4) + Image(asset: .X18.muted).foregroundColor(.Control.transparentSecondary) + } + } + + Spacer(minLength: 8) + + VStack(spacing: 0) { + lastMessageDate + Spacer.fixedHeight(2) + } + } + + HStack(alignment: .top) { + NewSpaceCardLastMessageView(model: message) + Spacer() + decoration + } + + Spacer(minLength: 0) + } + } + + private var mainContentWithoutMessage: some View { + VStack(alignment: .leading, spacing: 0) { + HStack { + AnytypeText(model.name.withPlaceholder, style: .bodySemibold) + .lineLimit(1) + .foregroundColor(Color.Text.primary) + if model.isMuted { + Spacer.fixedWidth(8) + Image(asset: .X18.muted).foregroundColor(.Control.transparentSecondary) + } + Spacer() + pin + } + } + } + + @ViewBuilder + private var lastMessageDate: some View { + if let lastMessage = model.lastMessage { + AnytypeText(lastMessage.chatPreviewDate, style: .relation2Regular) + .foregroundColor(.Control.transparentSecondary) + } + } + + @ViewBuilder + private var decoration: some View { + if model.hasCounters { + unreadCounters + } else { + pin + } + } + + private var unreadCounters: some View { + HStack(spacing: 4) { + if model.mentionCounter > 0 { + MentionBadge(style: model.isMuted ? .muted : .highlighted) + } + if model.unreadCounter > 0 { + CounterView( + count: model.unreadCounter, + style: model.isMuted ? .muted : .highlighted + ) + } + } + } + + @ViewBuilder + private var pin: some View { + if model.isPinned { + Image(asset: .X18.pin) + .foregroundColor(Color.Control.transparentSecondary) + .frame(width: 18, height: 18) + } + } +} diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/NewSpaceCardLastMessageView.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/NewSpaceCardLastMessageView.swift new file mode 100644 index 0000000000..5f79e6c033 --- /dev/null +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/NewSpaceCardLastMessageView.swift @@ -0,0 +1,60 @@ +import SwiftUI + +struct NewSpaceCardLastMessageView: View { + + let model: SpaceCardLastMessageModel + + var body: some View { + Group { + if model.attachments.isNotEmpty { + messageWithAttachements + } else if model.text.isNotEmpty { + messageWithoutAttachements + } else { + AnytypeText(model.creatorTitle ?? Loc.Chat.newMessages, style: .chatPreviewMedium) + .foregroundColor(.Text.transparentSecondary) + .lineLimit(1) + } + } + .multilineTextAlignment(.leading) + } + + private var messageWithoutAttachements: some View { + Group { + if let creatorTitle = model.creatorTitle { + VStack(alignment: .leading, spacing: 2) { + Text(creatorTitle).anytypeFontStyle(.chatPreviewMedium) + Text(model.text).anytypeFontStyle(.chatPreviewRegular) + } + .lineLimit(1) + } else { + Text(model.text).anytypeFontStyle(.chatPreviewRegular) + .lineLimit(2) + } + } + .foregroundColor(.Text.transparentSecondary) + .anytypeLineHeightStyle(.chatPreviewRegular) + } + + private var messageWithAttachements: some View { + VStack(alignment: .leading, spacing: 2) { + + if let creatorTitle = model.creatorTitle { + AnytypeText(creatorTitle, style: .chatPreviewMedium) + .foregroundColor(.Text.transparentSecondary) + .lineLimit(1) + } + + HStack(spacing: 2) { + ForEach(model.attachments) { + IconView(icon: $0.icon).frame(width: 18, height: 18) + } + + Spacer.fixedWidth(2) + AnytypeText(model.localizedAttachmentsText, style: .chatPreviewRegular) + .foregroundColor(.Text.transparentSecondary) + .lineLimit(1) + } + } + } +} diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCard.swift similarity index 65% rename from Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard.swift rename to Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCard.swift index 092180d1ad..e9f3937223 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCard.swift @@ -1,11 +1,10 @@ import SwiftUI import AnytypeCore -struct SpaceCard: View, @preconcurrency Equatable { - - let spaceData: ParticipantSpaceViewDataWithPreview - let wallpaper: SpaceWallpaperType - @Binding var draggedSpace: ParticipantSpaceViewDataWithPreview? +struct SpaceCard: View { + + let model: SpaceCardModel + @Binding var draggedSpaceViewId: String? let onTap: () -> Void let onTapCopy: () -> Void let onTapMute: () -> Void @@ -13,48 +12,48 @@ struct SpaceCard: View, @preconcurrency Equatable { let onTapUnpin: () async throws -> Void let onTapSettings: () -> Void let onTapDelete: () -> Void + + @State private var vaultBackToRootsToggle = FeatureFlags.vaultBackToRoots + @State private var muteSpacePossibilityToggle = FeatureFlags.muteSpacePossibility var body: some View { - let _ = Self._printChanges() - return Button { + Button { onTap() } label: { - if !FeatureFlags.vaultBackToRoots { + if !vaultBackToRootsToggle { SpaceCardLabel( - spaceData: spaceData, - wallpaper: wallpaper, - draggedSpace: $draggedSpace + model: model, + draggedSpaceViewId: $draggedSpaceViewId ) } else { NewSpaceCardLabel( - spaceData: spaceData, - wallpaper: wallpaper, - draggedSpace: $draggedSpace + model: model, + draggedSpaceViewId: $draggedSpaceViewId ) } } .contentShape([.dragPreview, .contextMenuPreview], RoundedRectangle(cornerRadius: 20, style: .continuous)) .contextMenu { menuItems.tint(Color.Text.primary) } } - + @ViewBuilder private var menuItems: some View { - if spaceData.spaceView.isLoading { + if model.isLoading { copyButton Divider() } - - if spaceData.spaceView.isPinned { + + if model.isPinned { unpinButton } else { pinButton } - - if FeatureFlags.muteSpacePossibility, spaceData.spaceView.isShared { + + if muteSpacePossibilityToggle, model.isShared { muteButton } - - if spaceData.spaceView.isLoading { + + if model.isLoading { deleteButton } else { settingsButton @@ -74,9 +73,9 @@ struct SpaceCard: View, @preconcurrency Equatable { onTapMute() } label: { HStack { - Text(spaceData.spaceView.pushNotificationMode.isUnmutedAll ? Loc.mute : Loc.unmute) + Text(model.allNotificationsUnmuted ? Loc.mute : Loc.unmute) Spacer() - Image(systemName: spaceData.spaceView.pushNotificationMode.isUnmutedAll ? "bell.slash" : "bell") + Image(systemName: model.allNotificationsUnmuted ? "bell.slash" : "bell") } } } @@ -123,10 +122,4 @@ struct SpaceCard: View, @preconcurrency Equatable { .tint(.red) } } - - static func == (lhs: Self, rhs: Self) -> Bool { - lhs.spaceData == rhs.spaceData - && lhs.wallpaper == rhs.wallpaper - && lhs.draggedSpace == rhs.draggedSpace - } } diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLabel.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLabel.swift new file mode 100644 index 0000000000..1f78b24d60 --- /dev/null +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLabel.swift @@ -0,0 +1,102 @@ +import SwiftUI +import AnytypeCore + +// SpaceCardLabel and SpaceCard are splitted for better SwiftUI diff. +struct SpaceCardLabel: View { + + let model: SpaceCardModel + @Binding var draggedSpaceViewId: String? + + @Namespace private var namespace + + var body: some View { + HStack(alignment: .center, spacing: 12) { + IconView(icon: model.objectIconImage) + .frame(width: 56, height: 56) + VStack(alignment: .leading, spacing: 0) { + HStack { + Text(model.name.withPlaceholder) + .anytypeFontStyle(.bodySemibold) + .lineLimit(1) + .foregroundStyle(Color.Text.primary) + if model.isMuted { + Spacer.fixedWidth(8) + Image(asset: .X18.muted).foregroundColor(.Control.secondary) + } + Spacer(minLength: 8) + createdDate + } + HStack { + info + Spacer() + unreadCounters + pin + } + Spacer(minLength: 1) + } + .matchedGeometryEffect(id: "content", in: namespace, properties: .position, anchor: .topLeading) + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + // Optimization for fast sizeThatFits + .frame(height: 80) + .background(Color.Background.primary) + .contentShape([.dragPreview, .contextMenuPreview], RoundedRectangle(cornerRadius: 20, style: .continuous)) + + .if(model.isPinned) { + $0.onDrag { + draggedSpaceViewId = model.spaceViewId + return NSItemProvider() + } preview: { + EmptyView() + } + } + } + + private var info: some View { + Group { + if let lastMessage = model.lastMessage { + SpaceCardLastMessageView(model: lastMessage) + } else { + Text(model.uxTypeName) + .anytypeStyle(.uxTitle2Regular) + .lineLimit(1) + } + } + .foregroundStyle(Color.Text.secondary) + .multilineTextAlignment(.leading) + } + + + @ViewBuilder + private var createdDate: some View { + if let lastMessage = model.lastMessage { + Text(lastMessage.historyDate) + .anytypeStyle(.relation2Regular) + .foregroundStyle(Color.Control.transparentSecondary) + } + } + + private var unreadCounters: some View { + HStack(spacing: 4) { + if model.mentionCounter > 0 { + MentionBadge(style: model.isMuted ? .muted : .highlighted) + } + if model.unreadCounter > 0 { + CounterView( + count: model.unreadCounter, + style: model.isMuted ? .muted : .highlighted + ) + } + } + } + + @ViewBuilder + private var pin: some View { + if !model.hasCounters && model.isPinned { + Image(asset: .X18.pin) + .foregroundStyle(Color.Control.secondary) + .frame(width: 18, height: 18) + } + } +} diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLastMessageModel.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLastMessageModel.swift new file mode 100644 index 0000000000..e3760f85c3 --- /dev/null +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLastMessageModel.swift @@ -0,0 +1,17 @@ +import Foundation + +struct SpaceCardLastMessageModel: Equatable { + let creatorTitle: String? + let text: String + let attachments: [Attachment] + let localizedAttachmentsText: String + let historyDate: String + let chatPreviewDate: String +} + +extension SpaceCardLastMessageModel { + struct Attachment: Equatable, Identifiable { + let id: String + let icon: Icon + } +} diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLastMessageView.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLastMessageView.swift new file mode 100644 index 0000000000..53564384fb --- /dev/null +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLastMessageView.swift @@ -0,0 +1,47 @@ +import SwiftUI + +struct SpaceCardLastMessageView: View { + + let model: SpaceCardLastMessageModel + + var body: some View { + if model.text.isNotEmpty { + // Do not show attachements due to SwiftUI limitations: + // Can not fit attachements in between two lines of text with proper multiline behaviour + messageWithoutAttachements + } else if model.attachments.isNotEmpty { + // Show attachements and 1 line of text + messageWithAttachements + } else { + Text(model.creatorTitle ?? Loc.Chat.newMessages) + .anytypeStyle(.uxTitle2Medium).lineLimit(1) + } + } + + private var messageWithoutAttachements: some View { + Group { + if let creatorTitle = model.creatorTitle { + Text(creatorTitle + ": ").anytypeFontStyle(.uxTitle2Medium) + + Text(model.text).anytypeFontStyle(.uxTitle2Regular) + } else { + Text(model.text).anytypeFontStyle(.uxTitle2Regular) + } + }.lineLimit(2).anytypeLineHeightStyle(.uxTitle2Regular) + } + + private var messageWithAttachements: some View { + HStack(spacing: 2) { + if let creatorTitle = model.creatorTitle { + Text(creatorTitle + ":").anytypeStyle(.uxTitle2Medium).lineLimit(1) + Spacer.fixedWidth(4) + } + + ForEach(model.attachments) { + IconView(icon: $0.icon).frame(width: 18, height: 18) + } + + Spacer.fixedWidth(4) + Text(model.localizedAttachmentsText).anytypeStyle(.uxTitle2Regular).lineLimit(1) + } + } +} diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModel.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModel.swift new file mode 100644 index 0000000000..266ab806c9 --- /dev/null +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModel.swift @@ -0,0 +1,24 @@ +import Services +import AnytypeCore + +struct SpaceCardModel: Equatable, Identifiable { + let spaceViewId: String + let targetSpaceId: String + let objectIconImage: Icon + let name: String + let isPinned: Bool + let isLoading: Bool + let isShared: Bool + let isMuted: Bool + let uxTypeName: String + let allNotificationsUnmuted: Bool + + let lastMessage: SpaceCardLastMessageModel? + let unreadCounter: Int + let mentionCounter: Int + let hasCounters: Bool + + let wallpaper: SpaceWallpaperType + + var id: String { spaceViewId } +} diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModelBuilder.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModelBuilder.swift new file mode 100644 index 0000000000..6165b73bdf --- /dev/null +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModelBuilder.swift @@ -0,0 +1,77 @@ +import Services +import AnytypeCore +import Factory + +protocol SpaceCardModelBuilderProtocol: AnyObject, Sendable { + func build( + from spaces: [ParticipantSpaceViewDataWithPreview], + wallpapers: [String: SpaceWallpaperType] + ) async -> [SpaceCardModel] +} + +final class SpaceCardModelBuilder: SpaceCardModelBuilderProtocol, Sendable { + + private let historyDateFormatter = HistoryDateFormatter() + private let chatPreviewDateFormatter = ChatPreviewDateFormatter() + + func build( + from spaces: [ParticipantSpaceViewDataWithPreview], + wallpapers: [String: SpaceWallpaperType] + ) async -> [SpaceCardModel] { + await Task.detached { + spaces.map { spaceData in + self.buildModel(from: spaceData, wallpapers: wallpapers) + } + }.value + } + + private func buildModel( + from spaceData: ParticipantSpaceViewDataWithPreview, + wallpapers: [String: SpaceWallpaperType] + ) -> SpaceCardModel { + let spaceView = spaceData.spaceView + let preview = spaceData.preview + + let lastMessage = preview.lastMessage.map { lastMessagePreview in + let attachments = lastMessagePreview.attachments.prefix(3).map { objectDetails in + SpaceCardLastMessageModel.Attachment( + id: objectDetails.id, + icon: objectDetails.objectIconImage + ) + } + + return SpaceCardLastMessageModel( + creatorTitle: lastMessagePreview.creator?.title, + text: lastMessagePreview.text, + attachments: Array(attachments), + localizedAttachmentsText: lastMessagePreview.localizedAttachmentsText, + historyDate: historyDateFormatter.localizedDateString(for: lastMessagePreview.createdAt, showTodayTime: true), + chatPreviewDate: chatPreviewDateFormatter.localizedDateString(for: lastMessagePreview.createdAt, showTodayTime: true) + ) + } + + return SpaceCardModel( + spaceViewId: spaceView.id, + targetSpaceId: spaceView.targetSpaceId, + objectIconImage: spaceView.objectIconImage, + name: spaceView.name, + isPinned: spaceView.isPinned, + isLoading: spaceView.isLoading, + isShared: spaceView.isShared, + isMuted: FeatureFlags.muteSpacePossibility && !spaceView.pushNotificationMode.isUnmutedAll, + uxTypeName: spaceView.uxType.name, + allNotificationsUnmuted: spaceView.pushNotificationMode.isUnmutedAll, + lastMessage: lastMessage, + unreadCounter: preview.unreadCounter, + mentionCounter: preview.mentionCounter, + hasCounters: preview.hasCounters, + wallpaper: wallpapers[spaceView.targetSpaceId] ?? .default + ) + } +} + +extension Container { + var spaceCardModelBuilder: Factory< any SpaceCardModelBuilderProtocol> { + self { SpaceCardModelBuilder() }.shared + } +} diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCardLabel.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCardLabel.swift deleted file mode 100644 index fa0f4c83a0..0000000000 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCardLabel.swift +++ /dev/null @@ -1,155 +0,0 @@ -import SwiftUI -import AnytypeCore - -// SpaceCardLabel and SpaceCard are splitted for better SwiftUI diff. -struct SpaceCardLabel: View { - - let spaceData: ParticipantSpaceViewDataWithPreview - let wallpaper: SpaceWallpaperType - private let dateFormatter = HistoryDateFormatter() - @Binding var draggedSpace: ParticipantSpaceViewDataWithPreview? - - @Namespace private var namespace - - var body: some View { - HStack(alignment: .center, spacing: 12) { - IconView(icon: spaceData.spaceView.objectIconImage) - .frame(width: 56, height: 56) - VStack(alignment: .leading, spacing: 0) { - HStack { - Text(spaceData.spaceView.name.withPlaceholder) - .anytypeFontStyle(.bodySemibold) - .lineLimit(1) - .foregroundStyle(Color.Text.primary) - if isMuted { - Spacer.fixedWidth(8) - Image(asset: .X18.muted).foregroundColor(.Control.secondary) - } - Spacer(minLength: 8) - createdDate - } - HStack { - info - Spacer() - unreadCounters - pin - } - Spacer(minLength: 1) - } - // Fixing the animation when the cell is moved and updated inside - // Optimization - create a data model for SpaceCard and map to in in SpaceHubViewModel on background thread - .id(spaceData.hashValue) - .matchedGeometryEffect(id: "content", in: namespace, properties: .position, anchor: .topLeading) - } - .padding(.horizontal, 16) - .padding(.vertical, 8) - // Optimization for fast sizeThatFits - .frame(height: 80) - .background(Color.Background.primary) - .contentShape([.dragPreview, .contextMenuPreview], RoundedRectangle(cornerRadius: 20, style: .continuous)) - - .if(spaceData.spaceView.isPinned) { - $0.onDrag { - draggedSpace = spaceData - return NSItemProvider() - } preview: { - EmptyView() - } - } - } - - private var info: some View { - Group { - if let lastMessage = spaceData.preview.lastMessage { - lastMessagePreview(lastMessage) - } else { - Text(spaceData.spaceView.uxType.name) - .anytypeStyle(.uxTitle2Regular) - .lineLimit(1) - } - } - .foregroundStyle(Color.Text.secondary) - .multilineTextAlignment(.leading) - } - - @ViewBuilder - func lastMessagePreview(_ message: LastMessagePreview) -> some View { - Group { - if message.text.isNotEmpty { - // Do not show attachements due to SwiftUI limitations: - // Can not fit attachements in between two lines of text with proper multiline behaviour - messageWithoutAttachements(message) - } else if message.attachments.isNotEmpty { - // Show attachements and 1 line of text - messageWithAttachements(message) - } else { - Text(message.creator?.title ?? Loc.Chat.newMessages) - .anytypeStyle(.uxTitle2Medium).lineLimit(1) - } - } - } - - func messageWithoutAttachements(_ message: LastMessagePreview) -> some View { - Group { - if let creator = message.creator { - Text(creator.title + ": ").anytypeFontStyle(.uxTitle2Medium) + - Text(message.text).anytypeFontStyle(.uxTitle2Regular) - } else { - Text(message.text).anytypeFontStyle(.uxTitle2Regular) - } - }.lineLimit(2).anytypeLineHeightStyle(.uxTitle2Regular) - } - - @ViewBuilder - func messageWithAttachements(_ message: LastMessagePreview) -> some View { - HStack(spacing: 2) { - if let creator = message.creator { - Text(creator.title + ":").anytypeStyle(.uxTitle2Medium).lineLimit(1) - Spacer.fixedWidth(4) - } - - ForEach(message.attachments.prefix(3)) { - IconView(icon: $0.objectIconImage).frame(width: 18, height: 18) - } - - Spacer.fixedWidth(4) - Text(message.localizedAttachmentsText).anytypeStyle(.uxTitle2Regular).lineLimit(1) - } - } - - @ViewBuilder - private var createdDate: some View { - if let lastMessage = spaceData.preview.lastMessage { - Text(dateFormatter.localizedDateString(for: lastMessage.createdAt, showTodayTime: true)) - .anytypeStyle(.relation2Regular) - .foregroundStyle(Color.Control.transparentSecondary) - } - } - - private var unreadCounters: some View { - HStack(spacing: 4) { - if spaceData.preview.mentionCounter > 0 { - MentionBadge(style: isMuted ? .muted : .highlighted) - } - if spaceData.preview.unreadCounter > 0 { - CounterView( - count: spaceData.preview.unreadCounter, - style: isMuted ? .muted : .highlighted - ) - } - } - } - - @ViewBuilder - private var pin: some View { - if !spaceData.preview.hasCounters && spaceData.spaceView.isPinned { - Image(asset: .X18.pin) - .foregroundStyle(Color.Control.secondary) - .frame(width: 18, height: 18) - } - } - - private var isMuted: Bool { - FeatureFlags.muteSpacePossibility && !spaceData.spaceView.pushNotificationMode.isUnmutedAll - } -} diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceHubList.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceHubList.swift index 9a2b68b927..cfdbf07fa8 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceHubList.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceHubList.swift @@ -5,17 +5,11 @@ struct SpaceHubList: View { @Bindable var model: SpaceHubViewModel - @State private var draggedSpace: ParticipantSpaceViewDataWithPreview? + @State private var draggedSpaceViewId: String? @State private var draggedInitialIndex: Int? @State private var vaultBackToRootsToggle = FeatureFlags.vaultBackToRoots var body: some View { - let _ = Self._printChanges() - return content - } - - @ViewBuilder - private var content: some View { if model.filteredSpaces.isEmpty && model.searchText.isEmpty { emptyStateView } else if model.filteredSpaces.isNotEmpty { @@ -46,45 +40,45 @@ struct SpaceHubList: View { } } - private func spaceCard(_ space: ParticipantSpaceViewDataWithPreview) -> some View { + @ViewBuilder + private func spaceCard(_ cardModel: SpaceCardModel) -> some View { SpaceCard( - spaceData: space, - wallpaper: model.wallpapers[space.spaceView.targetSpaceId] ?? .default, - draggedSpace: $draggedSpace, + model: cardModel, + draggedSpaceViewId: $draggedSpaceViewId, onTap: { - model.onSpaceTap(spaceId: space.spaceView.targetSpaceId) + model.onSpaceTap(spaceId: cardModel.targetSpaceId) }, onTapCopy: { - model.copySpaceInfo(spaceView: space.spaceView) + model.copySpaceInfo(spaceViewId: cardModel.spaceViewId) }, onTapMute: { - model.muteSpace(spaceView: space.spaceView) + model.muteSpace(spaceViewId: cardModel.spaceViewId) }, onTapPin: { - try await model.pin(spaceView: space.spaceView) + try await model.pin(spaceViewId: cardModel.spaceViewId) }, onTapUnpin: { - try await model.unpin(spaceView: space.spaceView) + try await model.unpin(spaceViewId: cardModel.spaceViewId) }, onTapSettings: { - model.openSpaceSettings(spaceId: space.spaceView.targetSpaceId) + model.openSpaceSettings(spaceId: cardModel.targetSpaceId) }, onTapDelete: { - model.onDeleteSpace(spaceId: space.spaceView.targetSpaceId) + model.onDeleteSpace(spaceId: cardModel.targetSpaceId) } ) - .equatable() .padding(.horizontal, vaultBackToRootsToggle ? 16 : 0) - .if(space.spaceView.isPinned) { + .if(cardModel.isPinned) { $0.onDrop( of: [.text], - delegate: SpaceHubDropDelegate( - destinationItem: space, + delegate: SpaceHubDropDelegate( + destinationSpaceViewId: cardModel.spaceViewId, allSpaces: $model.spaces, - draggedItem: $draggedSpace, + draggedSpaceViewId: $draggedSpaceViewId, initialIndex: $draggedInitialIndex ) ) } + .id(cardModel.id) } } From 8e14fc8c7d45ea71bc270120e7932f21e63cedba Mon Sep 17 00:00:00 2001 From: Mikhail Golovko Date: Thu, 30 Oct 2025 15:30:16 +0300 Subject: [PATCH 4/9] IOS-5387 Rollback --- .../SpaceHub/SpaceHubCoordinatorView.swift | 172 +++++++++--------- 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/Anytype/Sources/PresentationLayer/Flows/SpaceHub/SpaceHubCoordinatorView.swift b/Anytype/Sources/PresentationLayer/Flows/SpaceHub/SpaceHubCoordinatorView.swift index c3b277cfd1..e64d9fad43 100644 --- a/Anytype/Sources/PresentationLayer/Flows/SpaceHub/SpaceHubCoordinatorView.swift +++ b/Anytype/Sources/PresentationLayer/Flows/SpaceHub/SpaceHubCoordinatorView.swift @@ -27,92 +27,92 @@ struct SpaceHubCoordinatorView: View { .updateShortcuts(spaceId: model.fallbackSpaceId) .snackbar(toastBarData: $model.toastBarData) -// .sheet(item: $model.showGalleryImport) { data in -// GalleryInstallationCoordinatorView(data: data) -// } -// .sheet(isPresented: $model.showSpaceManager) { -// SpacesManagerView() -// } -// .sheet(item: $model.membershipTierId) { tierId in -// MembershipCoordinator(initialTierId: tierId.value) -// } -// .sheet(item: $model.membershipNameFinalizationData) { -// MembershipNameFinalizationView(tier: $0) -// } -// .sheet(item: $model.showGlobalSearchData) { -// GlobalSearchView(data: $0) -// } -// .sheet(item: $model.typeSearchForObjectCreationSpaceId) { -// model.typeSearchForObjectCreationModule(spaceId: $0.value) -// } -// .anytypeSheet(item: $model.spaceJoinData) { -// SpaceJoinView(data: $0, onManageSpaces: { -// model.onManageSpacesSelected() -// }) -// } -// .anytypeSheet(item: $model.userWarningAlert, dismissOnBackgroundView: false) { -// UserWarningAlertCoordinatorView(alert: $0) -// } -// .anytypeSheet(isPresented: $model.showObjectIsNotAvailableAlert) { -// ObjectIsNotAvailableAlert() -// } -// .sheet(item: $model.showSpaceShareData) { -// SpaceShareCoordinatorView(data: $0) -// .pageNavigation(model.pageNavigation) -// } -// .sheet(item: $model.showSpaceMembersData) { -// SpaceMembersView(data: $0) -// .pageNavigation(model.pageNavigation) -// } -// .anytypeSheet(item: $model.profileData) { -// ProfileView(info: $0) -// .pageNavigation(model.pageNavigation) -// } -// .anytypeSheet(item: $model.spaceProfileData) { -// SpaceProfileView(info: $0) -// } -// .safariBookmarkObject($model.bookmarkScreenData) { -// model.onOpenBookmarkAsObject($0) -// } -// .sheet(item: $model.spaceCreateData) { -// SpaceCreateCoordinatorView(data: $0) -// } -// .sheet(isPresented: $model.showSpaceTypeForCreate) { -// SpaceCreateTypePickerView(onSelectSpaceType: { type in -// model.onSpaceTypeSelected(type) -// }, onSelectQrCodeScan: { -// model.onSelectQrCodeScan() -// }) -// .navigationZoomTransition(sourceID: "SpaceCreateTypePickerView", in: namespace) -// } -// .qrCodeScanner(shouldScan: $model.shouldScanQrCode) -// .sheet(isPresented: $model.showSharingExtension) { -// SharingExtensionCoordinatorView() -// } -// .sheet(isPresented: $model.showAppSettings) { -// SettingsCoordinatorView() -// .pageNavigation(model.pageNavigation) -// } -// -// // load photos -// .photosPicker(isPresented: $model.showPhotosPicker, selection: $model.photosItems) -// .onChange(of: model.photosItems) { -// model.photosPickerFinished() -// } -// -// // load from camera -// .cameraAccessFullScreenCover(item: $model.cameraData) { -// SimpleCameraView(data: $0) -// } -// -// // load files -// .fileImporter( -// isPresented: $model.showFilesPicker, -// allowedContentTypes: [.data], -// allowsMultipleSelection: true -// ) { result in -// model.fileImporterFinished(result: result) -// } + .sheet(item: $model.showGalleryImport) { data in + GalleryInstallationCoordinatorView(data: data) + } + .sheet(isPresented: $model.showSpaceManager) { + SpacesManagerView() + } + .sheet(item: $model.membershipTierId) { tierId in + MembershipCoordinator(initialTierId: tierId.value) + } + .sheet(item: $model.membershipNameFinalizationData) { + MembershipNameFinalizationView(tier: $0) + } + .sheet(item: $model.showGlobalSearchData) { + GlobalSearchView(data: $0) + } + .sheet(item: $model.typeSearchForObjectCreationSpaceId) { + model.typeSearchForObjectCreationModule(spaceId: $0.value) + } + .anytypeSheet(item: $model.spaceJoinData) { + SpaceJoinView(data: $0, onManageSpaces: { + model.onManageSpacesSelected() + }) + } + .anytypeSheet(item: $model.userWarningAlert, dismissOnBackgroundView: false) { + UserWarningAlertCoordinatorView(alert: $0) + } + .anytypeSheet(isPresented: $model.showObjectIsNotAvailableAlert) { + ObjectIsNotAvailableAlert() + } + .sheet(item: $model.showSpaceShareData) { + SpaceShareCoordinatorView(data: $0) + .pageNavigation(model.pageNavigation) + } + .sheet(item: $model.showSpaceMembersData) { + SpaceMembersView(data: $0) + .pageNavigation(model.pageNavigation) + } + .anytypeSheet(item: $model.profileData) { + ProfileView(info: $0) + .pageNavigation(model.pageNavigation) + } + .anytypeSheet(item: $model.spaceProfileData) { + SpaceProfileView(info: $0) + } + .safariBookmarkObject($model.bookmarkScreenData) { + model.onOpenBookmarkAsObject($0) + } + .sheet(item: $model.spaceCreateData) { + SpaceCreateCoordinatorView(data: $0) + } + .sheet(isPresented: $model.showSpaceTypeForCreate) { + SpaceCreateTypePickerView(onSelectSpaceType: { type in + model.onSpaceTypeSelected(type) + }, onSelectQrCodeScan: { + model.onSelectQrCodeScan() + }) + .navigationZoomTransition(sourceID: "SpaceCreateTypePickerView", in: namespace) + } + .qrCodeScanner(shouldScan: $model.shouldScanQrCode) + .sheet(isPresented: $model.showSharingExtension) { + SharingExtensionCoordinatorView() + } + .sheet(isPresented: $model.showAppSettings) { + SettingsCoordinatorView() + .pageNavigation(model.pageNavigation) + } + + // load photos + .photosPicker(isPresented: $model.showPhotosPicker, selection: $model.photosItems) + .onChange(of: model.photosItems) { + model.photosPickerFinished() + } + + // load from camera + .cameraAccessFullScreenCover(item: $model.cameraData) { + SimpleCameraView(data: $0) + } + + // load files + .fileImporter( + isPresented: $model.showFilesPicker, + allowedContentTypes: [.data], + allowsMultipleSelection: true + ) { result in + model.fileImporterFinished(result: result) + } } private var content: some View { From 096385fe8d4b2b22e948be9442637da4f650346e Mon Sep 17 00:00:00 2001 From: Mikhail Golovko Date: Thu, 30 Oct 2025 16:31:55 +0300 Subject: [PATCH 5/9] IOS-5387 Fixes --- .../Modules/SpaceHub/SpaceHubViewModel.swift | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubViewModel.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubViewModel.swift index 42d769ea83..8785caeef9 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubViewModel.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubViewModel.swift @@ -124,16 +124,18 @@ final class SpaceHubViewModel { } func searchTextUpdated() { - updateFilteredSpaces() + Task { + await updateFilteredSpaces() + } } // MARK: - Private private func subscribeOnSpaces() async { for await spaces in await spaceHubSpacesStorage.spacesStream { self.spaces = spaces.sorted(by: sortSpacesForPinnedFeature) - self.dataLoaded = spaces.isNotEmpty showLoading = spaces.contains { $0.spaceView.isLoading } - updateFilteredSpaces() + await updateFilteredSpaces() + self.dataLoaded = spaces.isNotEmpty } } @@ -212,23 +214,21 @@ final class SpaceHubViewModel { } } - private func updateFilteredSpaces() { - Task { - guard let spaces else { - filteredSpaces = [] - return - } - - let spacesToFilter: [ParticipantSpaceViewDataWithPreview] - if searchText.isEmpty { - spacesToFilter = spaces - } else { - spacesToFilter = spaces.filter { space in - space.spaceView.name.localizedCaseInsensitiveContains(searchText) - } + private func updateFilteredSpaces() async { + guard let spaces else { + filteredSpaces = [] + return + } + + let spacesToFilter: [ParticipantSpaceViewDataWithPreview] + if searchText.isEmpty { + spacesToFilter = spaces + } else { + spacesToFilter = spaces.filter { space in + space.spaceView.name.localizedCaseInsensitiveContains(searchText) } - - self.filteredSpaces = await spaceCardModelBuilder.build(from: spacesToFilter, wallpapers: wallpapers) } + + self.filteredSpaces = await spaceCardModelBuilder.build(from: spacesToFilter, wallpapers: wallpapers) } } From 99c3649e183cb2973c79b11422ee689c55faba63 Mon Sep 17 00:00:00 2001 From: Mikhail Golovko Date: Thu, 30 Oct 2025 17:11:40 +0300 Subject: [PATCH 6/9] IOS-5387 Fix dnd --- .../Modules/SpaceHub/SpaceHubView.swift | 23 ++--- .../Subviews/SpaceCard/SpaceCardLabel.swift | 13 +-- .../SpaceHub/Subviews/SpaceHubList.swift | 19 ++-- .../SystemExtensions/View+DragDrop.swift | 91 +++++++++++++++++++ 4 files changed, 112 insertions(+), 34 deletions(-) create mode 100644 Modules/DesignKit/Sources/DesignKit/SystemExtensions/View+DragDrop.swift diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubView.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubView.swift index 4ebf434500..d503da8ec1 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubView.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/SpaceHubView.swift @@ -45,22 +45,13 @@ struct SpaceHubView: View { private func spacesView() -> some View { NavigationStack { SpaceHubList(model: model) -// Group { -// if model.filteredSpaces.isEmpty && model.searchText.isEmpty { -// emptyStateView -// } else if model.filteredSpaces.isNotEmpty { -// scrollView -// } else { -// SpaceHubSearchEmptySpaceView() -// } -// } - .navigationTitle(Loc.myChannels) - .navigationBarTitleDisplayMode(.inline) - .toolbar { toolbarItems } - .searchable(text: $model.searchText) - .onChange(of: model.searchText) { - model.searchTextUpdated() - } + .navigationTitle(Loc.myChannels) + .navigationBarTitleDisplayMode(.inline) + .toolbar { toolbarItems } + .searchable(text: $model.searchText) + .onChange(of: model.searchText) { + model.searchTextUpdated() + } }.tint(Color.Text.secondary) } diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLabel.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLabel.swift index 1f78b24d60..a65ef7b915 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLabel.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLabel.swift @@ -42,14 +42,11 @@ struct SpaceCardLabel: View { .frame(height: 80) .background(Color.Background.primary) .contentShape([.dragPreview, .contextMenuPreview], RoundedRectangle(cornerRadius: 20, style: .continuous)) - - .if(model.isPinned) { - $0.onDrag { - draggedSpaceViewId = model.spaceViewId - return NSItemProvider() - } preview: { - EmptyView() - } + .onDragIf(model.isPinned) { + draggedSpaceViewId = model.spaceViewId + return NSItemProvider() + } preview: { + EmptyView() } } diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceHubList.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceHubList.swift index cfdbf07fa8..f13a4d60e7 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceHubList.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceHubList.swift @@ -68,17 +68,16 @@ struct SpaceHubList: View { } ) .padding(.horizontal, vaultBackToRootsToggle ? 16 : 0) - .if(cardModel.isPinned) { - $0.onDrop( - of: [.text], - delegate: SpaceHubDropDelegate( - destinationSpaceViewId: cardModel.spaceViewId, - allSpaces: $model.spaces, - draggedSpaceViewId: $draggedSpaceViewId, - initialIndex: $draggedInitialIndex - ) + .onDropIf( + cardModel.isPinned, + of: [.text], + delegate: SpaceHubDropDelegate( + destinationSpaceViewId: cardModel.spaceViewId, + allSpaces: $model.spaces, + draggedSpaceViewId: $draggedSpaceViewId, + initialIndex: $draggedInitialIndex ) - } + ) .id(cardModel.id) } } diff --git a/Modules/DesignKit/Sources/DesignKit/SystemExtensions/View+DragDrop.swift b/Modules/DesignKit/Sources/DesignKit/SystemExtensions/View+DragDrop.swift new file mode 100644 index 0000000000..247c63e2f2 --- /dev/null +++ b/Modules/DesignKit/Sources/DesignKit/SystemExtensions/View+DragDrop.swift @@ -0,0 +1,91 @@ +import SwiftUI +import UniformTypeIdentifiers + +public extension View { + + func onDragIf( + _ condition: Bool, + data: @escaping () -> NSItemProvider + ) -> some View { + modifier(ConditionalDragModifier(condition: condition, data: data)) + } + + func onDragIf( + _ condition: Bool, + data: @escaping () -> NSItemProvider, + @ViewBuilder preview: @escaping () -> V + ) -> some View where V: View { + modifier(ConditionalDragWithPreviewModifier(condition: condition, data: data, preview: preview)) + } + + func onDropIf( + _ condition: Bool, + of supportedContentTypes: [UTType], + delegate: any DropDelegate + ) -> some View { + modifier( + ConditionalDropModifier( + condition: condition, + supportedContentTypes: supportedContentTypes, + delegate: delegate + ) + ) + } +} + +private struct ConditionalDragModifier: ViewModifier { + let condition: Bool + let data: () -> NSItemProvider + + @Namespace private var namespace + + func body(content: Content) -> some View { + if condition { + content + .onDrag(data) + .matchedGeometryEffect(id: "content", in: namespace) + } else { + content + .matchedGeometryEffect(id: "content", in: namespace) + } + } +} + +private struct ConditionalDragWithPreviewModifier: ViewModifier { + + let condition: Bool + let data: () -> NSItemProvider + let preview: () -> Preview + + @Namespace private var namespace + + func body(content: Content) -> some View { + if condition { + content + .onDrag(data, preview: preview) + .matchedGeometryEffect(id: "content", in: namespace) + } else { + content + .matchedGeometryEffect(id: "content", in: namespace) + } + } +} + +private struct ConditionalDropModifier: ViewModifier { + let condition: Bool + let supportedContentTypes: [UTType] + let delegate: any DropDelegate + + @Namespace private var namespace + + func body(content: Content) -> some View { + if condition { + content + .onDrop(of: supportedContentTypes, delegate: delegate) + .matchedGeometryEffect(id: "content", in: namespace) + } else { + content + .matchedGeometryEffect(id: "content", in: namespace) + } + } +} From f44fa771aacf28c5e0784d28cb9a4ad591c290d7 Mon Sep 17 00:00:00 2001 From: Mikhail Golovko Date: Thu, 30 Oct 2025 17:23:41 +0300 Subject: [PATCH 7/9] IOS-5387 Fixes --- .../Modules/SpaceHub/Subviews/SpaceCard/SpaceCard.swift | 8 ++++---- .../Modules/SpaceHub/Subviews/SpaceHubList.swift | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCard.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCard.swift index e9f3937223..98f82347d4 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCard.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCard.swift @@ -35,24 +35,24 @@ struct SpaceCard: View { .contentShape([.dragPreview, .contextMenuPreview], RoundedRectangle(cornerRadius: 20, style: .continuous)) .contextMenu { menuItems.tint(Color.Text.primary) } } - + @ViewBuilder private var menuItems: some View { if model.isLoading { copyButton Divider() } - + if model.isPinned { unpinButton } else { pinButton } - + if muteSpacePossibilityToggle, model.isShared { muteButton } - + if model.isLoading { deleteButton } else { diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceHubList.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceHubList.swift index f13a4d60e7..f42f52c264 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceHubList.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceHubList.swift @@ -1,6 +1,7 @@ import SwiftUI import AnytypeCore +// Is part of main view SpaceHubView. Related from SpaceHubViewModel struct SpaceHubList: View { @Bindable var model: SpaceHubViewModel From 7bb981cf4482a14c1ea1d1f9e5146838b0239bdd Mon Sep 17 00:00:00 2001 From: Mikhail Golovko Date: Thu, 30 Oct 2025 18:33:26 +0300 Subject: [PATCH 8/9] IOS-5387 Fix on drag for new card label --- .../Subviews/SpaceCard/NewSpaceCardLabel.swift | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/NewSpaceCardLabel.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/NewSpaceCardLabel.swift index 9da045530d..70511ceedf 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/NewSpaceCardLabel.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/NewSpaceCardLabel.swift @@ -11,13 +11,11 @@ struct NewSpaceCardLabel: View { var body: some View { content - .if(model.isPinned) { - $0.onDrag { - draggedSpaceViewId = model.spaceViewId - return NSItemProvider() - } preview: { - EmptyView() - } + .onDragIf(model.isPinned) { + draggedSpaceViewId = model.spaceViewId + return NSItemProvider() + } preview: { + EmptyView() } } From 92833f86e7075335b27d98eaee6d57598574f050 Mon Sep 17 00:00:00 2001 From: Mikhail Golovko Date: Thu, 30 Oct 2025 18:42:01 +0300 Subject: [PATCH 9/9] IOS-5387 Fixes --- .../SpaceHub/Subviews/SpaceCard/NewSpaceCardLabel.swift | 5 +++-- .../Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLabel.swift | 2 +- .../Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModel.swift | 2 +- .../SpaceHub/Subviews/SpaceCard/SpaceCardModelBuilder.swift | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/NewSpaceCardLabel.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/NewSpaceCardLabel.swift index 70511ceedf..20983907a7 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/NewSpaceCardLabel.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/NewSpaceCardLabel.swift @@ -44,6 +44,7 @@ struct NewSpaceCardLabel: View { wallpaper: model.wallpaper, spaceIcon: model.objectIconImage )) + .background(Color.Background.primary) .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) } @@ -51,7 +52,7 @@ struct NewSpaceCardLabel: View { VStack(alignment: .leading, spacing: 0) { HStack(alignment: .bottom) { HStack(alignment: .center) { - AnytypeText(model.name.withPlaceholder, style: .bodySemibold) + AnytypeText(model.nameWithPlaceholder, style: .bodySemibold) .lineLimit(1) .foregroundColor(Color.Text.primary) if model.isMuted { @@ -81,7 +82,7 @@ struct NewSpaceCardLabel: View { private var mainContentWithoutMessage: some View { VStack(alignment: .leading, spacing: 0) { HStack { - AnytypeText(model.name.withPlaceholder, style: .bodySemibold) + AnytypeText(model.nameWithPlaceholder, style: .bodySemibold) .lineLimit(1) .foregroundColor(Color.Text.primary) if model.isMuted { diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLabel.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLabel.swift index a65ef7b915..193747a17d 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLabel.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardLabel.swift @@ -15,7 +15,7 @@ struct SpaceCardLabel: View { .frame(width: 56, height: 56) VStack(alignment: .leading, spacing: 0) { HStack { - Text(model.name.withPlaceholder) + Text(model.nameWithPlaceholder) .anytypeFontStyle(.bodySemibold) .lineLimit(1) .foregroundStyle(Color.Text.primary) diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModel.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModel.swift index 266ab806c9..fe30045972 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModel.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModel.swift @@ -5,7 +5,7 @@ struct SpaceCardModel: Equatable, Identifiable { let spaceViewId: String let targetSpaceId: String let objectIconImage: Icon - let name: String + let nameWithPlaceholder: String let isPinned: Bool let isLoading: Bool let isShared: Bool diff --git a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModelBuilder.swift b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModelBuilder.swift index 6165b73bdf..4a3405ad5a 100644 --- a/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModelBuilder.swift +++ b/Anytype/Sources/PresentationLayer/Modules/SpaceHub/Subviews/SpaceCard/SpaceCardModelBuilder.swift @@ -54,7 +54,7 @@ final class SpaceCardModelBuilder: SpaceCardModelBuilderProtocol, Sendable { spaceViewId: spaceView.id, targetSpaceId: spaceView.targetSpaceId, objectIconImage: spaceView.objectIconImage, - name: spaceView.name, + nameWithPlaceholder: spaceView.name.withPlaceholder, isPinned: spaceView.isPinned, isLoading: spaceView.isLoading, isShared: spaceView.isShared,