From 697e0bf9baf1bba135e7e85ba466316441a3947e Mon Sep 17 00:00:00 2001 From: Lakr <25259084+Lakr233@users.noreply.github.com> Date: Wed, 25 Jun 2025 17:54:06 +0800 Subject: [PATCH] feat: completed input box ui + ux (#12927) ## Summary by CodeRabbit - **New Features** - Introduced a document picker UI for embedding and selecting AFFiNE documents, with improved search and recent document functionality. - Added support for searching and fetching recently updated documents within a workspace. - Enhanced error handling for login and metadata validation during document operations. - Added ability to attach documents directly from the input box. - **Improvements** - Refined UI animations and layout behaviors for attachment and document picker components. - Updated attachment and header views for clearer pluralization and display logic. - Improved selection indicators and search experience in the document picker. - Enhanced delegate handling for user actions across input and attachment components. - **Bug Fixes** - Fixed progress indicators and error alerts for failed document operations. - **Chores** - Updated project configuration for improved build and signing processes. --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../ios/App/App.xcodeproj/project.pbxproj | 46 ++-- .../App/AffineViewController+AIButton.swift | 17 +- .../Intelligents/Extension/DateTime.swift | 27 +++ .../IntelligentContext.swift | 45 +++- .../IntelligentContext/QLService.swift | 34 +++ .../AttachmentManagementController.swift | 8 +- ...tachmentManagementControllerDelegate.swift | 13 ++ .../MainViewController+Input.swift | 64 +++++- .../MainViewController.swift | 34 +++ .../Interface/Supplement/Animation.swift | 3 +- .../DocumentPickerView/DocumentItem.swift | 28 +++ .../DocumentPickerView.swift | 211 +++++++++++++----- .../DocumentPickerViewDelegate.swift | 12 + .../DocumentTableViewCell.swift | 21 +- .../FileAttachmentHeaderView.swift | 17 +- .../FileAttachmentHeaderViewDelegate.swift | 13 ++ .../ImageAttachmentBar.swift | 4 - .../ImageAttachmentBarDelegate.swift | 12 + .../View/InputBox/InputBox+Delegates.swift | 88 ++++++++ .../Interface/View/InputBox/InputBox.swift | 7 + .../View/InputBox/InputBoxDelegate.swift | 86 +------ .../View/InputBox/InputBoxFunctionBar.swift | 13 +- .../InputBoxFunctionBarDelegate.swift | 19 ++ .../ViewModel/DocumentAttachment.swift | 9 + .../View/MainHeaderView/MainHeaderView.swift | 6 - .../MainHeaderViewDelegate.swift | 14 ++ 26 files changed, 644 insertions(+), 207 deletions(-) create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/DateTime.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentManagementControllerDelegate.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentItem.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentPickerViewDelegate.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/FileAttachmentHeader/FileAttachmentHeaderViewDelegate.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageAttachmentBarDelegate.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox+Delegates.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBarDelegate.swift create mode 100644 packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderViewDelegate.swift diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj index b12a2b1397..2bba41391f 100644 --- a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj +++ b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj @@ -3,16 +3,17 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ 2E0DD47B57B994A104B25EED /* Pods_AFFiNE.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF48636D7DB5BEE00770FD9A /* Pods_AFFiNE.framework */; }; 5075136A2D1924C600AD60C0 /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507513692D1924C600AD60C0 /* RootViewController.swift */; }; 5075136E2D1925BC00AD60C0 /* IntelligentsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5075136D2D1925BC00AD60C0 /* IntelligentsPlugin.swift */; }; - 50802D612D112F8700694021 /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 50802D602D112F8700694021 /* Intelligents */; }; + 50802D612D112F8700694021 /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 50802D602D112F8700694021 /* Intelligents */; settings = {ATTRIBUTES = (Required, ); }; }; 50A285D72D112A5E000D5A6D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D62D112A5E000D5A6D /* Localizable.xcstrings */; }; 50A285D82D112A5E000D5A6D /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */; }; + 50E218302E0BE1A700EA4C6E /* Intelligents in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 50802D602D112F8700694021 /* Intelligents */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 50FF428A2D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FF42892D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift */; }; 50FF428C2D2E77CC0050AA83 /* AffineViewController+AIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FF428B2D2E77CC0050AA83 /* AffineViewController+AIButton.swift */; }; 9D52FC432D26CDBF00105D0A /* JSValueContainerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */; }; @@ -36,6 +37,20 @@ E93B276C2CED92B1001409B8 /* NavigationGesturePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93B276B2CED92B1001409B8 /* NavigationGesturePlugin.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXCopyFilesBuildPhase section */ + 50E218312E0BE1A700EA4C6E /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 50E218302E0BE1A700EA4C6E /* Intelligents in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 3256F4410D881A03FE77D092 /* Pods-AFFiNE.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AFFiNE.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AFFiNE/Pods-AFFiNE.debug.xcconfig"; sourceTree = ""; }; 5039CC962D1D42C700874F32 /* AffineGraphQL */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = AffineGraphQL; sourceTree = ""; }; @@ -77,8 +92,6 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ C45499AB2D140B5000E21978 /* NBStore */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); path = NBStore; sourceTree = ""; }; @@ -236,6 +249,7 @@ 504EC3011FED79650016851F /* Frameworks */, 504EC3021FED79650016851F /* Resources */, 9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */, + 50E218312E0BE1A700EA4C6E /* Embed Frameworks */, ); buildRules = ( ); @@ -326,9 +340,13 @@ ); inputFileListPaths = ( ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AFFiNE/Pods-AFFiNE-frameworks.sh\"\n"; @@ -514,11 +532,10 @@ baseConfigurationReference = 3256F4410D881A03FE77D092 /* Pods-AFFiNE.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_IDENTITY = "Apple Distribution: TOEVERYTHING PTE. LTD. (73YMMDVT2M)"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 12; - DEVELOPMENT_TEAM = 73YMMDVT2M; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 73YMMDVT2M; + DEVELOPMENT_TEAM = 964G86XT2P; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -534,8 +551,7 @@ OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = app.affine.pro; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "AppStore app.affine.pro"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "AppStore app.affine.pro"; + PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -552,11 +568,10 @@ baseConfigurationReference = E5E5070D1CA1200D4964D91F /* Pods-AFFiNE.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_IDENTITY = "Apple Distribution: TOEVERYTHING PTE. LTD. (73YMMDVT2M)"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 12; - DEVELOPMENT_TEAM = 73YMMDVT2M; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 73YMMDVT2M; + DEVELOPMENT_TEAM = 964G86XT2P; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -572,8 +587,7 @@ ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = app.affine.pro; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "AppStore app.affine.pro"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "AppStore app.affine.pro"; + PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; diff --git a/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift b/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift index f3e626ad7d..f2fc6614eb 100644 --- a/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift +++ b/packages/frontend/apps/ios/App/App/AffineViewController+AIButton.swift @@ -13,10 +13,21 @@ extension AFFiNEViewController: IntelligentsButtonDelegate { IntelligentContext.shared.webView = webView! button.beginProgress() - IntelligentContext.shared.preparePresent() { _ in + IntelligentContext.shared.preparePresent() { result in button.stopProgress() - let controller = IntelligentsController() - self.present(controller, animated: true) + switch result { + case .success(let success): + let controller = IntelligentsController() + self.present(controller, animated: true) + case .failure(let failure): + let alert = UIAlertController( + title: "Error", + message: failure.localizedDescription, + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + self.present(alert, animated: true) + } } } } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/DateTime.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/DateTime.swift new file mode 100644 index 0000000000..bbddd2e6b7 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Extension/DateTime.swift @@ -0,0 +1,27 @@ +// +// DateTime.swift +// Intelligents +// +// Created by 秋星桥 on 6/25/25. +// + +import AffineGraphQL +import Apollo +import ApolloAPI +import Foundation + +/// A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. +extension DateTime { +extension DateTime { + private static let formatter: DateFormatter = { + let fmt = DateFormatter() + fmt.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + fmt.timeZone = TimeZone(identifier: "UTC") + return fmt + }() + + var decoded: Date? { + return Self.formatter.date(from: self) + } +} +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentContext/IntelligentContext.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentContext/IntelligentContext.swift index 02b9c80683..310dccbee2 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentContext/IntelligentContext.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentContext/IntelligentContext.swift @@ -47,6 +47,17 @@ public class IntelligentContext { return tempDir.appendingPathComponent("IntelligentContext") }() + public enum IntelligentError: Error, LocalizedError { + case loginRequired(String) + + public var errorDescription: String? { + switch self { + case let .loginRequired(reason): + "Login required: \(reason)" + } + } + } + private init() {} public func preparePresent(_ completion: @escaping (Result) -> Void) { @@ -63,12 +74,28 @@ public class IntelligentContext { webViewGroup.wait() webViewMetadata = webViewMetadataResult - if let baseUrlString = webViewMetadataResult[.currentServerBaseUrl] as? String, - let url = URL(string: baseUrlString) - { - QLService.shared.setEndpoint(base: url) + // Check required webView metadata + guard let baseUrlString = webViewMetadataResult[.currentServerBaseUrl] as? String, + !baseUrlString.isEmpty, + let url = URL(string: baseUrlString) + else { + DispatchQueue.main.async { + completion(.failure(IntelligentError.loginRequired("Missing server base URL"))) + } + return } + guard let workspaceId = webViewMetadataResult[.currentWorkspaceId] as? String, + !workspaceId.isEmpty + else { + DispatchQueue.main.async { + completion(.failure(IntelligentError.loginRequired("Missing workspace ID"))) + } + return + } + + QLService.shared.setEndpoint(base: url) + let gqlGroup = DispatchGroup() var gqlMetadataResult: [QLMetadataKey: Any] = [:] gqlGroup.enter() @@ -79,6 +106,16 @@ public class IntelligentContext { gqlGroup.wait() qlMetadata = gqlMetadataResult + // Check required QL metadata + guard let userIdentifier = gqlMetadataResult[.userIdentifierKey] as? String, + !userIdentifier.isEmpty + else { + DispatchQueue.main.async { + completion(.failure(IntelligentError.loginRequired("Missing user identifier"))) + } + return + } + dumpMetadataContents() DispatchQueue.main.async { diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentContext/QLService.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentContext/QLService.swift index efc289070c..f080703d85 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentContext/QLService.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/IntelligentContext/QLService.swift @@ -89,4 +89,38 @@ public final class QLService { } } } + + public func searchDocuments( + workspaceId: String, + keyword: String, + limit: Int = 20, + completion: @escaping ([IndexerSearchDocsQuery.Data.Workspace.SearchDoc]) -> Void + ) { + let input = SearchDocsInput(keyword: keyword, limit: .some(limit)) + client.fetch(query: IndexerSearchDocsQuery(id: workspaceId, input: input)) { result in + switch result { + case let .success(graphQLResult): + completion(graphQLResult.data?.workspace.searchDocs ?? []) + case .failure: + completion([]) + } + } + } + + public func fetchRecentlyUpdatedDocs( + workspaceId: String, + first: Int = 20, + completion: @escaping ([GetRecentlyUpdatedDocsQuery.Data.Workspace.RecentlyUpdatedDocs.Edge.Node]) -> Void + ) { + let pagination = PaginationInput(first: .some(first)) + client.fetch(query: GetRecentlyUpdatedDocsQuery(workspaceId: workspaceId, pagination: pagination)) { result in + switch result { + case let .success(graphQLResult): + let docs = graphQLResult.data?.workspace.recentlyUpdatedDocs.edges.map(\.node) ?? [] + completion(docs) + case .failure: + completion([]) + } + } + } } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentManagementController.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentManagementController.swift index 844720f1b2..fb81870341 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentManagementController.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentManagementController.swift @@ -9,11 +9,6 @@ import SnapKit import Then import UIKit -protocol AttachmentManagementControllerDelegate: AnyObject { - func deleteFileAttachment(controller: AttachmentManagementController, _ attachment: FileAttachment) - func deleteDocumentAttachment(controller: AttachmentManagementController, _ attachment: DocumentAttachment) -} - class AttachmentManagementController: UINavigationController { private let _viewController: _AttachmentManagementController init(delegate: AttachmentManagementControllerDelegate) { @@ -171,17 +166,20 @@ private class AttachmentCell: UITableViewCell { let iconView = UIImageView().then { $0.contentMode = .scaleAspectFit $0.tintColor = .affineIconPrimary + $0.setContentCompressionResistancePriority(.required, for: .horizontal) } let titleLabel = UILabel().then { $0.textColor = .label $0.textAlignment = .left $0.font = .preferredFont(forTextStyle: .body) + $0.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) } let deleteButton = UIButton(type: .system).then { $0.setImage(UIImage(systemName: "xmark"), for: .normal) $0.tintColor = .affineIconPrimary + $0.setContentCompressionResistancePriority(.required, for: .horizontal) } var onDelete: (() -> Void)? diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentManagementControllerDelegate.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentManagementControllerDelegate.swift new file mode 100644 index 0000000000..8abccd6339 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentManagementControllerDelegate.swift @@ -0,0 +1,13 @@ +// +// AttachmentManagementControllerDelegate.swift +// Intelligents +// +// Created by 秋星桥 on 6/25/25. +// + +import Foundation + +protocol AttachmentManagementControllerDelegate: AnyObject { + func deleteFileAttachment(controller: AttachmentManagementController, _ attachment: FileAttachment) + func deleteDocumentAttachment(controller: AttachmentManagementController, _ attachment: DocumentAttachment) +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift index 6f12f88fc9..7c9c89b3dc 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController+Input.swift @@ -6,6 +6,7 @@ // import PhotosUI +import SnapKit import UIKit import UniformTypeIdentifiers @@ -37,12 +38,40 @@ extension MainViewController: InputBoxDelegate { present(documentPicker, animated: true) } - func inputBoxDidSelectEmbedDocs(_ inputBox: InputBox) { - print(#function, inputBox) + func inputBoxDidSelectEmbedDocs(_: InputBox) { + showDocumentPicker() } - func inputBoxDidSelectAttachment(_ inputBox: InputBox) { - print(#function, inputBox) + @objc func showDocumentPicker() { + view.endEditing(true) + terminateEditGesture.isEnabled = false + documentPickerView.snp.remakeConstraints { make in + make.bottom.equalTo(view.keyboardLayoutGuide.snp.top) + make.leading.trailing.equalToSuperview() + make.height.equalTo(300) + } + documentPickerHideDetector.isHidden = false + documentPickerView.setSelectedDocuments(inputBox.viewModel.documentAttachments) + + performWithAnimation(duration: 0.75) { + self.view.layoutIfNeeded() + } completion: { _ in + self.documentPickerView.updateDocumentsFromRecentDocs() + self.documentPickerView.searchTextField.becomeFirstResponder() + } + } + + @objc func hideDocumentPicker() { + terminateEditGesture.isEnabled = true + documentPickerView.snp.remakeConstraints { make in + make.top.equalTo(view.snp.bottom).offset(200) + make.leading.trailing.equalToSuperview() + make.height.equalTo(300) + } + documentPickerHideDetector.isHidden = true + performWithAnimation(duration: 0.75) { + self.view.layoutIfNeeded() + } } func inputBoxDidSend(_ inputBox: InputBox) { @@ -127,3 +156,30 @@ extension MainViewController: UIDocumentPickerDelegate { } } } + +// MARK: - DocumentPickerViewDelegate + +extension MainViewController: DocumentPickerViewDelegate { + func documentPickerView(_: DocumentPickerView, didSelectDocument document: DocumentItem) { + // Get current workspace ID + guard let workspaceId = IntelligentContext.shared.webViewMetadata[.currentWorkspaceId] as? String, + !workspaceId.isEmpty + else { + return + } + + // Create DocumentAttachment from DocumentItem + let documentAttachment = DocumentAttachment( + title: document.title, + workspaceID: workspaceId, + documentID: document.id, + updatedAt: document.updatedAt + ) + + // Add to InputBox + inputBox.addDocumentAttachment(documentAttachment) + + // Hide document picker + hideDocumentPicker() + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController.swift index 87afcd687d..d3fc723020 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/MainViewController/MainViewController.swift @@ -14,10 +14,23 @@ class MainViewController: UIViewController { $0.delegate = self } + lazy var documentPickerHideDetector = UIView().then { + $0.isUserInteractionEnabled = true + $0.isHidden = true + $0.addGestureRecognizer( + UITapGestureRecognizer(target: self, action: #selector(hideDocumentPicker)) + ) + } + + lazy var documentPickerView = DocumentPickerView().then { + $0.delegate = self + } + // MARK: - Properties private var cancellables = Set() private let intelligentContext = IntelligentContext.shared + var terminateEditGesture: UITapGestureRecognizer! // MARK: - Lifecycle @@ -32,6 +45,8 @@ class MainViewController: UIViewController { view.addSubview(headerView) view.addSubview(inputBox) + view.addSubview(documentPickerHideDetector) + view.addSubview(documentPickerView) headerView.snp.makeConstraints { make in make.top.equalTo(view.safeAreaLayoutGuide) @@ -42,11 +57,26 @@ class MainViewController: UIViewController { make.leading.trailing.equalToSuperview() make.bottom.equalTo(view.keyboardLayoutGuide.snp.top) } + + documentPickerHideDetector.snp.makeConstraints { make in + make.left.top.right.equalToSuperview() + make.bottom.equalTo(documentPickerView.snp.top) + } + documentPickerView.snp.makeConstraints { make in + make.top.equalTo(view.snp.bottom) + make.leading.trailing.equalToSuperview() + make.height.equalTo(500) + } + + view.isUserInteractionEnabled = true + terminateEditGesture = UITapGestureRecognizer(target: self, action: #selector(terminateEditing)) + view.addGestureRecognizer(terminateEditGesture) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController!.setNavigationBarHidden(true, animated: animated) + documentPickerView.updateDocumentsFromRecentDocs() DispatchQueue.main.async { self.inputBox.textView.becomeFirstResponder() } @@ -56,4 +86,8 @@ class MainViewController: UIViewController { super.viewWillDisappear(animated) navigationController!.setNavigationBarHidden(false, animated: animated) } + + @objc func terminateEditing() { + view.endEditing(true) + } } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Supplement/Animation.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Supplement/Animation.swift index bac7c40b81..4da95d324e 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Supplement/Animation.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Supplement/Animation.swift @@ -8,11 +8,12 @@ import UIKit func performWithAnimation( + duration: TimeInterval = 0.5, animations: @escaping () -> Void, completion: @escaping (Bool) -> Void = { _ in } ) { UIView.animate( - withDuration: 0.5, + withDuration: duration, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentItem.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentItem.swift new file mode 100644 index 0000000000..df4cd0d089 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentItem.swift @@ -0,0 +1,28 @@ +// +// DocumentItem.swift +// Intelligents +// +// Created by 秋星桥 on 6/25/25. +// + +import Foundation + +struct DocumentItem: Hashable { + let id: String + let title: String + let updatedAt: Date? + + init(id: String, title: String, updatedAt: Date? = nil) { + self.id = id + self.title = title + self.updatedAt = updatedAt + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + static func == (lhs: DocumentItem, rhs: DocumentItem) -> Bool { + lhs.id == rhs.id + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentPickerView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentPickerView.swift index 17d9eb951b..c11aa0e504 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentPickerView.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentPickerView.swift @@ -5,31 +5,33 @@ // Created by 秋星桥 on 6/24/25. // +import AffineGraphQL import SnapKit import Then import UIKit -protocol DocumentPickerViewDelegate: AnyObject { - func documentPickerView(_ view: DocumentPickerView, didSelectDocument document: DocumentItem) - func documentPickerView(_ view: DocumentPickerView, didSearchWithText text: String) -} - -struct DocumentItem { - let title: String - let icon: UIImage? -} - class DocumentPickerView: UIView { // MARK: - Properties weak var delegate: DocumentPickerViewDelegate? private var documents: [DocumentItem] = [] + private var selectedDocumentIds: Set = [] + private let updateQueue = DispatchQueue(label: "com.affine.documentpicker.update", qos: .userInitiated) + private var lastSearchKeyword: String = "" + + // MARK: - DiffableDataSource + + private enum Section { + case main + } + + private var dataSource: UITableViewDiffableDataSource! // MARK: - UI Components - private lazy var containerView = UIView().then { - $0.backgroundColor = .white + lazy var containerView = UIView().then { + $0.backgroundColor = .systemBackground $0.layer.cornerRadius = 10 $0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] $0.layer.shadowColor = UIColor.black.cgColor @@ -38,33 +40,37 @@ class DocumentPickerView: UIView { $0.layer.shadowOpacity = 0.07 } - private lazy var searchContainerView = UIView().then { - $0.backgroundColor = .white + lazy var searchContainerView = UIView().then { + $0.backgroundColor = .systemBackground $0.layer.cornerRadius = 10 $0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] $0.layer.borderWidth = 0.5 $0.layer.borderColor = UIColor(hex: 0xE6E6E6)?.cgColor } - private lazy var searchIconImageView = UIImageView().then { + lazy var searchIconImageView = UIImageView().then { $0.image = UIImage(systemName: "magnifyingglass") - $0.tintColor = UIColor(hex: 0x141414) + $0.tintColor = .affineIconPrimary $0.contentMode = .scaleAspectFit } - private lazy var searchTextField = UITextField().then { + lazy var searchTextField = UITextField().then { $0.placeholder = "Search documents..." $0.font = .systemFont(ofSize: 17, weight: .regular) - $0.textColor = UIColor(hex: 0x141414) + $0.textColor = .affineTextPrimary $0.backgroundColor = .clear $0.addTarget(self, action: #selector(searchTextChanged), for: .editingChanged) } + lazy var activityIndicator = UIActivityIndicatorView(style: .medium).then { + $0.hidesWhenStopped = true + $0.color = .affineIconPrimary + } + private lazy var tableView = UITableView().then { $0.backgroundColor = .white $0.separatorStyle = .none $0.delegate = self - $0.dataSource = self $0.register(DocumentTableViewCell.self, forCellReuseIdentifier: "DocumentCell") } @@ -72,7 +78,20 @@ class DocumentPickerView: UIView { init() { super.init(frame: .zero) - setupUI() + isUserInteractionEnabled = true + clipsToBounds = false // for shadow + backgroundColor = .systemBackground + + addSubview(containerView) + containerView.addSubview(searchContainerView) + containerView.addSubview(tableView) + + searchContainerView.addSubview(searchIconImageView) + searchContainerView.addSubview(searchTextField) + searchContainerView.addSubview(activityIndicator) + + setupConstraints() + setupDataSource() } @available(*, unavailable) @@ -82,24 +101,9 @@ class DocumentPickerView: UIView { // MARK: - Setup - private func setupUI() { - backgroundColor = .systemBackground - - addSubview(containerView) - containerView.addSubview(searchContainerView) - containerView.addSubview(tableView) - - searchContainerView.addSubview(searchIconImageView) - searchContainerView.addSubview(searchTextField) - - setupConstraints() - } - private func setupConstraints() { containerView.snp.makeConstraints { make in - make.center.equalToSuperview() - make.width.equalTo(393) - make.height.lessThanOrEqualTo(500) + make.edges.equalToSuperview() } searchContainerView.snp.makeConstraints { make in @@ -116,8 +120,14 @@ class DocumentPickerView: UIView { searchTextField.snp.makeConstraints { make in make.leading.equalTo(searchIconImageView.snp.trailing).offset(DocumentTableViewCell.spacing) + make.trailing.equalTo(activityIndicator.snp.leading).offset(-DocumentTableViewCell.cellInset) + make.centerY.equalToSuperview() + } + + activityIndicator.snp.makeConstraints { make in make.trailing.equalToSuperview().offset(-DocumentTableViewCell.cellInset) make.centerY.equalToSuperview() + make.width.height.equalTo(DocumentTableViewCell.iconSize) } tableView.snp.makeConstraints { make in @@ -126,15 +136,59 @@ class DocumentPickerView: UIView { } } + private func setupDataSource() { + dataSource = UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, document in + let cell = tableView.dequeueReusableCell(withIdentifier: "DocumentCell", for: indexPath) as! DocumentTableViewCell + let isSelected = self?.selectedDocumentIds.contains(document.id) ?? false + cell.configure(with: document, isSelected: isSelected) + return cell + } + } + // MARK: - Public Methods func updateDocuments(_ documents: [DocumentItem]) { - self.documents = documents - tableView.reloadData() + updateQueue.async { [weak self] in + guard let self else { return } - let tableHeight = min(CGFloat(documents.count) * 37.11 + 44, 500) - containerView.snp.updateConstraints { make in - make.height.lessThanOrEqualTo(tableHeight) + DispatchQueue.main.async { + self.documents = documents + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(documents) + self.dataSource.apply(snapshot, animatingDifferences: true) + } + } + } + + func updateDocumentsFromRecentDocs() { + guard searchTextField.text?.isEmpty ?? true else { + return + } + guard let workspaceId = IntelligentContext.shared.webViewMetadata[.currentWorkspaceId] as? String, + !workspaceId.isEmpty + else { + activityIndicator.stopAnimating() + return + } + activityIndicator.startAnimating() + QLService.shared.fetchRecentlyUpdatedDocs(workspaceId: workspaceId, first: 20) { [weak self] docs in + guard let self else { return } + DispatchQueue.main.async { + self.activityIndicator.stopAnimating() + self.updateDocuments(docs.compactMap { DocumentItem( + id: $0.id, + title: $0.title ?? "Unknown Document", + updatedAt: $0.updatedAt?.decoded + ) }) + } + } + } + + func setSelectedDocuments(_ documentAttachments: [DocumentAttachment]) { + selectedDocumentIds = Set(documentAttachments.map(\.documentID)) + DispatchQueue.main.async { [weak self] in + self?.tableView.reloadData() } } @@ -142,21 +196,56 @@ class DocumentPickerView: UIView { @objc private func searchTextChanged() { guard let text = searchTextField.text else { return } - delegate?.documentPickerView(self, didSearchWithText: text) - } -} -// MARK: - UITableViewDataSource + let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines) + lastSearchKeyword = trimmedText -extension DocumentPickerView: UITableViewDataSource { - func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { - documents.count + NSObject.cancelPreviousPerformRequests( + withTarget: self, + selector: #selector(performCloudIndexerDocumentSearch(_:)), + object: nil + ) + + if trimmedText.isEmpty { + DispatchQueue.main.async { [weak self] in + self?.activityIndicator.stopAnimating() + } + updateDocumentsFromRecentDocs() + } else { + DispatchQueue.main.async { [weak self] in + self?.activityIndicator.startAnimating() + } + perform(#selector(performCloudIndexerDocumentSearch(_:)), with: trimmedText, afterDelay: 0.25) + } } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "DocumentCell", for: indexPath) as! DocumentTableViewCell - cell.configure(with: documents[indexPath.row]) - return cell + @objc func performCloudIndexerDocumentSearch(_ keyword: String) { + let trimmedKeyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines) + + guard !trimmedKeyword.isEmpty else { return } + guard trimmedKeyword == lastSearchKeyword else { return } + guard let workspaceId = IntelligentContext.shared.webViewMetadata[.currentWorkspaceId] as? String, + !workspaceId.isEmpty + else { + activityIndicator.stopAnimating() + return + } + + QLService.shared.searchDocuments( + workspaceId: workspaceId, + keyword: trimmedKeyword, + limit: 20 + ) { [weak self] searchResults in + guard let self, lastSearchKeyword == trimmedKeyword else { + return + } + DispatchQueue.main.async { + self.activityIndicator.stopAnimating() + self.updateDocuments(searchResults.map { + .init(id: $0.docId, title: $0.title, updatedAt: $0.updatedAt.decoded) + }) + } + } } } @@ -169,7 +258,7 @@ extension DocumentPickerView: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - let document = documents[indexPath.row] + guard let document = dataSource.itemIdentifier(for: indexPath) else { return } delegate?.documentPickerView(self, didSelectDocument: document) } } @@ -185,14 +274,14 @@ extension DocumentPickerView: UITableViewDelegate { let view = DocumentPickerView() let mockDocuments = [ - DocumentItem(title: "Project Proposal.docx", icon: UIImage(systemName: "doc.text")), - DocumentItem(title: "Budget Analysis.xlsx", icon: UIImage(systemName: "tablecells")), - DocumentItem(title: "Meeting Notes.pdf", icon: UIImage(systemName: "doc.richtext")), - DocumentItem(title: "Design Guidelines.sketch", icon: UIImage(systemName: "paintbrush")), - DocumentItem(title: "Code Review.md", icon: UIImage(systemName: "doc.plaintext")), - DocumentItem(title: "User Research.pptx", icon: UIImage(systemName: "doc.on.doc")), - DocumentItem(title: "Technical Specification.docx", icon: UIImage(systemName: "doc.text")), - DocumentItem(title: "Database Schema.sql", icon: UIImage(systemName: "cylinder.split.1x2")), + DocumentItem(id: "1", title: "Project Proposal.docx"), + DocumentItem(id: "2", title: "Budget Analysis.xlsx"), + DocumentItem(id: "3", title: "Meeting Notes.pdf"), + DocumentItem(id: "4", title: "Design Guidelines.sketch"), + DocumentItem(id: "5", title: "Code Review.md"), + DocumentItem(id: "6", title: "User Research.pptx"), + DocumentItem(id: "7", title: "Technical Specification.docx"), + DocumentItem(id: "8", title: "Database Schema.sql"), ] view.updateDocuments(mockDocuments) diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentPickerViewDelegate.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentPickerViewDelegate.swift new file mode 100644 index 0000000000..2bd838f389 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentPickerViewDelegate.swift @@ -0,0 +1,12 @@ +// +// DocumentPickerViewDelegate.swift +// Intelligents +// +// Created by 秋星桥 on 6/25/25. +// + +import Foundation + +protocol DocumentPickerViewDelegate: AnyObject { + func documentPickerView(_ view: DocumentPickerView, didSelectDocument document: DocumentItem) +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentTableViewCell.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentTableViewCell.swift index 56802bdbfd..adb2b879cc 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentTableViewCell.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/DocumentPickerView/DocumentTableViewCell.swift @@ -25,6 +25,13 @@ class DocumentTableViewCell: UITableViewCell { $0.textAlignment = .left } + private lazy var checkmarkImageView = UIImageView().then { + $0.image = UIImage(systemName: "checkmark.circle.fill") + $0.tintColor = .systemBlue + $0.contentMode = .scaleAspectFit + $0.isHidden = true + } + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupUI() @@ -39,8 +46,11 @@ class DocumentTableViewCell: UITableViewCell { backgroundColor = .white selectionStyle = .none + contentView.clipsToBounds = true + contentView.addSubview(iconImageView) contentView.addSubview(titleLabel) + contentView.addSubview(checkmarkImageView) iconImageView.snp.makeConstraints { make in make.leading.equalToSuperview().offset(Self.cellInset) @@ -50,13 +60,20 @@ class DocumentTableViewCell: UITableViewCell { titleLabel.snp.makeConstraints { make in make.leading.equalTo(iconImageView.snp.trailing).offset(Self.spacing) + make.trailing.equalTo(checkmarkImageView.snp.leading).offset(-Self.spacing) + make.centerY.equalToSuperview() + } + + checkmarkImageView.snp.makeConstraints { make in make.trailing.equalToSuperview().offset(-Self.cellInset) make.centerY.equalToSuperview() + make.width.height.equalTo(Self.iconSize) } } - func configure(with document: DocumentItem) { - iconImageView.image = document.icon ?? UIImage(systemName: "doc.text") + func configure(with document: DocumentItem, isSelected: Bool = false) { + iconImageView.image = UIImage(systemName: "doc.text") titleLabel.text = document.title + checkmarkImageView.isHidden = !isSelected } } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/FileAttachmentHeader/FileAttachmentHeaderView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/FileAttachmentHeader/FileAttachmentHeaderView.swift index da3455f419..3505c63167 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/FileAttachmentHeader/FileAttachmentHeaderView.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/FileAttachmentHeader/FileAttachmentHeaderView.swift @@ -2,11 +2,6 @@ import SnapKit import Then import UIKit -protocol FileAttachmentHeaderViewDelegate: AnyObject { - func headerViewDidPickMore(_ headerView: FileAttachmentHeaderView) - func headerViewDidTapManagement(_ headerView: FileAttachmentHeaderView) -} - final class FileAttachmentHeaderView: UIView { // MARK: - Properties @@ -131,7 +126,17 @@ final class FileAttachmentHeaderView: UIView { // MARK: - Public Methods func updateContent(attachmentCount: Int, docsCount: Int) { - primaryLabel.text = "\(attachmentCount) attachment, \(docsCount) AFFiNE docs" + var components: [String] = [] + + if attachmentCount > 0 { + components.append("\(attachmentCount) attachment\(attachmentCount > 1 ? "s" : "")") + } + + if docsCount > 0 { + components.append("\(docsCount) AFFiNE doc\(docsCount > 1 ? "s" : "")") + } + + primaryLabel.text = components.joined(separator: ", ") } func setIconImage(_ image: UIImage?) { diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/FileAttachmentHeader/FileAttachmentHeaderViewDelegate.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/FileAttachmentHeader/FileAttachmentHeaderViewDelegate.swift new file mode 100644 index 0000000000..a660f78451 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/FileAttachmentHeader/FileAttachmentHeaderViewDelegate.swift @@ -0,0 +1,13 @@ +// +// FileAttachmentHeaderViewDelegate.swift +// Intelligents +// +// Created by 秋星桥 on 6/25/25. +// + +import Foundation + +protocol FileAttachmentHeaderViewDelegate: AnyObject { + func headerViewDidPickMore(_ headerView: FileAttachmentHeaderView) + func headerViewDidTapManagement(_ headerView: FileAttachmentHeaderView) +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageAttachmentBar.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageAttachmentBar.swift index 8519b6cecf..9b5dfeef99 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageAttachmentBar.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageAttachmentBar.swift @@ -9,10 +9,6 @@ import SnapKit import Then import UIKit -protocol ImageAttachmentBarDelegate: AnyObject { - func inputBoxImageBar(_ imageBar: ImageAttachmentBar, didRemoveImageWithId id: UUID) -} - class ImageAttachmentBar: UICollectionView { weak var imageBarDelegate: ImageAttachmentBarDelegate? diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageAttachmentBarDelegate.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageAttachmentBarDelegate.swift new file mode 100644 index 0000000000..18b77f91da --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/ImageAttachmentBar/ImageAttachmentBarDelegate.swift @@ -0,0 +1,12 @@ +// +// ImageAttachmentBarDelegate.swift +// Intelligents +// +// Created by 秋星桥 on 6/25/25. +// + +import Foundation + +protocol ImageAttachmentBarDelegate: AnyObject { + func inputBoxImageBar(_ imageBar: ImageAttachmentBar, didRemoveImageWithId id: UUID) +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox+Delegates.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox+Delegates.swift new file mode 100644 index 0000000000..e818f38e50 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox+Delegates.swift @@ -0,0 +1,88 @@ +// +// InputBox+Delegates.swift +// Intelligents +// +// Created by 秋星桥 on 6/18/25. +// + +import SwifterSwift +import UIKit + +extension InputBox: ImageAttachmentBarDelegate { + func inputBoxImageBar(_: ImageAttachmentBar, didRemoveImageWithId id: ImageAttachment.ID) { + performWithAnimation { [self] in + viewModel.removeImageAttachment(withId: id) + layoutIfNeeded() + } + } +} + +extension InputBox: FileAttachmentHeaderViewDelegate { + func headerViewDidPickMore(_: FileAttachmentHeaderView) { + delegate?.inputBoxDidSelectAttachFiles(self) + } + + func headerViewDidTapManagement(_: FileAttachmentHeaderView) { + let controller = AttachmentManagementController(delegate: self) + controller.set(fileAttachments: viewModel.fileAttachments) + controller.set(documentAttachments: viewModel.documentAttachments) + parentViewController?.present(controller, animated: true) + } +} + +extension InputBox: AttachmentManagementControllerDelegate { + func deleteFileAttachment(controller: AttachmentManagementController, _ attachment: FileAttachment) { + viewModel.removeFileAttachment(withId: attachment.id) + controller.set(fileAttachments: viewModel.fileAttachments) + layoutIfNeeded() + } + + func deleteDocumentAttachment(controller: AttachmentManagementController, _ attachment: DocumentAttachment) { + viewModel.removeDocumentAttachment(withId: attachment.id) + controller.set(documentAttachments: viewModel.documentAttachments) + layoutIfNeeded() + } +} + +extension InputBox: InputBoxFunctionBarDelegate { + func functionBarDidTapTakePhoto(_: InputBoxFunctionBar) { + delegate?.inputBoxDidSelectTakePhoto(self) + } + + func functionBarDidTapPhotoLibrary(_: InputBoxFunctionBar) { + delegate?.inputBoxDidSelectPhotoLibrary(self) + } + + func functionBarDidTapAttachFiles(_: InputBoxFunctionBar) { + delegate?.inputBoxDidSelectAttachFiles(self) + } + + func functionBarDidTapEmbedDocs(_: InputBoxFunctionBar) { + delegate?.inputBoxDidSelectEmbedDocs(self) + } + + func functionBarDidTapTool(_: InputBoxFunctionBar) { + viewModel.toggleTool() + } + + func functionBarDidTapNetwork(_: InputBoxFunctionBar) { + viewModel.toggleNetwork() + } + + func functionBarDidTapDeepThinking(_: InputBoxFunctionBar) { + viewModel.toggleDeepThinking() + } + + func functionBarDidTapSend(_: InputBoxFunctionBar) { + delegate?.inputBoxDidSend(self) + } +} + +extension InputBox: UITextViewDelegate { + func textViewDidChange(_ textView: UITextView) { + viewModel.updateText(textView.text ?? "") + delegate?.inputBoxTextDidChange(textView.text ?? "") + updatePlaceholderVisibility() + updateTextViewHeight() + } +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift index b8296e1ac2..8dfe62a157 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBox.swift @@ -291,6 +291,13 @@ class InputBox: UIView { } } + public func addDocumentAttachment(_ documentAttachment: DocumentAttachment) { + performWithAnimation { [self] in + viewModel.addDocumentAttachment(documentAttachment) + layoutIfNeeded() + } + } + public var inputBoxData: InputBoxData { viewModel.prepareSendData() } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxDelegate.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxDelegate.swift index 85177d3f17..5cc4471eb3 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxDelegate.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxDelegate.swift @@ -1,12 +1,11 @@ // -// InputBoxDelegate.swift +// InputBoxDelegate 2.swift // Intelligents // -// Created by 秋星桥 on 6/18/25. +// Created by 秋星桥 on 6/25/25. // -import SwifterSwift -import UIKit +import Foundation protocol InputBoxDelegate: AnyObject { func inputBoxDidSelectTakePhoto(_ inputBox: InputBox) @@ -16,82 +15,3 @@ protocol InputBoxDelegate: AnyObject { func inputBoxDidSend(_ inputBox: InputBox) func inputBoxTextDidChange(_ text: String) } - -extension InputBox: ImageAttachmentBarDelegate { - func inputBoxImageBar(_: ImageAttachmentBar, didRemoveImageWithId id: ImageAttachment.ID) { - performWithAnimation { [self] in - viewModel.removeImageAttachment(withId: id) - layoutIfNeeded() - } - } -} - -extension InputBox: FileAttachmentHeaderViewDelegate { - func headerViewDidPickMore(_: FileAttachmentHeaderView) { - delegate?.inputBoxDidSelectAttachFiles(self) - } - - func headerViewDidTapManagement(_: FileAttachmentHeaderView) { - let controller = AttachmentManagementController(delegate: self) - controller.set(fileAttachments: viewModel.fileAttachments) - controller.set(documentAttachments: viewModel.documentAttachments) - parentViewController?.present(controller, animated: true) - } -} - -extension InputBox: AttachmentManagementControllerDelegate { - func deleteFileAttachment(controller: AttachmentManagementController, _ attachment: FileAttachment) { - viewModel.removeFileAttachment(withId: attachment.id) - controller.set(fileAttachments: viewModel.fileAttachments) - layoutIfNeeded() - } - - func deleteDocumentAttachment(controller: AttachmentManagementController, _ attachment: DocumentAttachment) { - viewModel.removeDocumentAttachment(withId: attachment.id) - controller.set(documentAttachments: viewModel.documentAttachments) - layoutIfNeeded() - } -} - -extension InputBox: InputBoxFunctionBarDelegate { - func functionBarDidTapTakePhoto(_: InputBoxFunctionBar) { - delegate?.inputBoxDidSelectTakePhoto(self) - } - - func functionBarDidTapPhotoLibrary(_: InputBoxFunctionBar) { - delegate?.inputBoxDidSelectPhotoLibrary(self) - } - - func functionBarDidTapAttachFiles(_: InputBoxFunctionBar) { - delegate?.inputBoxDidSelectAttachFiles(self) - } - - func functionBarDidTapEmbedDocs(_: InputBoxFunctionBar) { - delegate?.inputBoxDidSelectEmbedDocs(self) - } - - func functionBarDidTapTool(_: InputBoxFunctionBar) { - viewModel.toggleTool() - } - - func functionBarDidTapNetwork(_: InputBoxFunctionBar) { - viewModel.toggleNetwork() - } - - func functionBarDidTapDeepThinking(_: InputBoxFunctionBar) { - viewModel.toggleDeepThinking() - } - - func functionBarDidTapSend(_: InputBoxFunctionBar) { - delegate?.inputBoxDidSend(self) - } -} - -extension InputBox: UITextViewDelegate { - func textViewDidChange(_ textView: UITextView) { - viewModel.updateText(textView.text ?? "") - delegate?.inputBoxTextDidChange(textView.text ?? "") - updatePlaceholderVisibility() - updateTextViewHeight() - } -} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift index 1e97df556b..5151eabe2c 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBar.swift @@ -2,17 +2,6 @@ import SnapKit import Then import UIKit -protocol InputBoxFunctionBarDelegate: AnyObject { - func functionBarDidTapTakePhoto(_ functionBar: InputBoxFunctionBar) - func functionBarDidTapPhotoLibrary(_ functionBar: InputBoxFunctionBar) - func functionBarDidTapAttachFiles(_ functionBar: InputBoxFunctionBar) - func functionBarDidTapEmbedDocs(_ functionBar: InputBoxFunctionBar) - func functionBarDidTapTool(_ functionBar: InputBoxFunctionBar) - func functionBarDidTapNetwork(_ functionBar: InputBoxFunctionBar) - func functionBarDidTapDeepThinking(_ functionBar: InputBoxFunctionBar) - func functionBarDidTapSend(_ functionBar: InputBoxFunctionBar) -} - private let unselectedColor: UIColor = .affineIconPrimary private let selectedColor: UIColor = .affineIconActivated @@ -164,7 +153,7 @@ class InputBoxFunctionBar: UIView { } let embedDocsAction = UIAction( - title: "Embed AFFINE Docs", + title: "Add AFFiNE Docs", image: UIImage.affinePage ) { [weak self] _ in guard let self else { return } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBarDelegate.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBarDelegate.swift new file mode 100644 index 0000000000..238b0590d6 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/InputBoxFunctionBarDelegate.swift @@ -0,0 +1,19 @@ +// +// InputBoxFunctionBarDelegate.swift +// Intelligents +// +// Created by 秋星桥 on 6/25/25. +// + +import Foundation + +protocol InputBoxFunctionBarDelegate: AnyObject { + func functionBarDidTapTakePhoto(_ functionBar: InputBoxFunctionBar) + func functionBarDidTapPhotoLibrary(_ functionBar: InputBoxFunctionBar) + func functionBarDidTapAttachFiles(_ functionBar: InputBoxFunctionBar) + func functionBarDidTapEmbedDocs(_ functionBar: InputBoxFunctionBar) + func functionBarDidTapTool(_ functionBar: InputBoxFunctionBar) + func functionBarDidTapNetwork(_ functionBar: InputBoxFunctionBar) + func functionBarDidTapDeepThinking(_ functionBar: InputBoxFunctionBar) + func functionBarDidTapSend(_ functionBar: InputBoxFunctionBar) +} diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/DocumentAttachment.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/DocumentAttachment.swift index 1f94bd79a9..d074bb57f8 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/DocumentAttachment.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/InputBox/ViewModel/DocumentAttachment.swift @@ -12,4 +12,13 @@ public struct DocumentAttachment: Identifiable, Equatable, Hashable, Codable { public var title: String = "" public var workspaceID: String = "" public var documentID: String = "" + public var updatedAt: Date? + + public init(id: UUID = .init(), title: String = "", workspaceID: String = "", documentID: String = "", updatedAt: Date? = nil) { + self.id = id + self.title = title + self.workspaceID = workspaceID + self.documentID = documentID + self.updatedAt = updatedAt + } } diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift index c9859acd13..6592051e3a 100644 --- a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderView.swift @@ -2,12 +2,6 @@ import SnapKit import Then import UIKit -protocol MainHeaderViewDelegate: AnyObject { - func mainHeaderViewDidTapClose() - func mainHeaderViewDidTapDropdown() - func mainHeaderViewDidTapMenu() -} - class MainHeaderView: UIView { weak var delegate: MainHeaderViewDelegate? diff --git a/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderViewDelegate.swift b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderViewDelegate.swift new file mode 100644 index 0000000000..3fa1230cf6 --- /dev/null +++ b/packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/View/MainHeaderView/MainHeaderViewDelegate.swift @@ -0,0 +1,14 @@ +// +// MainHeaderViewDelegate.swift +// Intelligents +// +// Created by 秋星桥 on 6/25/25. +// + +import Foundation + +protocol MainHeaderViewDelegate: AnyObject { + func mainHeaderViewDidTapClose() + func mainHeaderViewDidTapDropdown() + func mainHeaderViewDidTapMenu() +}