From 6d496b20885d0d28d74c2515b0d8f68faa41042e Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:50:21 +0800 Subject: [PATCH 01/26] chore: remove future result (#5960) * chore: remove future result * chore: fix test --- .github/workflows/tauri2_ci.yaml | 52 ++++++------ .../flowy-ai/src/local_ai/model_request.rs | 4 +- .../src/integrate/collab_interact.rs | 68 +++++++--------- .../flowy-core/src/integrate/trait_impls.rs | 55 ++++++------- .../rust-lib/flowy-folder-pub/src/cloud.rs | 7 +- .../src/af_cloud/impls/file_storage.rs | 80 ++++++++----------- .../src/supabase/file_storage/mod.rs | 2 - .../src/supabase/file_storage/plan.rs | 42 ---------- .../rust-lib/flowy-storage-pub/src/cloud.rs | 18 ++--- .../rust-lib/flowy-storage/src/manager.rs | 9 +-- .../src/services/collab_interact.rs | 30 +++---- 11 files changed, 145 insertions(+), 222 deletions(-) delete mode 100644 frontend/rust-lib/flowy-server/src/supabase/file_storage/plan.rs diff --git a/.github/workflows/tauri2_ci.yaml b/.github/workflows/tauri2_ci.yaml index 5bf8cbb09f..6bbb7928ee 100644 --- a/.github/workflows/tauri2_ci.yaml +++ b/.github/workflows/tauri2_ci.yaml @@ -20,34 +20,34 @@ concurrency: cancel-in-progress: true jobs: - tauri-build-self-hosted: - if: github.event.pull_request.head.repo.full_name == github.repository - runs-on: self-hosted - - steps: - - uses: actions/checkout@v4 - - name: install frontend dependencies - working-directory: frontend/appflowy_web_app - run: | - mkdir dist - pnpm install - cd src-tauri && cargo build - - - name: test and lint - working-directory: frontend/appflowy_web_app - run: | - pnpm run lint:tauri - - - uses: tauri-apps/tauri-action@v0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tauriScript: pnpm tauri - projectPath: frontend/appflowy_web_app - args: "--debug" + # tauri-build-self-hosted: + # if: github.event.pull_request.head.repo.full_name == github.repository + # runs-on: self-hosted + # + # steps: + # - uses: actions/checkout@v4 + # - name: install frontend dependencies + # working-directory: frontend/appflowy_web_app + # run: | + # mkdir dist + # pnpm install + # cd src-tauri && cargo build + # + # - name: test and lint + # working-directory: frontend/appflowy_web_app + # run: | + # pnpm run lint:tauri + # + # - uses: tauri-apps/tauri-action@v0 + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # with: + # tauriScript: pnpm tauri + # projectPath: frontend/appflowy_web_app + # args: "--debug" tauri-build-ubuntu: - if: github.event.pull_request.head.repo.full_name != github.repository + #if: github.event.pull_request.head.repo.full_name != github.repository runs-on: ubuntu-20.04 steps: diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/model_request.rs b/frontend/rust-lib/flowy-ai/src/local_ai/model_request.rs index 5d972de54f..c37a6f04ff 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/model_request.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/model_request.rs @@ -122,7 +122,7 @@ mod test { #[tokio::test] async fn retrieve_gpt4all_model_test() { for url in [ - "https://gpt4all.io/models/gguf/all-MiniLM-L6-v2-f16.gguf", + // "https://gpt4all.io/models/gguf/all-MiniLM-L6-v2-f16.gguf", "https://huggingface.co/second-state/All-MiniLM-L6-v2-Embedding-GGUF/resolve/main/all-MiniLM-L6-v2-Q3_K_L.gguf?download=true", // "https://huggingface.co/MaziyarPanahi/Mistral-7B-Instruct-v0.3-GGUF/resolve/main/Mistral-7B-Instruct-v0.3.Q4_K_M.gguf?download=true", ] { @@ -134,7 +134,7 @@ mod test { let cancel_token = CancellationToken::new(); let token = cancel_token.clone(); tokio::spawn(async move { - tokio::time::sleep(tokio::time::Duration::from_secs(30)).await; + tokio::time::sleep(tokio::time::Duration::from_secs(120)).await; token.cancel(); }); diff --git a/frontend/rust-lib/flowy-core/src/integrate/collab_interact.rs b/frontend/rust-lib/flowy-core/src/integrate/collab_interact.rs index 171fc20010..721c117cf3 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/collab_interact.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/collab_interact.rs @@ -7,7 +7,7 @@ use flowy_document::manager::DocumentManager; use flowy_document::reminder::{DocumentReminder, DocumentReminderAction}; use flowy_folder_pub::cloud::Error; use flowy_user::services::collab_interact::CollabInteract; -use lib_infra::future::FutureResult; +use lib_infra::async_trait::async_trait; pub struct CollabInteractImpl { #[allow(dead_code)] @@ -16,50 +16,42 @@ pub struct CollabInteractImpl { pub(crate) document_manager: Weak, } +#[async_trait] impl CollabInteract for CollabInteractImpl { - fn add_reminder(&self, reminder: Reminder) -> FutureResult<(), Error> { - let cloned_document_manager = self.document_manager.clone(); - FutureResult::new(async move { - if let Some(document_manager) = cloned_document_manager.upgrade() { - match DocumentReminder::try_from(reminder) { - Ok(reminder) => { - document_manager - .handle_reminder_action(DocumentReminderAction::Add { reminder }) - .await; - }, - Err(e) => tracing::error!("Failed to add reminder: {:?}", e), - } + async fn add_reminder(&self, reminder: Reminder) -> Result<(), Error> { + if let Some(document_manager) = self.document_manager.upgrade() { + match DocumentReminder::try_from(reminder) { + Ok(reminder) => { + document_manager + .handle_reminder_action(DocumentReminderAction::Add { reminder }) + .await; + }, + Err(e) => tracing::error!("Failed to add reminder: {:?}", e), } - Ok(()) - }) + } + Ok(()) } - fn remove_reminder(&self, reminder_id: &str) -> FutureResult<(), Error> { + async fn remove_reminder(&self, reminder_id: &str) -> Result<(), Error> { let reminder_id = reminder_id.to_string(); - let cloned_document_manager = self.document_manager.clone(); - FutureResult::new(async move { - if let Some(document_manager) = cloned_document_manager.upgrade() { - let action = DocumentReminderAction::Remove { reminder_id }; - document_manager.handle_reminder_action(action).await; - } - Ok(()) - }) + if let Some(document_manager) = self.document_manager.upgrade() { + let action = DocumentReminderAction::Remove { reminder_id }; + document_manager.handle_reminder_action(action).await; + } + Ok(()) } - fn update_reminder(&self, reminder: Reminder) -> FutureResult<(), Error> { - let cloned_document_manager = self.document_manager.clone(); - FutureResult::new(async move { - if let Some(document_manager) = cloned_document_manager.upgrade() { - match DocumentReminder::try_from(reminder) { - Ok(reminder) => { - document_manager - .handle_reminder_action(DocumentReminderAction::Update { reminder }) - .await; - }, - Err(e) => tracing::error!("Failed to update reminder: {:?}", e), - } + async fn update_reminder(&self, reminder: Reminder) -> Result<(), Error> { + if let Some(document_manager) = self.document_manager.upgrade() { + match DocumentReminder::try_from(reminder) { + Ok(reminder) => { + document_manager + .handle_reminder_action(DocumentReminderAction::Update { reminder }) + .await; + }, + Err(e) => tracing::error!("Failed to update reminder: {:?}", e), } - Ok(()) - }) + } + Ok(()) } } diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index 01ef68636f..cabc5cd97f 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -48,40 +48,39 @@ use crate::integrate::server::{Server, ServerProvider}; #[async_trait] impl StorageCloudService for ServerProvider { - fn get_object_url(&self, object_id: ObjectIdentity) -> FutureResult { - let server = self.get_server(); - FutureResult::new(async move { - let storage = server?.file_storage().ok_or(FlowyError::internal())?; - storage.get_object_url(object_id).await - }) + async fn get_object_url(&self, object_id: ObjectIdentity) -> Result { + let storage = self + .get_server()? + .file_storage() + .ok_or(FlowyError::internal())?; + storage.get_object_url(object_id).await } - fn put_object(&self, url: String, val: ObjectValue) -> FutureResult<(), FlowyError> { - let server = self.get_server(); - FutureResult::new(async move { - let storage = server?.file_storage().ok_or(FlowyError::internal())?; - storage.put_object(url, val).await - }) + async fn put_object(&self, url: String, val: ObjectValue) -> Result<(), FlowyError> { + let storage = self + .get_server()? + .file_storage() + .ok_or(FlowyError::internal())?; + storage.put_object(url, val).await } - fn delete_object(&self, url: &str) -> FutureResult<(), FlowyError> { - let server = self.get_server(); - let url = url.to_string(); - FutureResult::new(async move { - let storage = server?.file_storage().ok_or(FlowyError::internal())?; - storage.delete_object(&url).await - }) + async fn delete_object(&self, url: &str) -> Result<(), FlowyError> { + let storage = self + .get_server()? + .file_storage() + .ok_or(FlowyError::internal())?; + storage.delete_object(url).await } - fn get_object(&self, url: String) -> FutureResult { - let server = self.get_server(); - FutureResult::new(async move { - let storage = server?.file_storage().ok_or(FlowyError::internal())?; - storage.get_object(url).await - }) + async fn get_object(&self, url: String) -> Result { + let storage = self + .get_server()? + .file_storage() + .ok_or(FlowyError::internal())?; + storage.get_object(url).await } - fn get_object_url_v1( + async fn get_object_url_v1( &self, workspace_id: &str, parent_dir: &str, @@ -89,7 +88,9 @@ impl StorageCloudService for ServerProvider { ) -> FlowyResult { let server = self.get_server()?; let storage = server.file_storage().ok_or(FlowyError::internal())?; - storage.get_object_url_v1(workspace_id, parent_dir, file_id) + storage + .get_object_url_v1(workspace_id, parent_dir, file_id) + .await } async fn create_upload( diff --git a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs index c34de4a3af..4747a4eb51 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs @@ -1,12 +1,13 @@ +use crate::entities::{PublishInfoResponse, PublishPayload}; pub use anyhow::Error; use collab_entity::CollabType; pub use collab_folder::{Folder, FolderData, Workspace}; +use lib_infra::async_trait::async_trait; +use lib_infra::future::FutureResult; use uuid::Uuid; -use crate::entities::{PublishInfoResponse, PublishPayload}; -use lib_infra::future::FutureResult; - /// [FolderCloudService] represents the cloud service for folder. +#[async_trait] pub trait FolderCloudService: Send + Sync + 'static { /// Creates a new workspace for the user. /// Returns error if the cloud service doesn't support multiple workspaces diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs index 868559bcbb..d1ecd22449 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs @@ -1,11 +1,9 @@ +use crate::af_cloud::AFServer; use client_api::entity::{CompleteUploadRequest, CreateUploadRequest}; -use flowy_error::{FlowyError, FlowyResult}; +use flowy_error::FlowyError; use flowy_storage_pub::cloud::{ObjectIdentity, ObjectValue, StorageCloudService}; use flowy_storage_pub::storage::{CompletedPartRequest, CreateUploadResponse, UploadPartResponse}; use lib_infra::async_trait::async_trait; -use lib_infra::future::FutureResult; - -use crate::af_cloud::AFServer; pub struct AFCloudFileStorageServiceImpl(pub T); @@ -20,56 +18,44 @@ impl StorageCloudService for AFCloudFileStorageServiceImpl where T: AFServer, { - fn get_object_url(&self, object_id: ObjectIdentity) -> FutureResult { - let try_get_client = self.0.try_get_client(); - FutureResult::new(async move { - let file_name = format!("{}.{}", object_id.file_id, object_id.ext); - let client = try_get_client?; - let url = client.get_blob_url(&object_id.workspace_id, &file_name); - Ok(url) + async fn get_object_url(&self, object_id: ObjectIdentity) -> Result { + let file_name = format!("{}.{}", object_id.file_id, object_id.ext); + let url = self + .0 + .try_get_client()? + .get_blob_url(&object_id.workspace_id, &file_name); + Ok(url) + } + + async fn put_object(&self, url: String, file: ObjectValue) -> Result<(), FlowyError> { + let client = self.0.try_get_client()?; + client.put_blob(&url, file.raw, &file.mime).await?; + Ok(()) + } + + async fn delete_object(&self, url: &str) -> Result<(), FlowyError> { + self.0.try_get_client()?.delete_blob(url).await?; + Ok(()) + } + + async fn get_object(&self, url: String) -> Result { + let (mime, raw) = self.0.try_get_client()?.get_blob(&url).await?; + Ok(ObjectValue { + raw: raw.into(), + mime, }) } - fn put_object(&self, url: String, file: ObjectValue) -> FutureResult<(), FlowyError> { - let try_get_client = self.0.try_get_client(); - let file = file.clone(); - FutureResult::new(async move { - let client = try_get_client?; - client.put_blob(&url, file.raw, &file.mime).await?; - Ok(()) - }) - } - - fn delete_object(&self, url: &str) -> FutureResult<(), FlowyError> { - let url = url.to_string(); - let try_get_client = self.0.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - client.delete_blob(&url).await?; - Ok(()) - }) - } - - fn get_object(&self, url: String) -> FutureResult { - let try_get_client = self.0.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - let (mime, raw) = client.get_blob(&url).await?; - Ok(ObjectValue { - raw: raw.into(), - mime, - }) - }) - } - - fn get_object_url_v1( + async fn get_object_url_v1( &self, workspace_id: &str, parent_dir: &str, file_id: &str, - ) -> FlowyResult { - let client = self.0.try_get_client()?; - let url = client.get_blob_url_v1(workspace_id, parent_dir, file_id); + ) -> Result { + let url = self + .0 + .try_get_client()? + .get_blob_url_v1(workspace_id, parent_dir, file_id); Ok(url) } diff --git a/frontend/rust-lib/flowy-server/src/supabase/file_storage/mod.rs b/frontend/rust-lib/flowy-server/src/supabase/file_storage/mod.rs index ebfc707dcb..5da091c22c 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/file_storage/mod.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/file_storage/mod.rs @@ -1,7 +1,5 @@ pub use entities::*; -pub use plan::*; mod builder; pub mod core; mod entities; -pub mod plan; diff --git a/frontend/rust-lib/flowy-server/src/supabase/file_storage/plan.rs b/frontend/rust-lib/flowy-server/src/supabase/file_storage/plan.rs deleted file mode 100644 index 01482b1099..0000000000 --- a/frontend/rust-lib/flowy-server/src/supabase/file_storage/plan.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::sync::Weak; - -use parking_lot::RwLock; - -use flowy_error::FlowyError; -use flowy_storage_pub::cloud::{FileStoragePlan, StorageObject}; -use lib_infra::future::FutureResult; - -use crate::supabase::api::RESTfulPostgresServer; - -#[derive(Default)] -pub struct FileStoragePlanImpl { - #[allow(dead_code)] - uid: Weak>>, - #[allow(dead_code)] - postgrest: Option>, -} - -impl FileStoragePlanImpl { - pub fn new( - uid: Weak>>, - postgrest: Option>, - ) -> Self { - Self { uid, postgrest } - } -} - -impl FileStoragePlan for FileStoragePlanImpl { - fn storage_size(&self) -> FutureResult { - // 1 GB - FutureResult::new(async { Ok(1024 * 1024 * 1024) }) - } - - fn maximum_file_size(&self) -> FutureResult { - // 5 MB - FutureResult::new(async { Ok(5 * 1024 * 1024) }) - } - - fn check_upload_object(&self, _object: &StorageObject) -> FutureResult<(), FlowyError> { - FutureResult::new(async { Ok(()) }) - } -} diff --git a/frontend/rust-lib/flowy-storage-pub/src/cloud.rs b/frontend/rust-lib/flowy-storage-pub/src/cloud.rs index 816de17beb..7fb40c09fb 100644 --- a/frontend/rust-lib/flowy-storage-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-storage-pub/src/cloud.rs @@ -2,7 +2,6 @@ use crate::storage::{CompletedPartRequest, CreateUploadResponse, UploadPartRespo use async_trait::async_trait; use bytes::Bytes; use flowy_error::{FlowyError, FlowyResult}; -use lib_infra::future::FutureResult; use mime::Mime; #[async_trait] @@ -15,7 +14,7 @@ pub trait StorageCloudService: Send + Sync { /// # Returns /// - `Ok()` /// - `Err(Error)`: An error occurred during the operation. - fn get_object_url(&self, object_id: ObjectIdentity) -> FutureResult; + async fn get_object_url(&self, object_id: ObjectIdentity) -> Result; /// Creates a new storage object. /// @@ -25,7 +24,7 @@ pub trait StorageCloudService: Send + Sync { /// # Returns /// - `Ok()` /// - `Err(Error)`: An error occurred during the operation. - fn put_object(&self, url: String, object_value: ObjectValue) -> FutureResult<(), FlowyError>; + async fn put_object(&self, url: String, object_value: ObjectValue) -> Result<(), FlowyError>; /// Deletes a storage object by its URL. /// @@ -35,7 +34,7 @@ pub trait StorageCloudService: Send + Sync { /// # Returns /// - `Ok()` /// - `Err(Error)`: An error occurred during the operation. - fn delete_object(&self, url: &str) -> FutureResult<(), FlowyError>; + async fn delete_object(&self, url: &str) -> Result<(), FlowyError>; /// Fetches a storage object by its URL. /// @@ -45,8 +44,8 @@ pub trait StorageCloudService: Send + Sync { /// # Returns /// - `Ok(File)`: The returned file object. /// - `Err(Error)`: An error occurred during the operation. - fn get_object(&self, url: String) -> FutureResult; - fn get_object_url_v1( + async fn get_object(&self, url: String) -> Result; + async fn get_object_url_v1( &self, workspace_id: &str, parent_dir: &str, @@ -81,13 +80,6 @@ pub trait StorageCloudService: Send + Sync { ) -> Result<(), FlowyError>; } -pub trait FileStoragePlan: Send + Sync + 'static { - fn storage_size(&self) -> FutureResult; - fn maximum_file_size(&self) -> FutureResult; - - fn check_upload_object(&self, object: &StorageObject) -> FutureResult<(), FlowyError>; -} - pub struct ObjectIdentity { pub workspace_id: String, pub file_id: String, diff --git a/frontend/rust-lib/flowy-storage/src/manager.rs b/frontend/rust-lib/flowy-storage/src/manager.rs index fb96e18529..69455666e8 100644 --- a/frontend/rust-lib/flowy-storage/src/manager.rs +++ b/frontend/rust-lib/flowy-storage/src/manager.rs @@ -263,11 +263,10 @@ impl StorageService for StorageServiceImpl { let conn = self .user_service .sqlite_connection(self.user_service.user_id()?)?; - let url = self.cloud_service.get_object_url_v1( - &record.workspace_id, - &record.parent_dir, - &record.file_id, - )?; + let url = self + .cloud_service + .get_object_url_v1(&record.workspace_id, &record.parent_dir, &record.file_id) + .await?; let file_id = record.file_id.clone(); match insert_upload_file(conn, &record) { Ok(_) => { diff --git a/frontend/rust-lib/flowy-user/src/services/collab_interact.rs b/frontend/rust-lib/flowy-user/src/services/collab_interact.rs index d01d3cffde..1a54dd5f7e 100644 --- a/frontend/rust-lib/flowy-user/src/services/collab_interact.rs +++ b/frontend/rust-lib/flowy-user/src/services/collab_interact.rs @@ -1,25 +1,21 @@ use anyhow::Error; use collab_entity::reminder::Reminder; +use lib_infra::async_trait::async_trait; -use lib_infra::future::FutureResult; - +#[async_trait] pub trait CollabInteract: Send + Sync + 'static { - fn add_reminder(&self, reminder: Reminder) -> FutureResult<(), Error>; - fn remove_reminder(&self, reminder_id: &str) -> FutureResult<(), Error>; - fn update_reminder(&self, reminder: Reminder) -> FutureResult<(), Error>; + async fn add_reminder(&self, _reminder: Reminder) -> Result<(), Error> { + Ok(()) + } + async fn remove_reminder(&self, _reminder_id: &str) -> Result<(), Error> { + Ok(()) + } + async fn update_reminder(&self, _reminder: Reminder) -> Result<(), Error> { + Ok(()) + } } pub struct DefaultCollabInteract; -impl CollabInteract for DefaultCollabInteract { - fn add_reminder(&self, _reminder: Reminder) -> FutureResult<(), Error> { - FutureResult::new(async { Ok(()) }) - } - fn remove_reminder(&self, _reminder_id: &str) -> FutureResult<(), Error> { - FutureResult::new(async { Ok(()) }) - } - - fn update_reminder(&self, _reminder: Reminder) -> FutureResult<(), Error> { - FutureResult::new(async { Ok(()) }) - } -} +#[async_trait] +impl CollabInteract for DefaultCollabInteract {} From fa230907ca361e6d145b73db592af093a4c246c3 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:58:56 +0800 Subject: [PATCH 02/26] fix: refresh local ai state when opening workspace (#5961) * chore: fix local ai state when open other workspace * chore: fix duplicate message --- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 5 ++ frontend/rust-lib/flowy-ai/src/chat.rs | 50 +++++++++---------- .../rust-lib/flowy-ai/src/event_handler.rs | 2 +- .../flowy-ai/src/local_ai/local_llm_chat.rs | 40 ++++++++++----- .../rust-lib/flowy-core/src/integrate/user.rs | 3 +- 5 files changed, 58 insertions(+), 42 deletions(-) diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index a8b9f5fc8f..6adbd69813 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -66,6 +66,11 @@ impl AIManager { } } + pub async fn initialize(&self, _workspace_id: &str) -> Result<(), FlowyError> { + self.local_ai_controller.refresh().await?; + Ok(()) + } + pub async fn open_chat(&self, chat_id: &str) -> Result<(), FlowyError> { trace!("open chat: {}", chat_id); self.chats.entry(chat_id.to_string()).or_insert_with(|| { diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 667e072ea2..1d6af09503 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -140,11 +140,7 @@ impl Chat { let _ = question_sink.send(StreamMessage::Done.to_string()).await; // Save message to disk - save_chat_message( - self.user_service.sqlite_connection(uid)?, - &self.chat_id, - vec![question.clone()], - )?; + save_and_notify_message(uid, &self.chat_id, &self.user_service, question.clone())?; let stop_stream = self.stop_stream.clone(); let chat_id = self.chat_id.clone(); @@ -222,7 +218,7 @@ impl Chat { let answer = cloud_service .create_answer(&workspace_id, &chat_id, &content, question_id, metadata) .await?; - Self::save_answer(uid, &chat_id, &user_service, answer)?; + save_and_notify_message(uid, &chat_id, &user_service, answer)?; Ok::<(), FlowyError>(()) }); @@ -230,26 +226,6 @@ impl Chat { Ok(question_pb) } - fn save_answer( - uid: i64, - chat_id: &str, - user_service: &Arc, - answer: ChatMessage, - ) -> Result<(), FlowyError> { - trace!("[Chat] save answer: answer={:?}", answer); - save_chat_message( - user_service.sqlite_connection(uid)?, - chat_id, - vec![answer.clone()], - )?; - let pb = ChatMessagePB::from(answer); - make_notification(chat_id, ChatNotification::DidReceiveChatMessage) - .payload(pb) - .send(); - - Ok(()) - } - /// Load chat messages for a given `chat_id`. /// /// 1. When opening a chat: @@ -453,7 +429,7 @@ impl Chat { .get_answer(&workspace_id, &self.chat_id, question_message_id) .await?; - Self::save_answer(self.uid, &self.chat_id, &self.user_service, answer.clone())?; + save_and_notify_message(self.uid, &self.chat_id, &self.user_service, answer.clone())?; let pb = ChatMessagePB::from(answer); Ok(pb) } @@ -581,3 +557,23 @@ impl StringBuffer { std::mem::take(&mut self.content) } } + +pub(crate) fn save_and_notify_message( + uid: i64, + chat_id: &str, + user_service: &Arc, + message: ChatMessage, +) -> Result<(), FlowyError> { + trace!("[Chat] save answer: answer={:?}", message); + save_chat_message( + user_service.sqlite_connection(uid)?, + chat_id, + vec![message.clone()], + )?; + let pb = ChatMessagePB::from(message); + make_notification(chat_id, ChatNotification::DidReceiveChatMessage) + .payload(pb) + .send(); + + Ok(()) +} diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index 99933456f5..18efb66887 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -150,7 +150,7 @@ pub(crate) async fn refresh_local_ai_info_handler( ai_manager: AFPluginState>, ) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; - let model_info = ai_manager.local_ai_controller.refresh().await; + let model_info = ai_manager.local_ai_controller.refresh_model_info().await; if model_info.is_err() { if let Some(llm_model) = ai_manager.local_ai_controller.get_current_model() { let model_info = LLMModelInfo { diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs b/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs index cf69120d80..2ab15b94dc 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs @@ -49,6 +49,7 @@ pub struct LocalAIController { local_ai_resource: Arc, current_chat_id: Mutex>, store_preferences: Arc, + user_service: Arc, } impl Deref for LocalAIController { @@ -74,7 +75,11 @@ impl LocalAIController { }; let (tx, mut rx) = tokio::sync::mpsc::channel(1); - let llm_res = Arc::new(LocalAIResourceController::new(user_service, res_impl, tx)); + let llm_res = Arc::new(LocalAIResourceController::new( + user_service.clone(), + res_impl, + tx, + )); let current_chat_id = Mutex::new(None); let mut running_state_rx = local_ai.subscribe_running_state(); @@ -101,6 +106,7 @@ impl LocalAIController { local_ai_resource: llm_res, current_chat_id, store_preferences, + user_service, }; let rag_enabled = this.is_rag_enabled(); @@ -142,7 +148,13 @@ impl LocalAIController { this } - pub async fn refresh(&self) -> FlowyResult { + pub async fn refresh(&self) -> FlowyResult<()> { + let is_enabled = self.is_enabled(); + self.enable_chat_plugin(is_enabled).await?; + Ok(()) + } + + pub async fn refresh_model_info(&self) -> FlowyResult { self.local_ai_resource.refresh_llm_resource().await } @@ -158,10 +170,16 @@ impl LocalAIController { /// Indicate whether the local AI is enabled. pub fn is_enabled(&self) -> bool { - self - .store_preferences - .get_bool(APPFLOWY_LOCAL_AI_ENABLED) - .unwrap_or(true) + if let Ok(key) = self.local_ai_enabled_key() { + self.store_preferences.get_bool(&key).unwrap_or(true) + } else { + false + } + } + + fn local_ai_enabled_key(&self) -> FlowyResult { + let workspace_id = self.user_service.workspace_id()?; + Ok(format!("{}:{}", APPFLOWY_LOCAL_AI_ENABLED, workspace_id)) } /// Indicate whether the local AI chat is enabled. In the future, we can support multiple @@ -297,13 +315,9 @@ impl LocalAIController { } pub async fn toggle_local_ai(&self) -> FlowyResult { - let enabled = !self - .store_preferences - .get_bool(APPFLOWY_LOCAL_AI_ENABLED) - .unwrap_or(true); - self - .store_preferences - .set_bool(APPFLOWY_LOCAL_AI_ENABLED, enabled)?; + let key = self.local_ai_enabled_key()?; + let enabled = !self.store_preferences.get_bool(&key).unwrap_or(true); + self.store_preferences.set_bool(&key, enabled)?; // when enable local ai. we need to check if chat is enabled, if enabled, we need to init chat plugin // otherwise, we need to destroy the plugin diff --git a/frontend/rust-lib/flowy-core/src/integrate/user.rs b/frontend/rust-lib/flowy-core/src/integrate/user.rs index f9bfb46280..f84a4ec6a8 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/user.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/user.rs @@ -182,13 +182,14 @@ impl UserStatusCallback for UserStatusCallbackImpl { Ok(()) } - async fn open_workspace(&self, user_id: i64, _user_workspace: &UserWorkspace) -> FlowyResult<()> { + async fn open_workspace(&self, user_id: i64, user_workspace: &UserWorkspace) -> FlowyResult<()> { self .folder_manager .initialize_with_workspace_id(user_id) .await?; self.database_manager.initialize(user_id).await?; self.document_manager.initialize(user_id).await?; + self.ai_manager.initialize(&user_workspace.id).await?; Ok(()) } From 8935b7158c53527f98b4598e734580cebd0e9cea Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Wed, 14 Aug 2024 19:44:15 +0800 Subject: [PATCH 03/26] chore: remove workspac id in user profile (#5962) * chore: remove workspac id in user profile * chore: fix test * chore: clippy * chore: clippy * chore: fix cloud test * chore: fix checklist test --- .../cloud/user_setting_sync_test.dart | 41 +++++++++---------- .../desktop/database/database_cell_test.dart | 8 ++-- .../integration_test/shared/settings.dart | 2 +- .../ai/local_ai_on_boarding_bloc.dart | 15 ++++--- .../settings/ai/settings_ai_bloc.dart | 15 ++++--- .../setting_ai_view/settings_ai_view.dart | 15 ++++--- .../settings/settings_dialog.dart | 6 ++- .../tests/user/af_cloud_test/member_test.rs | 27 ++++++------ .../user/af_cloud_test/workspace_test.rs | 5 +-- .../user/local_test/user_profile_test.rs | 1 - frontend/rust-lib/flowy-ai/src/chat.rs | 6 +-- .../flowy-user/src/entities/user_profile.rs | 6 +-- 12 files changed, 76 insertions(+), 71 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart b/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart index 8bd9cffc03..d0377908c3 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart @@ -47,31 +47,28 @@ void main() { await tester.openSettingsPage(SettingsPage.account); await tester.enterUserName(name); - await tester.tapEscButton(); - - // wait 2 seconds for the sync to finish await tester.pumpAndSettle(const Duration(seconds: 6)); - }); + await tester.logout(); - - testWidgets('get user icon and name from server', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - email: email, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - await tester.pumpAndSettle(); - - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.account); - - // Verify name - final profileSetting = - tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting; - - expect(profileSetting.name, name); + await tester.pumpAndSettle(const Duration(seconds: 2)); }); }); + testWidgets('get user icon and name from server', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + email: email, + ); + await tester.tapGoogleLoginInButton(); + await tester.expectToSeeHomePageWithGetStartedPage(); + await tester.pumpAndSettle(); + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.account); + + // Verify name + final profileSetting = + tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting; + + expect(profileSetting.name, name); + }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart index cb8338fbb6..66ef6cfd95 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart @@ -461,22 +461,22 @@ void main() { tester.assertChecklistEditorVisible(visible: true); // create a new task with enter - await tester.createNewChecklistTask(name: "task 0", enter: true); + await tester.createNewChecklistTask(name: "task 1", enter: true); // assert that the task is displayed tester.assertChecklistTaskInEditor( index: 0, - name: "task 0", + name: "task 1", isChecked: false, ); // update the task's name - await tester.renameChecklistTask(index: 0, name: "task 1"); + await tester.renameChecklistTask(index: 0, name: "task 11"); // assert that the task's name is updated tester.assertChecklistTaskInEditor( index: 0, - name: "task 1", + name: "task 11", isChecked: false, ); diff --git a/frontend/appflowy_flutter/integration_test/shared/settings.dart b/frontend/appflowy_flutter/integration_test/shared/settings.dart index 9dec3209a4..20193dfd9b 100644 --- a/frontend/appflowy_flutter/integration_test/shared/settings.dart +++ b/frontend/appflowy_flutter/integration_test/shared/settings.dart @@ -80,7 +80,7 @@ extension AppFlowySettings on WidgetTester { of: find.byType(UserProfileSetting), matching: find.byFlowySvg(FlowySvgs.edit_s), ); - await tap(editUsernameFinder); + await tap(editUsernameFinder, warnIfMissed: false); await pumpAndSettle(); final userNameFinder = find.descendant( diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart index 3a56e93a32..902cb948b8 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart @@ -15,8 +15,11 @@ part 'local_ai_on_boarding_bloc.freezed.dart'; class LocalAIOnBoardingBloc extends Bloc { - LocalAIOnBoardingBloc(this.userProfile) - : super(const LocalAIOnBoardingState()) { + LocalAIOnBoardingBloc( + this.userProfile, + this.member, + this.workspaceId, + ) : super(const LocalAIOnBoardingState()) { _userService = UserBackendService(userId: userProfile.id); _successListenable = getIt(); _successListenable.addListener(_onPaymentSuccessful); @@ -36,6 +39,8 @@ class LocalAIOnBoardingBloc } final UserProfilePB userProfile; + final WorkspaceMemberPB member; + final String workspaceId; late final IUserBackendService _userService; late final SubscriptionSuccessListenable _successListenable; @@ -48,7 +53,7 @@ class LocalAIOnBoardingBloc addSubscription: (plan) async { emit(state.copyWith(isLoading: true)); final result = await _userService.createSubscription( - userProfile.workspaceId, + workspaceId, plan, ); @@ -72,7 +77,7 @@ class LocalAIOnBoardingBloc ); }, (err) { - Log.error("Failed to get subscription plans: $err"); + Log.warn("Failed to get subscription plans: $err"); }, ); }, @@ -86,7 +91,7 @@ class LocalAIOnBoardingBloc } void _loadSubscriptionPlans() { - final payload = UserWorkspaceIdPB()..workspaceId = userProfile.workspaceId; + final payload = UserWorkspaceIdPB()..workspaceId = workspaceId; UserEventGetWorkspaceSubscriptionInfo(payload).send().then((result) { if (!isClosed) { add(LocalAIOnBoardingEvent.didGetSubscriptionPlans(result)); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart index fd07db4d48..af0b390ebd 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart @@ -11,8 +11,11 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'settings_ai_bloc.freezed.dart'; class SettingsAIBloc extends Bloc { - SettingsAIBloc(this.userProfile, WorkspaceMemberPB? member) - : _userListener = UserListener(userProfile: userProfile), + SettingsAIBloc( + this.userProfile, + this.workspaceId, + WorkspaceMemberPB? member, + ) : _userListener = UserListener(userProfile: userProfile), _userService = UserBackendService(userId: userProfile.id), super(SettingsAIState(userProfile: userProfile, member: member)) { _dispatch(); @@ -36,6 +39,7 @@ class SettingsAIBloc extends Bloc { final UserListener _userListener; final UserProfilePB userProfile; final UserBackendService _userService; + final String workspaceId; @override Future close() async { @@ -92,7 +96,7 @@ class SettingsAIBloc extends Bloc { AIModelPB? model, }) { final payload = UpdateUserWorkspaceSettingPB( - workspaceId: userProfile.workspaceId, + workspaceId: workspaceId, ); if (disableSearchIndexing != null) { payload.disableSearchIndexing = disableSearchIndexing; @@ -112,7 +116,7 @@ class SettingsAIBloc extends Bloc { ); void _loadUserWorkspaceSetting() { - final payload = UserWorkspaceIdPB(workspaceId: userProfile.workspaceId); + final payload = UserWorkspaceIdPB(workspaceId: workspaceId); UserEventGetWorkspaceSetting(payload).send().then((result) { result.fold((settings) { if (!isClosed) { @@ -133,7 +137,8 @@ class SettingsAIEvent with _$SettingsAIEvent { ) = _DidLoadWorkspaceSetting; const factory SettingsAIEvent.toggleAISearch() = _toggleAISearch; - const factory SettingsAIEvent.refreshMember(WorkspaceMemberPB member) = _RefreshMember; + const factory SettingsAIEvent.refreshMember(WorkspaceMemberPB member) = + _RefreshMember; const factory SettingsAIEvent.selectModel(AIModelPB model) = _SelectAIModel; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart index c5e160fb03..48319e1b4a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart @@ -6,7 +6,6 @@ import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/m import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -40,15 +39,17 @@ class SettingsAIView extends StatelessWidget { super.key, required this.userProfile, required this.member, + required this.workspaceId, }); final UserProfilePB userProfile; final WorkspaceMemberPB? member; + final String workspaceId; @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => SettingsAIBloc(userProfile, member) + create: (_) => SettingsAIBloc(userProfile, workspaceId, member) ..add(const SettingsAIEvent.started()), child: BlocBuilder( builder: (context, state) { @@ -63,6 +64,7 @@ class SettingsAIView extends StatelessWidget { _LocalAIOnBoarding( userProfile: userProfile, member: state.member!, + workspaceId: workspaceId, ), ); } @@ -127,9 +129,11 @@ class _LocalAIOnBoarding extends StatelessWidget { const _LocalAIOnBoarding({ required this.userProfile, required this.member, + required this.workspaceId, }); final UserProfilePB userProfile; final WorkspaceMemberPB member; + final String workspaceId; @override Widget build(BuildContext context) { @@ -137,12 +141,13 @@ class _LocalAIOnBoarding extends StatelessWidget { return BillingGateGuard( builder: (context) { return BlocProvider( - create: (context) => LocalAIOnBoardingBloc(userProfile) - ..add(const LocalAIOnBoardingEvent.started()), + create: (context) => + LocalAIOnBoardingBloc(userProfile, member, workspaceId) + ..add(const LocalAIOnBoardingEvent.started()), child: BlocBuilder( builder: (context, state) { // Show the local AI settings if the user has purchased the AI Local plan - if (kDebugMode || state.isPurchaseAILocal) { + if (state.isPurchaseAILocal) { return const LocalAISetting(); } else { if (member.role.isOwner) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 5a3905ed21..c633aa3577 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -120,7 +120,11 @@ class SettingsDialog extends StatelessWidget { return const SettingsShortcutsView(); case SettingsPage.ai: if (user.authenticator == AuthenticatorPB.AppFlowyCloud) { - return SettingsAIView(userProfile: user, member: member); + return SettingsAIView( + userProfile: user, + member: member, + workspaceId: workspaceId, + ); } else { return const AIFeatureOnlySupportedWhenUsingAppFlowyCloud(); } diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/member_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/member_test.rs index 9eba50b404..ca962ae726 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/member_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/member_test.rs @@ -38,19 +38,18 @@ async fn af_cloud_add_workspace_member_test() { user_localhost_af_cloud().await; let test_1 = EventIntegrationTest::new().await; let user_1 = test_1.af_cloud_sign_up().await; + let workspace_id_1 = test_1.get_current_workspace().await.id; let test_2 = EventIntegrationTest::new().await; let user_2 = test_2.af_cloud_sign_up().await; - let members = test_1.get_workspace_members(&user_1.workspace_id).await; + let members = test_1.get_workspace_members(&workspace_id_1).await; assert_eq!(members.len(), 1); assert_eq!(members[0].email, user_1.email); - test_1 - .add_workspace_member(&user_1.workspace_id, &test_2) - .await; + test_1.add_workspace_member(&workspace_id_1, &test_2).await; - let members = test_1.get_workspace_members(&user_1.workspace_id).await; + let members = test_1.get_workspace_members(&workspace_id_1).await; assert_eq!(members.len(), 2); assert_eq!(members[0].email, user_1.email); assert_eq!(members[1].email, user_2.email); @@ -61,19 +60,18 @@ async fn af_cloud_delete_workspace_member_test() { user_localhost_af_cloud().await; let test_1 = EventIntegrationTest::new().await; let user_1 = test_1.af_cloud_sign_up().await; + let workspace_id_1 = test_1.get_current_workspace().await.id; let test_2 = EventIntegrationTest::new().await; let user_2 = test_2.af_cloud_sign_up().await; - test_1 - .add_workspace_member(&user_1.workspace_id, &test_2) - .await; + test_1.add_workspace_member(&workspace_id_1, &test_2).await; test_1 - .delete_workspace_member(&user_1.workspace_id, &user_2.email) + .delete_workspace_member(&workspace_id_1, &user_2.email) .await; - let members = test_1.get_workspace_members(&user_1.workspace_id).await; + let members = test_1.get_workspace_members(&workspace_id_1).await; assert_eq!(members.len(), 1); assert_eq!(members[0].email, user_1.email); } @@ -82,21 +80,20 @@ async fn af_cloud_delete_workspace_member_test() { async fn af_cloud_leave_workspace_test() { user_localhost_af_cloud().await; let test_1 = EventIntegrationTest::new().await; - let user_1 = test_1.af_cloud_sign_up().await; + test_1.af_cloud_sign_up().await; + let workspace_id_1 = test_1.get_current_workspace().await.id; let test_2 = EventIntegrationTest::new().await; let user_2 = test_2.af_cloud_sign_up().await; - test_1 - .add_workspace_member(&user_1.workspace_id, &test_2) - .await; + test_1.add_workspace_member(&workspace_id_1, &test_2).await; // test_2 should have 2 workspace let workspaces = get_synced_workspaces(&test_2, user_2.id).await; assert_eq!(workspaces.len(), 2); // user_2 leaves the workspace - test_2.leave_workspace(&user_1.workspace_id).await; + test_2.leave_workspace(&workspace_id_1).await; // user_2 should have 1 workspace let workspaces = get_synced_workspaces(&test_2, user_2.id).await; diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs index c35224ea99..03c0930f74 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs @@ -163,9 +163,6 @@ async fn af_cloud_different_open_same_workspace_test() { let owner_profile = test_runner.af_cloud_sign_up().await; let shared_workspace_id = test_runner.get_current_workspace().await.id.clone(); - // Verify that the workspace ID from the profile matches the current session's workspace ID. - assert_eq!(shared_workspace_id, owner_profile.workspace_id); - // Define the number of additional clients let num_clients = 5; let mut clients = Vec::new(); @@ -183,7 +180,7 @@ async fn af_cloud_different_open_same_workspace_test() { } test_runner - .add_workspace_member(&owner_profile.workspace_id, &client) + .add_workspace_member(&shared_workspace_id, &client) .await; clients.push((client, client_profile)); } diff --git a/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs b/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs index 798054dccf..00df14e8e1 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs @@ -26,7 +26,6 @@ async fn anon_user_profile_get() { assert_eq!(user_profile.id, user.id); assert_eq!(user_profile.openai_key, user.openai_key); assert_eq!(user_profile.stability_ai_key, user.stability_ai_key); - assert_eq!(user_profile.workspace_id, user.workspace_id); assert_eq!(user_profile.authenticator, AuthenticatorPB::Local); } diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 1d6af09503..494a8f3980 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -351,7 +351,7 @@ impl Chat { { Ok(resp) => { // Save chat messages to local disk - if let Err(err) = save_chat_message( + if let Err(err) = save_chat_message_disk( user_service.sqlite_connection(uid)?, &chat_id, resp.messages.clone(), @@ -503,7 +503,7 @@ impl Chat { } } -fn save_chat_message( +fn save_chat_message_disk( conn: DBConnection, chat_id: &str, messages: Vec, @@ -565,7 +565,7 @@ pub(crate) fn save_and_notify_message( message: ChatMessage, ) -> Result<(), FlowyError> { trace!("[Chat] save answer: answer={:?}", message); - save_chat_message( + save_chat_message_disk( user_service.sqlite_connection(uid)?, chat_id, vec![message.clone()], diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index a637cef6d2..ffb22b976f 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -53,12 +53,9 @@ pub struct UserProfilePB { pub encryption_type: EncryptionTypePB, #[pb(index = 10)] - pub workspace_id: String, - - #[pb(index = 11)] pub stability_ai_key: String, - #[pb(index = 12)] + #[pb(index = 11)] pub ai_model: AIModelPB, } @@ -90,7 +87,6 @@ impl From for UserProfilePB { authenticator: user_profile.authenticator.into(), encryption_sign, encryption_type: encryption_ty, - workspace_id: user_profile.workspace_id, stability_ai_key: user_profile.stability_ai_key, ai_model: AIModelPB::from_str(&user_profile.ai_model).unwrap_or_default(), } From 7eb8ea347d5a7680ea6264d61359c51feef00cb6 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Thu, 15 Aug 2024 07:44:32 +0800 Subject: [PATCH 04/26] fix: local ai toggle (#5968) * chore: fix disable local ai * chore: do not index file --- .../pages/setting_ai_view/settings_ai_view.dart | 3 ++- .../rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart index 48319e1b4a..0c3965c731 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart @@ -6,6 +6,7 @@ import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/m import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -147,7 +148,7 @@ class _LocalAIOnBoarding extends StatelessWidget { child: BlocBuilder( builder: (context, state) { // Show the local AI settings if the user has purchased the AI Local plan - if (state.isPurchaseAILocal) { + if (kDebugMode || state.isPurchaseAILocal) { return const LocalAISetting(); } else { if (member.role.isOwner) { diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs b/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs index 2ab15b94dc..af1d67b913 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs @@ -326,9 +326,12 @@ impl LocalAIController { .store_preferences .get_bool(APPFLOWY_LOCAL_AI_CHAT_ENABLED) .unwrap_or(true); - self.enable_chat_plugin(chat_enabled).await?; + + if self.local_ai_resource.is_resource_ready() { + self.enable_chat_plugin(chat_enabled).await?; + } } else { - self.enable_chat_plugin(false).await?; + let _ = self.enable_chat_plugin(false).await; } Ok(enabled) } @@ -361,6 +364,10 @@ impl LocalAIController { metadata_list: &[ChatMessageMetadata], index_process_sink: &mut (impl Sink + Unpin), ) -> FlowyResult<()> { + if !self.is_enabled() { + return Ok(()); + } + for metadata in metadata_list { if let Err(err) = metadata.data.validate() { error!( From 88cc0caab70428822c8b8282bfb944a764ef4edc Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 15 Aug 2024 10:00:27 +0800 Subject: [PATCH 05/26] feat: integrate Sentry Flutter and enable it if SENTRY_DSN is not empty (#5959) * chore: add dart dependency validator * feat: integrate sentry flutter * chore: remove user info collection * fix: flutter analyze * fix: ios compile * chore: add log --- .../dart_dependency_validator.yaml | 12 +++++++ frontend/appflowy_flutter/ios/Podfile.lock | 11 +++++++ frontend/appflowy_flutter/lib/env/env.dart | 7 +++++ .../user_profile/user_profile_bloc.dart | 4 +-- .../presentation/home/mobile_home_page.dart | 9 ++++++ .../appflowy_flutter/lib/startup/startup.dart | 1 + .../lib/startup/tasks/prelude.dart | 3 +- .../lib/startup/tasks/sentry.dart | 31 +++++++++++++++++++ .../home/desktop_home_screen.dart | 10 ++++++ frontend/appflowy_flutter/pubspec.lock | 24 +++++++++++--- frontend/appflowy_flutter/pubspec.yaml | 4 ++- 11 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 frontend/appflowy_flutter/dart_dependency_validator.yaml create mode 100644 frontend/appflowy_flutter/lib/startup/tasks/sentry.dart diff --git a/frontend/appflowy_flutter/dart_dependency_validator.yaml b/frontend/appflowy_flutter/dart_dependency_validator.yaml new file mode 100644 index 0000000000..cb1df68bb6 --- /dev/null +++ b/frontend/appflowy_flutter/dart_dependency_validator.yaml @@ -0,0 +1,12 @@ +# dart_dependency_validator.yaml + +allow_pins: true + +include: + - "lib/**" + +exclude: + - "packages/**" + +ignore: + - analyzer diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index d7647a9d4a..0e8dab5d86 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -69,6 +69,11 @@ PODS: - SDWebImage (5.14.2): - SDWebImage/Core (= 5.14.2) - SDWebImage/Core (5.14.2) + - Sentry/HybridSDK (8.33.0) + - sentry_flutter (8.7.0): + - Flutter + - FlutterMacOS + - Sentry/HybridSDK (= 8.33.0) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -101,6 +106,7 @@ DEPENDENCIES: - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - printing (from `.symlinks/plugins/printing/ios`) + - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/darwin`) @@ -113,6 +119,7 @@ SPEC REPOS: - DKPhotoGallery - ReachabilitySwift - SDWebImage + - Sentry - SwiftyGif - Toast @@ -149,6 +156,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" printing: :path: ".symlinks/plugins/printing/ios" + sentry_flutter: + :path: ".symlinks/plugins/sentry_flutter/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: @@ -181,6 +190,8 @@ SPEC CHECKSUMS: printing: 233e1b73bd1f4a05615548e9b5a324c98588640b ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 + Sentry: 8560050221424aef0bebc8e31eedf00af80f90a6 + sentry_flutter: e26b861f744e5037a3faf9bf56603ec65d658a61 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec diff --git a/frontend/appflowy_flutter/lib/env/env.dart b/frontend/appflowy_flutter/lib/env/env.dart index b861b4cfb8..cfd9837944 100644 --- a/frontend/appflowy_flutter/lib/env/env.dart +++ b/frontend/appflowy_flutter/lib/env/env.dart @@ -36,4 +36,11 @@ abstract class Env { defaultValue: '', ) static const String internalBuild = _Env.internalBuild; + + @EnviedField( + obfuscate: false, + varName: 'SENTRY_DSN', + defaultValue: '', + ) + static const String sentryDsn = _Env.sentryDsn; } diff --git a/frontend/appflowy_flutter/lib/mobile/application/user_profile/user_profile_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/user_profile/user_profile_bloc.dart index 7edec07cc1..1480cc02e9 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/user_profile/user_profile_bloc.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/user_profile/user_profile_bloc.dart @@ -12,12 +12,12 @@ class UserProfileBloc extends Bloc { UserProfileBloc() : super(const _Initial()) { on((event, emit) async { await event.when( - started: () async => _initalize(emit), + started: () async => _initialize(emit), ); }); } - Future _initalize(Emitter emit) async { + Future _initialize(Emitter emit) async { emit(const UserProfileState.loading()); final workspaceOrFailure = diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index 215c9433b5..dd9512b0ef 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -22,6 +22,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; +import 'package:sentry/sentry.dart'; class MobileHomeScreen extends StatelessWidget { const MobileHomeScreen({super.key}); @@ -59,6 +60,14 @@ class MobileHomeScreen extends StatelessWidget { return const WorkspaceFailedScreen(); } + Sentry.configureScope( + (scope) => scope.setUser( + SentryUser( + id: userProfile.id.toString(), + ), + ), + ); + return Scaffold( body: SafeArea( bottom: false, diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index 3dac4f229c..213e5f6227 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -110,6 +110,7 @@ class FlowyRunner { // this task should be first task, for handling platform errors. // don't catch errors in test mode if (!mode.isUnitTest) const PlatformErrorCatcherTask(), + if (!mode.isUnitTest) const InitSentryTask(), // this task should be second task, for handling memory leak. // there's a flag named _enable in memory_leak_detector.dart. If it's false, the task will be ignored. MemoryLeakDetectorTask(), diff --git a/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart b/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart index 84c379da24..2c3aced3ab 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart @@ -9,7 +9,8 @@ export 'localization.dart'; export 'memory_leak_detector.dart'; export 'platform_error_catcher.dart'; export 'platform_service.dart'; +export 'recent_service_task.dart'; export 'rust_sdk.dart'; +export 'sentry.dart'; export 'supabase_task.dart'; export 'windows.dart'; -export 'recent_service_task.dart'; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/sentry.dart b/frontend/appflowy_flutter/lib/startup/tasks/sentry.dart new file mode 100644 index 0000000000..13a280fdf1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/startup/tasks/sentry.dart @@ -0,0 +1,31 @@ +import 'package:appflowy/env/env.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../startup.dart'; + +class InitSentryTask extends LaunchTask { + const InitSentryTask(); + + @override + Future initialize(LaunchContext context) async { + const dsn = Env.sentryDsn; + if (dsn.isEmpty) { + Log.info('Sentry DSN is not set, skipping initialization'); + return; + } + + Log.info('Initializing Sentry'); + + await SentryFlutter.init( + (options) { + options.dsn = dsn; + options.tracesSampleRate = 1.0; + options.profilesSampleRate = 1.0; + }, + ); + } + + @override + Future dispose() async {} +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart index b2f9c89889..a8d768aa79 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart @@ -26,6 +26,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' import 'package:flowy_infra_ui/style_widget/container.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:sentry/sentry.dart'; import 'package:sized_context/sized_context.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -55,6 +56,7 @@ class DesktopHomeScreen extends StatelessWidget { (workspaceSettingPB) => workspaceSettingPB as WorkspaceSettingPB, (error) => null, ); + final userProfile = snapshots.data?[1].fold( (userProfilePB) => userProfilePB as UserProfilePB, (error) => null, @@ -66,6 +68,14 @@ class DesktopHomeScreen extends StatelessWidget { return const WorkspaceFailedScreen(); } + Sentry.configureScope( + (scope) => scope.setUser( + SentryUser( + id: userProfile.id.toString(), + ), + ), + ); + return AFFocusManager( child: MultiBlocProvider( key: ValueKey(userProfile.id), diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 5d16830e41..c39019f2ff 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -742,10 +742,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "4.0.0" flutter_localizations: dependency: transitive description: flutter @@ -1215,10 +1215,10 @@ packages: dependency: transitive description: name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.0" loading_indicator: dependency: transitive description: @@ -1747,6 +1747,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.8" + sentry: + dependency: "direct main" + description: + name: sentry + sha256: "0f787e27ff617e4f88f7074977240406a9c5509444bac64a4dfa5b3200fb5632" + url: "https://pub.dev" + source: hosted + version: "8.7.0" + sentry_flutter: + dependency: "direct main" + description: + name: sentry_flutter + sha256: fbbb47d72ccca48be25bf3c2ced6ab6e872991af3a0ba78e54be8d138f2e053f + url: "https://pub.dev" + source: hosted + version: "8.7.0" share_plus: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index b8caf2c183..a93fa0c511 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -154,9 +154,11 @@ dependencies: scroll_to_index: ^3.0.1 extended_text_field: ^15.0.0 extended_text_library: ^12.0.0 + sentry_flutter: ^8.7.0 + sentry: ^8.7.0 dev_dependencies: - flutter_lints: ^3.0.1 + flutter_lints: ^4.0.0 flutter_test: sdk: flutter From 6283649a6b51e3c61dd72b00611470269a74ab11 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 15 Aug 2024 20:12:09 +0800 Subject: [PATCH 06/26] feat: open the row page on mobile (#5975) * chore: add dart dependency validator * feat: open the row page on mobile * Revert "chore: add dart dependency validator" This reverts commit c81e5ef0ed7b0f1e74d6ba499722a9e2b566862f. * chore: update translations * feat: preload row page to reduce open time * chore: don't add orphan doc into recent records * fix: bloc error * fix: migrate the row page title to latest design * chore: optimize database mobile UI --- .../lib/mobile/application/mobile_router.dart | 11 +- .../presentation/base/app_bar/app_bar.dart | 2 +- .../base/app_bar/app_bar_actions.dart | 25 +++ .../presentation/base/mobile_view_page.dart | 16 +- .../bottom_sheet_rename_widget.dart | 1 + .../mobile_card_detail_screen.dart | 4 + .../card_detail/widgets/row_page_button.dart | 116 +++++++++++++ .../calculations/calculations_bloc.dart | 16 +- .../presentation/database_document_title.dart | 162 ++++-------------- .../document_collaborators_bloc.dart | 4 + .../lib/startup/tasks/app_widget.dart | 9 +- .../presentation/widgets/view_title_bar.dart | 23 +-- frontend/resources/translations/en.json | 3 +- 13 files changed, 229 insertions(+), 163 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart index 8b9f1e70ff..7fc1f0824b 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart @@ -2,20 +2,23 @@ import 'dart:async'; import 'dart:convert'; import 'package:appflowy/mobile/presentation/chat/mobile_chat_screen.dart'; -import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/recent/cached_recent_service.dart'; +import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; extension MobileRouter on BuildContext { - Future pushView(ViewPB view, [Map? arguments]) async { + Future pushView( + ViewPB view, { + Map? arguments, + bool addInRecent = true, + }) async { // set the current view before pushing the new view getIt().latestOpenView = view; unawaited(getIt().updateRecentViews([view.id], true)); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar.dart index 335f1af489..396ecd6bb8 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar.dart @@ -10,7 +10,7 @@ enum FlowyAppBarLeadingType { Widget getWidget(VoidCallback? onTap) { switch (this) { case FlowyAppBarLeadingType.back: - return AppBarBackButton(onTap: onTap); + return AppBarImmersiveBackButton(onTap: onTap); case FlowyAppBarLeadingType.close: return AppBarCloseButton(onTap: onTap); case FlowyAppBarLeadingType.cancel: diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar_actions.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar_actions.dart index b59c1e68cc..72142d446b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar_actions.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar_actions.dart @@ -26,6 +26,31 @@ class AppBarBackButton extends StatelessWidget { } } +class AppBarImmersiveBackButton extends StatelessWidget { + const AppBarImmersiveBackButton({ + super.key, + this.onTap, + }); + + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return AppBarButton( + onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(), + padding: const EdgeInsets.only( + left: 12.0, + top: 8.0, + bottom: 8.0, + right: 4.0, + ), + child: const FlowySvg( + FlowySvgs.m_app_bar_back_s, + ), + ); + } +} + class AppBarCloseButton extends StatelessWidget { const AppBarCloseButton({ super.key, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index 603005fc38..55d67ad6af 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -4,7 +4,6 @@ import 'package:appflowy/mobile/application/page_style/document_page_style_bloc. import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/document/presentation/document_collaborators.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; @@ -232,19 +231,20 @@ class _MobileViewPageState extends State { return Row( mainAxisSize: MainAxisSize.min, children: [ - if (icon != null && icon.isNotEmpty) - ConstrainedBox( - constraints: const BoxConstraints.tightFor(width: 34.0), - child: EmojiText( - emoji: '$icon ', - fontSize: 22.0, - ), + if (icon != null && icon.isNotEmpty) ...[ + FlowyText.emoji( + icon, + fontSize: 15.0, + figmaLineHeight: 18.0, ), + const HSpace(4), + ], Expanded( child: FlowyText.medium( view?.name ?? widget.title ?? '', fontSize: 15.0, overflow: TextOverflow.ellipsis, + figmaLineHeight: 18.0, ), ), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart index e61f27b6b2..cabc234fec 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart @@ -52,6 +52,7 @@ class _MobileBottomSheetRenameWidgetState height: 42.0, child: FlowyTextField( controller: controller, + textStyle: Theme.of(context).textTheme.bodyMedium, keyboardType: TextInputType.text, onSubmitted: (text) => widget.onRename(text), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart index c65f899c34..17c97b1f85 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart @@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; @@ -366,6 +367,9 @@ class MobileRowDetailPageContentState if (rowDetailState.numHiddenFields != 0) ...[ const ToggleHiddenFieldsVisibilityButton(), ], + OpenRowPageButton( + documentId: rowController.rowMeta.documentId, + ), MobileRowDetailCreateFieldButton( viewId: viewId, fieldController: fieldController, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart new file mode 100644 index 0000000000..c90205e85a --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart @@ -0,0 +1,116 @@ +import 'dart:async'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class OpenRowPageButton extends StatefulWidget { + const OpenRowPageButton({ + super.key, + required this.documentId, + }); + + final String documentId; + + @override + State createState() => _OpenRowPageButtonState(); +} + +class _OpenRowPageButtonState extends State { + ViewPB? view; + + @override + void initState() { + super.initState(); + + _preloadView(context, createDocumentIfMissed: true); + } + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: BoxConstraints( + minWidth: double.infinity, + minHeight: GridSize.headerHeight, + ), + child: TextButton.icon( + style: Theme.of(context).textButtonTheme.style?.copyWith( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + ), + overlayColor: WidgetStateProperty.all( + Theme.of(context).hoverColor, + ), + alignment: AlignmentDirectional.centerStart, + splashFactory: NoSplash.splashFactory, + padding: const WidgetStatePropertyAll( + EdgeInsets.symmetric(vertical: 14, horizontal: 6), + ), + ), + label: FlowyText.medium( + LocaleKeys.grid_field_openRowDocument.tr(), + fontSize: 15, + ), + icon: const Padding( + padding: EdgeInsets.all(4.0), + child: FlowySvg( + FlowySvgs.full_view_s, + size: Size.square(16.0), + ), + ), + onPressed: () => _openRowPage(context), + ), + ); + } + + Future _openRowPage(BuildContext context) async { + Log.info('Open row page(${widget.documentId})'); + + if (view == null) { + showToastNotification(context, message: 'Failed to open row page'); + // reload the view again + unawaited(_preloadView(context)); + Log.error('Failed to open row page(${widget.documentId})'); + return; + } + + if (context.mounted) { + // the document in row is an orphan document, so we don't add it to recent + await context.pushView( + view!, + addInRecent: false, + ); + } + } + + // preload view to reduce the time to open the view + Future _preloadView( + BuildContext context, { + bool createDocumentIfMissed = false, + }) async { + Log.info('Preload row page(${widget.documentId})'); + final result = await ViewBackendService.getView(widget.documentId); + view = result.fold((s) => s, (f) => null); + + if (view == null && createDocumentIfMissed) { + // create view if not exists + Log.info('Create row page(${widget.documentId})'); + final result = await ViewBackendService.createOrphanView( + name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + viewId: widget.documentId, + layoutType: ViewLayoutPB.Document, + ); + view = result.fold((s) => s, (f) => null); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart index e41fa61b2f..a2b80a29df 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart @@ -39,11 +39,13 @@ class CalculationsBloc extends Bloc { _startListening(); await _getAllCalculations(); - add( - CalculationsEvent.didReceiveFieldUpdate( - _fieldController.fieldInfos, - ), - ); + if (!isClosed) { + add( + CalculationsEvent.didReceiveFieldUpdate( + _fieldController.fieldInfos, + ), + ); + } }, didReceiveFieldUpdate: (fields) async { emit( @@ -131,6 +133,10 @@ class CalculationsBloc extends Bloc { Future _getAllCalculations() async { final calculationsOrFailure = await _calculationsService.getCalculations(); + if (isClosed) { + return; + } + final RepeatedCalculationsPB? calculations = calculationsOrFailure.fold((s) => s, (e) => null); if (calculations != null) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart index 8a71e26efa..d509aa2f25 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart @@ -1,15 +1,12 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; -import 'package:appflowy/startup/tasks/app_window_size_manager.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_listener.dart'; +import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; @@ -47,20 +44,16 @@ class ViewTitleBarWithRow extends StatelessWidget { if (state.ancestors.isEmpty) { return const SizedBox.shrink(); } - const maxWidth = WindowSizeManager.minWindowWidth - 200; - return LayoutBuilder( - builder: (context, constraints) { - return Visibility( - visible: maxWidth < constraints.maxWidth, - // if the width is too small, only show one view title bar without the ancestors - replacement: _buildRowName(), - child: Row( - // refresh the view title bar when the ancestors changed - key: ValueKey(state.ancestors.hashCode), - children: _buildViewTitles(state.ancestors), - ), - ); - }, + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + height: 24, + child: Row( + // refresh the view title bar when the ancestors changed + key: ValueKey(state.ancestors.hashCode), + children: _buildViewTitles(state.ancestors), + ), + ), ); }, ), @@ -71,16 +64,22 @@ class ViewTitleBarWithRow extends StatelessWidget { // if the level is too deep, only show the root view, the database view and the row return views.length > 2 ? [ - _buildViewButton(views.first), - const FlowyText.regular('/'), - const FlowyText.regular(' ... /'), + _buildViewButton(views[1]), + const FlowySvg(FlowySvgs.title_bar_divider_s), + const FlowyText.regular(' ... '), + const FlowySvg(FlowySvgs.title_bar_divider_s), _buildViewButton(views.last), - const FlowyText.regular('/'), + const FlowySvg(FlowySvgs.title_bar_divider_s), _buildRowName(), ] : [ ...views - .map((e) => [_buildViewButton(e), const FlowyText.regular('/')]) + .map( + (e) => [ + _buildViewButton(e), + const FlowySvg(FlowySvgs.title_bar_divider_s), + ], + ) .flattened, _buildRowName(), ]; @@ -89,9 +88,9 @@ class ViewTitleBarWithRow extends StatelessWidget { Widget _buildViewButton(ViewPB view) { return FlowyTooltip( message: view.name, - child: _ViewTitle( + child: ViewTitle( view: view, - behavior: _ViewTitleBehavior.uneditable, + behavior: ViewTitleBehavior.uneditable, onUpdated: () {}, ), ); @@ -180,11 +179,14 @@ class _TitleSkin extends IEditableTextCellSkin { onTap: () {}, text: Row( children: [ - EmojiText( - emoji: state.icon ?? "", - fontSize: 18.0, - ), - const HSpace(2.0), + if (state.icon != null) ...[ + FlowyText.emoji( + state.icon!, + fontSize: 14.0, + figmaLineHeight: 18.0, + ), + const HSpace(4.0), + ], ConstrainedBox( constraints: const BoxConstraints(maxWidth: 180), child: FlowyText.regular( @@ -204,106 +206,6 @@ class _TitleSkin extends IEditableTextCellSkin { } } -enum _ViewTitleBehavior { - editable, - uneditable, -} - -class _ViewTitle extends StatefulWidget { - const _ViewTitle({ - required this.view, - this.behavior = _ViewTitleBehavior.editable, - required this.onUpdated, - }) : maxTitleWidth = 180; - - final ViewPB view; - final _ViewTitleBehavior behavior; - final double maxTitleWidth; - final VoidCallback onUpdated; - - @override - State<_ViewTitle> createState() => _ViewTitleState(); -} - -class _ViewTitleState extends State<_ViewTitle> { - late final viewListener = ViewListener(viewId: widget.view.id); - - String name = ''; - String icon = ''; - - @override - void initState() { - super.initState(); - - name = widget.view.name.isEmpty - ? LocaleKeys.document_title_placeholder.tr() - : widget.view.name; - icon = widget.view.icon.value; - - viewListener.start( - onViewUpdated: (view) { - if (name != view.name || icon != view.icon.value) { - widget.onUpdated(); - } - setState(() { - name = view.name.isEmpty - ? LocaleKeys.document_title_placeholder.tr() - : view.name; - icon = view.icon.value; - }); - }, - ); - } - - @override - void dispose() { - viewListener.stop(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - // root view - if (widget.view.parentViewId.isEmpty) { - return Row( - children: [ - FlowyText.regular(name), - const HSpace(4.0), - ], - ); - } - - final child = Row( - children: [ - EmojiText( - emoji: icon, - fontSize: 18.0, - ), - const HSpace(2.0), - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: widget.maxTitleWidth, - ), - child: FlowyText.regular( - name, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ); - - return Listener( - onPointerDown: (_) => context.read().openPlugin(widget.view), - child: FlowyButton( - useIntrinsicWidth: true, - onTap: () {}, - text: child, - ), - ); - } -} - class RenameRowPopover extends StatefulWidget { const RenameRowPopover({ super.key, diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart index 7e5e4eb528..b6352b0430 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart @@ -39,6 +39,10 @@ class DocumentCollaboratorsBloc if (userProfile != null) { _listener.start( onDocAwarenessUpdate: (states) { + if (isClosed) { + return; + } + add( DocumentCollaboratorsEvent.update( userProfile, diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index d879f07578..5574749d64 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -190,9 +190,12 @@ class _ApplicationWidgetState extends State { if (view != null) { final view = action.arguments?[ActionArgumentKeys.view]; final rowId = action.arguments?[ActionArgumentKeys.rowId]; - AppGlobals.rootNavKey.currentContext?.pushView(view, { - PluginArgumentKeys.rowId: rowId, - }); + AppGlobals.rootNavKey.currentContext?.pushView( + view, + arguments: { + PluginArgumentKeys.rowId: rowId, + }, + ); } } }); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index 43b14d9d1a..68849661b5 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -79,11 +79,11 @@ class ViewTitleBar extends StatelessWidget { final child = FlowyTooltip( key: ValueKey(view.id), message: view.name, - child: _ViewTitle( + child: ViewTitle( view: view, behavior: i == views.length - 1 - ? _ViewTitleBehavior.editable // only the last one is editable - : _ViewTitleBehavior.uneditable, // others are not editable + ? ViewTitleBehavior.editable // only the last one is editable + : ViewTitleBehavior.uneditable, // others are not editable onUpdated: () { context .read() @@ -103,27 +103,28 @@ class ViewTitleBar extends StatelessWidget { } } -enum _ViewTitleBehavior { +enum ViewTitleBehavior { editable, uneditable, } -class _ViewTitle extends StatefulWidget { - const _ViewTitle({ +class ViewTitle extends StatefulWidget { + const ViewTitle({ + super.key, required this.view, - this.behavior = _ViewTitleBehavior.editable, + this.behavior = ViewTitleBehavior.editable, required this.onUpdated, }); final ViewPB view; - final _ViewTitleBehavior behavior; + final ViewTitleBehavior behavior; final VoidCallback onUpdated; @override - State<_ViewTitle> createState() => _ViewTitleState(); + State createState() => _ViewTitleState(); } -class _ViewTitleState extends State<_ViewTitle> { +class _ViewTitleState extends State { final popoverController = PopoverController(); final textEditingController = TextEditingController(); @@ -137,7 +138,7 @@ class _ViewTitleState extends State<_ViewTitle> { @override Widget build(BuildContext context) { - final isEditable = widget.behavior == _ViewTitleBehavior.editable; + final isEditable = widget.behavior == ViewTitleBehavior.editable; return BlocProvider( create: (_) => diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 7196a64640..8e5bbb8dc6 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1327,6 +1327,7 @@ "addOption": "Add option", "editProperty": "Edit property", "newProperty": "New property", + "openRowDocument": "Open document", "deleteFieldPromptMessage": "Are you sure? This property will be deleted", "clearFieldPromptMessage": "Are you sure? All cells in this column will be emptied", "newColumn": "New Column", @@ -2411,4 +2412,4 @@ "commentAddedSuccessfully": "Comment added successfully.", "commentAddedSuccessTip": "You've just added or replied to a comment. Would you like to jump to the top to see the latest comments?" } -} \ No newline at end of file +} From f7a2d9e581883df07a4bb9bb067373c053dce5ea Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 15 Aug 2024 20:12:25 +0800 Subject: [PATCH 07/26] chore: add loading indicator when generating freezed file (#5978) --- .../freezed/generate_freezed.sh | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/frontend/scripts/code_generation/freezed/generate_freezed.sh b/frontend/scripts/code_generation/freezed/generate_freezed.sh index 391aea08b2..abc0126325 100755 --- a/frontend/scripts/code_generation/freezed/generate_freezed.sh +++ b/frontend/scripts/code_generation/freezed/generate_freezed.sh @@ -75,6 +75,21 @@ if [ "$exclude_packages" = false ]; then cd .. fi +# Function to display animated loading text +display_loading() { + local pid=$1 + local delay=0.5 + local spinstr='|/-\' + while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do + local temp=${spinstr#?} + printf " [%c] Generating freezed files..." "$spinstr" + local spinstr=$temp${spinstr%"$temp"} + sleep $delay + printf "\r" + done + printf " \r" +} + # Navigate to the appflowy_flutter directory and generate files echo "🧊 Start generating freezed files (AppFlowy)." @@ -86,13 +101,28 @@ if [ "$skip_pub_packages_get" = false ]; then fi fi +# Start the build_runner in the background if [ "$verbose" = true ]; then - dart run build_runner build -d + dart run build_runner build -d & else - dart run build_runner build >/dev/null 2>&1 + dart run build_runner build >/dev/null 2>&1 & fi -# Return to the original directory +# Get the PID of the background process +build_pid=$! + +# Start the loading animation +display_loading $build_pid & + +# Get the PID of the loading animation +loading_pid=$! + +# Wait for the build_runner to finish +wait $build_pid + +# Clear the line +printf "\r%*s\r" $(($(tput cols))) "" + cd "$original_dir" echo "🧊 Done generating freezed files." From d3d929b68e497d1dae5e63b0fe87d861fa679884 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 16 Aug 2024 11:58:48 +0800 Subject: [PATCH 08/26] fix: unable to insert todo list via slash menu (#5980) * fix: unable to insert todo list via slash menu * fix: unable to insert divider via slash menu * chore: update editor version * chore: update translations * chore: decrease sentry sample rate to 0.1 * fix: integration test --- .../document/document_with_database_test.dart | 1 + .../document/presentation/editor_page.dart | 4 + .../slash_menu/slash_menu_items.dart | 105 +++++++++++++++++- .../lib/startup/tasks/sentry.dart | 4 +- frontend/appflowy_flutter/pubspec.lock | 36 +----- frontend/appflowy_flutter/pubspec.yaml | 5 +- frontend/resources/translations/en.json | 4 +- 7 files changed, 116 insertions(+), 43 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_database_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_database_test.dart index 0ee7610b93..eb07a2e7a8 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_database_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_database_test.dart @@ -176,6 +176,7 @@ Future createInlineDatabase( await tester.editor.showSlashMenu(); await tester.editor.tapSlashMenuItemWithName( layout.slashMenuName, + offset: 100, ); await tester.pumpAndSettle(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 87eec5b47c..d4e767bb43 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -410,6 +410,7 @@ class _AppFlowyEditorPageState extends State { } List _customSlashMenuItems() { + return [ aiWriterSlashMenuItem, textSlashMenuItem, @@ -419,7 +420,10 @@ class _AppFlowyEditorPageState extends State { imageSlashMenuItem, bulletedListSlashMenuItem, numberedListSlashMenuItem, + todoListSlashMenuItem, + dividerSlashMenuItem, quoteSlashMenuItem, + tableSlashMenuItem, referencedDocSlashMenuItem, gridSlashMenuItem(documentBloc), referencedGridSlashMenuItem, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items.dart index 8b92d51ab2..0c3868a50a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items.dart @@ -125,6 +125,21 @@ final numberedListSlashMenuItem = SelectionMenuItem( }, ); +// todo list menu item +final todoListSlashMenuItem = SelectionMenuItem( + getName: () => LocaleKeys.document_slashMenu_name_todoList.tr(), + nameBuilder: _slashMenuItemNameBuilder, + icon: (editorState, isSelected, style) => SelectableSvgWidget( + data: FlowySvgs.slash_menu_icon_checkbox_s, + isSelected: isSelected, + style: style, + ), + keywords: ['checkbox', 'todo', 'list', 'to-do', 'task'], + handler: (editorState, _, __) { + insertCheckboxAfterSelection(editorState); + }, +); + // quote menu item final quoteSlashMenuItem = SelectionMenuItem( getName: () => LocaleKeys.document_slashMenu_name_quote.tr(), @@ -134,12 +149,42 @@ final quoteSlashMenuItem = SelectionMenuItem( isSelected: isSelected, style: style, ), - keywords: ['quote', 'refer'], + keywords: ['quote', 'refer', 'blockquote', 'citation'], handler: (editorState, _, __) { insertQuoteAfterSelection(editorState); }, ); +// divider menu item +final dividerSlashMenuItem = SelectionMenuItem( + getName: () => LocaleKeys.document_slashMenu_name_divider.tr(), + nameBuilder: _slashMenuItemNameBuilder, + icon: (editorState, isSelected, style) => SelectableSvgWidget( + data: FlowySvgs.slash_menu_icon_divider_s, + isSelected: isSelected, + style: style, + ), + keywords: ['divider', 'separator', 'line', 'break', 'horizontal line'], + handler: (editorState, _, __) { + final selection = editorState.selection; + if (selection == null || !selection.isCollapsed) { + return; + } + final path = selection.end.path; + final node = editorState.getNodeAtPath(path); + final delta = node?.delta; + if (node == null || delta == null) { + return; + } + final insertedPath = delta.isEmpty ? path : path.next; + final transaction = editorState.transaction + ..insertNode(insertedPath, dividerNode()) + ..insertNode(insertedPath, paragraphNode()) + ..afterSelection = Selection.collapsed(Position(path: insertedPath.next)); + editorState.apply(transaction); + }, +); + // grid & board & calendar menu item SelectionMenuItem gridSlashMenuItem(DocumentBloc documentBloc) { return SelectionMenuItem( @@ -347,7 +392,7 @@ SelectionMenuItem toggleListSlashMenuItem = SelectionMenuItem.node( isSelected: isSelected, style: style, ), - keywords: ['collapsed list', 'toggle list', 'list'], + keywords: ['collapsed list', 'toggle list', 'list', 'dropdown'], nodeBuilder: (editorState, _) => toggleListBlockNode(), replace: (_, node) => node.delta?.isEmpty ?? false, ); @@ -361,7 +406,7 @@ SelectionMenuItem emojiSlashMenuItem = SelectionMenuItem( isSelected: isSelected, style: style, ), - keywords: ['emoji'], + keywords: ['emoji', 'reaction', 'emoticon'], handler: (editorState, menuService, context) { final container = Overlay.of(context); menuService.dismiss(); @@ -391,6 +436,56 @@ SelectionMenuItem aiWriterSlashMenuItem = SelectionMenuItem.node( replace: (_, node) => false, ); +// table menu item +SelectionMenuItem tableSlashMenuItem = SelectionMenuItem( + getName: () => LocaleKeys.document_slashMenu_name_table.tr(), + nameBuilder: _slashMenuItemNameBuilder, + icon: (editorState, isSelected, style) => SelectableSvgWidget( + data: FlowySvgs.slash_menu_icon_simple_table_s, + isSelected: isSelected, + style: style, + ), + keywords: ['table', 'rows', 'columns', 'data'], + handler: (editorState, _, __) async { + final selection = editorState.selection; + if (selection == null || !selection.isCollapsed) { + return; + } + + final currentNode = editorState.getNodeAtPath(selection.end.path); + if (currentNode == null) { + return; + } + + final tableNode = TableNode.fromList([ + ['', ''], + ['', ''], + ]); + + final transaction = editorState.transaction; + final delta = currentNode.delta; + if (delta != null && delta.isEmpty) { + transaction + ..insertNode(selection.end.path, tableNode.node) + ..deleteNode(currentNode); + transaction.afterSelection = Selection.collapsed( + Position( + path: selection.end.path + [0, 0], + ), + ); + } else { + transaction.insertNode(selection.end.path.next, tableNode.node); + transaction.afterSelection = Selection.collapsed( + Position( + path: selection.end.path.next + [0, 0], + ), + ); + } + + await editorState.apply(transaction); + }, +); + // date or reminder menu item SelectionMenuItem dateOrReminderSlashMenuItem = SelectionMenuItem( getName: () => LocaleKeys.document_slashMenu_name_dateOrReminder.tr(), @@ -400,7 +495,7 @@ SelectionMenuItem dateOrReminderSlashMenuItem = SelectionMenuItem( isSelected: isSelected, style: style, ), - keywords: ['insert date', 'date', 'time', 'reminder'], + keywords: ['insert date', 'date', 'time', 'reminder', 'schedule'], handler: (editorState, menuService, context) => insertDateReference(editorState), ); @@ -439,7 +534,7 @@ SelectionMenuItem fileSlashMenuItem = SelectionMenuItem( isSelected: isSelected, style: style, ), - keywords: ['file upload', 'pdf', 'zip', 'archive', 'upload'], + keywords: ['file upload', 'pdf', 'zip', 'archive', 'upload', 'attachment'], handler: (editorState, _, __) async => editorState.insertEmptyFileBlock(), ); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/sentry.dart b/frontend/appflowy_flutter/lib/startup/tasks/sentry.dart index 13a280fdf1..9076569a9c 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/sentry.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/sentry.dart @@ -20,8 +20,8 @@ class InitSentryTask extends LaunchTask { await SentryFlutter.init( (options) { options.dsn = dsn; - options.tracesSampleRate = 1.0; - options.profilesSampleRate = 1.0; + options.tracesSampleRate = 0.1; + options.profilesSampleRate = 0.1; }, ); } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index c39019f2ff..267fb80c21 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,11 +53,11 @@ packages: dependency: "direct main" description: path: "." - ref: "9d3e854" - resolved-ref: "9d3e854f11fd9d732535ce5f5b1c8f41517479a1" + ref: "8e17d14" + resolved-ref: "8e17d1447eea0b57ff92e31dbe88796ce759fb37" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git - version: "3.1.0" + version: "3.2.0" appflowy_editor_plugins: dependency: "direct main" description: @@ -808,7 +808,7 @@ packages: source: hosted version: "0.6.5" flutter_svg: - dependency: "direct main" + dependency: transitive description: name: flutter_svg sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c @@ -1083,14 +1083,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" - intl_utils: - dependency: transitive - description: - name: intl_utils - sha256: c2b1f5c72c25512cbeef5ab015c008fc50fe7e04813ba5541c25272300484bf4 - url: "https://pub.dev" - source: hosted - version: "2.8.7" io: dependency: transitive description: @@ -1116,7 +1108,7 @@ packages: source: hosted version: "0.7.0" isolates: - dependency: "direct main" + dependency: transitive description: name: isolates sha256: ce89e4141b27b877326d3715be2dceac7a7ba89f3229785816d2d318a75ddf28 @@ -1236,7 +1228,7 @@ packages: source: hosted version: "0.1.5" logger: - dependency: "direct main" + dependency: transitive description: name: logger sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" @@ -1475,14 +1467,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.11.1" - pdf_widget_wrapper: - dependency: transitive - description: - name: pdf_widget_wrapper - sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5 - url: "https://pub.dev" - source: hosted - version: "1.0.4" percent_indicator: dependency: "direct main" description: @@ -1603,14 +1587,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" - printing: - dependency: transitive - description: - name: printing - sha256: cc4b256a5a89d5345488e3318897b595867f5181b8c5ed6fc63bfa5f2044aec3 - url: "https://pub.dev" - source: hosted - version: "5.13.1" process: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index a93fa0c511..d6a7f61930 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -77,7 +77,6 @@ dependencies: linked_scroll_controller: ^0.2.0 hotkey_manager: ^0.1.7 fixnum: ^1.1.0 - flutter_svg: ^2.0.7 protobuf: ^3.1.0 collection: ^1.17.1 bloc: ^8.1.2 @@ -133,10 +132,8 @@ dependencies: auto_size_text_field: ^2.2.3 reorderable_tabbar: ^1.0.6 shimmer: ^3.0.0 - isolates: ^3.0.3+8 markdown_widget: ^2.3.2+6 markdown: - logger: ^2.4.0 # Desktop Drop uses Cross File (XFile) data type desktop_drop: ^0.4.4 @@ -193,7 +190,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "9d3e854" + ref: "8e17d14" appflowy_editor_plugins: git: diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 8e5bbb8dc6..b5c76c4f50 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1327,7 +1327,7 @@ "addOption": "Add option", "editProperty": "Edit property", "newProperty": "New property", - "openRowDocument": "Open document", + "openRowDocument": "Open as a page", "deleteFieldPromptMessage": "Are you sure? This property will be deleted", "clearFieldPromptMessage": "Are you sure? All cells in this column will be emptied", "newColumn": "New Column", @@ -1471,7 +1471,7 @@ "image": "Image", "bulletedList": "Bulleted List", "numberedList": "Numbered List", - "checkbox": "Checkbox", + "todoList": "To-do List", "doc": "Doc", "linkedDoc": "Link to page", "grid": "Grid", From e6bf6a5c7d3fd4ec8b7b5c6feca30ebf1c1831cb Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sat, 17 Aug 2024 11:04:43 +0800 Subject: [PATCH 09/26] feat: support inviting members on mobile (#5986) * feat: support inviting members on mobile * feat: support workspace member list on mobile * feat: support leave workspace on mobile * chore: adjust member list ui * fix: flutter analyze --- frontend/appflowy_flutter/ios/Podfile.lock | 6 - .../bottom_sheet/bottom_sheet_view_item.dart | 12 +- .../home/mobile_home_setting_page.dart | 4 +- .../home/tab/mobile_space_tab.dart | 17 + .../setting/user_session_setting_group.dart | 24 +- .../workspace/invite_members_screen.dart | 329 ++++++++++++++++++ .../setting/workspace/member_list.dart | 164 +++++++++ .../workspace/workspace_setting_group.dart | 29 ++ .../show_flowy_mobile_confirm_dialog.dart | 5 +- .../document/presentation/editor_page.dart | 1 - .../lib/startup/tasks/generate_router.dart | 16 + .../presentation/widgets/dialogs.dart | 19 +- .../lib/style_widget/divider.dart | 14 +- frontend/resources/translations/en.json | 6 +- 14 files changed, 610 insertions(+), 36 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/member_list.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/workspace_setting_group.dart diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 0e8dab5d86..8829c71074 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -63,8 +63,6 @@ PODS: - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter - - printing (1.0.0): - - Flutter - ReachabilitySwift (5.0.0) - SDWebImage (5.14.2): - SDWebImage/Core (= 5.14.2) @@ -105,7 +103,6 @@ DEPENDENCIES: - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - - printing (from `.symlinks/plugins/printing/ios`) - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) @@ -154,8 +151,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" - printing: - :path: ".symlinks/plugins/printing/ios" sentry_flutter: :path: ".symlinks/plugins/sentry_flutter/ios" share_plus: @@ -187,7 +182,6 @@ SPEC CHECKSUMS: package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 - printing: 233e1b73bd1f4a05615548e9b5a324c98588640b ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 Sentry: 8560050221424aef0bebc8e31eedf00af80f90a6 diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart index b76dc63b1d..c26cf759de 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart @@ -116,12 +116,18 @@ class _MobileViewItemBottomSheetState extends State { Future _showConfirmDialog({required VoidCallback onDelete}) async { await showFlowyCupertinoConfirmDialog( title: LocaleKeys.sideBar_removePageFromRecent.tr(), - leftButton: FlowyText.regular( + leftButton: FlowyText( LocaleKeys.button_cancel.tr(), - color: const Color(0xFF1456F0), + fontSize: 17.0, + figmaLineHeight: 24.0, + fontWeight: FontWeight.w500, + color: const Color(0xFF007AFF), ), - rightButton: FlowyText.medium( + rightButton: FlowyText( LocaleKeys.button_delete.tr(), + fontSize: 17.0, + figmaLineHeight: 24.0, + fontWeight: FontWeight.w400, color: const Color(0xFFFE0220), ), onRightButtonPressed: (context) { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart index 964f9e5aa5..07ee4de7d6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart @@ -5,6 +5,7 @@ import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/mobile/presentation/setting/cloud/cloud_setting_group.dart'; import 'package:appflowy/mobile/presentation/setting/user_session_setting_group.dart'; +import 'package:appflowy/mobile/presentation/setting/workspace/workspace_setting_group.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; @@ -79,8 +80,7 @@ class _MobileHomeSettingPageState extends State { PersonalInfoSettingGroup( userProfile: userProfile, ), - // TODO: Enable and implement along with Push Notifications - // const NotificationsSettingGroup(), + const WorkspaceSettingGroup(), const AppearanceSettingGroup(), const LanguageSettingGroup(), if (Env.enableCustomCloud) const CloudSettingGroup(), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart index fe36c392b4..77c26005c4 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart @@ -6,9 +6,12 @@ import 'package:appflowy/mobile/presentation/home/recent_folder/recent_space.dar import 'package:appflowy/mobile/presentation/home/tab/_tab_bar.dart'; import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; +import 'package:appflowy/mobile/presentation/setting/workspace/invite_members_screen.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -38,6 +41,7 @@ class _MobileSpaceTabState extends State super.initState(); mobileCreateNewPageNotifier.addListener(_createNewPage); + mobileLeaveWorkspaceNotifier.addListener(_leaveWorkspace); } @override @@ -45,6 +49,7 @@ class _MobileSpaceTabState extends State tabController?.removeListener(_onTabChange); tabController?.dispose(); mobileCreateNewPageNotifier.removeListener(_createNewPage); + mobileLeaveWorkspaceNotifier.removeListener(_leaveWorkspace); super.dispose(); } @@ -171,4 +176,16 @@ class _MobileSpaceTabState extends State ); } } + + void _leaveWorkspace() { + final workspaceId = + context.read().state.currentWorkspace?.workspaceId; + if (workspaceId == null) { + Log.error('Workspace ID is null'); + return; + } + context + .read() + .add(UserWorkspaceEvent.leaveWorkspace(workspaceId)); + } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart index 8f8fd99ecb..1145d08048 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart @@ -42,14 +42,24 @@ class UserSessionSettingGroup extends StatelessWidget { MobileSignInOrLogoutButton( labelText: LocaleKeys.settings_menu_logout.tr(), onPressed: () async { - await showFlowyMobileConfirmDialog( - context, - content: FlowyText( - LocaleKeys.settings_menu_logoutPrompt.tr(), + await showFlowyCupertinoConfirmDialog( + title: LocaleKeys.settings_menu_logoutPrompt.tr(), + leftButton: FlowyText( + LocaleKeys.button_cancel.tr(), + fontSize: 17.0, + figmaLineHeight: 24.0, + fontWeight: FontWeight.w500, + color: const Color(0xFF007AFF), ), - actionButtonTitle: LocaleKeys.button_yes.tr(), - actionButtonColor: Theme.of(context).colorScheme.error, - onActionButtonPressed: () async { + rightButton: FlowyText( + LocaleKeys.button_logout.tr(), + fontSize: 17.0, + figmaLineHeight: 24.0, + fontWeight: FontWeight.w400, + color: const Color(0xFFFE0220), + ), + onRightButtonPressed: (context) async { + Navigator.of(context).pop(); await getIt().signOut(); await runAppFlowy(); }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart new file mode 100644 index 0000000000..775669b970 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart @@ -0,0 +1,329 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; +import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart'; +import 'package:appflowy/shared/af_role_pb_extension.dart'; +import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:string_validator/string_validator.dart'; +import 'package:toastification/toastification.dart'; + +import 'member_list.dart'; + +ValueNotifier mobileLeaveWorkspaceNotifier = ValueNotifier(0); + +class InviteMembersScreen extends StatelessWidget { + const InviteMembersScreen({ + super.key, + }); + + static const routeName = '/invite_member'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: FlowyAppBar( + titleText: LocaleKeys.settings_appearance_members_label.tr(), + ), + body: const _InviteMemberPage(), + ); + } +} + +class _InviteMemberPage extends StatefulWidget { + const _InviteMemberPage(); + + @override + State<_InviteMemberPage> createState() => _InviteMemberPageState(); +} + +class _InviteMemberPageState extends State<_InviteMemberPage> { + final emailController = TextEditingController(); + late final Future userProfile; + + @override + void initState() { + super.initState(); + userProfile = UserBackendService.getCurrentUserProfile().fold( + (s) => s, + (f) => null, + ); + } + + @override + void dispose() { + emailController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: userProfile, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const SizedBox.shrink(); + } + if (snapshot.hasError || snapshot.data == null) { + return _buildError(context); + } + + final userProfile = snapshot.data!; + + return BlocProvider( + create: (context) => WorkspaceMemberBloc(userProfile: userProfile) + ..add(const WorkspaceMemberEvent.initial()), + child: BlocConsumer( + listener: _onListener, + builder: (context, state) { + return Column( + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (state.myRole.isOwner) ...[ + Padding( + padding: const EdgeInsets.all(16.0), + child: _buildInviteMemberArea(context), + ), + const VSpace(16), + ], + if (state.members.isNotEmpty) ...[ + const VSpace(8), + MobileMemberList( + members: state.members, + userProfile: userProfile, + myRole: state.myRole, + ), + ], + ], + ), + ), + if (state.myRole.isMember) const _LeaveWorkspaceButton(), + const VSpace(48), + ], + ); + }, + ), + ); + }, + ); + } + + Widget _buildInviteMemberArea(BuildContext context) { + return Column( + children: [ + TextFormField( + autofocus: true, + controller: emailController, + keyboardType: TextInputType.text, + decoration: InputDecoration( + hintText: LocaleKeys.settings_appearance_members_inviteHint.tr(), + ), + ), + const VSpace(16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => _inviteMember(context), + child: Text( + LocaleKeys.settings_appearance_members_sendInvite.tr(), + ), + ), + ), + ], + ); + } + + Widget _buildError(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 48.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FlowyText.medium( + LocaleKeys.settings_appearance_members_workspaceMembersError.tr(), + fontSize: 18.0, + textAlign: TextAlign.center, + ), + const VSpace(8.0), + FlowyText.regular( + LocaleKeys + .settings_appearance_members_workspaceMembersErrorDescription + .tr(), + fontSize: 17.0, + maxLines: 10, + textAlign: TextAlign.center, + lineHeight: 1.3, + color: Theme.of(context).hintColor, + ), + ], + ), + ), + ); + } + + void _onListener(BuildContext context, WorkspaceMemberState state) { + final actionResult = state.actionResult; + if (actionResult == null) { + return; + } + + final actionType = actionResult.actionType; + final result = actionResult.result; + + // only show the result dialog when the action is WorkspaceMemberActionType.add + if (actionType == WorkspaceMemberActionType.add) { + result.fold( + (s) { + showToastNotification( + context, + message: + LocaleKeys.settings_appearance_members_addMemberSuccess.tr(), + ); + }, + (f) { + Log.error('add workspace member failed: $f'); + final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded + ? LocaleKeys.settings_appearance_members_memberLimitExceeded.tr() + : LocaleKeys.settings_appearance_members_failedToAddMember.tr(); + showToastNotification( + context, + type: ToastificationType.error, + message: message, + ); + }, + ); + } else if (actionType == WorkspaceMemberActionType.invite) { + result.fold( + (s) { + showToastNotification( + context, + message: + LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(), + ); + }, + (f) { + Log.error('invite workspace member failed: $f'); + final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded + ? LocaleKeys.settings_appearance_members_inviteFailedMemberLimit + .tr() + : LocaleKeys.settings_appearance_members_failedToInviteMember + .tr(); + showToastNotification( + context, + type: ToastificationType.error, + message: message, + ); + }, + ); + } else if (actionType == WorkspaceMemberActionType.remove) { + result.fold( + (s) { + showToastNotification( + context, + message: LocaleKeys + .settings_appearance_members_removeFromWorkspaceSuccess + .tr(), + ); + }, + (f) { + showToastNotification( + context, + type: ToastificationType.error, + message: LocaleKeys + .settings_appearance_members_removeFromWorkspaceFailed + .tr(), + ); + }, + ); + } + } + + void _inviteMember(BuildContext context) { + final email = emailController.text; + if (!isEmail(email)) { + return showToastNotification( + context, + message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(), + ); + } + context + .read() + .add(WorkspaceMemberEvent.inviteWorkspaceMember(email)); + // clear the email field after inviting + emailController.clear(); + } +} + +class _LeaveWorkspaceButton extends StatelessWidget { + const _LeaveWorkspaceButton(); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(horizontal: 16), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + foregroundColor: Theme.of(context).colorScheme.error, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + side: BorderSide( + color: Theme.of(context).colorScheme.error, + width: 0.5, + ), + ), + ), + onPressed: () => _leaveWorkspace(context), + child: FlowyText( + LocaleKeys.workspace_leaveCurrentWorkspace.tr(), + fontSize: 14.0, + color: Theme.of(context).colorScheme.error, + fontWeight: FontWeight.w500, + ), + ), + ); + } + + void _leaveWorkspace(BuildContext context) { + showFlowyCupertinoConfirmDialog( + title: LocaleKeys.workspace_leaveCurrentWorkspacePrompt.tr(), + leftButton: FlowyText( + LocaleKeys.button_cancel.tr(), + fontSize: 17.0, + figmaLineHeight: 24.0, + fontWeight: FontWeight.w500, + color: const Color(0xFF007AFF), + ), + rightButton: FlowyText( + LocaleKeys.button_confirm.tr(), + fontSize: 17.0, + figmaLineHeight: 24.0, + fontWeight: FontWeight.w400, + color: const Color(0xFFFE0220), + ), + onRightButtonPressed: (buttonContext) async { + // try to use popUntil with a specific route name but failed + // so use pop twice as a workaround + Navigator.of(buttonContext).pop(); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + + mobileLeaveWorkspaceNotifier.value = + mobileLeaveWorkspaceNotifier.value + 1; + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/member_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/member_list.dart new file mode 100644 index 0000000000..1d9f250d3a --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/member_list.dart @@ -0,0 +1,164 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy/shared/af_role_pb_extension.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; + +class MobileMemberList extends StatelessWidget { + const MobileMemberList({ + super.key, + required this.members, + required this.myRole, + required this.userProfile, + }); + + final List members; + final AFRolePB myRole; + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + return SlidableAutoCloseBehavior( + child: SeparatedColumn( + crossAxisAlignment: CrossAxisAlignment.start, + separatorBuilder: () => const FlowyDivider( + padding: EdgeInsets.symmetric(horizontal: 16.0), + ), + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: FlowyText.semibold( + LocaleKeys.settings_appearance_members_label.tr(), + fontSize: 16.0, + ), + ), + ...members.map( + (member) => _MemberItem( + member: member, + myRole: myRole, + userProfile: userProfile, + ), + ), + ], + ), + ); + } +} + +class _MemberItem extends StatelessWidget { + const _MemberItem({ + required this.member, + required this.myRole, + required this.userProfile, + }); + + final WorkspaceMemberPB member; + final AFRolePB myRole; + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + final canDelete = myRole.canDelete && member.email != userProfile.email; + final textColor = member.role.isOwner ? Theme.of(context).hintColor : null; + + Widget child = Container( + height: 48, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + Expanded( + child: FlowyText.medium( + member.name, + color: textColor, + fontSize: 15.0, + ), + ), + Expanded( + child: FlowyText.medium( + member.role.description, + color: textColor, + fontSize: 15.0, + textAlign: TextAlign.end, + ), + ), + ], + ), + ); + + if (canDelete) { + child = Slidable( + key: ValueKey(member.email), + endActionPane: ActionPane( + extentRatio: 1 / 6.0, + motion: const ScrollMotion(), + children: [ + CustomSlidableAction( + backgroundColor: const Color(0xE5515563), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10), + bottomLeft: Radius.circular(10), + ), + onPressed: (context) { + HapticFeedback.mediumImpact(); + _showDeleteMenu(context); + }, + padding: EdgeInsets.zero, + child: const FlowySvg( + FlowySvgs.three_dots_s, + size: Size.square(24), + color: Colors.white, + ), + ), + ], + ), + child: child, + ); + } + + return child; + } + + void _showDeleteMenu(BuildContext context) { + final workspaceMemberBloc = context.read(); + showMobileBottomSheet( + context, + showDragHandle: true, + showDivider: false, + useRootNavigator: true, + backgroundColor: Theme.of(context).colorScheme.surface, + builder: (context) { + return FlowyOptionTile.text( + text: LocaleKeys.settings_appearance_members_removeFromWorkspace.tr(), + height: 52.0, + textColor: Theme.of(context).colorScheme.error, + leftIcon: FlowySvg( + FlowySvgs.trash_s, + size: const Size.square(18), + color: Theme.of(context).colorScheme.error, + ), + showTopBorder: false, + showBottomBorder: false, + onTap: () { + workspaceMemberBloc.add( + WorkspaceMemberEvent.removeWorkspaceMember( + member.email, + ), + ); + Navigator.of(context).pop(); + }, + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/workspace_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/workspace_setting_group.dart new file mode 100644 index 0000000000..9c2161a4d1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/workspace_setting_group.dart @@ -0,0 +1,29 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../widgets/widgets.dart'; +import 'invite_members_screen.dart'; + +class WorkspaceSettingGroup extends StatelessWidget { + const WorkspaceSettingGroup({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return MobileSettingGroup( + groupTitle: LocaleKeys.settings_appearance_members_label.tr(), + settingItemList: [ + MobileSettingItem( + name: LocaleKeys.settings_appearance_members_label.tr(), + trailing: const Icon(Icons.chevron_right), + onTap: () { + context.push(InviteMembersScreen.routeName); + }, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart index 321632a36a..90bb12120a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart @@ -98,12 +98,13 @@ Future showFlowyCupertinoConfirmDialog({ }) { return showDialog( context: context ?? AppGlobals.context, + barrierColor: Colors.black.withOpacity(0.25), builder: (context) => CupertinoAlertDialog( title: FlowyText.medium( title, - fontSize: 18, + fontSize: 16, maxLines: 10, - lineHeight: 1.3, + figmaLineHeight: 22.0, ), actions: [ CupertinoDialogAction( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index d4e767bb43..731a0b6d6d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -410,7 +410,6 @@ class _AppFlowyEditorPageState extends State { } List _customSlashMenuItems() { - return [ aiWriterSlashMenuItem, textSlashMenuItem, diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index 7e10166fe4..765081be21 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -17,6 +17,7 @@ import 'package:appflowy/mobile/presentation/setting/cloud/appflowy_cloud_page.d import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart'; import 'package:appflowy/mobile/presentation/setting/language/language_picker_screen.dart'; import 'package:appflowy/mobile/presentation/setting/launch_settings_page.dart'; +import 'package:appflowy/mobile/presentation/setting/workspace/invite_members_screen.dart'; import 'package:appflowy/plugins/base/color/color_picker_screen.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart'; @@ -97,6 +98,9 @@ GoRouter generateRouter(Widget child) { // notifications _mobileNotificationMultiSelectPageRoute(), + + // invite members + _mobileInviteMembersPageRoute(), ], // Desktop and Mobile @@ -198,6 +202,18 @@ GoRoute _mobileNotificationMultiSelectPageRoute() { ); } +GoRoute _mobileInviteMembersPageRoute() { + return GoRoute( + parentNavigatorKey: AppGlobals.rootNavKey, + path: InviteMembersScreen.routeName, + pageBuilder: (context, state) { + return const MaterialExtendedPage( + child: InviteMembersScreen(), + ); + }, + ); +} + GoRoute _mobileCloudSettingAppFlowyCloudPageRoute() { return GoRoute( parentNavigatorKey: AppGlobals.rootNavKey, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index d57f2a5442..20ca5a3605 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; @@ -13,6 +11,7 @@ import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; import 'package:toastification/toastification.dart'; export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; @@ -354,9 +353,6 @@ class _MToast extends StatelessWidget { @override Widget build(BuildContext context) { - // only support success type - assert(type == ToastificationType.success); - return Container( alignment: Alignment.bottomCenter, padding: const EdgeInsets.only(bottom: 100), @@ -369,16 +365,19 @@ class _MToast extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - const FlowySvg( - FlowySvgs.success_s, - blendMode: null, - ), - const HSpace(8.0), + if (type == ToastificationType.success) ...[ + const FlowySvg( + FlowySvgs.success_s, + blendMode: null, + ), + const HSpace(8.0), + ], FlowyText.regular( message, fontSize: 16.0, figmaLineHeight: 18.0, color: Colors.white, + maxLines: 3, ), ], ), diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/divider.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/divider.dart index d53362dbdb..7f4b630386 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/divider.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/divider.dart @@ -4,14 +4,20 @@ import 'package:flutter/material.dart'; class FlowyDivider extends StatelessWidget { const FlowyDivider({ super.key, + this.padding, }); + final EdgeInsets? padding; + @override Widget build(BuildContext context) { - return Divider( - height: 1.0, - thickness: 1.0, - color: AFThemeExtension.of(context).borderColor, + return Padding( + padding: padding ?? EdgeInsets.zero, + child: Divider( + height: 1.0, + thickness: 1.0, + color: AFThemeExtension.of(context).borderColor, + ), ); } } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index b5c76c4f50..05d15177a7 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1086,6 +1086,8 @@ "user": "User", "role": "Role", "removeFromWorkspace": "Remove from Workspace", + "removeFromWorkspaceSuccess": "Remove from workspace successfully", + "removeFromWorkspaceFailed": "Remove from workspace failed", "owner": "Owner", "guest": "Guest", "member": "Member", @@ -1110,7 +1112,9 @@ "removeMember": "Remove Member", "areYouSureToRemoveMember": "Are you sure you want to remove this member?", "inviteMemberSuccess": "The invitation has been sent successfully", - "failedToInviteMember": "Failed to invite member" + "failedToInviteMember": "Failed to invite member", + "workspaceMembersError": "Oops, something went wrong", + "workspaceMembersErrorDescription": "We couldn't load the member list at this time. Please try again later" } }, "files": { From 44fb61026918c235a688dbf61b17de7dfa7c1530 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sat, 17 Aug 2024 11:04:56 +0800 Subject: [PATCH 10/26] fix: support pasting web image on mobile (#5987) * fix: support pasting web image on mobile * fix: permission check will deactive editor focus --- .../copy_and_paste/clipboard_service.dart | 6 +- .../copy_and_paste/custom_paste_command.dart | 1 + .../copy_and_paste/paste_from_image.dart | 75 ++++++++++++++++--- .../custom_mobile_floating_toolbar.dart | 5 +- 4 files changed, 70 insertions(+), 17 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart index 436de2c601..1dde980f03 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart @@ -100,9 +100,7 @@ class ClipboardService { for (final item in reader.items) { final availableFormats = await item.rawReader!.getAvailableFormats(); - Log.debug( - 'availableFormats: $availableFormats', - ); + Log.info('availableFormats: $availableFormats'); } final plainText = await reader.readValue(Formats.plainText); @@ -115,6 +113,8 @@ class ClipboardService { image = ('jpeg', await reader.readFile(Formats.jpeg)); } else if (reader.canProvide(Formats.gif)) { image = ('gif', await reader.readFile(Formats.gif)); + } else if (reader.canProvide(Formats.webp)) { + image = ('webp', await reader.readFile(Formats.webp)); } return ClipboardServiceData( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart index 067d6b766c..2e0cef8b8b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart @@ -76,6 +76,7 @@ CommandShortcutEventHandler _pasteCommandHandler = (editorState) { image.$1, image.$2!, documentId, + selection: selection, ); if (result) { Log.info('Pasted image'); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart index 989798dcf2..4ce4f6c405 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart @@ -9,7 +9,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/imag import 'package:appflowy/shared/patterns/common_patterns.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:cross_file/cross_file.dart'; @@ -23,6 +23,7 @@ extension PasteFromImage on EditorState { 'png', 'jpeg', 'gif', + 'webp', ]; Future dropImages( @@ -64,18 +65,26 @@ extension PasteFromImage on EditorState { Future pasteImage( String format, Uint8List imageBytes, - String documentId, - ) async { - if (!supportedImageFormats.contains(format)) { - return false; - } - + String documentId, { + Selection? selection, + }) async { final context = document.root.context; if (context == null) { return false; } + if (!supportedImageFormats.contains(format)) { + Log.info('unsupported format: $format'); + if (PlatformExtension.isMobile) { + showToastNotification( + context, + message: LocaleKeys.document_imageBlock_error_invalidImageFormat.tr(), + ); + } + return false; + } + final isLocalMode = context.read().isLocalMode; final path = await getIt().getPath(); @@ -105,9 +114,9 @@ extension PasteFromImage on EditorState { final errorMessage = result.$2; if (errorMessage != null && context.mounted) { - showSnackBarMessage( + showToastNotification( context, - errorMessage, + message: errorMessage, ); return false; } @@ -116,7 +125,7 @@ extension PasteFromImage on EditorState { } if (path != null) { - await insertImageNode(path); + await insertImageNode(path, selection: selection); } await File(copyToPath).delete(); @@ -124,13 +133,55 @@ extension PasteFromImage on EditorState { } catch (e) { Log.error('cannot copy image file', e); if (context.mounted) { - showSnackBarMessage( + showToastNotification( context, - LocaleKeys.document_imageBlock_error_invalidImage.tr(), + message: LocaleKeys.document_imageBlock_error_invalidImage.tr(), ); } } return false; } + + Future insertImageNode( + String src, { + Selection? selection, + }) async { + selection ??= this.selection; + if (selection == null || !selection.isCollapsed) { + return; + } + final node = getNodeAtPath(selection.end.path); + if (node == null) { + return; + } + final transaction = this.transaction; + // if the current node is empty paragraph, replace it with image node + if (node.type == ParagraphBlockKeys.type && + (node.delta?.isEmpty ?? false)) { + transaction + ..insertNode( + node.path, + imageNode( + url: src, + ), + ) + ..deleteNode(node); + } else { + transaction.insertNode( + node.path.next, + imageNode( + url: src, + ), + ); + } + + transaction.afterSelection = Selection.collapsed( + Position( + path: node.path.next, + ), + ); + + return apply(transaction); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_floating_toolbar/custom_mobile_floating_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_floating_toolbar/custom_mobile_floating_toolbar.dart index e3b320a63d..ba170e8d24 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_floating_toolbar/custom_mobile_floating_toolbar.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_floating_toolbar/custom_mobile_floating_toolbar.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -23,7 +24,7 @@ List buildMobileFloatingToolbarItems( ContextMenuButtonItem( label: LocaleKeys.editor_copy.tr(), onPressed: () { - copyCommand.execute(editorState); + customCopyCommand.execute(editorState); closeToolbar(); }, ), @@ -34,7 +35,7 @@ List buildMobileFloatingToolbarItems( ContextMenuButtonItem( label: LocaleKeys.editor_paste.tr(), onPressed: () { - pasteCommand.execute(editorState); + customPasteCommand.execute(editorState); closeToolbar(); }, ), From 9853fbfc10af45d07cf41e98061c4a81d704a41e Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sat, 17 Aug 2024 11:05:06 +0800 Subject: [PATCH 11/26] chore: support monochrome icon on Android (#5989) --- .../app/src/main/ic_launcher-playstore.png | Bin 0 -> 45380 bytes ...background.xml => launcher_background.xml} | 0 .../main/res/drawable/launcher_foreground.xml | 12 ++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 8 ++++++++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 +++++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 6037 -> 2322 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 3710 bytes .../mipmap-hdpi/ic_launcher_monochrome.png | Bin 0 -> 4548 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4461 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 3745 -> 1480 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 2146 bytes .../mipmap-mdpi/ic_launcher_monochrome.png | Bin 0 -> 2876 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2748 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 8276 -> 3275 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 5453 bytes .../mipmap-xhdpi/ic_launcher_monochrome.png | Bin 0 -> 6265 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6430 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 13071 -> 5322 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 10339 bytes .../mipmap-xxhdpi/ic_launcher_monochrome.png | Bin 0 -> 10194 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10620 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 17845 -> 8012 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 16598 bytes .../mipmap-xxxhdpi/ic_launcher_monochrome.png | Bin 0 -> 11187 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15848 bytes .../res/values/ic_launcher_background.xml | 4 ++++ 26 files changed, 29 insertions(+) create mode 100644 frontend/appflowy_flutter/android/app/src/main/ic_launcher-playstore.png rename frontend/appflowy_flutter/android/app/src/main/res/drawable/{launch_background.xml => launcher_background.xml} (100%) create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/drawable/launcher_foreground.xml create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 frontend/appflowy_flutter/android/app/src/main/res/values/ic_launcher_background.xml diff --git a/frontend/appflowy_flutter/android/app/src/main/ic_launcher-playstore.png b/frontend/appflowy_flutter/android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..c691e14bdc563ada43b7e74aa83fd38ff4dd925e GIT binary patch literal 45380 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

@zhy_@ZV@vzj86Y@>OG} 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%0D*ALgThlH|R7Qjb2X~Ikk`alAA-R{=eE4 zKq76`;Q(Quclv%p*>hoC5S_HfXWgbQjxs{oSFNp~K3QS2*fJZvTGz>YjnevNQ#b3}27*{JnLfB&F7u%L9nx_}xUw>m^Mj0n z&lHfbVg=qhH@EbLF&1$U8_wr* zonW&$zHbyM;D9n4y?Vh@PXLl?C3ed{JR>gxpn!`{;qfcQrQVFH1`IaW>mn$gePY;q8v2e%DD4AES6-81b15=7C6sv^&ad%%^!0`W+J~s6zpXf70n4EF=_nug@uK4M#zQa zCi_QCQ@(8&*(TkaJ}i)W=0FWDv}eG7enr(jHz92o2PFXE^-Rj;(yNon3^z{Crz#pTZo5y2?pdR#8oK3MGam2#E=%74l0>mbr>7refWF8`bv7fh zMD;%fp_NQ-^{{MKv}G8?HYB*u)?n}xRRwvT{;Ts@=+`nWtCu=i-L<5*oxpG7*mHJtq0<{pnr>3UPv})>g%-nS76*NQ1 zK=NU6WLd4&Jp+NMUr<7VW>)JtXb{Ht%+D{p#&8(-)Y3@ixCzx(LEpp|I6wHop!Zr zC;&;1GuUhE*6)|fr4tZBW-8qK&JP3n zp#ap7h1_oUKX_cu2MDE6lZCxD6HkD@rjt|R@i=cV+|9Ts02B^~uf_~fu8cR6lmOdMlo3|**elMw!TadGKTquSPb97q6cudQ3}E|p71A%x7VjSk9% zhUP%>!y>zKb~bt`C6t$0;l;saCD%a*w;9h-h#;y~D{qO%le{8Zry7j1KZJcY8hwN? zy8kZXx@cXKkpZYt7+^LU{SIODRFnG31Q%fd+`qK6e2j4%w`;Q??LX5~KJP<1C3~CL zpgPP(9T|Z5AVl1d&*yVF?AF^TWrvy2KEz{&7{+Qjl}?>hC=|G(wH+4{0P=dRdpaHd z`w1aD?#GOwzQcfrKRZ$YHH2zj)z$5B-wFvmgh61gSYX2yK`8si%F4>w9U>P6fNVC~ z?9`O+L88;WV7+#a8C^QVfN10Z;)4;wrO#^JVh>E+%m_KqWSoc-1tE+U^M&FojH2eP zJ+Z|92ZKSUrm2rmUjBGD2il>?<}k<y~>fmC6x2dFW~$icmO`XT2=ab3{WI=C4%VuUC@m&78$5@A>__gW@n>U0tVhk>pc`FJrGTF3xE)= zX0o|`^Z9(dO&x0KNIlwW)$U@vcg@b3d9mKc94iNsA8s@Q@%#PnwA-v#QtmgZ3#*eH zGt3}@pi-%wm`EgeiI`57Y0LC<N0eE>o~qm(tN#G9E8EZiCp;RhBL77kwrKn>ehAP{)1)uP=(Ddn4F zS8%S1IUVkkbxNj`NW&o)U0LFf_ zxV*w!8FdTq5Dw)~XzFxT)xORswSsypZBqu>^|+Wi15CjX-#fp!_*wu!L29cTxnK=P zEFW~h>pk3Rx4(k{dJ20+Ae9cyjSLg13(=LzRf$CM13eb%6d((QLMN#z{u-q;49#5w zu^W!6j4@47wa?GZ&2d{;+n%Z2UNWlR*LEBZhhL5{dOL#f6vmiG6#3*~vj~cFcLV%R zI)p%}mMX7|*Msd_-5H(cEd$M-r(paA2*HuO(-H41#G3~joNOaE=&07G^*vNH-G!)F zELXNAlga-!<|g9FH*5KuI*~y(~2?m4bs)}+RLU12K2)9l)=GHWRGgW0hhI0K2~$sj;koz5sZ>&wLB#IV_G;x( z3_!+M12Ft}K?nlM*k7vE%9V*k>N+E`Qe00_}*l&VX~WHM{Ks=;MjUp=uC zX&Sg3;|?LG4u>Ox8QX(lFpUA83Sj6dIb1FPs0ATJ5mL(8Y&un|kpx0$J3{Es>2#VO zRq_%A6NcnG>mAMVw{d)92$=Nz=o-zr(?olI+XUR$IEsvgAB1a2;e9g})oem^*CTz> zIna8Tm5b5*cc=TMz|;`dCtx$|6hkmGfOd)nF#9BS1IX+vxXI)H|BuM$kfKa43IG5A M07*qoM6N<$g2>1*9RL6T literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..727cb0c58a7f3c09c1422364d0c230c07f2e7de6 GIT binary patch literal 10620 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 literal 17845 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%0DjHkaoFAzX;XSd+9p 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 LhC-#BWzhczk#P|v literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..ad1543e064b8c70ad56f15ef59f466e559f76b14 GIT binary patch literal 11187 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%0DMe<;O_PWK6tpq#W~Vf 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 Ds|0zN literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..010733d23d4a744c4ff21d7f3a0b5a7e98d133a7 GIT binary patch literal 15848 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 + + #FFFFFF + \ No newline at end of file From c2d7c5360d6b1e3bc0eafcfc149a771063bb5500 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sat, 17 Aug 2024 21:51:36 +0800 Subject: [PATCH 12/26] chore: remove file size limit (#5974) * chore: remove file size limit * chore: flutter analyze --- .../editor_plugins/file/file_util.dart | 9 --------- .../editor_plugins/image/image_util.dart | 9 --------- .../_shared/image_upload/UploadImage.tsx | 16 +++++++--------- 3 files changed, 7 insertions(+), 27 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart index cedeaec2ee..f4066a94f2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_service.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/file_extension.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; import 'package:appflowy_backend/dispatch/error.dart'; import 'package:appflowy_backend/log.dart'; @@ -39,14 +38,6 @@ Future<(String? path, String? errorMessage)> saveFileToCloudStorage( String localFilePath, String documentId, ) async { - final size = localFilePath.fileSize; - if (size == null || size > 10 * 1024 * 1024) { - // 10MB - return ( - null, - LocaleKeys.document_plugins_file_fileTooBigError.tr(), - ); - } final documentService = DocumentService(); Log.debug("Uploading file from local path: $localFilePath"); final result = await documentService.uploadFile( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart index 6e650f1bf8..e7b818ead7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart @@ -7,7 +7,6 @@ import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; import 'package:appflowy/shared/custom_image_cache_manager.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/file_extension.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_backend/dispatch/error.dart'; @@ -47,14 +46,6 @@ Future<(String? path, String? errorMessage)> saveImageToCloudStorage( String localImagePath, String documentId, ) async { - final size = localImagePath.fileSize; - if (size == null || size > 10 * 1024 * 1024) { - // 10MB - return ( - null, - LocaleKeys.document_imageBlock_uploadImageErrorImageSizeTooBig.tr(), - ); - } final documentService = DocumentService(); Log.debug("Uploading image local path: $localImagePath"); final result = await documentService.uploadFile( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadImage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadImage.tsx index a6b66a4c1f..d39da68caf 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadImage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadImage.tsx @@ -11,15 +11,13 @@ export function UploadImage({ onDone }: { onDone?: (url: string) => void }) { const checkTauriFile = useCallback( async (url: string) => { - const { readBinaryFile } = await import('@tauri-apps/api/fs'); - - const buffer = await readBinaryFile(url); - const blob = new Blob([buffer]); - - if (blob.size > MAX_IMAGE_SIZE) { - notify.error(t('document.imageBlock.error.invalidImageSize')); - return false; - } + // const { readBinaryFile } = await import('@tauri-apps/api/fs'); + // const buffer = await readBinaryFile(url); + // const blob = new Blob([buffer]); + // if (blob.size > MAX_IMAGE_SIZE) { + // notify.error(t('document.imageBlock.error.invalidImageSize')); + // return false; + // } return true; }, From fd5299a13d2c6782282cc5caf5a46cef5872ae7d Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Sun, 18 Aug 2024 05:16:42 +0200 Subject: [PATCH 13/26] move to latest appflowy collab version (#5894) * chore: move to latest appflowy collab version * chore: filter mapping * chore: remove mutex folder * chore: cleanup borrow checker issues * chore: fixed flowy user crate compilation errors * chore: removed parking lot crate * chore: adjusting non locking approach * chore: remove with folder method * chore: fix folder manager * chore: fixed workspace database compilation errors * chore: initialize database plugins * chore: fix locks in flowy core * chore: remove supabase * chore: async traits * chore: add mutexes in dart ffi * chore: post rebase fixes * chore: remove supabase dart code * chore: fix deadlock * chore: fix page_id is empty * chore: use data source to init collab * chore: fix user awareness test * chore: fix database deadlock * fix: initialize user awareness * chore: fix open workspace test * chore: fix import csv * chore: fix update row meta deadlock * chore: fix document size test * fix: timestamp set/get type convert * fix: calculation * chore: revert Arc to Rc * chore: attach plugin to database and database row * chore: async get row * chore: clippy * chore: fix tauri build * chore: clippy * fix: duplicate view deadlock * chore: fmt * chore: tauri build --------- Co-authored-by: nathan --- .../cloud/supabase_auth_test.dart | 146 +-- .../shared/auth_operation.dart | 30 - .../integration_test/shared/base.dart | 17 - frontend/appflowy_flutter/ios/Podfile.lock | 4 +- .../appflowy_flutter/lib/env/backend_env.dart | 30 - .../appflowy_flutter/lib/env/cloud_env.dart | 67 +- .../database/application/row/row_service.dart | 8 + .../grid/application/row/row_bloc.dart | 2 + .../lib/startup/deps_resolver.dart | 4 - .../appflowy_flutter/lib/startup/startup.dart | 1 - .../startup/tasks/appflowy_cloud_task.dart | 3 +- .../lib/startup/tasks/prelude.dart | 1 - .../lib/startup/tasks/rust_sdk.dart | 1 - .../lib/startup/tasks/supabase_task.dart | 118 --- .../auth/af_cloud_mock_auth_service.dart | 2 +- .../auth/supabase_auth_service.dart | 252 ----- .../auth/supabase_mock_auth_service.dart | 113 --- .../settings/settings_dialog_bloc.dart | 1 - .../settings/supabase_cloud_setting_bloc.dart | 103 -- .../settings/supabase_cloud_urls_bloc.dart | 128 --- .../settings/widgets/setting_cloud.dart | 8 - .../widgets/setting_supabase_cloud.dart | 339 ------- frontend/appflowy_tauri/src-tauri/Cargo.lock | 75 +- frontend/appflowy_tauri/src-tauri/Cargo.toml | 14 +- frontend/appflowy_tauri/src-tauri/src/init.rs | 18 +- frontend/appflowy_tauri/src-tauri/src/main.rs | 7 +- .../appflowy_tauri/src-tauri/src/request.rs | 4 +- .../appflowy_web_app/src-tauri/Cargo.lock | 75 +- .../appflowy_web_app/src-tauri/Cargo.toml | 14 +- .../appflowy_web_app/src-tauri/src/init.rs | 18 +- .../appflowy_web_app/src-tauri/src/main.rs | 2 +- .../appflowy_web_app/src-tauri/src/request.rs | 4 +- frontend/rust-lib/Cargo.lock | 77 +- frontend/rust-lib/Cargo.toml | 20 +- frontend/rust-lib/collab-integrate/Cargo.toml | 6 +- .../collab-integrate/src/collab_builder.rs | 371 ++++---- frontend/rust-lib/collab-integrate/src/lib.rs | 1 - .../src/native/plugin_provider.rs | 11 +- frontend/rust-lib/dart-ffi/Cargo.toml | 1 - frontend/rust-lib/dart-ffi/src/env_serde.rs | 5 +- frontend/rust-lib/dart-ffi/src/lib.rs | 19 +- .../event-integration-test/Cargo.toml | 1 - .../src/database_event.rs | 2 +- .../src/document/document_event.rs | 4 +- .../src/document_event.rs | 17 +- .../src/event_builder.rs | 3 +- .../src/folder_event.rs | 12 +- .../event-integration-test/src/lib.rs | 21 +- .../event-integration-test/src/user_event.rs | 47 +- .../tests/folder/local_test/script.rs | 16 + .../user/af_cloud_test/workspace_test.rs | 20 +- .../event-integration-test/tests/util.rs | 113 +-- frontend/rust-lib/flowy-ai/Cargo.toml | 2 +- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 3 +- .../flowy-ai/src/local_ai/local_llm_chat.rs | 14 +- .../src/local_ai/local_llm_resource.rs | 80 +- frontend/rust-lib/flowy-core/Cargo.toml | 3 +- frontend/rust-lib/flowy-core/src/config.rs | 9 +- .../src/deps_resolve/folder_deps.rs | 7 +- .../flowy-core/src/integrate/server.rs | 70 +- .../flowy-core/src/integrate/trait_impls.rs | 303 +++--- .../rust-lib/flowy-core/src/integrate/user.rs | 11 +- frontend/rust-lib/flowy-core/src/lib.rs | 12 - .../rust-lib/flowy-database-pub/src/cloud.rs | 17 +- frontend/rust-lib/flowy-database2/Cargo.toml | 2 +- .../flowy-database2/src/event_handler.rs | 249 +++-- .../rust-lib/flowy-database2/src/event_map.rs | 162 ++-- .../rust-lib/flowy-database2/src/manager.rs | 345 ++++--- .../src/services/calculations/cache.rs | 3 +- .../src/services/calculations/controller.rs | 45 +- .../src/services/calculations/entities.rs | 45 +- .../src/services/cell/cell_data_cache.rs | 3 +- .../src/services/database/database_editor.rs | 883 ++++++++++-------- .../src/services/database/database_observe.rs | 25 +- .../src/services/database_view/layout_deps.rs | 72 +- .../database_view/view_calculations.rs | 38 +- .../src/services/database_view/view_editor.rs | 154 +-- .../src/services/database_view/view_filter.rs | 28 +- .../src/services/database_view/view_group.rs | 62 +- .../services/database_view/view_operation.rs | 85 +- .../src/services/database_view/view_sort.rs | 41 +- .../src/services/database_view/views.rs | 19 +- .../src/services/field/field_operation.rs | 17 +- .../checkbox_type_option.rs | 2 +- .../checkbox_type_option_entities.rs | 10 +- .../checklist_type_option/checklist.rs | 2 +- .../checklist_entities.rs | 10 +- .../date_type_option/date_type_option.rs | 18 +- .../date_type_option_entities.rs | 29 +- .../number_type_option/number_type_option.rs | 52 +- .../relation_type_option/relation.rs | 10 +- .../relation_type_option/relation_entities.rs | 6 +- .../multi_select_type_option.rs | 10 +- .../selection_type_option/select_ids.rs | 10 +- .../single_select_type_option.rs | 8 +- .../summary_type_option/summary.rs | 8 +- .../summary_type_option/summary_entities.rs | 10 +- .../text_type_option/text_type_option.rs | 19 +- .../type_options/time_type_option/time.rs | 2 +- .../time_type_option/time_entities.rs | 10 +- .../timestamp_type_option.rs | 22 +- .../timestamp_type_option_entities.rs | 12 +- .../translate_type_option/translate.rs | 23 +- .../translate_entities.rs | 10 +- .../field/type_options/type_option_cell.rs | 38 +- .../url_type_option/url_type_option.rs | 17 +- .../url_type_option_entities.rs | 13 +- .../src/services/field_settings/entities.rs | 29 +- .../field_settings/field_settings_builder.rs | 5 +- .../src/services/filter/controller.rs | 21 +- .../src/services/filter/entities.rs | 67 +- .../src/services/group/action.rs | 18 +- .../src/services/group/configuration.rs | 10 +- .../src/services/group/controller.rs | 26 +- .../controller_impls/checkbox_controller.rs | 12 +- .../group/controller_impls/date_controller.rs | 11 +- .../controller_impls/default_controller.rs | 11 +- .../multi_select_controller.rs | 17 +- .../single_select_controller.rs | 17 +- .../group/controller_impls/url_controller.rs | 11 +- .../src/services/group/entities.rs | 62 +- .../src/services/setting/entities.rs | 72 +- .../src/services/share/csv/export.rs | 8 +- .../src/services/share/csv/import.rs | 21 +- .../src/services/sort/controller.rs | 13 +- .../src/services/sort/entities.rs | 19 +- .../flowy-database2/src/utils/cache.rs | 42 +- .../tests/database/block_test/row_test.rs | 6 +- .../tests/database/cell_test/test.rs | 10 +- .../tests/database/database_editor.rs | 35 +- .../database/field_settings_test/test.rs | 9 +- .../tests/database/field_test/script.rs | 14 +- .../tests/database/field_test/test.rs | 24 +- .../filter_test/checklist_filter_test.rs | 2 +- .../tests/database/filter_test/script.rs | 2 +- .../filter_test/select_option_filter_test.rs | 24 +- .../tests/database/group_test/script.rs | 5 +- .../tests/database/layout_test/script.rs | 5 +- .../pre_fill_row_according_to_filter_test.rs | 32 +- .../pre_fill_row_with_payload_test.rs | 38 +- .../database/pre_fill_cell_test/script.rs | 2 +- .../tests/database/share_test/export_test.rs | 4 +- .../database/sort_test/multi_sort_test.rs | 8 +- .../tests/database/sort_test/script.rs | 2 +- .../database/sort_test/single_sort_test.rs | 34 +- .../rust-lib/flowy-document-pub/src/cloud.rs | 15 +- frontend/rust-lib/flowy-document/Cargo.toml | 1 - .../rust-lib/flowy-document/src/document.rs | 124 +-- .../flowy-document/src/event_handler.rs | 36 +- .../rust-lib/flowy-document/src/manager.rs | 221 +++-- .../tests/document/document_insert_test.rs | 8 +- .../tests/document/document_redo_undo_test.rs | 6 +- .../tests/document/document_test.rs | 55 +- .../flowy-document/tests/document/util.rs | 43 +- frontend/rust-lib/flowy-error/src/code.rs | 4 + .../rust-lib/flowy-folder-pub/src/cloud.rs | 37 +- frontend/rust-lib/flowy-folder/Cargo.toml | 2 +- .../flowy-folder/src/event_handler.rs | 10 +- frontend/rust-lib/flowy-folder/src/manager.rs | 844 +++++++++-------- .../rust-lib/flowy-folder/src/manager_init.rs | 104 +-- .../flowy-folder/src/manager_observer.rs | 99 +- .../flowy-folder/src/manager_test_util.rs | 8 +- frontend/rust-lib/flowy-folder/src/util.rs | 2 +- .../flowy-folder/src/view_operation.rs | 1 - frontend/rust-lib/flowy-server-pub/src/lib.rs | 4 - .../flowy-server-pub/src/supabase_config.rs | 41 - frontend/rust-lib/flowy-server/Cargo.toml | 3 +- .../src/af_cloud/impls/database.rs | 124 ++- .../src/af_cloud/impls/document.rs | 101 +- .../flowy-server/src/af_cloud/impls/folder.rs | 279 +++--- .../af_cloud/impls/user/cloud_service_impl.rs | 826 ++++++++-------- frontend/rust-lib/flowy-server/src/lib.rs | 3 - .../src/local_server/impls/database.rs | 79 +- .../src/local_server/impls/document.rs | 30 +- .../src/local_server/impls/folder.rs | 102 +- .../src/local_server/impls/user.rs | 231 +++-- .../flowy-server/src/local_server/server.rs | 5 +- frontend/rust-lib/flowy-server/src/server.rs | 10 +- .../src/supabase/api/collab_storage.rs | 17 +- .../flowy-server/src/supabase/api/document.rs | 2 +- .../src/supabase/api/postgres_server.rs | 10 +- .../flowy-server/src/supabase/api/user.rs | 29 +- .../src/supabase/file_storage/plan.rs | 37 + .../flowy-server/src/supabase/server.rs | 31 +- .../flowy-server/tests/af_cloud_test/util.rs | 5 +- frontend/rust-lib/flowy-sqlite/Cargo.toml | 1 - frontend/rust-lib/flowy-sqlite/src/kv/kv.rs | 6 +- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 167 ++-- .../rust-lib/flowy-user-pub/src/entities.rs | 3 - frontend/rust-lib/flowy-user/Cargo.toml | 5 +- .../src/anon_user/migrate_anon_user_collab.rs | 124 +-- .../rust-lib/flowy-user/src/anon_user/mod.rs | 6 +- .../anon_user/sync_supabase_user_collab.rs | 350 ++++--- .../rust-lib/flowy-user/src/entities/auth.rs | 4 +- .../rust-lib/flowy-user/src/event_handler.rs | 10 +- .../src/migrations/document_empty_content.rs | 19 +- .../rust-lib/flowy-user/src/migrations/mod.rs | 3 +- .../flowy-user/src/migrations/util.rs | 15 +- .../migrations/workspace_and_favorite_v1.rs | 8 +- .../src/migrations/workspace_trash_v1.rs | 4 +- .../src/services/authenticate_user.rs | 53 +- .../flowy-user/src/services/cloud_config.rs | 4 +- .../data_import/appflowy_data_import.rs | 188 ++-- .../src/services/data_import/importer.rs | 7 +- .../rust-lib/flowy-user/src/services/db.rs | 86 +- .../flowy-user/src/user_manager/manager.rs | 71 +- .../src/user_manager/manager_history_user.rs | 5 +- .../user_manager/manager_user_awareness.rs | 330 ++++--- .../user_manager/manager_user_workspace.rs | 11 +- frontend/rust-lib/lib-dispatch/Cargo.toml | 1 - .../lib-dispatch/src/module/module.rs | 25 +- .../rust-lib/lib-infra/src/native/future.rs | 30 - 212 files changed, 5068 insertions(+), 6341 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart delete mode 100644 frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart delete mode 100644 frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/supabase_cloud_setting_bloc.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/supabase_cloud_urls_bloc.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart delete mode 100644 frontend/rust-lib/flowy-server-pub/src/supabase_config.rs create mode 100644 frontend/rust-lib/flowy-server/src/supabase/file_storage/plan.rs diff --git a/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart b/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart index 15c9c3c347..71cbc11431 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart @@ -1,93 +1,93 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/workspace/application/settings/prelude.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; +// import 'package:appflowy/env/cloud_env.dart'; +// import 'package:appflowy/workspace/application/settings/prelude.dart'; +// import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; +// import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:integration_test/integration_test.dart'; -import '../shared/util.dart'; +// import '../shared/util.dart'; -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); +// void main() { +// IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('supabase auth', () { - testWidgets('sign in with supabase', (tester) async { - await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - }); +// group('supabase auth', () { +// testWidgets('sign in with supabase', (tester) async { +// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase); +// await tester.tapGoogleLoginInButton(); +// await tester.expectToSeeHomePageWithGetStartedPage(); +// }); - testWidgets('sign out with supabase', (tester) async { - await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase); - await tester.tapGoogleLoginInButton(); +// testWidgets('sign out with supabase', (tester) async { +// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase); +// await tester.tapGoogleLoginInButton(); - // Open the setting page and sign out - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.account); - await tester.logout(); +// // Open the setting page and sign out +// await tester.openSettings(); +// await tester.openSettingsPage(SettingsPage.account); +// await tester.logout(); - // Go to the sign in page again - await tester.pumpAndSettle(const Duration(seconds: 1)); - tester.expectToSeeGoogleLoginButton(); - }); +// // Go to the sign in page again +// await tester.pumpAndSettle(const Duration(seconds: 1)); +// tester.expectToSeeGoogleLoginButton(); +// }); - testWidgets('sign in as anonymous', (tester) async { - await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase); - await tester.tapSignInAsGuest(); +// testWidgets('sign in as anonymous', (tester) async { +// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase); +// await tester.tapSignInAsGuest(); - // should not see the sync setting page when sign in as anonymous - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.account); +// // should not see the sync setting page when sign in as anonymous +// await tester.openSettings(); +// await tester.openSettingsPage(SettingsPage.account); - // Scroll to sign-out - await tester.scrollUntilVisible( - find.byType(SignInOutButton), - 100, - scrollable: find.findSettingsScrollable(), - ); - await tester.tapButton(find.byType(SignInOutButton)); +// // Scroll to sign-out +// await tester.scrollUntilVisible( +// find.byType(SignInOutButton), +// 100, +// scrollable: find.findSettingsScrollable(), +// ); +// await tester.tapButton(find.byType(SignInOutButton)); - tester.expectToSeeGoogleLoginButton(); - }); +// tester.expectToSeeGoogleLoginButton(); +// }); - // testWidgets('enable encryption', (tester) async { - // await tester.initializeAppFlowy(cloudType: CloudType.supabase); - // await tester.tapGoogleLoginInButton(); +// // testWidgets('enable encryption', (tester) async { +// // await tester.initializeAppFlowy(cloudType: CloudType.supabase); +// // await tester.tapGoogleLoginInButton(); - // // Open the setting page and sign out - // await tester.openSettings(); - // await tester.openSettingsPage(SettingsPage.cloud); +// // // Open the setting page and sign out +// // await tester.openSettings(); +// // await tester.openSettingsPage(SettingsPage.cloud); - // // the switch should be off by default - // tester.assertEnableEncryptSwitchValue(false); - // await tester.toggleEnableEncrypt(); +// // // the switch should be off by default +// // tester.assertEnableEncryptSwitchValue(false); +// // await tester.toggleEnableEncrypt(); - // // the switch should be on after toggling - // tester.assertEnableEncryptSwitchValue(true); +// // // the switch should be on after toggling +// // tester.assertEnableEncryptSwitchValue(true); - // // the switch can not be toggled back to off - // await tester.toggleEnableEncrypt(); - // tester.assertEnableEncryptSwitchValue(true); - // }); +// // // the switch can not be toggled back to off +// // await tester.toggleEnableEncrypt(); +// // tester.assertEnableEncryptSwitchValue(true); +// // }); - testWidgets('enable sync', (tester) async { - await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase); - await tester.tapGoogleLoginInButton(); +// testWidgets('enable sync', (tester) async { +// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase); +// await tester.tapGoogleLoginInButton(); - // Open the setting page and sign out - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.cloud); +// // Open the setting page and sign out +// await tester.openSettings(); +// await tester.openSettingsPage(SettingsPage.cloud); - // the switch should be on by default - tester.assertSupabaseEnableSyncSwitchValue(true); - await tester.toggleEnableSync(SupabaseEnableSync); +// // the switch should be on by default +// tester.assertSupabaseEnableSyncSwitchValue(true); +// await tester.toggleEnableSync(SupabaseEnableSync); - // the switch should be off - tester.assertSupabaseEnableSyncSwitchValue(false); +// // the switch should be off +// tester.assertSupabaseEnableSyncSwitchValue(false); - // the switch should be on after toggling - await tester.toggleEnableSync(SupabaseEnableSync); - tester.assertSupabaseEnableSyncSwitchValue(true); - }); - }); -} +// // the switch should be on after toggling +// await tester.toggleEnableSync(SupabaseEnableSync); +// tester.assertSupabaseEnableSyncSwitchValue(true); +// }); +// }); +// } diff --git a/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart b/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart index 56815714c0..e01e02c6e1 100644 --- a/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart +++ b/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart @@ -2,7 +2,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -52,26 +51,6 @@ extension AppFlowyAuthTest on WidgetTester { assert(isSwitched == value); } - void assertEnableEncryptSwitchValue(bool value) { - assertSwitchValue( - find.descendant( - of: find.byType(EnableEncrypt), - matching: find.byWidgetPredicate((widget) => widget is Switch), - ), - value, - ); - } - - void assertSupabaseEnableSyncSwitchValue(bool value) { - assertSwitchValue( - find.descendant( - of: find.byType(SupabaseEnableSync), - matching: find.byWidgetPredicate((widget) => widget is Switch), - ), - value, - ); - } - void assertAppFlowyCloudEnableSyncSwitchValue(bool value) { assertToggleValue( find.descendant( @@ -82,15 +61,6 @@ extension AppFlowyAuthTest on WidgetTester { ); } - Future toggleEnableEncrypt() async { - final finder = find.descendant( - of: find.byType(EnableEncrypt), - matching: find.byWidgetPredicate((widget) => widget is Switch), - ); - - await tapButton(finder); - } - Future toggleEnableSync(Type syncButton) async { final finder = find.descendant( of: find.byType(syncButton), diff --git a/frontend/appflowy_flutter/integration_test/shared/base.dart b/frontend/appflowy_flutter/integration_test/shared/base.dart index 16a576154f..371cd9b839 100644 --- a/frontend/appflowy_flutter/integration_test/shared/base.dart +++ b/frontend/appflowy_flutter/integration_test/shared/base.dart @@ -7,7 +7,6 @@ import 'package:appflowy/startup/entry_point.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy/user/application/auth/supabase_mock_auth_service.dart'; import 'package:appflowy/user/presentation/presentation.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; @@ -55,8 +54,6 @@ extension AppFlowyTestBase on WidgetTester { switch (cloudType) { case AuthenticatorType.local: break; - case AuthenticatorType.supabase: - break; case AuthenticatorType.appflowyCloudSelfHost: rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com"; rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password"; @@ -75,13 +72,6 @@ extension AppFlowyTestBase on WidgetTester { case AuthenticatorType.local: await useLocalServer(); break; - case AuthenticatorType.supabase: - await useTestSupabaseCloud(); - getIt.unregister(); - getIt.registerFactory( - () => SupabaseMockAuthService(), - ); - break; case AuthenticatorType.appflowyCloudSelfHost: await useTestSelfHostedAppFlowyCloud(); getIt.unregister(); @@ -242,13 +232,6 @@ extension AppFlowyFinderTestBase on CommonFinders { } } -Future useTestSupabaseCloud() async { - await useSupabaseCloud( - url: TestEnv.supabaseUrl, - anonKey: TestEnv.supabaseAnonKey, - ); -} - Future useTestSelfHostedAppFlowyCloud() async { await useSelfHostedAppFlowyCloudWithURL(TestEnv.afCloudUrl); } diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 8829c71074..af96ce7ccb 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -174,7 +174,7 @@ SPEC CHECKSUMS: file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c + fluttertoast: 723e187574b149e68e63ca4d39b837586b903cfa image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 @@ -196,4 +196,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca -COCOAPODS: 1.15.2 +COCOAPODS: 1.11.3 diff --git a/frontend/appflowy_flutter/lib/env/backend_env.dart b/frontend/appflowy_flutter/lib/env/backend_env.dart index fa0bf575a3..f8aa715a40 100644 --- a/frontend/appflowy_flutter/lib/env/backend_env.dart +++ b/frontend/appflowy_flutter/lib/env/backend_env.dart @@ -13,7 +13,6 @@ class AppFlowyConfiguration { required this.device_id, required this.platform, required this.authenticator_type, - required this.supabase_config, required this.appflowy_cloud_config, required this.envs, }); @@ -28,41 +27,12 @@ class AppFlowyConfiguration { final String device_id; final String platform; final int authenticator_type; - final SupabaseConfiguration supabase_config; final AppFlowyCloudConfiguration appflowy_cloud_config; final Map envs; Map toJson() => _$AppFlowyConfigurationToJson(this); } -@JsonSerializable() -class SupabaseConfiguration { - SupabaseConfiguration({ - required this.url, - required this.anon_key, - }); - - factory SupabaseConfiguration.fromJson(Map json) => - _$SupabaseConfigurationFromJson(json); - - /// Indicates whether the sync feature is enabled. - final String url; - final String anon_key; - - Map toJson() => _$SupabaseConfigurationToJson(this); - - static SupabaseConfiguration defaultConfig() { - return SupabaseConfiguration( - url: '', - anon_key: '', - ); - } - - bool get isValid { - return url.isNotEmpty && anon_key.isNotEmpty; - } -} - @JsonSerializable() class AppFlowyCloudConfiguration { AppFlowyCloudConfiguration({ diff --git a/frontend/appflowy_flutter/lib/env/cloud_env.dart b/frontend/appflowy_flutter/lib/env/cloud_env.dart index 9e8ea0d4f9..fcad1a1f2f 100644 --- a/frontend/appflowy_flutter/lib/env/cloud_env.dart +++ b/frontend/appflowy_flutter/lib/env/cloud_env.dart @@ -21,9 +21,6 @@ Future _setAuthenticatorType(AuthenticatorType ty) async { case AuthenticatorType.local: await getIt().set(KVKeys.kCloudType, 0.toString()); break; - case AuthenticatorType.supabase: - await getIt().set(KVKeys.kCloudType, 1.toString()); - break; case AuthenticatorType.appflowyCloud: await getIt().set(KVKeys.kCloudType, 2.toString()); break; @@ -63,8 +60,6 @@ Future getAuthenticatorType() async { switch (value ?? "0") { case "0": return AuthenticatorType.local; - case "1": - return AuthenticatorType.supabase; case "2": return AuthenticatorType.appflowyCloud; case "3": @@ -93,10 +88,6 @@ Future getAuthenticatorType() async { /// Returns `false` otherwise. bool get isAuthEnabled { final env = getIt(); - if (env.authenticatorType == AuthenticatorType.supabase) { - return env.supabaseConfig.isValid; - } - if (env.authenticatorType.isAppFlowyCloudEnabled) { return env.appflowyCloudConfig.isValid; } @@ -104,19 +95,6 @@ bool get isAuthEnabled { return false; } -/// Checks if Supabase is enabled. -/// -/// This getter evaluates if Supabase should be enabled based on the -/// current integration mode and cloud type setting. -/// -/// Returns: -/// A boolean value indicating whether Supabase is enabled. It returns `true` -/// if the application is in release or develop mode and the current cloud type -/// is `CloudType.supabase`. Otherwise, it returns `false`. -bool get isSupabaseEnabled { - return currentCloudType().isSupabaseEnabled; -} - /// Determines if AppFlowy Cloud is enabled. bool get isAppFlowyCloudEnabled { return currentCloudType().isAppFlowyCloudEnabled; @@ -124,7 +102,6 @@ bool get isAppFlowyCloudEnabled { enum AuthenticatorType { local, - supabase, appflowyCloud, appflowyCloudSelfHost, // The 'appflowyCloudDevelop' type is used for develop purposes only. @@ -137,14 +114,10 @@ enum AuthenticatorType { this == AuthenticatorType.appflowyCloudDevelop || this == AuthenticatorType.appflowyCloud; - bool get isSupabaseEnabled => this == AuthenticatorType.supabase; - int get value { switch (this) { case AuthenticatorType.local: return 0; - case AuthenticatorType.supabase: - return 1; case AuthenticatorType.appflowyCloud: return 2; case AuthenticatorType.appflowyCloudSelfHost: @@ -158,8 +131,6 @@ enum AuthenticatorType { switch (value) { case 0: return AuthenticatorType.local; - case 1: - return AuthenticatorType.supabase; case 2: return AuthenticatorType.appflowyCloud; case 3: @@ -197,25 +168,15 @@ Future useLocalServer() async { await _setAuthenticatorType(AuthenticatorType.local); } -Future useSupabaseCloud({ - required String url, - required String anonKey, -}) async { - await _setAuthenticatorType(AuthenticatorType.supabase); - await setSupabaseServer(url, anonKey); -} - /// Use getIt() to get the shared environment. class AppFlowyCloudSharedEnv { AppFlowyCloudSharedEnv({ required AuthenticatorType authenticatorType, required this.appflowyCloudConfig, - required this.supabaseConfig, }) : _authenticatorType = authenticatorType; final AuthenticatorType _authenticatorType; final AppFlowyCloudConfiguration appflowyCloudConfig; - final SupabaseConfiguration supabaseConfig; AuthenticatorType get authenticatorType => _authenticatorType; @@ -229,10 +190,6 @@ class AppFlowyCloudSharedEnv { ? await getAppFlowyCloudConfig(authenticatorType) : AppFlowyCloudConfiguration.defaultConfig(); - final supabaseCloudConfig = authenticatorType.isSupabaseEnabled - ? await getSupabaseCloudConfig() - : SupabaseConfiguration.defaultConfig(); - // In the backend, the value '2' represents the use of AppFlowy Cloud. However, in the frontend, // we distinguish between [AuthenticatorType.appflowyCloudSelfHost] and [AuthenticatorType.appflowyCloud]. // When the cloud type is [AuthenticatorType.appflowyCloudSelfHost] in the frontend, it should be @@ -244,7 +201,6 @@ class AppFlowyCloudSharedEnv { return AppFlowyCloudSharedEnv( authenticatorType: authenticatorType, appflowyCloudConfig: appflowyCloudConfig, - supabaseConfig: supabaseCloudConfig, ); } else { // Using the cloud settings from the .env file. @@ -257,7 +213,6 @@ class AppFlowyCloudSharedEnv { return AppFlowyCloudSharedEnv( authenticatorType: AuthenticatorType.fromValue(Env.authenticatorType), appflowyCloudConfig: appflowyCloudConfig, - supabaseConfig: SupabaseConfiguration.defaultConfig(), ); } } @@ -265,8 +220,7 @@ class AppFlowyCloudSharedEnv { @override String toString() { return 'authenticator: $_authenticatorType\n' - 'appflowy: ${appflowyCloudConfig.toJson()}\n' - 'supabase: ${supabaseConfig.toJson()})\n'; + 'appflowy: ${appflowyCloudConfig.toJson()}\n'; } } @@ -354,22 +308,3 @@ Future setSupabaseServer( await getIt().set(KVKeys.kSupabaseAnonKey, anonKey); } } - -Future getSupabaseCloudConfig() async { - final url = await _getSupabaseUrl(); - final anonKey = await _getSupabaseAnonKey(); - return SupabaseConfiguration( - url: url, - anon_key: anonKey, - ); -} - -Future _getSupabaseUrl() async { - final result = await getIt().get(KVKeys.kSupabaseURL); - return result ?? ''; -} - -Future _getSupabaseAnonKey() async { - final result = await getIt().get(KVKeys.kSupabaseAnonKey); - return result ?? ''; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart index 1866891336..c5e71ba78b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart @@ -37,6 +37,14 @@ class RowBackendService { return DatabaseEventCreateRow(payload).send(); } + Future> initRow(RowId rowId) async { + final payload = RowIdPB() + ..viewId = viewId + ..rowId = rowId; + + return DatabaseEventInitRow(payload).send(); + } + Future> createRowBefore(RowId rowId) { return createRow( viewId: viewId, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_bloc.dart index a0c0467b95..322d7d59a4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_bloc.dart @@ -23,6 +23,8 @@ class RowBloc extends Bloc { }) : _rowBackendSvc = RowBackendService(viewId: viewId), _rowController = rowController, super(RowState.initial()) { + _rowBackendSvc.initRow(rowId); + _dispatch(); _startListening(); _init(); diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index d19e0b3f7a..4136cfd07d 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -12,7 +12,6 @@ import 'package:appflowy/startup/tasks/appflowy_cloud_task.dart'; import 'package:appflowy/user/application/ai_service.dart'; import 'package:appflowy/user/application/auth/af_cloud_auth_service.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy/user/application/auth/supabase_auth_service.dart'; import 'package:appflowy/user/application/prelude.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/user/application/user_listener.dart'; @@ -124,9 +123,6 @@ void _resolveUserDeps(GetIt getIt, IntegrationMode mode) { ), ); break; - case AuthenticatorType.supabase: - getIt.registerFactory(() => SupabaseAuthService()); - break; case AuthenticatorType.appflowyCloud: case AuthenticatorType.appflowyCloudSelfHost: case AuthenticatorType.appflowyCloudDevelop: diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index 213e5f6227..85be02f6a6 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -133,7 +133,6 @@ class FlowyRunner { // It is unable to get the device information from the test environment. const ApplicationInfoTask(), const HotKeyTask(), - if (isSupabaseEnabled) InitSupabaseTask(), if (isAppFlowyCloudEnabled) InitAppFlowyCloudTask(), const InitAppWidgetTask(), const InitPlatformServiceTask(), diff --git a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart index 542e8b75a2..5aad45b3c5 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart @@ -7,7 +7,6 @@ import 'package:app_links/app_links.dart'; import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; -import 'package:appflowy/startup/tasks/supabase_task.dart'; import 'package:appflowy/user/application/auth/auth_error.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/auth/device_id.dart'; @@ -22,6 +21,8 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:url_protocol/url_protocol.dart'; +const appflowyDeepLinkSchema = 'appflowy-flutter'; + class AppFlowyCloudDeepLink { AppFlowyCloudDeepLink() { if (_deeplinkSubscription == null) { diff --git a/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart b/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart index 2c3aced3ab..4be5f0f6f7 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart @@ -12,5 +12,4 @@ export 'platform_service.dart'; export 'recent_service_task.dart'; export 'rust_sdk.dart'; export 'sentry.dart'; -export 'supabase_task.dart'; export 'windows.dart'; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart index c02b450d79..58d6aacbc3 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart @@ -63,7 +63,6 @@ AppFlowyConfiguration _makeAppFlowyConfiguration( device_id: deviceId, platform: Platform.operatingSystem, authenticator_type: env.authenticatorType.value, - supabase_config: env.supabaseConfig, appflowy_cloud_config: env.appflowyCloudConfig, envs: rustEnvs, ); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart deleted file mode 100644 index cb8981acdd..0000000000 --- a/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/user/application/supabase_realtime.dart'; -import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; -import 'package:flutter/foundation.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:path/path.dart' as p; -import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:url_protocol/url_protocol.dart'; - -import '../startup.dart'; - -// ONLY supports in macOS and Windows now. -// -// If you need to update the schema, please update the following files: -// - appflowy_flutter/macos/Runner/Info.plist (macOS) -// - the callback url in Supabase dashboard -const appflowyDeepLinkSchema = 'appflowy-flutter'; -const supabaseLoginCallback = '$appflowyDeepLinkSchema://login-callback'; - -const hiveBoxName = 'appflowy_supabase_authentication'; - -// Used to store the session of the supabase in case of the user switch the different folder. -Supabase? supabase; -SupabaseRealtimeService? realtimeService; - -class InitSupabaseTask extends LaunchTask { - @override - Future initialize(LaunchContext context) async { - if (!isSupabaseEnabled) { - return; - } - - await supabase?.dispose(); - supabase = null; - final initializedSupabase = await Supabase.initialize( - url: getIt().supabaseConfig.url, - anonKey: getIt().supabaseConfig.anon_key, - debug: kDebugMode, - authOptions: const FlutterAuthClientOptions( - localStorage: SupabaseLocalStorage(), - ), - ); - - if (realtimeService != null) { - await realtimeService?.dispose(); - realtimeService = null; - } - realtimeService = SupabaseRealtimeService(supabase: initializedSupabase); - - supabase = initializedSupabase; - - if (Platform.isWindows) { - // register deep link for Windows - registerProtocolHandler(appflowyDeepLinkSchema); - } - } - - @override - Future dispose() async { - await realtimeService?.dispose(); - realtimeService = null; - await supabase?.dispose(); - supabase = null; - } -} - -/// customize the supabase auth storage -/// -/// We don't use the default one because it always save the session in the document directory. -/// When we switch to the different folder, the session still exists. -class SupabaseLocalStorage extends LocalStorage { - const SupabaseLocalStorage(); - - @override - Future initialize() async { - HiveCipher? encryptionCipher; - - // customize the path for Hive - final path = await getIt().getPath(); - Hive.init(p.join(path, 'supabase_auth')); - await Hive.openBox( - hiveBoxName, - encryptionCipher: encryptionCipher, - ); - } - - @override - Future hasAccessToken() { - return Future.value( - Hive.box(hiveBoxName).containsKey( - supabasePersistSessionKey, - ), - ); - } - - @override - Future accessToken() { - return Future.value( - Hive.box(hiveBoxName).get(supabasePersistSessionKey) as String?, - ); - } - - @override - Future removePersistedSession() { - return Hive.box(hiveBoxName).delete(supabasePersistSessionKey); - } - - @override - Future persistSession(String persistSessionString) { - return Hive.box(hiveBoxName).put( - supabasePersistSessionKey, - persistSessionString, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart index 7c33143ff0..fac655b7fc 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart @@ -20,7 +20,7 @@ class AppFlowyCloudMockAuthService implements AuthService { final String userEmail; final BackendAuthService _appFlowyAuthService = - BackendAuthService(AuthenticatorPB.Supabase); + BackendAuthService(AuthenticatorPB.AppFlowyCloud); @override Future> signUp({ diff --git a/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart deleted file mode 100644 index 0dc48d7ef7..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart +++ /dev/null @@ -1,252 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/startup/tasks/prelude.dart'; -import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy/user/application/auth/backend_auth_service.dart'; -import 'package:appflowy/user/application/auth/device_id.dart'; -import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter/foundation.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - -import 'auth_error.dart'; - -class SupabaseAuthService implements AuthService { - SupabaseAuthService(); - - SupabaseClient get _client => Supabase.instance.client; - GoTrueClient get _auth => _client.auth; - - final BackendAuthService _backendAuthService = BackendAuthService( - AuthenticatorPB.Supabase, - ); - - @override - Future> signUp({ - required String name, - required String email, - required String password, - Map params = const {}, - }) async { - // fetch the uuid from supabase. - final response = await _auth.signUp( - email: email, - password: password, - ); - final uuid = response.user?.id; - if (uuid == null) { - return FlowyResult.failure(AuthError.supabaseSignUpError); - } - // assign the uuid to our backend service. - // and will transfer this logic to backend later. - return _backendAuthService.signUp( - name: name, - email: email, - password: password, - params: { - AuthServiceMapKeys.uuid: uuid, - }, - ); - } - - @override - Future> signInWithEmailPassword({ - required String email, - required String password, - Map params = const {}, - }) async { - try { - final response = await _auth.signInWithPassword( - email: email, - password: password, - ); - final uuid = response.user?.id; - if (uuid == null) { - return FlowyResult.failure(AuthError.supabaseSignInError); - } - return _backendAuthService.signInWithEmailPassword( - email: email, - password: password, - params: { - AuthServiceMapKeys.uuid: uuid, - }, - ); - } on AuthException catch (e) { - Log.error(e); - return FlowyResult.failure(AuthError.supabaseSignInError); - } - } - - @override - Future> signUpWithOAuth({ - required String platform, - Map params = const {}, - }) async { - // Before signing in, sign out any existing users. Otherwise, the callback will be triggered even if the user doesn't click the 'Sign In' button on the website - if (_auth.currentUser != null) { - await _auth.signOut(); - } - - final provider = platform.toProvider(); - final completer = supabaseLoginCompleter( - onSuccess: (userId, userEmail) async { - return _setupAuth( - map: { - AuthServiceMapKeys.uuid: userId, - AuthServiceMapKeys.email: userEmail, - AuthServiceMapKeys.deviceId: await getDeviceId(), - }, - ); - }, - ); - - final response = await _auth.signInWithOAuth( - provider, - queryParams: queryParamsForProvider(provider), - redirectTo: supabaseLoginCallback, - ); - if (!response) { - completer.complete( - FlowyResult.failure(AuthError.supabaseSignInWithOauthError), - ); - } - return completer.future; - } - - @override - Future signOut() async { - await _auth.signOut(); - await _backendAuthService.signOut(); - } - - @override - Future> signUpAsGuest({ - Map params = const {}, - }) async { - // supabase don't support guest login. - // so, just forward to our backend. - return _backendAuthService.signUpAsGuest(); - } - - @override - Future> signInWithMagicLink({ - required String email, - Map params = const {}, - }) async { - final completer = supabaseLoginCompleter( - onSuccess: (userId, userEmail) async { - return _setupAuth( - map: { - AuthServiceMapKeys.uuid: userId, - AuthServiceMapKeys.email: userEmail, - AuthServiceMapKeys.deviceId: await getDeviceId(), - }, - ); - }, - ); - - await _auth.signInWithOtp( - email: email, - emailRedirectTo: kIsWeb ? null : supabaseLoginCallback, - ); - return completer.future; - } - - @override - Future> getUser() async { - return UserBackendService.getCurrentUserProfile(); - } - - Future> getSupabaseUser() async { - final user = _auth.currentUser; - if (user == null) { - return FlowyResult.failure(AuthError.supabaseGetUserError); - } - return FlowyResult.success(user); - } - - Future> _setupAuth({ - required Map map, - }) async { - final payload = OauthSignInPB( - authenticator: AuthenticatorPB.Supabase, - map: map, - ); - - return UserEventOauthSignIn(payload).send().then((value) => value); - } -} - -extension on String { - OAuthProvider toProvider() { - switch (this) { - case 'github': - return OAuthProvider.github; - case 'google': - return OAuthProvider.google; - case 'discord': - return OAuthProvider.discord; - default: - throw UnimplementedError(); - } - } -} - -/// Creates a completer that listens to Supabase authentication state changes and -/// completes when a user signs in. -/// -/// This function sets up a listener on Supabase's authentication state. When a user -/// signs in, it triggers the provided [onSuccess] callback with the user's `id` and -/// `email`. Once the [onSuccess] callback is executed and a response is received, -/// the completer completes with the response, and the listener is canceled. -/// -/// Parameters: -/// - [onSuccess]: A callback function that's executed when a user signs in. It -/// should take in a user's `id` and `email` and return a `Future` containing either -/// a `FlowyError` or a `UserProfilePB`. -/// -/// Returns: -/// A completer of type `FlowyResult`. This completer completes -/// with the response from the [onSuccess] callback when a user signs in. -Completer> supabaseLoginCompleter({ - required Future> Function( - String userId, - String userEmail, - ) onSuccess, -}) { - final completer = Completer>(); - late final StreamSubscription subscription; - final auth = Supabase.instance.client.auth; - - subscription = auth.onAuthStateChange.listen((event) async { - final user = event.session?.user; - if (event.event == AuthChangeEvent.signedIn && user != null) { - final response = await onSuccess( - user.id, - user.email ?? user.newEmail ?? '', - ); - // Only cancel the subscription if the Event is signedIn. - await subscription.cancel(); - completer.complete(response); - } - }); - return completer; -} - -Map queryParamsForProvider(OAuthProvider provider) { - switch (provider) { - case OAuthProvider.google: - return { - 'access_type': 'offline', - 'prompt': 'consent', - }; - case OAuthProvider.github: - case OAuthProvider.discord: - default: - return {}; - } -} diff --git a/frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart deleted file mode 100644 index bd2620caaa..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy/user/application/auth/backend_auth_service.dart'; -import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - -import 'auth_error.dart'; - -/// Only used for testing. -class SupabaseMockAuthService implements AuthService { - SupabaseMockAuthService(); - static OauthSignInPB? signInPayload; - - SupabaseClient get _client => Supabase.instance.client; - GoTrueClient get _auth => _client.auth; - - final BackendAuthService _appFlowyAuthService = - BackendAuthService(AuthenticatorPB.Supabase); - - @override - Future> signUp({ - required String name, - required String email, - required String password, - Map params = const {}, - }) async { - throw UnimplementedError(); - } - - @override - Future> signInWithEmailPassword({ - required String email, - required String password, - Map params = const {}, - }) async { - throw UnimplementedError(); - } - - @override - Future> signUpWithOAuth({ - required String platform, - Map params = const {}, - }) async { - const password = "AppFlowyTest123!"; - const email = "supabase_integration_test@appflowy.io"; - try { - if (_auth.currentSession == null) { - try { - await _auth.signInWithPassword( - password: password, - email: email, - ); - } catch (e) { - Log.error(e); - return FlowyResult.failure(AuthError.supabaseSignUpError); - } - } - // Check if the user is already logged in. - final session = _auth.currentSession!; - final uuid = session.user.id; - - // Create the OAuth sign-in payload. - final payload = OauthSignInPB( - authenticator: AuthenticatorPB.Supabase, - map: { - AuthServiceMapKeys.uuid: uuid, - AuthServiceMapKeys.email: email, - AuthServiceMapKeys.deviceId: 'MockDeviceId', - }, - ); - - // Send the sign-in event and handle the response. - return UserEventOauthSignIn(payload).send().then((value) => value); - } on AuthException catch (e) { - Log.error(e); - return FlowyResult.failure(AuthError.supabaseSignInError); - } - } - - @override - Future signOut() async { - // await _auth.signOut(); - await _appFlowyAuthService.signOut(); - } - - @override - Future> signUpAsGuest({ - Map params = const {}, - }) async { - // supabase don't support guest login. - // so, just forward to our backend. - return _appFlowyAuthService.signUpAsGuest(); - } - - @override - Future> signInWithMagicLink({ - required String email, - Map params = const {}, - }) async { - throw UnimplementedError(); - } - - @override - Future> getUser() async { - return UserBackendService.getCurrentUserProfile(); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart index 36f2603dda..f28900d18a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -90,7 +90,6 @@ class SettingsDialogBloc ]) async { if ([ AuthenticatorPB.Local, - AuthenticatorPB.Supabase, ].contains(userProfile.authenticator)) { return false; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/supabase_cloud_setting_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/supabase_cloud_setting_bloc.dart deleted file mode 100644 index 9308a06a98..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/supabase_cloud_setting_bloc.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'package:appflowy/env/backend_env.dart'; -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/plugins/database/application/defines.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'cloud_setting_listener.dart'; - -part 'supabase_cloud_setting_bloc.freezed.dart'; - -class SupabaseCloudSettingBloc - extends Bloc { - SupabaseCloudSettingBloc({ - required CloudSettingPB setting, - }) : _listener = UserCloudConfigListener(), - super(SupabaseCloudSettingState.initial(setting)) { - _dispatch(); - } - - final UserCloudConfigListener _listener; - - @override - Future close() async { - await _listener.stop(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.when( - initial: () async { - _listener.start( - onSettingChanged: (result) { - if (isClosed) { - return; - } - result.fold( - (setting) => - add(SupabaseCloudSettingEvent.didReceiveSetting(setting)), - (error) => Log.error(error), - ); - }, - ); - }, - enableSync: (bool enable) async { - final update = UpdateCloudConfigPB.create()..enableSync = enable; - await updateCloudConfig(update); - }, - didReceiveSetting: (CloudSettingPB setting) { - emit( - state.copyWith( - setting: setting, - loadingState: LoadingState.finish(FlowyResult.success(null)), - ), - ); - }, - enableEncrypt: (bool enable) { - final update = UpdateCloudConfigPB.create()..enableEncrypt = enable; - updateCloudConfig(update); - emit(state.copyWith(loadingState: const LoadingState.loading())); - }, - ); - }, - ); - } - - Future updateCloudConfig(UpdateCloudConfigPB setting) async { - await UserEventSetCloudConfig(setting).send(); - } -} - -@freezed -class SupabaseCloudSettingEvent with _$SupabaseCloudSettingEvent { - const factory SupabaseCloudSettingEvent.initial() = _Initial; - const factory SupabaseCloudSettingEvent.didReceiveSetting( - CloudSettingPB setting, - ) = _DidSyncSupabaseConfig; - const factory SupabaseCloudSettingEvent.enableSync(bool enable) = _EnableSync; - const factory SupabaseCloudSettingEvent.enableEncrypt(bool enable) = - _EnableEncrypt; -} - -@freezed -class SupabaseCloudSettingState with _$SupabaseCloudSettingState { - const factory SupabaseCloudSettingState({ - required LoadingState loadingState, - required SupabaseConfiguration config, - required CloudSettingPB setting, - }) = _SupabaseCloudSettingState; - - factory SupabaseCloudSettingState.initial(CloudSettingPB setting) => - SupabaseCloudSettingState( - loadingState: LoadingState.finish(FlowyResult.success(null)), - setting: setting, - config: getIt().supabaseConfig, - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/supabase_cloud_urls_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/supabase_cloud_urls_bloc.dart deleted file mode 100644 index fdd4cbef21..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/supabase_cloud_urls_bloc.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:appflowy/env/backend_env.dart'; -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'appflowy_cloud_setting_bloc.dart'; - -part 'supabase_cloud_urls_bloc.freezed.dart'; - -class SupabaseCloudURLsBloc - extends Bloc { - SupabaseCloudURLsBloc() : super(SupabaseCloudURLsState.initial()) { - on((event, emit) async { - await event.when( - updateUrl: (String url) { - emit( - state.copyWith( - updatedUrl: url, - showRestartHint: url.isNotEmpty && state.upatedAnonKey.isNotEmpty, - urlError: null, - ), - ); - }, - updateAnonKey: (String anonKey) { - emit( - state.copyWith( - upatedAnonKey: anonKey, - showRestartHint: - anonKey.isNotEmpty && state.updatedUrl.isNotEmpty, - anonKeyError: null, - ), - ); - }, - confirmUpdate: () async { - if (state.updatedUrl.isEmpty) { - emit( - state.copyWith( - urlError: - LocaleKeys.settings_menu_cloudSupabaseUrlCanNotBeEmpty.tr(), - anonKeyError: null, - restartApp: false, - ), - ); - return; - } - - if (state.upatedAnonKey.isEmpty) { - emit( - state.copyWith( - urlError: null, - anonKeyError: LocaleKeys - .settings_menu_cloudSupabaseAnonKeyCanNotBeEmpty - .tr(), - restartApp: false, - ), - ); - return; - } - - validateUrl(state.updatedUrl).fold( - (_) async { - await useSupabaseCloud( - url: state.updatedUrl, - anonKey: state.upatedAnonKey, - ); - - add(const SupabaseCloudURLsEvent.didSaveConfig()); - }, - (error) => emit(state.copyWith(urlError: error)), - ); - }, - didSaveConfig: () { - emit( - state.copyWith( - urlError: null, - anonKeyError: null, - restartApp: true, - ), - ); - }, - ); - }); - } - - Future updateCloudConfig(UpdateCloudConfigPB setting) async { - await UserEventSetCloudConfig(setting).send(); - } -} - -@freezed -class SupabaseCloudURLsEvent with _$SupabaseCloudURLsEvent { - const factory SupabaseCloudURLsEvent.updateUrl(String text) = _UpdateUrl; - const factory SupabaseCloudURLsEvent.updateAnonKey(String text) = - _UpdateAnonKey; - const factory SupabaseCloudURLsEvent.confirmUpdate() = _UpdateConfig; - const factory SupabaseCloudURLsEvent.didSaveConfig() = _DidSaveConfig; -} - -@freezed -class SupabaseCloudURLsState with _$SupabaseCloudURLsState { - const factory SupabaseCloudURLsState({ - required SupabaseConfiguration config, - required String updatedUrl, - required String upatedAnonKey, - required String? urlError, - required String? anonKeyError, - required bool restartApp, - required bool showRestartHint, - }) = _SupabaseCloudURLsState; - - factory SupabaseCloudURLsState.initial() { - final config = getIt().supabaseConfig; - return SupabaseCloudURLsState( - updatedUrl: config.url, - upatedAnonKey: config.anon_key, - urlError: null, - anonKeyError: null, - restartApp: false, - showRestartHint: config.url.isNotEmpty && config.anon_key.isNotEmpty, - config: config, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart index 7191e2cc9d..1b8248d376 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart @@ -22,7 +22,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'setting_appflowy_cloud.dart'; -import 'setting_supabase_cloud.dart'; class SettingCloud extends StatelessWidget { const SettingCloud({required this.restartAppFlowy, super.key}); @@ -80,8 +79,6 @@ class SettingCloud extends StatelessWidget { switch (cloudType) { case AuthenticatorType.local: return SettingLocalCloud(restartAppFlowy: restartAppFlowy); - case AuthenticatorType.supabase: - return SettingSupabaseCloudView(restartAppFlowy: restartAppFlowy); case AuthenticatorType.appflowyCloud: return AppFlowyCloudViewSetting(restartAppFlowy: restartAppFlowy); case AuthenticatorType.appflowyCloudSelfHost: @@ -112,9 +109,6 @@ class CloudTypeSwitcher extends StatelessWidget { // Only show the appflowyCloudDevelop in develop mode final values = AuthenticatorType.values.where((element) { // Supabase will going to be removed in the future - if (element == AuthenticatorType.supabase) { - return false; - } return isDevelopMode || element != AuthenticatorType.appflowyCloudDevelop; }).toList(); @@ -218,8 +212,6 @@ String titleFromCloudType(AuthenticatorType cloudType) { switch (cloudType) { case AuthenticatorType.local: return LocaleKeys.settings_menu_cloudLocal.tr(); - case AuthenticatorType.supabase: - return LocaleKeys.settings_menu_cloudSupabase.tr(); case AuthenticatorType.appflowyCloud: return LocaleKeys.settings_menu_cloudAppFlowy.tr(); case AuthenticatorType.appflowyCloudSelfHost: diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart deleted file mode 100644 index 6751213251..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart +++ /dev/null @@ -1,339 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/supabase_cloud_setting_bloc.dart'; -import 'package:appflowy/workspace/application/settings/supabase_cloud_urls_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/_restart_app_button.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/widget/error_page.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SettingSupabaseCloudView extends StatelessWidget { - const SettingSupabaseCloudView({required this.restartAppFlowy, super.key}); - - final VoidCallback restartAppFlowy; - - @override - Widget build(BuildContext context) { - return FutureBuilder>( - future: UserEventGetCloudConfig().send(), - builder: (context, snapshot) { - if (snapshot.data != null && - snapshot.connectionState == ConnectionState.done) { - return snapshot.data!.fold( - (setting) { - return BlocProvider( - create: (context) => SupabaseCloudSettingBloc( - setting: setting, - )..add(const SupabaseCloudSettingEvent.initial()), - child: Column( - children: [ - BlocBuilder( - builder: (context, state) { - return const Column( - children: [ - SupabaseEnableSync(), - EnableEncrypt(), - ], - ); - }, - ), - const VSpace(40), - const SupabaseSelfhostTip(), - SupabaseCloudURLs( - didUpdateUrls: restartAppFlowy, - ), - ], - ), - ); - }, - (err) { - return FlowyErrorPage.message(err.toString(), howToFix: ""); - }, - ); - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } - }, - ); - } -} - -class SupabaseCloudURLs extends StatelessWidget { - const SupabaseCloudURLs({super.key, required this.didUpdateUrls}); - - final VoidCallback didUpdateUrls; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => SupabaseCloudURLsBloc(), - child: BlocListener( - listener: (context, state) async { - if (state.restartApp) { - didUpdateUrls(); - } - }, - child: BlocBuilder( - builder: (context, state) { - return Column( - children: [ - SupabaseInput( - title: LocaleKeys.settings_menu_cloudSupabaseUrl.tr(), - url: state.config.url, - hint: LocaleKeys.settings_menu_cloudURLHint.tr(), - onChanged: (text) { - context - .read() - .add(SupabaseCloudURLsEvent.updateUrl(text)); - }, - error: state.urlError, - ), - SupabaseInput( - title: LocaleKeys.settings_menu_cloudSupabaseAnonKey.tr(), - url: state.config.anon_key, - hint: LocaleKeys.settings_menu_cloudURLHint.tr(), - onChanged: (text) { - context - .read() - .add(SupabaseCloudURLsEvent.updateAnonKey(text)); - }, - error: state.anonKeyError, - ), - const VSpace(20), - RestartButton( - onClick: () => _restartApp(context), - showRestartHint: state.showRestartHint, - ), - ], - ); - }, - ), - ), - ); - } - - void _restartApp(BuildContext context) { - NavigatorAlertDialog( - title: LocaleKeys.settings_menu_restartAppTip.tr(), - confirm: () => context - .read() - .add(const SupabaseCloudURLsEvent.confirmUpdate()), - ).show(context); - } -} - -class EnableEncrypt extends StatelessWidget { - const EnableEncrypt({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final indicator = state.loadingState.when( - loading: () => const CircularProgressIndicator.adaptive(), - finish: (successOrFail) => const SizedBox.shrink(), - idle: () => const SizedBox.shrink(), - ); - - return Column( - children: [ - Row( - children: [ - FlowyText.medium(LocaleKeys.settings_menu_enableEncrypt.tr()), - const Spacer(), - indicator, - const HSpace(3), - Switch.adaptive( - activeColor: Theme.of(context).colorScheme.primary, - onChanged: state.setting.enableEncrypt - ? null - : (bool value) { - context.read().add( - SupabaseCloudSettingEvent.enableEncrypt(value), - ); - }, - value: state.setting.enableEncrypt, - ), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - IntrinsicHeight( - child: Opacity( - opacity: 0.6, - child: FlowyText.medium( - LocaleKeys.settings_menu_enableEncryptPrompt.tr(), - maxLines: 13, - ), - ), - ), - const VSpace(6), - SizedBox( - height: 40, - child: FlowyTooltip( - message: LocaleKeys.settings_menu_clickToCopySecret.tr(), - child: FlowyButton( - disable: !state.setting.enableEncrypt, - decoration: BoxDecoration( - borderRadius: Corners.s5Border, - border: Border.all( - color: Theme.of(context).colorScheme.secondary, - ), - ), - text: FlowyText.medium(state.setting.encryptSecret), - onTap: () async { - await Clipboard.setData( - ClipboardData(text: state.setting.encryptSecret), - ); - showMessageToast(LocaleKeys.message_copy_success.tr()); - }, - ), - ), - ), - ], - ), - ], - ); - }, - ); - } -} - -class SupabaseEnableSync extends StatelessWidget { - const SupabaseEnableSync({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Row( - children: [ - FlowyText.medium(LocaleKeys.settings_menu_enableSync.tr()), - const Spacer(), - Switch.adaptive( - activeColor: Theme.of(context).colorScheme.primary, - onChanged: (bool value) { - context.read().add( - SupabaseCloudSettingEvent.enableSync(value), - ); - }, - value: state.setting.enableSync, - ), - ], - ); - }, - ); - } -} - -@visibleForTesting -class SupabaseInput extends StatefulWidget { - const SupabaseInput({ - super.key, - required this.title, - required this.url, - required this.hint, - required this.error, - required this.onChanged, - }); - - final String title; - final String url; - final String hint; - final String? error; - final Function(String) onChanged; - - @override - SupabaseInputState createState() => SupabaseInputState(); -} - -class SupabaseInputState extends State { - late final _controller = TextEditingController(text: widget.url); - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return TextField( - controller: _controller, - style: const TextStyle(fontSize: 12.0), - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric(vertical: 6), - labelText: widget.title, - labelStyle: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontWeight: FontWeight.w400, fontSize: 16), - enabledBorder: UnderlineInputBorder( - borderSide: - BorderSide(color: AFThemeExtension.of(context).onBackground), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), - ), - hintText: widget.hint, - errorText: widget.error, - ), - onChanged: widget.onChanged, - ); - } -} - -class SupabaseSelfhostTip extends StatelessWidget { - const SupabaseSelfhostTip({super.key}); - - final url = - "https://docs.appflowy.io/docs/guides/appflowy/self-hosting-appflowy-using-supabase"; - - @override - Widget build(BuildContext context) { - return Opacity( - opacity: 0.6, - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.settings_menu_selfHostStart.tr(), - style: Theme.of(context).textTheme.bodySmall!, - ), - TextSpan( - text: " ${LocaleKeys.settings_menu_selfHostContent.tr()} ", - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: FontSizes.s14, - color: Theme.of(context).colorScheme.primary, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => afLaunchUrlString(url), - ), - TextSpan( - text: LocaleKeys.settings_menu_selfHostEnd.tr(), - style: Theme.of(context).textTheme.bodySmall!, - ), - ], - ), - ), - ); - } -} diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 64cc436a15..816bc62b34 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "bincode", @@ -192,7 +192,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "bytes", @@ -826,11 +826,12 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "again", "anyhow", "app-error", + "arc-swap", "async-trait", "bincode", "brotli", @@ -876,7 +877,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "collab-entity", "collab-rt-entity", @@ -888,7 +889,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "futures-channel", "futures-util", @@ -962,15 +963,16 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", + "arc-swap", "async-trait", "bincode", "bytes", "chrono", "js-sys", - "parking_lot 0.12.1", + "lazy_static", "serde", "serde_json", "serde_repr", @@ -986,7 +988,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", "async-trait", @@ -995,11 +997,11 @@ dependencies = [ "collab-entity", "collab-plugins", "dashmap 5.5.3", + "futures", "getrandom 0.2.10", "js-sys", "lazy_static", "nanoid", - "parking_lot 0.12.1", "rayon", "serde", "serde_json", @@ -1016,14 +1018,14 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", + "arc-swap", "collab", "collab-entity", "getrandom 0.2.10", "nanoid", - "parking_lot 0.12.1", "serde", "serde_json", "thiserror", @@ -1036,7 +1038,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", "bytes", @@ -1055,14 +1057,15 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", + "arc-swap", "chrono", "collab", "collab-entity", + "dashmap 5.5.3", "getrandom 0.2.10", - "parking_lot 0.12.1", "serde", "serde_json", "serde_repr", @@ -1077,13 +1080,17 @@ name = "collab-integrate" version = "0.1.0" dependencies = [ "anyhow", + "arc-swap", "async-trait", "collab", + "collab-database", + "collab-document", "collab-entity", + "collab-folder", "collab-plugins", + "collab-user", "futures", "lib-infra", - "parking_lot 0.12.1", "serde", "serde_json", "tokio", @@ -1093,7 +1100,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", "async-stream", @@ -1109,7 +1116,6 @@ dependencies = [ "indexed_db_futures", "js-sys", "lazy_static", - "parking_lot 0.12.1", "rand 0.8.5", "rocksdb", "serde", @@ -1132,7 +1138,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "bincode", @@ -1157,7 +1163,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "async-trait", @@ -1174,13 +1180,12 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", "collab", "collab-entity", "getrandom 0.2.10", - "parking_lot 0.12.1", "serde", "serde_json", "tokio", @@ -1546,7 +1551,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "app-error", @@ -1972,6 +1977,7 @@ dependencies = [ "anyhow", "appflowy-local-ai", "appflowy-plugin", + "arc-swap", "base64 0.21.5", "bytes", "dashmap 6.0.1", @@ -1989,7 +1995,6 @@ dependencies = [ "log", "md5", "notify", - "parking_lot 0.12.1", "pin-project", "protobuf", "reqwest", @@ -2072,6 +2077,7 @@ version = "0.1.0" dependencies = [ "anyhow", "appflowy-local-ai", + "arc-swap", "base64 0.21.5", "bytes", "client-api", @@ -2079,6 +2085,7 @@ dependencies = [ "collab-entity", "collab-integrate", "collab-plugins", + "dashmap 6.0.1", "diesel", "flowy-ai", "flowy-ai-pub", @@ -2105,7 +2112,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "lib-log", - "parking_lot 0.12.1", "semver", "serde", "serde_json", @@ -2135,6 +2141,7 @@ name = "flowy-database2" version = "0.1.0" dependencies = [ "anyhow", + "arc-swap", "async-stream", "async-trait", "bytes", @@ -2159,7 +2166,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "nanoid", - "parking_lot 0.12.1", "protobuf", "rayon", "rust_decimal", @@ -2231,7 +2237,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "nanoid", - "parking_lot 0.12.1", "protobuf", "scraper 0.18.1", "serde", @@ -2302,6 +2307,7 @@ dependencies = [ name = "flowy-folder" version = "0.1.0" dependencies = [ + "arc-swap", "async-trait", "bytes", "chrono", @@ -2323,7 +2329,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "nanoid", - "parking_lot 0.12.1", "protobuf", "regex", "serde", @@ -2418,6 +2423,7 @@ name = "flowy-server" version = "0.1.0" dependencies = [ "anyhow", + "arc-swap", "bytes", "chrono", "client-api", @@ -2426,6 +2432,7 @@ dependencies = [ "collab-entity", "collab-folder", "collab-plugins", + "dashmap 6.0.1", "flowy-ai-pub", "flowy-database-pub", "flowy-document-pub", @@ -2445,7 +2452,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "mime_guess", - "parking_lot 0.12.1", "postgrest", "rand 0.8.5", "reqwest", @@ -2481,7 +2487,6 @@ dependencies = [ "diesel_derives", "diesel_migrations", "libsqlite3-sys", - "parking_lot 0.12.1", "r2d2", "scheduled-thread-pool", "serde", @@ -2539,6 +2544,7 @@ name = "flowy-user" version = "0.1.0" dependencies = [ "anyhow", + "arc-swap", "base64 0.21.5", "bytes", "chrono", @@ -2551,6 +2557,7 @@ dependencies = [ "collab-integrate", "collab-plugins", "collab-user", + "dashmap 6.0.1", "diesel", "diesel_derives", "fancy-regex 0.11.0", @@ -2567,7 +2574,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "once_cell", - "parking_lot 0.12.1", "protobuf", "semver", "serde", @@ -3068,7 +3074,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "futures-util", @@ -3085,7 +3091,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "app-error", @@ -3517,7 +3523,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "bytes", @@ -3782,7 +3788,6 @@ dependencies = [ "futures-util", "getrandom 0.2.10", "nanoid", - "parking_lot 0.12.1", "pin-project", "protobuf", "serde", @@ -6115,7 +6120,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 25aab8120f..eba3aa40de 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -116,13 +116,13 @@ custom-protocol = ["tauri/custom-protocol"] # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/appflowy_tauri/src-tauri/src/init.rs b/frontend/appflowy_tauri/src-tauri/src/init.rs index 636735e5f4..72e60b4a41 100644 --- a/frontend/appflowy_tauri/src-tauri/src/init.rs +++ b/frontend/appflowy_tauri/src-tauri/src/init.rs @@ -1,9 +1,9 @@ +use dotenv::dotenv; use flowy_core::config::AppFlowyCoreConfig; -use flowy_core::{AppFlowyCore, MutexAppFlowyCore, DEFAULT_NAME}; +use flowy_core::{AppFlowyCore, DEFAULT_NAME}; use lib_dispatch::runtime::AFPluginRuntime; use std::rc::Rc; - -use dotenv::dotenv; +use std::sync::Mutex; pub fn read_env() { dotenv().ok(); @@ -25,7 +25,7 @@ pub fn read_env() { } } -pub fn init_flowy_core() -> MutexAppFlowyCore { +pub(crate) fn init_appflowy_core() -> MutexAppFlowyCore { let config_json = include_str!("../tauri.conf.json"); let config: tauri_utils::config::Config = serde_json::from_str(config_json).unwrap(); @@ -67,3 +67,13 @@ pub fn init_flowy_core() -> MutexAppFlowyCore { MutexAppFlowyCore::new(AppFlowyCore::new(config, cloned_runtime, None).await) }) } + +pub struct MutexAppFlowyCore(pub Rc>); + +impl MutexAppFlowyCore { + fn new(appflowy_core: AppFlowyCore) -> Self { + Self(Rc::new(Mutex::new(appflowy_core))) + } +} +unsafe impl Sync for MutexAppFlowyCore {} +unsafe impl Send for MutexAppFlowyCore {} diff --git a/frontend/appflowy_tauri/src-tauri/src/main.rs b/frontend/appflowy_tauri/src-tauri/src/main.rs index 6a69de07fd..5f12d1be81 100644 --- a/frontend/appflowy_tauri/src-tauri/src/main.rs +++ b/frontend/appflowy_tauri/src-tauri/src/main.rs @@ -11,17 +11,18 @@ mod init; mod notification; mod request; +use crate::init::init_appflowy_core; +use crate::request::invoke_request; use flowy_notification::{register_notification_sender, unregister_all_notification_sender}; -use init::*; use notification::*; -use request::*; use tauri::Manager; + extern crate dotenv; fn main() { tauri_plugin_deep_link::prepare(DEEP_LINK_SCHEME); - let flowy_core = init_flowy_core(); + let flowy_core = init_appflowy_core(); tauri::Builder::default() .invoke_handler(tauri::generate_handler![invoke_request]) .manage(flowy_core) diff --git a/frontend/appflowy_tauri/src-tauri/src/request.rs b/frontend/appflowy_tauri/src-tauri/src/request.rs index 146d303cc0..ff69a438c9 100644 --- a/frontend/appflowy_tauri/src-tauri/src/request.rs +++ b/frontend/appflowy_tauri/src-tauri/src/request.rs @@ -1,4 +1,4 @@ -use flowy_core::; +use crate::init::MutexAppFlowyCore; use lib_dispatch::prelude::{ AFPluginDispatcher, AFPluginEventResponse, AFPluginRequest, StatusCode, }; @@ -39,7 +39,7 @@ pub async fn invoke_request( ) -> AFTauriResponse { let request: AFPluginRequest = request.into(); let state: State = app_handler.state(); - let dispatcher = state.0.lock().dispatcher(); + let dispatcher = state.0.lock().unwrap().dispatcher(); let response = AFPluginDispatcher::sync_send(dispatcher, request); response.into() } diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index 7af7287706..6a6d842bf7 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "bytes", @@ -800,11 +800,12 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "again", "anyhow", "app-error", + "arc-swap", "async-trait", "bincode", "brotli", @@ -850,7 +851,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "collab-entity", "collab-rt-entity", @@ -862,7 +863,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "futures-channel", "futures-util", @@ -945,15 +946,16 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", + "arc-swap", "async-trait", "bincode", "bytes", "chrono", "js-sys", - "parking_lot 0.12.1", + "lazy_static", "serde", "serde_json", "serde_repr", @@ -969,7 +971,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", "async-trait", @@ -978,11 +980,11 @@ dependencies = [ "collab-entity", "collab-plugins", "dashmap 5.5.3", + "futures", "getrandom 0.2.12", "js-sys", "lazy_static", "nanoid", - "parking_lot 0.12.1", "rayon", "serde", "serde_json", @@ -999,14 +1001,14 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", + "arc-swap", "collab", "collab-entity", "getrandom 0.2.12", "nanoid", - "parking_lot 0.12.1", "serde", "serde_json", "thiserror", @@ -1019,7 +1021,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", "bytes", @@ -1038,14 +1040,15 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", + "arc-swap", "chrono", "collab", "collab-entity", + "dashmap 5.5.3", "getrandom 0.2.12", - "parking_lot 0.12.1", "serde", "serde_json", "serde_repr", @@ -1060,13 +1063,17 @@ name = "collab-integrate" version = "0.1.0" dependencies = [ "anyhow", + "arc-swap", "async-trait", "collab", + "collab-database", + "collab-document", "collab-entity", + "collab-folder", "collab-plugins", + "collab-user", "futures", "lib-infra", - "parking_lot 0.12.1", "serde", "serde_json", "tokio", @@ -1076,7 +1083,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", "async-stream", @@ -1092,7 +1099,6 @@ dependencies = [ "indexed_db_futures", "js-sys", "lazy_static", - "parking_lot 0.12.1", "rand 0.8.5", "rocksdb", "serde", @@ -1115,7 +1121,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "bincode", @@ -1140,7 +1146,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "async-trait", @@ -1157,13 +1163,12 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", "collab", "collab-entity", "getrandom 0.2.12", - "parking_lot 0.12.1", "serde", "serde_json", "tokio", @@ -1536,7 +1541,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "app-error", @@ -2002,6 +2007,7 @@ dependencies = [ "anyhow", "appflowy-local-ai", "appflowy-plugin", + "arc-swap", "base64 0.21.7", "bytes", "dashmap 6.0.1", @@ -2019,7 +2025,6 @@ dependencies = [ "log", "md5", "notify", - "parking_lot 0.12.1", "pin-project", "protobuf", "reqwest", @@ -2102,6 +2107,7 @@ version = "0.1.0" dependencies = [ "anyhow", "appflowy-local-ai", + "arc-swap", "base64 0.21.7", "bytes", "client-api", @@ -2109,6 +2115,7 @@ dependencies = [ "collab-entity", "collab-integrate", "collab-plugins", + "dashmap 6.0.1", "diesel", "flowy-ai", "flowy-ai-pub", @@ -2135,7 +2142,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "lib-log", - "parking_lot 0.12.1", "semver", "serde", "serde_json", @@ -2165,6 +2171,7 @@ name = "flowy-database2" version = "0.1.0" dependencies = [ "anyhow", + "arc-swap", "async-stream", "async-trait", "bytes", @@ -2189,7 +2196,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "nanoid", - "parking_lot 0.12.1", "protobuf", "rayon", "rust_decimal", @@ -2261,7 +2267,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "nanoid", - "parking_lot 0.12.1", "protobuf", "scraper 0.18.1", "serde", @@ -2332,6 +2337,7 @@ dependencies = [ name = "flowy-folder" version = "0.1.0" dependencies = [ + "arc-swap", "async-trait", "bytes", "chrono", @@ -2353,7 +2359,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "nanoid", - "parking_lot 0.12.1", "protobuf", "regex", "serde", @@ -2448,6 +2453,7 @@ name = "flowy-server" version = "0.1.0" dependencies = [ "anyhow", + "arc-swap", "bytes", "chrono", "client-api", @@ -2456,6 +2462,7 @@ dependencies = [ "collab-entity", "collab-folder", "collab-plugins", + "dashmap 6.0.1", "flowy-ai-pub", "flowy-database-pub", "flowy-document-pub", @@ -2475,7 +2482,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "mime_guess", - "parking_lot 0.12.1", "postgrest", "rand 0.8.5", "reqwest", @@ -2511,7 +2517,6 @@ dependencies = [ "diesel_derives", "diesel_migrations", "libsqlite3-sys", - "parking_lot 0.12.1", "r2d2", "scheduled-thread-pool", "serde", @@ -2569,6 +2574,7 @@ name = "flowy-user" version = "0.1.0" dependencies = [ "anyhow", + "arc-swap", "base64 0.21.7", "bytes", "chrono", @@ -2581,6 +2587,7 @@ dependencies = [ "collab-integrate", "collab-plugins", "collab-user", + "dashmap 6.0.1", "diesel", "diesel_derives", "fancy-regex 0.11.0", @@ -2597,7 +2604,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "once_cell", - "parking_lot 0.12.1", "protobuf", "semver", "serde", @@ -3135,7 +3141,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "futures-util", @@ -3152,7 +3158,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "app-error", @@ -3589,7 +3595,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "bytes", @@ -3859,7 +3865,6 @@ dependencies = [ "futures-util", "getrandom 0.2.12", "nanoid", - "parking_lot 0.12.1", "pin-project", "protobuf", "serde", @@ -6179,7 +6184,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index 5d5dc9ec3a..b3d45657bc 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -116,13 +116,13 @@ custom-protocol = ["tauri/custom-protocol"] # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/appflowy_web_app/src-tauri/src/init.rs b/frontend/appflowy_web_app/src-tauri/src/init.rs index 636735e5f4..b4c771b1b5 100644 --- a/frontend/appflowy_web_app/src-tauri/src/init.rs +++ b/frontend/appflowy_web_app/src-tauri/src/init.rs @@ -1,9 +1,9 @@ +use dotenv::dotenv; use flowy_core::config::AppFlowyCoreConfig; -use flowy_core::{AppFlowyCore, MutexAppFlowyCore, DEFAULT_NAME}; +use flowy_core::{AppFlowyCore, DEFAULT_NAME}; use lib_dispatch::runtime::AFPluginRuntime; use std::rc::Rc; - -use dotenv::dotenv; +use std::sync::Mutex; pub fn read_env() { dotenv().ok(); @@ -25,7 +25,7 @@ pub fn read_env() { } } -pub fn init_flowy_core() -> MutexAppFlowyCore { +pub fn init_appflowy_core() -> MutexAppFlowyCore { let config_json = include_str!("../tauri.conf.json"); let config: tauri_utils::config::Config = serde_json::from_str(config_json).unwrap(); @@ -67,3 +67,13 @@ pub fn init_flowy_core() -> MutexAppFlowyCore { MutexAppFlowyCore::new(AppFlowyCore::new(config, cloned_runtime, None).await) }) } + +pub struct MutexAppFlowyCore(pub Rc>); + +impl MutexAppFlowyCore { + pub(crate) fn new(appflowy_core: AppFlowyCore) -> Self { + Self(Rc::new(Mutex::new(appflowy_core))) + } +} +unsafe impl Sync for MutexAppFlowyCore {} +unsafe impl Send for MutexAppFlowyCore {} diff --git a/frontend/appflowy_web_app/src-tauri/src/main.rs b/frontend/appflowy_web_app/src-tauri/src/main.rs index 6a69de07fd..781ce55098 100644 --- a/frontend/appflowy_web_app/src-tauri/src/main.rs +++ b/frontend/appflowy_web_app/src-tauri/src/main.rs @@ -21,7 +21,7 @@ extern crate dotenv; fn main() { tauri_plugin_deep_link::prepare(DEEP_LINK_SCHEME); - let flowy_core = init_flowy_core(); + let flowy_core = init_appflowy_core(); tauri::Builder::default() .invoke_handler(tauri::generate_handler![invoke_request]) .manage(flowy_core) diff --git a/frontend/appflowy_web_app/src-tauri/src/request.rs b/frontend/appflowy_web_app/src-tauri/src/request.rs index 6d2d01fb6e..ff69a438c9 100644 --- a/frontend/appflowy_web_app/src-tauri/src/request.rs +++ b/frontend/appflowy_web_app/src-tauri/src/request.rs @@ -1,4 +1,4 @@ -use flowy_core::MutexAppFlowyCore; +use crate::init::MutexAppFlowyCore; use lib_dispatch::prelude::{ AFPluginDispatcher, AFPluginEventResponse, AFPluginRequest, StatusCode, }; @@ -39,7 +39,7 @@ pub async fn invoke_request( ) -> AFTauriResponse { let request: AFPluginRequest = request.into(); let state: State = app_handler.state(); - let dispatcher = state.0.lock().dispatcher(); + let dispatcher = state.0.lock().unwrap().dispatcher(); let response = AFPluginDispatcher::sync_send(dispatcher, request); response.into() } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 3b5e6896f4..4f8ce47d41 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "bytes", @@ -718,11 +718,12 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "again", "anyhow", "app-error", + "arc-swap", "async-trait", "bincode", "brotli", @@ -768,7 +769,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "collab-entity", "collab-rt-entity", @@ -780,7 +781,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "futures-channel", "futures-util", @@ -823,15 +824,16 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", + "arc-swap", "async-trait", "bincode", "bytes", "chrono", "js-sys", - "parking_lot 0.12.1", + "lazy_static", "serde", "serde_json", "serde_repr", @@ -847,7 +849,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", "async-trait", @@ -856,11 +858,11 @@ dependencies = [ "collab-entity", "collab-plugins", "dashmap 5.5.3", + "futures", "getrandom 0.2.10", "js-sys", "lazy_static", "nanoid", - "parking_lot 0.12.1", "rayon", "serde", "serde_json", @@ -877,14 +879,14 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", + "arc-swap", "collab", "collab-entity", "getrandom 0.2.10", "nanoid", - "parking_lot 0.12.1", "serde", "serde_json", "thiserror", @@ -897,7 +899,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", "bytes", @@ -916,14 +918,15 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", + "arc-swap", "chrono", "collab", "collab-entity", + "dashmap 5.5.3", "getrandom 0.2.10", - "parking_lot 0.12.1", "serde", "serde_json", "serde_repr", @@ -938,13 +941,17 @@ name = "collab-integrate" version = "0.1.0" dependencies = [ "anyhow", + "arc-swap", "async-trait", "collab", + "collab-database", + "collab-document", "collab-entity", + "collab-folder", "collab-plugins", + "collab-user", "futures", "lib-infra", - "parking_lot 0.12.1", "serde", "serde_json", "tokio", @@ -954,7 +961,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", "async-stream", @@ -970,7 +977,6 @@ dependencies = [ "indexed_db_futures", "js-sys", "lazy_static", - "parking_lot 0.12.1", "rand 0.8.5", "rocksdb", "serde", @@ -993,7 +999,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "bincode", @@ -1018,7 +1024,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "async-trait", @@ -1035,13 +1041,12 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6adf750#6adf750dcb7a3f74806b8ffe8c7865bc9d5f85db" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" dependencies = [ "anyhow", "collab", "collab-entity", "getrandom 0.2.10", - "parking_lot 0.12.1", "serde", "serde_json", "tokio", @@ -1323,7 +1328,6 @@ dependencies = [ "lazy_static", "lib-dispatch", "lib-log", - "parking_lot 0.12.1", "protobuf", "semver", "serde", @@ -1370,7 +1374,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "app-error", @@ -1662,7 +1666,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "nanoid", - "parking_lot 0.12.1", "protobuf", "rand 0.8.5", "semver", @@ -1795,6 +1798,7 @@ dependencies = [ "anyhow", "appflowy-local-ai", "appflowy-plugin", + "arc-swap", "base64 0.21.5", "bytes", "dashmap 6.0.1", @@ -1813,7 +1817,6 @@ dependencies = [ "log", "md5", "notify", - "parking_lot 0.12.1", "pin-project", "protobuf", "reqwest", @@ -1898,6 +1901,7 @@ version = "0.1.0" dependencies = [ "anyhow", "appflowy-local-ai", + "arc-swap", "base64 0.21.5", "bytes", "client-api", @@ -1906,6 +1910,7 @@ dependencies = [ "collab-integrate", "collab-plugins", "console-subscriber", + "dashmap 6.0.1", "diesel", "flowy-ai", "flowy-ai-pub", @@ -1932,7 +1937,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "lib-log", - "parking_lot 0.12.1", "semver", "serde", "serde_json", @@ -1962,6 +1966,7 @@ name = "flowy-database2" version = "0.1.0" dependencies = [ "anyhow", + "arc-swap", "async-stream", "async-trait", "bytes", @@ -1987,7 +1992,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "nanoid", - "parking_lot 0.12.1", "protobuf", "rayon", "rust_decimal", @@ -2059,7 +2063,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "nanoid", - "parking_lot 0.12.1", "protobuf", "scraper 0.18.1", "serde", @@ -2132,6 +2135,7 @@ dependencies = [ name = "flowy-folder" version = "0.1.0" dependencies = [ + "arc-swap", "async-trait", "bytes", "chrono", @@ -2153,7 +2157,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "nanoid", - "parking_lot 0.12.1", "protobuf", "regex", "serde", @@ -2249,6 +2252,7 @@ name = "flowy-server" version = "0.1.0" dependencies = [ "anyhow", + "arc-swap", "assert-json-diff", "bytes", "chrono", @@ -2258,6 +2262,7 @@ dependencies = [ "collab-entity", "collab-folder", "collab-plugins", + "dashmap 6.0.1", "dotenv", "flowy-ai-pub", "flowy-database-pub", @@ -2278,7 +2283,6 @@ dependencies = [ "lib-dispatch", "lib-infra", "mime_guess", - "parking_lot 0.12.1", "postgrest", "rand 0.8.5", "reqwest", @@ -2317,7 +2321,6 @@ dependencies = [ "libsqlite3-sys", "openssl", "openssl-sys", - "parking_lot 0.12.1", "r2d2", "scheduled-thread-pool", "serde", @@ -2378,6 +2381,7 @@ name = "flowy-user" version = "0.1.0" dependencies = [ "anyhow", + "arc-swap", "base64 0.21.5", "bytes", "chrono", @@ -2390,6 +2394,7 @@ dependencies = [ "collab-integrate", "collab-plugins", "collab-user", + "dashmap 6.0.1", "diesel", "diesel_derives", "fake", @@ -2408,7 +2413,6 @@ dependencies = [ "lib-infra", "nanoid", "once_cell", - "parking_lot 0.12.1", "protobuf", "quickcheck", "quickcheck_macros", @@ -2747,7 +2751,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "futures-util", @@ -2764,7 +2768,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "app-error", @@ -3129,7 +3133,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "bytes", @@ -3295,7 +3299,6 @@ dependencies = [ "futures-util", "getrandom 0.2.10", "nanoid", - "parking_lot 0.12.1", "pin-project", "protobuf", "serde", @@ -5338,7 +5341,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7878a018a18553e3d8201e572a0c066c14ba3b35#7878a018a18553e3d8201e572a0c066c14ba3b35" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d503905#d5039059313804103f34eee49ee9844c255a99c0" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 3875726ff9..da234b004e 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -68,6 +68,7 @@ flowy-date = { workspace = true, path = "flowy-date" } flowy-ai = { workspace = true, path = "flowy-ai" } flowy-ai-pub = { workspace = true, path = "flowy-ai-pub" } anyhow = "1.0" +arc-swap = "1.7" tracing = "0.1.40" bytes = "1.5.0" serde_json = "1.0.108" @@ -76,7 +77,6 @@ protobuf = { version = "2.28.0" } diesel = { version = "2.1.0", features = ["sqlite", "chrono", "r2d2", "serde_json"] } uuid = { version = "1.5.0", features = ["serde", "v4", "v5"] } serde_repr = "0.1" -parking_lot = "0.12" futures = "0.3.29" tokio = "1.38.0" tokio-stream = "0.1.14" @@ -100,8 +100,8 @@ dashmap = "6.0.1" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "7878a018a18553e3d8201e572a0c066c14ba3b35" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "7878a018a18553e3d8201e572a0c066c14ba3b35" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d503905" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d503905" } [profile.dev] opt-level = 0 @@ -136,13 +136,13 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6adf750" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/collab-integrate/Cargo.toml b/frontend/rust-lib/collab-integrate/Cargo.toml index ffddb6a911..8b0a530b19 100644 --- a/frontend/rust-lib/collab-integrate/Cargo.toml +++ b/frontend/rust-lib/collab-integrate/Cargo.toml @@ -11,15 +11,19 @@ crate-type = ["cdylib", "rlib"] collab = { workspace = true } collab-plugins = { workspace = true } collab-entity = { workspace = true } +collab-document = { workspace = true } +collab-folder = { workspace = true } +collab-user = { workspace = true } +collab-database = { workspace = true } serde.workspace = true serde_json.workspace = true anyhow.workspace = true tracing.workspace = true -parking_lot.workspace = true async-trait.workspace = true tokio = { workspace = true, features = ["sync"] } lib-infra = { workspace = true } futures = "0.3" +arc-swap = "1.7" [features] default = [] diff --git a/frontend/rust-lib/collab-integrate/src/collab_builder.rs b/frontend/rust-lib/collab-integrate/src/collab_builder.rs index 571264d1d2..3742b2fc72 100644 --- a/frontend/rust-lib/collab-integrate/src/collab_builder.rs +++ b/frontend/rust-lib/collab-integrate/src/collab_builder.rs @@ -1,11 +1,18 @@ +use std::borrow::BorrowMut; use std::fmt::{Debug, Display}; use std::sync::{Arc, Weak}; use crate::CollabKVDB; use anyhow::Error; -use collab::core::collab::{DataSource, MutexCollab}; -use collab::preclude::CollabBuilder; +use arc_swap::{ArcSwap, ArcSwapOption}; +use collab::core::collab::DataSource; +use collab::core::collab_plugin::CollabPersistence; +use collab::preclude::{Collab, CollabBuilder}; +use collab_database::workspace_database::{DatabaseCollabService, WorkspaceDatabase}; +use collab_document::blocks::DocumentData; +use collab_document::document::Document; use collab_entity::{CollabObject, CollabType}; +use collab_folder::{Folder, FolderData, FolderNotify}; use collab_plugins::connect_state::{CollabConnectReachability, CollabConnectState}; use collab_plugins::local_storage::kv::snapshot::SnapshotPersistence; if_native! { @@ -17,17 +24,19 @@ use collab_plugins::local_storage::indexeddb::IndexeddbDiskPlugin; } pub use crate::plugin_provider::CollabCloudPluginProvider; +use collab_plugins::local_storage::kv::doc::CollabKVAction; +use collab_plugins::local_storage::kv::KVTransactionDB; use collab_plugins::local_storage::CollabPersistenceConfig; +use collab_user::core::{UserAwareness, UserAwarenessNotifier}; +use tokio::sync::RwLock; use lib_infra::{if_native, if_wasm}; -use parking_lot::{Mutex, RwLock}; -use tracing::{instrument, trace}; +use tracing::{error, instrument, trace, warn}; #[derive(Clone, Debug)] pub enum CollabPluginProviderType { Local, AppFlowyCloud, - Supabase, } pub enum CollabPluginProviderContext { @@ -35,13 +44,7 @@ pub enum CollabPluginProviderContext { AppFlowyCloud { uid: i64, collab_object: CollabObject, - local_collab: Weak, - }, - Supabase { - uid: i64, - collab_object: CollabObject, - local_collab: Weak, - local_collab_db: Weak, + local_collab: Weak + Send + Sync + 'static>>, }, } @@ -52,13 +55,7 @@ impl Display for CollabPluginProviderContext { CollabPluginProviderContext::AppFlowyCloud { uid: _, collab_object, - local_collab: _, - } => collab_object.to_string(), - CollabPluginProviderContext::Supabase { - uid: _, - collab_object, - local_collab: _, - local_collab_db: _, + .. } => collab_object.to_string(), }; write!(f, "{}", str) @@ -72,10 +69,10 @@ pub trait WorkspaceCollabIntegrate: Send + Sync { pub struct AppFlowyCollabBuilder { network_reachability: CollabConnectReachability, - plugin_provider: RwLock>, - snapshot_persistence: Mutex>>, + plugin_provider: ArcSwap>, + snapshot_persistence: ArcSwapOption>, #[cfg(not(target_arch = "wasm32"))] - rocksdb_backup: Mutex>>, + rocksdb_backup: ArcSwapOption>, workspace_integrate: Arc, } @@ -86,7 +83,7 @@ impl AppFlowyCollabBuilder { ) -> Self { Self { network_reachability: CollabConnectReachability::new(), - plugin_provider: RwLock::new(Arc::new(storage_provider)), + plugin_provider: ArcSwap::new(Arc::new(Arc::new(storage_provider))), snapshot_persistence: Default::default(), #[cfg(not(target_arch = "wasm32"))] rocksdb_backup: Default::default(), @@ -95,12 +92,14 @@ impl AppFlowyCollabBuilder { } pub fn set_snapshot_persistence(&self, snapshot_persistence: Arc) { - *self.snapshot_persistence.lock() = Some(snapshot_persistence); + self + .snapshot_persistence + .store(Some(snapshot_persistence.into())); } #[cfg(not(target_arch = "wasm32"))] pub fn set_rocksdb_backup(&self, rocksdb_backup: Arc) { - *self.rocksdb_backup.lock() = Some(rocksdb_backup); + self.rocksdb_backup.store(Some(rocksdb_backup.into())); } pub fn update_network(&self, reachable: bool) { @@ -115,12 +114,23 @@ impl AppFlowyCollabBuilder { } } - fn collab_object( + pub fn collab_object( &self, + workspace_id: &str, uid: i64, object_id: &str, collab_type: CollabType, ) -> Result { + // Compare the workspace_id with the currently opened workspace_id. Return an error if they do not match. + // This check is crucial in asynchronous code contexts where the workspace_id might change during operation. + let actual_workspace_id = self.workspace_integrate.workspace_id()?; + if workspace_id != actual_workspace_id { + return Err(anyhow::anyhow!( + "workspace_id not match when build collab. expect workspace_id: {}, actual workspace_id: {}", + workspace_id, + actual_workspace_id + )); + } let device_id = self.workspace_integrate.device_id()?; let workspace_id = self.workspace_integrate.workspace_id()?; Ok(CollabObject::new( @@ -132,170 +142,155 @@ impl AppFlowyCollabBuilder { )) } - /// Creates a new collaboration builder with the default configuration. - /// - /// This function will initiate the creation of a [MutexCollab] object if it does not already exist. - /// To check for the existence of the object prior to creation, you should utilize a transaction - /// returned by the [read_txn] method of the [CollabKVDB]. Then, invoke the [is_exist] method - /// to confirm the object's presence. - /// - /// # Parameters - /// - `uid`: The user ID associated with the collaboration. - /// - `object_id`: A string reference representing the ID of the object. - /// - `object_type`: The type of the collaboration, defined by the [CollabType] enum. - /// - `raw_data`: The raw data of the collaboration object, defined by the [CollabDocState] type. - /// - `collab_db`: A weak reference to the [CollabKVDB]. - /// #[allow(clippy::too_many_arguments)] - pub async fn build( + #[instrument( + level = "trace", + skip(self, data_source, collab_db, builder_config, data) + )] + pub fn create_document( &self, - workspace_id: &str, - uid: i64, - object_id: &str, - object_type: CollabType, - collab_doc_state: DataSource, + object: CollabObject, + data_source: DataSource, collab_db: Weak, - build_config: CollabBuilderConfig, - ) -> Result, Error> { - self.build_with_config( - workspace_id, - uid, - object_id, - object_type, - collab_db, - collab_doc_state, - build_config, - ) + builder_config: CollabBuilderConfig, + data: Option, + ) -> Result>, Error> { + assert_eq!(object.collab_type, CollabType::Document); + let collab = self.build_collab(&object, &collab_db, data_source)?; + let document = Document::open_with(collab, data)?; + let document = Arc::new(RwLock::new(document)); + self.finalize(object, builder_config, document) } - /// Creates a new collaboration builder with the custom configuration. - /// - /// This function will initiate the creation of a [MutexCollab] object if it does not already exist. - /// To check for the existence of the object prior to creation, you should utilize a transaction - /// returned by the [read_txn] method of the [CollabKVDB]. Then, invoke the [is_exist] method - /// to confirm the object's presence. - /// - /// # Parameters - /// - `uid`: The user ID associated with the collaboration. - /// - `object_id`: A string reference representing the ID of the object. - /// - `object_type`: The type of the collaboration, defined by the [CollabType] enum. - /// - `raw_data`: The raw data of the collaboration object, defined by the [CollabDocState] type. - /// - `collab_db`: A weak reference to the [CollabKVDB]. - /// #[allow(clippy::too_many_arguments)] - #[instrument(level = "trace", skip(self, collab_db, collab_doc_state, build_config))] - pub fn build_with_config( + #[instrument( + level = "trace", + skip(self, object, doc_state, collab_db, builder_config, folder_notifier) + )] + pub fn create_folder( &self, - workspace_id: &str, - uid: i64, - object_id: &str, - object_type: CollabType, + object: CollabObject, + doc_state: DataSource, collab_db: Weak, - collab_doc_state: DataSource, - build_config: CollabBuilderConfig, - ) -> Result, Error> { - let collab = CollabBuilder::new(uid, object_id) - .with_doc_state(collab_doc_state) + builder_config: CollabBuilderConfig, + folder_notifier: Option, + folder_data: Option, + ) -> Result>, Error> { + assert_eq!(object.collab_type, CollabType::Folder); + let collab = self.build_collab(&object, &collab_db, doc_state)?; + let folder = Folder::open_with(object.uid, collab, folder_notifier, folder_data); + let folder = Arc::new(RwLock::new(folder)); + self.finalize(object, builder_config, folder) + } + + #[allow(clippy::too_many_arguments)] + #[instrument( + level = "trace", + skip(self, object, doc_state, collab_db, builder_config, notifier) + )] + pub fn create_user_awareness( + &self, + object: CollabObject, + doc_state: DataSource, + collab_db: Weak, + builder_config: CollabBuilderConfig, + notifier: Option, + ) -> Result>, Error> { + assert_eq!(object.collab_type, CollabType::UserAwareness); + let collab = self.build_collab(&object, &collab_db, doc_state)?; + let user_awareness = UserAwareness::open(collab, notifier); + let user_awareness = Arc::new(RwLock::new(user_awareness)); + self.finalize(object, builder_config, user_awareness) + } + + #[allow(clippy::too_many_arguments)] + #[instrument( + level = "trace", + skip(self, object, doc_state, collab_db, builder_config, collab_service) + )] + pub fn create_workspace_database( + &self, + object: CollabObject, + doc_state: DataSource, + collab_db: Weak, + builder_config: CollabBuilderConfig, + collab_service: impl DatabaseCollabService, + ) -> Result>, Error> { + assert_eq!(object.collab_type, CollabType::WorkspaceDatabase); + let collab = self.build_collab(&object, &collab_db, doc_state)?; + let workspace = WorkspaceDatabase::open(object.uid, collab, collab_db.clone(), collab_service); + let workspace = Arc::new(RwLock::new(workspace)); + self.finalize(object, builder_config, workspace) + } + + pub fn build_collab( + &self, + object: &CollabObject, + collab_db: &Weak, + data_source: DataSource, + ) -> Result { + let collab = CollabBuilder::new(object.uid, &object.object_id, data_source) .with_device_id(self.workspace_integrate.device_id()?) .build()?; - // Compare the workspace_id with the currently opened workspace_id. Return an error if they do not match. - // This check is crucial in asynchronous code contexts where the workspace_id might change during operation. - let actual_workspace_id = self.workspace_integrate.workspace_id()?; - if workspace_id != actual_workspace_id { - return Err(anyhow::anyhow!( - "workspace_id not match when build collab. expect workspace_id: {}, actual workspace_id: {}", - workspace_id, - actual_workspace_id - )); - } let persistence_config = CollabPersistenceConfig::default(); + let db_plugin = RocksdbDiskPlugin::new_with_config( + object.uid, + object.object_id.to_string(), + object.collab_type.clone(), + collab_db.clone(), + persistence_config.clone(), + ); + collab.add_plugin(Box::new(db_plugin)); - #[cfg(target_arch = "wasm32")] - { - collab.lock().add_plugin(Box::new(IndexeddbDiskPlugin::new( - uid, - object_id.to_string(), - object_type.clone(), - collab_db.clone(), - ))); + Ok(collab) + } + + pub fn finalize( + &self, + object: CollabObject, + build_config: CollabBuilderConfig, + collab: Arc>, + ) -> Result>, Error> + where + T: BorrowMut + Send + Sync + 'static, + { + let mut write_collab = collab.try_write()?; + if !write_collab.borrow().get_state().is_uninitialized() { + drop(write_collab); + return Ok(collab); } + trace!("🚀finalize collab:{}", object); + if build_config.sync_enable { + let plugin_provider = self.plugin_provider.load_full(); + let provider_type = plugin_provider.provider_type(); + let span = + tracing::span!(tracing::Level::TRACE, "collab_builder", object_id = %object.object_id); + let _enter = span.enter(); + match provider_type { + CollabPluginProviderType::AppFlowyCloud => { + let local_collab = Arc::downgrade(&collab); + let plugins = plugin_provider.get_plugins(CollabPluginProviderContext::AppFlowyCloud { + uid: object.uid, + collab_object: object, + local_collab, + }); - #[cfg(not(target_arch = "wasm32"))] - { - collab - .lock() - .add_plugin(Box::new(RocksdbDiskPlugin::new_with_config( - uid, - object_id.to_string(), - object_type.clone(), - collab_db.clone(), - persistence_config.clone(), - None, - ))); - } - - let arc_collab = Arc::new(collab); - - { - let collab_object = self.collab_object(uid, object_id, object_type.clone())?; - if build_config.sync_enable { - let provider_type = self.plugin_provider.read().provider_type(); - let span = tracing::span!(tracing::Level::TRACE, "collab_builder", object_id = %object_id); - let _enter = span.enter(); - match provider_type { - CollabPluginProviderType::AppFlowyCloud => { - let local_collab = Arc::downgrade(&arc_collab); - let plugins = - self - .plugin_provider - .read() - .get_plugins(CollabPluginProviderContext::AppFlowyCloud { - uid, - collab_object, - local_collab, - }); - - for plugin in plugins { - arc_collab.lock().add_plugin(plugin); - } - }, - CollabPluginProviderType::Supabase => { - #[cfg(not(target_arch = "wasm32"))] - { - trace!("init supabase collab plugins"); - let local_collab = Arc::downgrade(&arc_collab); - let local_collab_db = collab_db.clone(); - let plugins = - self - .plugin_provider - .read() - .get_plugins(CollabPluginProviderContext::Supabase { - uid, - collab_object, - local_collab, - local_collab_db, - }); - for plugin in plugins { - arc_collab.lock().add_plugin(plugin); - } - } - }, - CollabPluginProviderType::Local => {}, - } + // at the moment when we get the lock, the collab object is not yet exposed outside + for plugin in plugins { + write_collab.borrow().add_plugin(plugin); + } + }, + CollabPluginProviderType::Local => {}, } } if build_config.auto_initialize { - #[cfg(target_arch = "wasm32")] - futures::executor::block_on(arc_collab.lock().initialize()); - - #[cfg(not(target_arch = "wasm32"))] - arc_collab.lock().initialize(); + // at the moment when we get the lock, the collab object is not yet exposed outside + (*write_collab).borrow_mut().initialize(); } - - trace!("collab initialized: {}:{}", object_type, object_id); - Ok(arc_collab) + drop(write_collab); + Ok(collab) } } @@ -328,3 +323,39 @@ impl CollabBuilderConfig { self } } + +pub struct KVDBCollabPersistenceImpl { + pub db: Weak, + pub uid: i64, +} + +impl KVDBCollabPersistenceImpl { + pub fn new(db: Weak, uid: i64) -> Self { + Self { db, uid } + } + + pub fn into_data_source(self) -> DataSource { + DataSource::Disk(Some(Box::new(self))) + } +} + +impl CollabPersistence for KVDBCollabPersistenceImpl { + fn load_collab(&self, collab: &mut Collab) { + if let Some(collab_db) = self.db.upgrade() { + let object_id = collab.object_id().to_string(); + let rocksdb_read = collab_db.read_txn(); + + if rocksdb_read.is_exist(self.uid, &object_id) { + let mut txn = collab.transact_mut(); + if let Err(err) = rocksdb_read.load_doc_with_txn(self.uid, &object_id, &mut txn) { + error!("🔴 load doc:{} failed: {}", object_id, err); + } + drop(rocksdb_read); + txn.commit(); + drop(txn); + } + } else { + warn!("collab_db is dropped"); + } + } +} diff --git a/frontend/rust-lib/collab-integrate/src/lib.rs b/frontend/rust-lib/collab-integrate/src/lib.rs index a7df75d72e..d24700f8d5 100644 --- a/frontend/rust-lib/collab-integrate/src/lib.rs +++ b/frontend/rust-lib/collab-integrate/src/lib.rs @@ -1,4 +1,3 @@ -pub use collab::core::collab::MutexCollab; pub use collab::preclude::Snapshot; pub use collab_plugins::local_storage::CollabPersistenceConfig; pub use collab_plugins::CollabKVDB; diff --git a/frontend/rust-lib/collab-integrate/src/native/plugin_provider.rs b/frontend/rust-lib/collab-integrate/src/native/plugin_provider.rs index a26fb8d933..b5b3b1f6e6 100644 --- a/frontend/rust-lib/collab-integrate/src/native/plugin_provider.rs +++ b/frontend/rust-lib/collab-integrate/src/native/plugin_provider.rs @@ -1,6 +1,7 @@ -use crate::collab_builder::{CollabPluginProviderContext, CollabPluginProviderType}; use collab::preclude::CollabPlugin; +use crate::collab_builder::{CollabPluginProviderContext, CollabPluginProviderType}; + #[cfg(target_arch = "wasm32")] pub trait CollabCloudPluginProvider: 'static { fn provider_type(&self) -> CollabPluginProviderType; @@ -11,9 +12,9 @@ pub trait CollabCloudPluginProvider: 'static { } #[cfg(target_arch = "wasm32")] -impl CollabCloudPluginProvider for std::rc::Rc +impl CollabCloudPluginProvider for std::rc::Rc where - T: CollabCloudPluginProvider, + U: CollabCloudPluginProvider, { fn provider_type(&self) -> CollabPluginProviderType { (**self).provider_type() @@ -38,9 +39,9 @@ pub trait CollabCloudPluginProvider: Send + Sync + 'static { } #[cfg(not(target_arch = "wasm32"))] -impl CollabCloudPluginProvider for std::sync::Arc +impl CollabCloudPluginProvider for std::sync::Arc where - T: CollabCloudPluginProvider, + U: CollabCloudPluginProvider, { fn provider_type(&self) -> CollabPluginProviderType { (**self).provider_type() diff --git a/frontend/rust-lib/dart-ffi/Cargo.toml b/frontend/rust-lib/dart-ffi/Cargo.toml index 22e07f3483..c60c09e0c9 100644 --- a/frontend/rust-lib/dart-ffi/Cargo.toml +++ b/frontend/rust-lib/dart-ffi/Cargo.toml @@ -22,7 +22,6 @@ serde_json.workspace = true bytes.workspace = true crossbeam-utils = "0.8.15" lazy_static = "1.4.0" -parking_lot.workspace = true tracing.workspace = true lib-log.workspace = true semver = "1.0.22" diff --git a/frontend/rust-lib/dart-ffi/src/env_serde.rs b/frontend/rust-lib/dart-ffi/src/env_serde.rs index db443a78f7..476c27bb46 100644 --- a/frontend/rust-lib/dart-ffi/src/env_serde.rs +++ b/frontend/rust-lib/dart-ffi/src/env_serde.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; use serde::Deserialize; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; -use flowy_server_pub::supabase_config::SupabaseConfiguration; use flowy_server_pub::AuthenticatorType; #[derive(Deserialize, Debug)] @@ -17,7 +16,7 @@ pub struct AppFlowyDartConfiguration { pub device_id: String, pub platform: String, pub authenticator_type: AuthenticatorType, - pub(crate) supabase_config: SupabaseConfiguration, + //pub(crate) supabase_config: SupabaseConfiguration, pub(crate) appflowy_cloud_config: AFCloudConfiguration, #[serde(default)] pub(crate) envs: HashMap, @@ -31,7 +30,7 @@ impl AppFlowyDartConfiguration { pub fn write_env(&self) { self.authenticator_type.write_env(); self.appflowy_cloud_config.write_env(); - self.supabase_config.write_env(); + //self.supabase_config.write_env(); for (k, v) in self.envs.iter() { std::env::set_var(k, v); diff --git a/frontend/rust-lib/dart-ffi/src/lib.rs b/frontend/rust-lib/dart-ffi/src/lib.rs index 14b5a13a24..85281c8cb0 100644 --- a/frontend/rust-lib/dart-ffi/src/lib.rs +++ b/frontend/rust-lib/dart-ffi/src/lib.rs @@ -2,10 +2,9 @@ use allo_isolate::Isolate; use lazy_static::lazy_static; -use parking_lot::Mutex; use semver::Version; use std::rc::Rc; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::{ffi::CStr, os::raw::c_char}; use tracing::{debug, error, info, trace, warn}; @@ -38,6 +37,10 @@ lazy_static! { static ref LOG_STREAM_ISOLATE: Mutex> = Mutex::new(None); } +unsafe impl Send for MutexAppFlowyCore {} +unsafe impl Sync for MutexAppFlowyCore {} + +///FIXME: I'm pretty sure that there's a better way to do this struct MutexAppFlowyCore(Rc>>); impl MutexAppFlowyCore { @@ -46,15 +49,12 @@ impl MutexAppFlowyCore { } fn dispatcher(&self) -> Option> { - let binding = self.0.lock(); + let binding = self.0.lock().unwrap(); let core = binding.as_ref(); core.map(|core| core.event_dispatcher.clone()) } } -unsafe impl Sync for MutexAppFlowyCore {} -unsafe impl Send for MutexAppFlowyCore {} - #[no_mangle] pub extern "C" fn init_sdk(_port: i64, data: *mut c_char) -> i64 { // and sent it the `Rust's` result @@ -87,7 +87,7 @@ pub extern "C" fn init_sdk(_port: i64, data: *mut c_char) -> i64 { // Ensure that the database is closed before initialization. Also, verify that the init_sdk function can be called // multiple times (is reentrant). Currently, only the database resource is exclusive. - if let Some(core) = &*APPFLOWY_CORE.0.lock() { + if let Some(core) = &*APPFLOWY_CORE.0.lock().unwrap() { core.close_db(); } @@ -96,11 +96,12 @@ pub extern "C" fn init_sdk(_port: i64, data: *mut c_char) -> i64 { let log_stream = LOG_STREAM_ISOLATE .lock() + .unwrap() .take() .map(|isolate| Arc::new(LogStreamSenderImpl { isolate }) as Arc); // let isolate = allo_isolate::Isolate::new(port); - *APPFLOWY_CORE.0.lock() = runtime.block_on(async move { + *APPFLOWY_CORE.0.lock().unwrap() = runtime.block_on(async move { Some(AppFlowyCore::new(config, cloned_runtime, log_stream).await) // isolate.post("".to_string()); }); @@ -168,7 +169,7 @@ pub extern "C" fn set_stream_port(notification_port: i64) -> i32 { #[no_mangle] pub extern "C" fn set_log_stream_port(port: i64) -> i32 { - *LOG_STREAM_ISOLATE.lock() = Some(Isolate::new(port)); + *LOG_STREAM_ISOLATE.lock().unwrap() = Some(Isolate::new(port)); 0 } diff --git a/frontend/rust-lib/event-integration-test/Cargo.toml b/frontend/rust-lib/event-integration-test/Cargo.toml index 01f2f2aad3..33e4f4b184 100644 --- a/frontend/rust-lib/event-integration-test/Cargo.toml +++ b/frontend/rust-lib/event-integration-test/Cargo.toml @@ -37,7 +37,6 @@ thread-id = "3.3.0" bytes.workspace = true nanoid = "0.4.0" tracing.workspace = true -parking_lot.workspace = true uuid.workspace = true collab = { workspace = true } collab-document = { workspace = true } diff --git a/frontend/rust-lib/event-integration-test/src/database_event.rs b/frontend/rust-lib/event-integration-test/src/database_event.rs index b16f9d5ab5..221734fb78 100644 --- a/frontend/rust-lib/event-integration-test/src/database_event.rs +++ b/frontend/rust-lib/event-integration-test/src/database_event.rs @@ -24,7 +24,7 @@ impl EventIntegrationTest { self .appflowy_core .database_manager - .get_database_with_view_id(database_view_id) + .get_database_editor_with_view_id(database_view_id) .await .unwrap() .export_csv(CSVFormat::Original) diff --git a/frontend/rust-lib/event-integration-test/src/document/document_event.rs b/frontend/rust-lib/event-integration-test/src/document/document_event.rs index a6cab721d7..71e779389e 100644 --- a/frontend/rust-lib/event-integration-test/src/document/document_event.rs +++ b/frontend/rust-lib/event-integration-test/src/document/document_event.rs @@ -42,10 +42,10 @@ impl DocumentEventTest { .event_test .appflowy_core .document_manager - .get_opened_document(doc_id) + .editable_document(doc_id) .await .unwrap(); - let guard = doc.lock(); + let guard = doc.read().await; guard.encode_collab().unwrap() } diff --git a/frontend/rust-lib/event-integration-test/src/document_event.rs b/frontend/rust-lib/event-integration-test/src/document_event.rs index 28f27bdedd..407dcfe066 100644 --- a/frontend/rust-lib/event-integration-test/src/document_event.rs +++ b/frontend/rust-lib/event-integration-test/src/document_event.rs @@ -1,6 +1,3 @@ -use std::sync::Arc; - -use collab::core::collab::MutexCollab; use collab::core::origin::CollabOrigin; use collab::preclude::updates::decoder::Decode; use collab::preclude::{Collab, Update}; @@ -107,17 +104,13 @@ impl EventIntegrationTest { } pub fn assert_document_data_equal(doc_state: &[u8], doc_id: &str, expected: DocumentData) { - let collab = MutexCollab::new(Collab::new_with_origin( - CollabOrigin::Server, - doc_id, - vec![], - false, - )); - collab.lock().with_origin_transact_mut(|txn| { + let mut collab = Collab::new_with_origin(CollabOrigin::Server, doc_id, vec![], false); + { let update = Update::decode_v1(doc_state).unwrap(); + let mut txn = collab.transact_mut(); txn.apply_update(update); - }); - let document = Document::open(Arc::new(collab)).unwrap(); + }; + let document = Document::open(collab).unwrap(); let actual = document.get_document_data().unwrap(); assert_eq!(actual, expected); } diff --git a/frontend/rust-lib/event-integration-test/src/event_builder.rs b/frontend/rust-lib/event-integration-test/src/event_builder.rs index 5168723981..c4149378e5 100644 --- a/frontend/rust-lib/event-integration-test/src/event_builder.rs +++ b/frontend/rust-lib/event-integration-test/src/event_builder.rs @@ -1,3 +1,4 @@ +use crate::EventIntegrationTest; use flowy_user::errors::{internal_error, FlowyError}; use lib_dispatch::prelude::{ AFPluginDispatcher, AFPluginEventResponse, AFPluginFromBytes, AFPluginRequest, ToBytes, *, @@ -9,8 +10,6 @@ use std::{ hash::Hash, }; -use crate::EventIntegrationTest; - #[derive(Clone)] pub struct EventBuilder { context: TestContext, diff --git a/frontend/rust-lib/event-integration-test/src/folder_event.rs b/frontend/rust-lib/event-integration-test/src/folder_event.rs index 0c554df4b2..a6a7683c78 100644 --- a/frontend/rust-lib/event-integration-test/src/folder_event.rs +++ b/frontend/rust-lib/event-integration-test/src/folder_event.rs @@ -166,10 +166,14 @@ impl EventIntegrationTest { .await; } - pub fn get_folder_data(&self) -> FolderData { - let mutex_folder = self.appflowy_core.folder_manager.get_mutex_folder().clone(); - let folder_lock_guard = mutex_folder.read(); - let folder = folder_lock_guard.as_ref().unwrap(); + pub async fn get_folder_data(&self) -> FolderData { + let mutex_folder = self + .appflowy_core + .folder_manager + .get_mutex_folder() + .clone() + .unwrap(); + let folder = mutex_folder.read().await; let workspace_id = self.appflowy_core.user_manager.workspace_id().unwrap(); folder.get_folder_data(&workspace_id).clone().unwrap() } diff --git a/frontend/rust-lib/event-integration-test/src/lib.rs b/frontend/rust-lib/event-integration-test/src/lib.rs index e368c4168c..88100b2f85 100644 --- a/frontend/rust-lib/event-integration-test/src/lib.rs +++ b/frontend/rust-lib/event-integration-test/src/lib.rs @@ -6,11 +6,11 @@ use collab_entity::CollabType; use std::env::temp_dir; use std::path::PathBuf; use std::rc::Rc; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::Arc; use std::time::Duration; use nanoid::nanoid; -use parking_lot::{Mutex, RwLock}; use semver::Version; use tokio::select; use tokio::time::sleep; @@ -35,10 +35,10 @@ pub mod user_event; #[derive(Clone)] pub struct EventIntegrationTest { - pub authenticator: Arc>, + pub authenticator: Arc, pub appflowy_core: AppFlowyCore, #[allow(dead_code)] - cleaner: Arc>, + cleaner: Arc, pub notification_sender: TestNotificationSender, } @@ -57,7 +57,7 @@ impl EventIntegrationTest { let clean_path = config.storage_path.clone(); let inner = init_core(config).await; let notification_sender = TestNotificationSender::new(); - let authenticator = Arc::new(RwLock::new(AuthenticatorPB::Local)); + let authenticator = Arc::new(AtomicU8::new(AuthenticatorPB::Local as u8)); register_notification_sender(notification_sender.clone()); // In case of dropping the runtime that runs the core, we need to forget the dispatcher @@ -66,7 +66,7 @@ impl EventIntegrationTest { appflowy_core: inner, authenticator, notification_sender, - cleaner: Arc::new(Mutex::new(Cleaner::new(PathBuf::from(clean_path)))), + cleaner: Arc::new(Cleaner::new(PathBuf::from(clean_path))), } } @@ -93,7 +93,7 @@ impl EventIntegrationTest { } pub fn skip_clean(&mut self) { - self.cleaner.lock().should_clean = false; + self.cleaner.should_clean.store(false, Ordering::Release); } pub fn instance_name(&self) -> String { @@ -154,7 +154,7 @@ pub fn document_data_from_document_doc_state(doc_id: &str, doc_state: Vec) - } pub fn document_from_document_doc_state(doc_id: &str, doc_state: Vec) -> Document { - Document::from_doc_state( + Document::open_with_options( CollabOrigin::Empty, DataSource::DocStateV1(doc_state), doc_id, @@ -177,17 +177,16 @@ impl std::ops::Deref for EventIntegrationTest { } } -#[derive(Clone)] pub struct Cleaner { dir: PathBuf, - should_clean: bool, + should_clean: AtomicBool, } impl Cleaner { pub fn new(dir: PathBuf) -> Self { Self { dir, - should_clean: true, + should_clean: AtomicBool::new(true), } } @@ -198,7 +197,7 @@ impl Cleaner { impl Drop for Cleaner { fn drop(&mut self) { - if self.should_clean { + if self.should_clean.load(Ordering::Acquire) { Self::cleanup(&self.dir) } } diff --git a/frontend/rust-lib/event-integration-test/src/user_event.rs b/frontend/rust-lib/event-integration-test/src/user_event.rs index 54e3673d34..d4de053426 100644 --- a/frontend/rust-lib/event-integration-test/src/user_event.rs +++ b/frontend/rust-lib/event-integration-test/src/user_event.rs @@ -1,11 +1,12 @@ use std::collections::HashMap; use std::convert::TryFrom; +use std::sync::atomic::Ordering; use std::sync::Arc; use bytes::Bytes; use flowy_folder::entities::{RepeatedViewPB, WorkspacePB}; -use nanoid::nanoid; + use protobuf::ProtobufError; use tokio::sync::broadcast::{channel, Sender}; use tracing::error; @@ -101,21 +102,6 @@ impl EventIntegrationTest { } } - pub async fn supabase_party_sign_up(&self) -> UserProfilePB { - let map = third_party_sign_up_param(Uuid::new_v4().to_string()); - let payload = OauthSignInPB { - map, - authenticator: AuthenticatorPB::Supabase, - }; - - EventBuilder::new(self.clone()) - .event(UserEvent::OauthSignIn) - .payload(payload) - .async_send() - .await - .parse::() - } - pub async fn sign_out(&self) { EventBuilder::new(self.clone()) .event(UserEvent::SignOut) @@ -124,7 +110,7 @@ impl EventIntegrationTest { } pub fn set_auth_type(&self, auth_type: AuthenticatorPB) { - *self.authenticator.write() = auth_type; + self.authenticator.store(auth_type as u8, Ordering::Release); } pub async fn init_anon_user(&self) -> UserProfilePB { @@ -178,33 +164,6 @@ impl EventIntegrationTest { Ok(user_profile) } - pub async fn supabase_sign_up_with_uuid( - &self, - uuid: &str, - email: Option, - ) -> FlowyResult { - let mut map = HashMap::new(); - map.insert(USER_UUID.to_string(), uuid.to_string()); - map.insert(USER_DEVICE_ID.to_string(), uuid.to_string()); - map.insert( - USER_EMAIL.to_string(), - email.unwrap_or_else(|| format!("{}@appflowy.io", nanoid!(10))), - ); - let payload = OauthSignInPB { - map, - authenticator: AuthenticatorPB::Supabase, - }; - - let user_profile = EventBuilder::new(self.clone()) - .event(UserEvent::OauthSignIn) - .payload(payload) - .async_send() - .await - .try_parse::()?; - - Ok(user_profile) - } - pub async fn import_appflowy_data( &self, path: String, diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs index 49fbc01384..3b6d560a4e 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs @@ -207,6 +207,22 @@ impl FolderTest { }, } } + + // pub async fn duplicate_view(&self, view_id: &str) { + // let payload = DuplicateViewPayloadPB { + // view_id: view_id.to_string(), + // open_after_duplicate: false, + // include_children: false, + // parent_view_id: None, + // suffix: None, + // sync_after_create: false, + // }; + // EventBuilder::new(self.sdk.clone()) + // .event(DuplicateView) + // .payload(payload) + // .async_send() + // .await; + // } } pub async fn create_workspace(sdk: &EventIntegrationTest, name: &str, desc: &str) -> WorkspacePB { let request = CreateWorkspacePayloadPB { diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs index 03c0930f74..60f4595f53 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs @@ -125,7 +125,7 @@ async fn af_cloud_open_workspace_test() { assert_eq!(views[2].name, "D"); // simulate open workspace and check if the views are correct - for i in 0..30 { + for i in 0..10 { if i % 2 == 0 { test.open_workspace(&first_workspace.id).await; sleep(Duration::from_millis(300)).await; @@ -142,16 +142,16 @@ async fn af_cloud_open_workspace_test() { } test.open_workspace(&first_workspace.id).await; - let views = test.get_all_workspace_views().await; - assert_eq!(views[0].name, default_document_name); - assert_eq!(views[1].name, "A"); - assert_eq!(views[2].name, "B"); + let views_1 = test.get_all_workspace_views().await; + assert_eq!(views_1[0].name, default_document_name); + assert_eq!(views_1[1].name, "A"); + assert_eq!(views_1[2].name, "B"); test.open_workspace(&second_workspace.id).await; - let views = test.get_all_workspace_views().await; - assert_eq!(views[0].name, default_document_name); - assert_eq!(views[1].name, "C"); - assert_eq!(views[2].name, "D"); + let views_2 = test.get_all_workspace_views().await; + assert_eq!(views_2[0].name, default_document_name); + assert_eq!(views_2[1].name, "C"); + assert_eq!(views_2[2].name, "D"); } #[tokio::test] @@ -240,7 +240,7 @@ async fn af_cloud_different_open_same_workspace_test() { // Retrieve and verify the views associated with the workspace. let views = folder.get_views_belong_to(&shared_workspace_id); let folder_workspace_id = folder.get_workspace_id(); - assert_eq!(folder_workspace_id, shared_workspace_id); + assert_eq!(folder_workspace_id, Some(shared_workspace_id)); assert_eq!(views.len(), 1, "only get: {:?}", views); // Expecting two views. assert_eq!(views[0].name, "Getting started"); diff --git a/frontend/rust-lib/event-integration-test/tests/util.rs b/frontend/rust-lib/event-integration-test/tests/util.rs index ad1a01bcff..aabc528fa7 100644 --- a/frontend/rust-lib/event-integration-test/tests/util.rs +++ b/frontend/rust-lib/event-integration-test/tests/util.rs @@ -2,16 +2,11 @@ use std::fs::{create_dir_all, File, OpenOptions}; use std::io::copy; use std::ops::Deref; use std::path::{Path, PathBuf}; -use std::sync::Arc; use std::time::Duration; use std::{fs, io}; -use anyhow::Error; -use collab_folder::FolderData; -use collab_plugins::cloud_storage::RemoteCollabStorage; use nanoid::nanoid; use tokio::sync::mpsc::Receiver; - use tokio::time::timeout; use uuid::Uuid; use walkdir::WalkDir; @@ -21,22 +16,9 @@ use zip::{CompressionMethod, ZipArchive, ZipWriter}; use event_integration_test::event_builder::EventBuilder; use event_integration_test::Cleaner; use event_integration_test::EventIntegrationTest; -use flowy_database_pub::cloud::DatabaseCloudService; -use flowy_folder_pub::cloud::{FolderCloudService, FolderSnapshot}; -use flowy_server::supabase::api::*; -use flowy_server::{AppFlowyEncryption, EncryptionImpl}; -use flowy_server_pub::supabase_config::SupabaseConfiguration; -use flowy_user::entities::{AuthenticatorPB, UpdateUserProfilePayloadPB}; +use flowy_user::entities::UpdateUserProfilePayloadPB; use flowy_user::errors::FlowyError; - use flowy_user::event_map::UserEvent::*; -use flowy_user_pub::cloud::UserCloudService; -use flowy_user_pub::entities::Authenticator; - -pub fn get_supabase_config() -> Option { - dotenv::from_path(".env.ci").ok()?; - SupabaseConfiguration::from_env().ok() -} pub struct FlowySupabaseTest { event_test: EventIntegrationTest, @@ -44,13 +26,7 @@ pub struct FlowySupabaseTest { impl FlowySupabaseTest { pub async fn new() -> Option { - let _ = get_supabase_config()?; let event_test = EventIntegrationTest::new().await; - event_test.set_auth_type(AuthenticatorPB::Supabase); - event_test - .server_provider - .set_authenticator(Authenticator::Supabase); - Some(Self { event_test }) } @@ -79,93 +55,6 @@ pub async fn receive_with_timeout(mut receiver: Receiver, duration: Durati timeout(duration, receiver.recv()).await.ok()? } -pub fn get_supabase_ci_config() -> Option { - dotenv::from_filename("./.env.ci").ok()?; - SupabaseConfiguration::from_env().ok() -} - -#[allow(dead_code)] -pub fn get_supabase_dev_config() -> Option { - dotenv::from_filename("./.env.dev").ok()?; - SupabaseConfiguration::from_env().ok() -} - -pub fn collab_service() -> Arc { - let (server, encryption_impl) = appflowy_server(None); - Arc::new(SupabaseCollabStorageImpl::new( - server, - None, - Arc::downgrade(&encryption_impl), - )) -} - -pub fn database_service() -> Arc { - let (server, _encryption_impl) = appflowy_server(None); - Arc::new(SupabaseDatabaseServiceImpl::new(server)) -} - -pub fn user_auth_service() -> Arc { - let (server, _encryption_impl) = appflowy_server(None); - Arc::new(SupabaseUserServiceImpl::new(server, vec![], None)) -} - -pub fn folder_service() -> Arc { - let (server, _encryption_impl) = appflowy_server(None); - Arc::new(SupabaseFolderServiceImpl::new(server)) -} - -#[allow(dead_code)] -pub fn encryption_folder_service( - secret: Option, -) -> (Arc, Arc) { - let (server, encryption_impl) = appflowy_server(secret); - let service = Arc::new(SupabaseFolderServiceImpl::new(server)); - (service, encryption_impl) -} - -pub fn encryption_collab_service( - secret: Option, -) -> (Arc, Arc) { - let (server, encryption_impl) = appflowy_server(secret); - let service = Arc::new(SupabaseCollabStorageImpl::new( - server, - None, - Arc::downgrade(&encryption_impl), - )); - (service, encryption_impl) -} - -pub async fn get_folder_data_from_server( - uid: &i64, - folder_id: &str, - encryption_secret: Option, -) -> Result, Error> { - let (cloud_service, _encryption) = encryption_folder_service(encryption_secret); - cloud_service.get_folder_data(folder_id, uid).await -} - -pub async fn get_folder_snapshots( - folder_id: &str, - encryption_secret: Option, -) -> Vec { - let (cloud_service, _encryption) = encryption_folder_service(encryption_secret); - cloud_service - .get_folder_snapshots(folder_id, 10) - .await - .unwrap() -} - -pub fn appflowy_server( - encryption_secret: Option, -) -> (SupabaseServerServiceImpl, Arc) { - let config = SupabaseConfiguration::from_env().unwrap(); - let encryption_impl: Arc = - Arc::new(EncryptionImpl::new(encryption_secret)); - let encryption = Arc::downgrade(&encryption_impl); - let server = Arc::new(RESTfulPostgresServer::new(config, encryption)); - (SupabaseServerServiceImpl::new(server), encryption_impl) -} - /// zip the asset to the destination /// Zips the specified directory into a zip file. /// diff --git a/frontend/rust-lib/flowy-ai/Cargo.toml b/frontend/rust-lib/flowy-ai/Cargo.toml index 3e26f38a5e..74af2731ee 100644 --- a/frontend/rust-lib/flowy-ai/Cargo.toml +++ b/frontend/rust-lib/flowy-ai/Cargo.toml @@ -19,6 +19,7 @@ uuid.workspace = true strum_macros = "0.21" protobuf.workspace = true bytes.workspace = true +arc-swap.workspace = true validator = { workspace = true, features = ["derive"] } lib-infra = { workspace = true, features = ["isolate_flutter"] } flowy-ai-pub.workspace = true @@ -33,7 +34,6 @@ serde_json = { workspace = true } anyhow = "1.0.86" tokio-stream = "0.1.15" tokio-util = { workspace = true, features = ["full"] } -parking_lot.workspace = true appflowy-local-ai = { version = "0.1.0", features = ["verbose"] } appflowy-plugin = { version = "0.1.0" } reqwest = "0.11.27" diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 6adbd69813..8b3bf782a1 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -67,7 +67,8 @@ impl AIManager { } pub async fn initialize(&self, _workspace_id: &str) -> Result<(), FlowyError> { - self.local_ai_controller.refresh().await?; + // Ignore following error + let _ = self.local_ai_controller.refresh().await; Ok(()) } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs b/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs index af1d67b913..12466af8ca 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs @@ -17,8 +17,8 @@ use lib_infra::async_trait::async_trait; use std::collections::HashMap; use crate::stream_message::StreamMessage; +use arc_swap::ArcSwapOption; use futures_util::SinkExt; -use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use serde_json::json; use std::ops::Deref; @@ -47,7 +47,7 @@ const LOCAL_AI_SETTING_KEY: &str = "appflowy_local_ai_setting:v0"; pub struct LocalAIController { local_ai: Arc, local_ai_resource: Arc, - current_chat_id: Mutex>, + current_chat_id: ArcSwapOption, store_preferences: Arc, user_service: Arc, } @@ -80,7 +80,7 @@ impl LocalAIController { res_impl, tx, )); - let current_chat_id = Mutex::new(None); + let current_chat_id = ArcSwapOption::default(); let mut running_state_rx = local_ai.subscribe_running_state(); let cloned_llm_res = llm_res.clone(); @@ -205,12 +205,14 @@ impl LocalAIController { // Only keep one chat open at a time. Since loading multiple models at the same time will cause // memory issues. - if let Some(current_chat_id) = self.current_chat_id.lock().as_ref() { + if let Some(current_chat_id) = self.current_chat_id.load().as_ref() { debug!("[AI Plugin] close previous chat: {}", current_chat_id); self.close_chat(current_chat_id); } - *self.current_chat_id.lock() = Some(chat_id.to_string()); + self + .current_chat_id + .store(Some(Arc::new(chat_id.to_string()))); let chat_id = chat_id.to_string(); let weak_ctrl = Arc::downgrade(&self.local_ai); tokio::spawn(async move { @@ -534,7 +536,7 @@ impl LLMResourceService for LLMResourceServiceImpl { fn store_setting(&self, setting: LLMSetting) -> Result<(), Error> { self .store_preferences - .set_object(LOCAL_AI_SETTING_KEY, setting)?; + .set_object(LOCAL_AI_SETTING_KEY, &setting)?; Ok(()) } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_resource.rs index 457322a111..6d3ff1953b 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_resource.rs @@ -9,8 +9,8 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use futures::Sink; use futures_util::SinkExt; use lib_infra::async_trait::async_trait; -use parking_lot::RwLock; +use arc_swap::ArcSwapOption; use lib_infra::util::{get_operating_system, OperatingSystem}; use std::path::PathBuf; use std::sync::Arc; @@ -64,10 +64,10 @@ impl DownloadTask { pub struct LocalAIResourceController { user_service: Arc, resource_service: Arc, - llm_setting: RwLock>, + llm_setting: ArcSwapOption, // The ai_config will be set when user try to get latest local ai config from server - ai_config: RwLock>, - download_task: Arc>>, + ai_config: ArcSwapOption, + download_task: Arc>, resource_notify: tokio::sync::mpsc::Sender<()>, #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] #[allow(dead_code)] @@ -82,7 +82,7 @@ impl LocalAIResourceController { resource_notify: tokio::sync::mpsc::Sender<()>, ) -> Self { let (offline_app_state_sender, _) = tokio::sync::broadcast::channel(1); - let llm_setting = RwLock::new(resource_service.retrieve_setting()); + let llm_setting = resource_service.retrieve_setting().map(Arc::new); #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] let mut offline_app_disk_watch: Option = None; @@ -109,7 +109,7 @@ impl LocalAIResourceController { Self { user_service, resource_service: Arc::new(resource_service), - llm_setting, + llm_setting: ArcSwapOption::new(llm_setting), ai_config: Default::default(), download_task: Default::default(), resource_notify, @@ -125,7 +125,7 @@ impl LocalAIResourceController { } fn set_llm_setting(&self, llm_setting: LLMSetting) { - *self.llm_setting.write() = Some(llm_setting); + self.llm_setting.store(Some(llm_setting.into())); } /// Returns true when all resources are downloaded and ready to use. @@ -153,7 +153,7 @@ impl LocalAIResourceController { return Err(FlowyError::local_ai().with_context("No model found")); } - *self.ai_config.write() = Some(ai_config.clone()); + self.ai_config.store(Some(ai_config.clone().into())); let selected_model = self.select_model(&ai_config)?; let llm_setting = LLMSetting { @@ -173,7 +173,7 @@ impl LocalAIResourceController { pub fn use_local_llm(&self, llm_id: i64) -> FlowyResult { let (app, llm_model) = self .ai_config - .read() + .load() .as_ref() .and_then(|config| { config @@ -209,7 +209,7 @@ impl LocalAIResourceController { let pending_resources = self.calculate_pending_resources().ok()?; let is_ready = pending_resources.is_empty(); - let is_downloading = self.download_task.read().is_some(); + let is_downloading = self.download_task.load().is_some(); let pending_resources: Vec<_> = pending_resources .into_iter() .flat_map(|res| match res { @@ -243,7 +243,7 @@ impl LocalAIResourceController { /// Returns true when all resources are downloaded and ready to use. pub fn calculate_pending_resources(&self) -> FlowyResult> { - match self.llm_setting.read().as_ref() { + match self.llm_setting.load().as_ref() { None => Err(FlowyError::local_ai().with_context("Can't find any llm config")), Some(llm_setting) => { let mut resources = vec![]; @@ -296,7 +296,7 @@ impl LocalAIResourceController { info!("notify download finish, need to reload resources"); let _ = resource_notify.send(()).await; if let Some(download_task) = weak_download_task.upgrade() { - if let Some(task) = download_task.write().take() { + if let Some(task) = download_task.swap(None) { task.cancel(); } } @@ -307,25 +307,27 @@ impl LocalAIResourceController { }; // return immediately if download task already exists - if let Some(download_task) = self.download_task.read().as_ref() { - trace!( - "Download task already exists, return the task id: {}", - task_id - ); - progress_notify(download_task.tx.subscribe()); - return Ok(task_id); + { + let guard = self.download_task.load(); + if let Some(download_task) = &*guard { + trace!( + "Download task already exists, return the task id: {}", + task_id + ); + progress_notify(download_task.tx.subscribe()); + return Ok(task_id); + } } // If download task is not exists, create a new download task. info!("[LLM Resource] Start new download task"); let llm_setting = self .llm_setting - .read() - .clone() + .load_full() .ok_or_else(|| FlowyError::local_ai().with_context("No local ai config found"))?; - let download_task = DownloadTask::new(); - *self.download_task.write() = Some(download_task.clone()); + let download_task = Arc::new(DownloadTask::new()); + self.download_task.store(Some(download_task.clone())); progress_notify(download_task.tx.subscribe()); let model_dir = self.user_model_folder()?; @@ -339,15 +341,15 @@ impl LocalAIResourceController { // After download the plugin, start downloading models let chat_model_file = ( model_dir.join(&llm_setting.llm_model.chat_model.file_name), - llm_setting.llm_model.chat_model.file_name, - llm_setting.llm_model.chat_model.name, - llm_setting.llm_model.chat_model.download_url, + &llm_setting.llm_model.chat_model.file_name, + &llm_setting.llm_model.chat_model.name, + &llm_setting.llm_model.chat_model.download_url, ); let embedding_model_file = ( model_dir.join(&llm_setting.llm_model.embedding_model.file_name), - llm_setting.llm_model.embedding_model.file_name, - llm_setting.llm_model.embedding_model.name, - llm_setting.llm_model.embedding_model.download_url, + &llm_setting.llm_model.embedding_model.file_name, + &llm_setting.llm_model.embedding_model.name, + &llm_setting.llm_model.embedding_model.download_url, ); for (file_path, file_name, model_name, url) in [chat_model_file, embedding_model_file] { if file_path.exists() { @@ -370,9 +372,9 @@ impl LocalAIResourceController { } }); match download_model( - &url, + url, &model_dir, - &file_name, + file_name, Some(progress), Some(download_task.cancel_token.clone()), ) @@ -400,7 +402,7 @@ impl LocalAIResourceController { } pub fn cancel_download(&self) -> FlowyResult<()> { - if let Some(cancel_token) = self.download_task.write().take() { + if let Some(cancel_token) = self.download_task.swap(None) { info!("[LLM Resource] Cancel download"); cancel_token.cancel(); } @@ -416,9 +418,7 @@ impl LocalAIResourceController { let llm_setting = self .llm_setting - .read() - .as_ref() - .cloned() + .load_full() .ok_or_else(|| FlowyError::local_ai().with_context("No local llm setting found"))?; let model_dir = self.user_model_folder()?; @@ -475,16 +475,14 @@ impl LocalAIResourceController { } pub fn get_selected_model(&self) -> Option { - self - .llm_setting - .read() - .as_ref() - .map(|setting| setting.llm_model.clone()) + let setting = self.llm_setting.load(); + Some(setting.as_ref()?.llm_model.clone()) } /// Selects the appropriate model based on the current settings or defaults to the first model. fn select_model(&self, ai_config: &LocalAIConfig) -> FlowyResult { - let selected_model = match self.llm_setting.read().as_ref() { + let llm_setting = self.llm_setting.load(); + let selected_model = match &*llm_setting { None => ai_config.models[0].clone(), Some(llm_setting) => { match ai_config diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index 8b24a615dc..d6cd0c6635 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -44,8 +44,9 @@ bytes.workspace = true tokio = { workspace = true, features = ["full"] } tokio-stream = { workspace = true, features = ["sync"] } console-subscriber = { version = "0.2", optional = true } -parking_lot.workspace = true anyhow.workspace = true +dashmap.workspace = true +arc-swap.workspace = true base64 = "0.21.5" lib-infra = { workspace = true } diff --git a/frontend/rust-lib/flowy-core/src/config.rs b/frontend/rust-lib/flowy-core/src/config.rs index c910064a0a..395f3aebe2 100644 --- a/frontend/rust-lib/flowy-core/src/config.rs +++ b/frontend/rust-lib/flowy-core/src/config.rs @@ -6,7 +6,6 @@ use semver::Version; use tracing::{error, info}; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; -use flowy_server_pub::supabase_config::SupabaseConfiguration; use flowy_user::services::entities::URL_SAFE_ENGINE; use lib_infra::file_util::copy_dir_recursive; use lib_infra::util::OperatingSystem; @@ -85,13 +84,7 @@ impl AppFlowyCoreConfig { ) -> Self { let cloud_config = AFCloudConfiguration::from_env().ok(); let storage_path = match &cloud_config { - None => { - let supabase_config = SupabaseConfiguration::from_env().ok(); - match &supabase_config { - None => custom_application_path, - Some(config) => make_user_data_folder(&custom_application_path, &config.url), - } - }, + None => custom_application_path, Some(config) => make_user_data_folder(&custom_application_path, &config.base_url), }; let log_filter = create_log_filter("info".to_owned(), vec![], OperatingSystem::from(&platform)); diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs index 78994e8a34..62e192446a 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs @@ -11,7 +11,7 @@ use flowy_database2::DatabaseManager; use flowy_document::entities::DocumentDataPB; use flowy_document::manager::DocumentManager; use flowy_document::parser::json::parser::JsonToDocumentParser; -use flowy_error::FlowyError; +use flowy_error::{FlowyError, FlowyResult}; use flowy_folder::entities::{CreateViewParams, ViewLayoutPB}; use flowy_folder::manager::{FolderManager, FolderUser}; use flowy_folder::share::ImportType; @@ -26,7 +26,6 @@ use flowy_sqlite::kv::KVStorePreferences; use flowy_user::services::authenticate_user::AuthenticateUser; use flowy_user::services::data_import::{load_collab_by_object_id, load_collab_by_object_ids}; use lib_dispatch::prelude::ToBytes; - use std::collections::HashMap; use std::convert::TryFrom; use std::sync::{Arc, Weak}; @@ -111,6 +110,10 @@ impl FolderUser for FolderUserImpl { fn collab_db(&self, uid: i64) -> Result, FlowyError> { self.upgrade_user()?.get_collab_db(uid) } + + fn is_folder_exist_on_disk(&self, uid: i64, workspace_id: &str) -> FlowyResult { + self.upgrade_user()?.is_collab_on_disk(uid, workspace_id) + } } struct DocumentFolderOperation(Arc); diff --git a/frontend/rust-lib/flowy-core/src/integrate/server.rs b/frontend/rust-lib/flowy-core/src/integrate/server.rs index 86b85f16af..6314976c66 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/server.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/server.rs @@ -1,18 +1,17 @@ -use std::collections::HashMap; +use arc_swap::ArcSwapOption; +use dashmap::DashMap; use std::fmt::{Display, Formatter}; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::{Arc, Weak}; -use parking_lot::RwLock; use serde_repr::*; use flowy_error::{FlowyError, FlowyResult}; use flowy_server::af_cloud::define::ServerUser; use flowy_server::af_cloud::AppFlowyCloudServer; use flowy_server::local_server::{LocalServer, LocalServerDB}; -use flowy_server::supabase::SupabaseServer; use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; -use flowy_server_pub::supabase_config::SupabaseConfiguration; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; use flowy_user_pub::entities::*; @@ -26,12 +25,8 @@ pub enum Server { /// Offline mode, no user authentication and the data is stored locally. Local = 0, /// AppFlowy Cloud server provider. - /// The [AppFlowy-Server](https://github.com/AppFlowy-IO/AppFlowy-Cloud) is still a work in - /// progress. + /// See: https://github.com/AppFlowy-IO/AppFlowy-Cloud AppFlowyCloud = 1, - /// Supabase server provider. - /// It uses supabase postgresql database to store data and user authentication. - Supabase = 2, } impl Server { @@ -45,7 +40,6 @@ impl Display for Server { match self { Server::Local => write!(f, "Local"), Server::AppFlowyCloud => write!(f, "AppFlowyCloud"), - Server::Supabase => write!(f, "Supabase"), } } } @@ -56,16 +50,16 @@ impl Display for Server { /// Each server implements the [AppFlowyServer] trait, which provides the [UserCloudService], etc. pub struct ServerProvider { config: AppFlowyCoreConfig, - providers: RwLock>>, - pub(crate) encryption: RwLock>, + providers: DashMap>, + pub(crate) encryption: Arc, #[allow(dead_code)] pub(crate) store_preferences: Weak, - pub(crate) user_enable_sync: RwLock, + pub(crate) user_enable_sync: AtomicBool, /// The authenticator type of the user. - authenticator: RwLock, + authenticator: AtomicU8, user: Arc, - pub(crate) uid: Arc>>, + pub(crate) uid: Arc>, } impl ServerProvider { @@ -79,10 +73,10 @@ impl ServerProvider { let encryption = EncryptionImpl::new(None); Self { config, - providers: RwLock::new(HashMap::new()), - user_enable_sync: RwLock::new(true), - authenticator: RwLock::new(Authenticator::from(server)), - encryption: RwLock::new(Arc::new(encryption)), + providers: DashMap::new(), + user_enable_sync: AtomicBool::new(true), + authenticator: AtomicU8::new(Authenticator::from(server) as u8), + encryption: Arc::new(encryption), store_preferences, uid: Default::default(), user, @@ -90,33 +84,34 @@ impl ServerProvider { } pub fn get_server_type(&self) -> Server { - match &*self.authenticator.read() { + match Authenticator::from(self.authenticator.load(Ordering::Acquire) as i32) { Authenticator::Local => Server::Local, Authenticator::AppFlowyCloud => Server::AppFlowyCloud, - Authenticator::Supabase => Server::Supabase, } } pub fn set_authenticator(&self, authenticator: Authenticator) { let old_server_type = self.get_server_type(); - *self.authenticator.write() = authenticator; + self + .authenticator + .store(authenticator as u8, Ordering::Release); let new_server_type = self.get_server_type(); if old_server_type != new_server_type { - self.providers.write().remove(&old_server_type); + self.providers.remove(&old_server_type); } } pub fn get_authenticator(&self) -> Authenticator { - self.authenticator.read().clone() + Authenticator::from(self.authenticator.load(Ordering::Acquire) as i32) } /// Returns a [AppFlowyServer] trait implementation base on the provider_type. pub fn get_server(&self) -> FlowyResult> { let server_type = self.get_server_type(); - if let Some(provider) = self.providers.read().get(&server_type) { - return Ok(provider.clone()); + if let Some(provider) = self.providers.get(&server_type) { + return Ok(provider.value().clone()); } let server = match server_type { @@ -131,7 +126,7 @@ impl ServerProvider { let config = AFCloudConfiguration::from_env()?; let server = Arc::new(AppFlowyCloudServer::new( config, - *self.user_enable_sync.read(), + self.user_enable_sync.load(Ordering::Acquire), self.config.device_id.clone(), self.config.app_version.clone(), self.user.clone(), @@ -139,25 +134,9 @@ impl ServerProvider { Ok::, FlowyError>(server) }, - Server::Supabase => { - let config = SupabaseConfiguration::from_env()?; - let uid = self.uid.clone(); - tracing::trace!("🔑Supabase config: {:?}", config); - let encryption = Arc::downgrade(&*self.encryption.read()); - Ok::, FlowyError>(Arc::new(SupabaseServer::new( - uid, - config, - *self.user_enable_sync.read(), - self.config.device_id.clone(), - encryption, - ))) - }, }?; - self - .providers - .write() - .insert(server_type.clone(), server.clone()); + self.providers.insert(server_type.clone(), server.clone()); Ok(server) } } @@ -167,7 +146,6 @@ impl From for Server { match auth_provider { Authenticator::Local => Server::Local, Authenticator::AppFlowyCloud => Server::AppFlowyCloud, - Authenticator::Supabase => Server::Supabase, } } } @@ -177,7 +155,6 @@ impl From for Authenticator { match ty { Server::Local => Authenticator::Local, Server::AppFlowyCloud => Authenticator::AppFlowyCloud, - Server::Supabase => Authenticator::Supabase, } } } @@ -190,7 +167,6 @@ impl From<&Authenticator> for Server { pub fn current_server_type() -> Server { match AuthenticatorType::from_env() { AuthenticatorType::Local => Server::Local, - AuthenticatorType::Supabase => Server::Supabase, AuthenticatorType::AppFlowyCloud => Server::AppFlowyCloud, } } diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index cabc5cd97f..963f7fe159 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -2,6 +2,7 @@ use client_api::entity::search_dto::SearchDocumentResponseItem; use flowy_search_pub::cloud::SearchCloudService; use std::collections::HashMap; use std::path::Path; +use std::sync::atomic::Ordering; use std::sync::Arc; use anyhow::Error; @@ -9,10 +10,9 @@ use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin}; use client_api::entity::ai_dto::{CompletionType, RepeatedRelatedQuestion}; use client_api::entity::ChatMessageType; use collab::core::origin::{CollabClient, CollabOrigin}; - +use collab::entity::EncodedCollab; use collab::preclude::CollabPlugin; use collab_entity::CollabType; -use collab_plugins::cloud_storage::postgres::SupabaseDBPlugin; use serde_json::Value; use tokio_stream::wrappers::WatchStream; use tracing::{debug, info}; @@ -25,8 +25,8 @@ use flowy_ai_pub::cloud::{ RepeatedChatMessage, StreamAnswer, StreamComplete, }; use flowy_database_pub::cloud::{ - CollabDocStateByOid, DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, - SummaryRowContent, TranslateRowContent, TranslateRowResponse, + DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid, SummaryRowContent, + TranslateRowContent, TranslateRowResponse, }; use flowy_document::deps::DocumentData; use flowy_document_pub::cloud::{DocumentCloudService, DocumentSnapshot}; @@ -36,13 +36,11 @@ use flowy_folder_pub::cloud::{ }; use flowy_folder_pub::entities::{PublishInfoResponse, PublishPayload}; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; -use flowy_server_pub::supabase_config::SupabaseConfiguration; use flowy_storage_pub::cloud::{ObjectIdentity, ObjectValue, StorageCloudService}; use flowy_storage_pub::storage::{CompletedPartRequest, CreateUploadResponse, UploadPartResponse}; use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; use flowy_user_pub::entities::{Authenticator, UserTokenState}; use lib_infra::async_trait::async_trait; -use lib_infra::future::FutureResult; use crate::integrate::server::{Server, ServerProvider}; @@ -168,8 +166,8 @@ impl UserCloudServiceProvider for ServerProvider { fn set_enable_sync(&self, uid: i64, enable_sync: bool) { if let Ok(server) = self.get_server() { server.set_enable_sync(uid, enable_sync); - *self.user_enable_sync.write() = enable_sync; - *self.uid.write() = Some(uid); + self.user_enable_sync.store(enable_sync, Ordering::Release); + self.uid.store(Some(uid.into())); } } @@ -195,7 +193,7 @@ impl UserCloudServiceProvider for ServerProvider { fn set_encrypt_secret(&self, secret: String) { tracing::info!("🔑Set encrypt secret"); - self.encryption.write().set_secret(secret); + self.encryption.set_secret(secret); } /// Returns the [UserCloudService] base on the current [Server]. @@ -211,93 +209,87 @@ impl UserCloudServiceProvider for ServerProvider { Server::AppFlowyCloud => AFCloudConfiguration::from_env() .map(|config| config.base_url) .unwrap_or_default(), - Server::Supabase => SupabaseConfiguration::from_env() - .map(|config| config.url) - .unwrap_or_default(), } } } +#[async_trait] impl FolderCloudService for ServerProvider { - fn create_workspace(&self, uid: i64, name: &str) -> FutureResult { - let server = self.get_server(); + async fn create_workspace(&self, uid: i64, name: &str) -> Result { + let server = self.get_server()?; let name = name.to_string(); - FutureResult::new(async move { server?.folder_service().create_workspace(uid, &name).await }) + server.folder_service().create_workspace(uid, &name).await } - fn open_workspace(&self, workspace_id: &str) -> FutureResult<(), Error> { + async fn open_workspace(&self, workspace_id: &str) -> Result<(), Error> { let workspace_id = workspace_id.to_string(); - let server = self.get_server(); - FutureResult::new(async move { server?.folder_service().open_workspace(&workspace_id).await }) + let server = self.get_server()?; + server.folder_service().open_workspace(&workspace_id).await } - fn get_all_workspace(&self) -> FutureResult, Error> { - let server = self.get_server(); - FutureResult::new(async move { server?.folder_service().get_all_workspace().await }) + async fn get_all_workspace(&self) -> Result, Error> { + let server = self.get_server()?; + server.folder_service().get_all_workspace().await } - fn get_folder_data( + async fn get_folder_data( &self, workspace_id: &str, uid: &i64, - ) -> FutureResult, Error> { + ) -> Result, Error> { let uid = *uid; - let server = self.get_server(); + let server = self.get_server()?; let workspace_id = workspace_id.to_string(); - FutureResult::new(async move { - server? - .folder_service() - .get_folder_data(&workspace_id, &uid) - .await - }) + + server + .folder_service() + .get_folder_data(&workspace_id, &uid) + .await } - fn get_folder_snapshots( + async fn get_folder_snapshots( &self, workspace_id: &str, limit: usize, - ) -> FutureResult, Error> { + ) -> Result, Error> { let workspace_id = workspace_id.to_string(); - let server = self.get_server(); - FutureResult::new(async move { - server? - .folder_service() - .get_folder_snapshots(&workspace_id, limit) - .await - }) + let server = self.get_server()?; + + server + .folder_service() + .get_folder_snapshots(&workspace_id, limit) + .await } - fn get_folder_doc_state( + async fn get_folder_doc_state( &self, workspace_id: &str, uid: i64, collab_type: CollabType, object_id: &str, - ) -> FutureResult, Error> { + ) -> Result, Error> { let object_id = object_id.to_string(); let workspace_id = workspace_id.to_string(); - let server = self.get_server(); - FutureResult::new(async move { - server? - .folder_service() - .get_folder_doc_state(&workspace_id, uid, collab_type, &object_id) - .await - }) + let server = self.get_server()?; + + server + .folder_service() + .get_folder_doc_state(&workspace_id, uid, collab_type, &object_id) + .await } - fn batch_create_folder_collab_objects( + async fn batch_create_folder_collab_objects( &self, workspace_id: &str, objects: Vec, - ) -> FutureResult<(), Error> { + ) -> Result<(), Error> { let workspace_id = workspace_id.to_string(); - let server = self.get_server(); - FutureResult::new(async move { - server? - .folder_service() - .batch_create_folder_collab_objects(&workspace_id, objects) - .await - }) + let server = self.get_server()?; + + server + .folder_service() + .batch_create_folder_collab_objects(&workspace_id, objects) + .await } fn service_name(&self) -> String { @@ -307,114 +299,106 @@ impl FolderCloudService for ServerProvider { .unwrap_or_default() } - fn publish_view( + async fn publish_view( &self, workspace_id: &str, payload: Vec, - ) -> FutureResult<(), Error> { + ) -> Result<(), Error> { let workspace_id = workspace_id.to_string(); - let server = self.get_server(); - FutureResult::new(async move { - server? - .folder_service() - .publish_view(&workspace_id, payload) - .await - }) + let server = self.get_server()?; + + server + .folder_service() + .publish_view(&workspace_id, payload) + .await } - fn unpublish_views(&self, workspace_id: &str, view_ids: Vec) -> FutureResult<(), Error> { + async fn unpublish_views(&self, workspace_id: &str, view_ids: Vec) -> Result<(), Error> { let workspace_id = workspace_id.to_string(); - let server = self.get_server(); - FutureResult::new(async move { - server? - .folder_service() - .unpublish_views(&workspace_id, view_ids) - .await - }) + let server = self.get_server()?; + + server + .folder_service() + .unpublish_views(&workspace_id, view_ids) + .await } - fn get_publish_info(&self, view_id: &str) -> FutureResult { + async fn get_publish_info(&self, view_id: &str) -> Result { let view_id = view_id.to_string(); - let server = self.get_server(); - FutureResult::new(async move { server?.folder_service().get_publish_info(&view_id).await }) + let server = self.get_server()?; + server.folder_service().get_publish_info(&view_id).await } - fn set_publish_namespace( + async fn set_publish_namespace( &self, workspace_id: &str, new_namespace: &str, - ) -> FutureResult<(), Error> { + ) -> Result<(), Error> { let workspace_id = workspace_id.to_string(); let new_namespace = new_namespace.to_string(); - let server = self.get_server(); - FutureResult::new(async move { - server? - .folder_service() - .set_publish_namespace(&workspace_id, &new_namespace) - .await - }) + let server = self.get_server()?; + + server + .folder_service() + .set_publish_namespace(&workspace_id, &new_namespace) + .await } - fn get_publish_namespace(&self, workspace_id: &str) -> FutureResult { + async fn get_publish_namespace(&self, workspace_id: &str) -> Result { let workspace_id = workspace_id.to_string(); - let server = self.get_server(); - FutureResult::new(async move { - server? - .folder_service() - .get_publish_namespace(&workspace_id) - .await - }) + let server = self.get_server()?; + + server + .folder_service() + .get_publish_namespace(&workspace_id) + .await } } #[async_trait] impl DatabaseCloudService for ServerProvider { - fn get_database_object_doc_state( + async fn get_database_encode_collab( &self, object_id: &str, collab_type: CollabType, workspace_id: &str, - ) -> FutureResult>, Error> { + ) -> Result, Error> { let workspace_id = workspace_id.to_string(); - let server = self.get_server(); + let server = self.get_server()?; let database_id = object_id.to_string(); - FutureResult::new(async move { - server? - .database_service() - .get_database_object_doc_state(&database_id, collab_type, &workspace_id) - .await - }) + server + .database_service() + .get_database_encode_collab(&database_id, collab_type, &workspace_id) + .await } - fn batch_get_database_object_doc_state( + async fn batch_get_database_encode_collab( &self, object_ids: Vec, object_ty: CollabType, workspace_id: &str, - ) -> FutureResult { + ) -> Result { let workspace_id = workspace_id.to_string(); - let server = self.get_server(); - FutureResult::new(async move { - server? - .database_service() - .batch_get_database_object_doc_state(object_ids, object_ty, &workspace_id) - .await - }) + let server = self.get_server()?; + + server + .database_service() + .batch_get_database_encode_collab(object_ids, object_ty, &workspace_id) + .await } - fn get_database_collab_object_snapshots( + async fn get_database_collab_object_snapshots( &self, object_id: &str, limit: usize, - ) -> FutureResult, Error> { - let server = self.get_server(); + ) -> Result, Error> { + let server = self.get_server()?; let database_id = object_id.to_string(); - FutureResult::new(async move { - server? - .database_service() - .get_database_collab_object_snapshots(&database_id, limit) - .await - }) + + server + .database_service() + .get_database_collab_object_snapshots(&database_id, limit) + .await } } @@ -449,54 +433,52 @@ impl DatabaseAIService for ServerProvider { } } +#[async_trait] impl DocumentCloudService for ServerProvider { - fn get_document_doc_state( + async fn get_document_doc_state( &self, document_id: &str, workspace_id: &str, - ) -> FutureResult, FlowyError> { + ) -> Result, FlowyError> { let workspace_id = workspace_id.to_string(); let document_id = document_id.to_string(); - let server = self.get_server(); - FutureResult::new(async move { - server? - .document_service() - .get_document_doc_state(&document_id, &workspace_id) - .await - }) + let server = self.get_server()?; + + server + .document_service() + .get_document_doc_state(&document_id, &workspace_id) + .await } - fn get_document_snapshots( + async fn get_document_snapshots( &self, document_id: &str, limit: usize, workspace_id: &str, - ) -> FutureResult, Error> { + ) -> Result, Error> { let workspace_id = workspace_id.to_string(); - let server = self.get_server(); + let server = self.get_server()?; let document_id = document_id.to_string(); - FutureResult::new(async move { - server? - .document_service() - .get_document_snapshots(&document_id, limit, &workspace_id) - .await - }) + + server + .document_service() + .get_document_snapshots(&document_id, limit, &workspace_id) + .await } - fn get_document_data( + async fn get_document_data( &self, document_id: &str, workspace_id: &str, - ) -> FutureResult, Error> { + ) -> Result, Error> { let workspace_id = workspace_id.to_string(); - let server = self.get_server(); + let server = self.get_server()?; let document_id = document_id.to_string(); - FutureResult::new(async move { - server? - .document_service() - .get_document_data(&document_id, &workspace_id) - .await - }) + + server + .document_service() + .get_document_data(&document_id, &workspace_id) + .await } } @@ -563,34 +545,11 @@ impl CollabCloudPluginProvider for ServerProvider { vec![] } }, - CollabPluginProviderContext::Supabase { - uid, - collab_object, - local_collab, - local_collab_db, - } => { - let mut plugins: Vec> = vec![]; - if let Some(remote_collab_storage) = self - .get_server() - .ok() - .and_then(|provider| provider.collab_storage(&collab_object)) - { - plugins.push(Box::new(SupabaseDBPlugin::new( - uid, - collab_object, - local_collab, - 1, - remote_collab_storage, - local_collab_db, - ))); - } - plugins - }, } } fn is_sync_enabled(&self) -> bool { - *self.user_enable_sync.read() + self.user_enable_sync.load(Ordering::Acquire) } } diff --git a/frontend/rust-lib/flowy-core/src/integrate/user.rs b/frontend/rust-lib/flowy-core/src/integrate/user.rs index f84a4ec6a8..c165eda9c2 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/user.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/user.rs @@ -131,21 +131,12 @@ impl UserStatusCallback for UserStatusCallbackImpl { create_if_not_exist: true, }, Server::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), - Server::Supabase => { - if is_new_user { - FolderInitDataSource::LocalDisk { - create_if_not_exist: true, - } - } else { - FolderInitDataSource::Cloud(doc_state) - } - }, }, Err(err) => match server_type { Server::Local => FolderInitDataSource::LocalDisk { create_if_not_exist: true, }, - Server::AppFlowyCloud | Server::Supabase => { + Server::AppFlowyCloud => { return Err(FlowyError::from(err)); }, }, diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index ae5b1d801d..55b4753c66 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -2,7 +2,6 @@ use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_search::services::manager::SearchManager; -use parking_lot::Mutex; use std::rc::Rc; use std::sync::{Arc, Weak}; use std::time::Duration; @@ -302,7 +301,6 @@ impl From for CollabPluginProviderType { match server_type { Server::Local => CollabPluginProviderType::Local, Server::AppFlowyCloud => CollabPluginProviderType::AppFlowyCloud, - Server::Supabase => CollabPluginProviderType::Supabase, } } } @@ -323,13 +321,3 @@ impl ServerUser for ServerUserImpl { self.upgrade_user()?.workspace_id() } } - -pub struct MutexAppFlowyCore(pub Rc>); - -impl MutexAppFlowyCore { - pub fn new(appflowy_core: AppFlowyCore) -> Self { - Self(Rc::new(Mutex::new(appflowy_core))) - } -} -unsafe impl Sync for MutexAppFlowyCore {} -unsafe impl Send for MutexAppFlowyCore {} diff --git a/frontend/rust-lib/flowy-database-pub/src/cloud.rs b/frontend/rust-lib/flowy-database-pub/src/cloud.rs index 24da5c72a1..f35ef42cfb 100644 --- a/frontend/rust-lib/flowy-database-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-database-pub/src/cloud.rs @@ -1,13 +1,12 @@ use anyhow::Error; pub use client_api::entity::ai_dto::{TranslateItem, TranslateRowResponse}; -use collab::core::collab::DataSource; +use collab::entity::EncodedCollab; use collab_entity::CollabType; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; -use lib_infra::future::FutureResult; use std::collections::HashMap; -pub type CollabDocStateByOid = HashMap; +pub type EncodeCollabByOid = HashMap; pub type SummaryRowContent = HashMap; pub type TranslateRowContent = Vec; @@ -41,25 +40,25 @@ pub trait DatabaseAIService: Send + Sync { /// #[async_trait] pub trait DatabaseCloudService: Send + Sync { - fn get_database_object_doc_state( + async fn get_database_encode_collab( &self, object_id: &str, collab_type: CollabType, workspace_id: &str, - ) -> FutureResult>, Error>; + ) -> Result, Error>; - fn batch_get_database_object_doc_state( + async fn batch_get_database_encode_collab( &self, object_ids: Vec, object_ty: CollabType, workspace_id: &str, - ) -> FutureResult; + ) -> Result; - fn get_database_collab_object_snapshots( + async fn get_database_collab_object_snapshots( &self, object_id: &str, limit: usize, - ) -> FutureResult, Error>; + ) -> Result, Error>; } pub struct DatabaseSnapshot { diff --git a/frontend/rust-lib/flowy-database2/Cargo.toml b/frontend/rust-lib/flowy-database2/Cargo.toml index f4acee0d4d..ee05a8d73f 100644 --- a/frontend/rust-lib/flowy-database2/Cargo.toml +++ b/frontend/rust-lib/flowy-database2/Cargo.toml @@ -15,7 +15,6 @@ flowy-database-pub = { workspace = true } flowy-derive.workspace = true flowy-notification = { workspace = true } -parking_lot.workspace = true protobuf.workspace = true flowy-error = { path = "../flowy-error", features = [ "impl_from_dispatch_error", @@ -29,6 +28,7 @@ tracing.workspace = true serde.workspace = true serde_json.workspace = true serde_repr.workspace = true +arc-swap.workspace = true lib-infra = { workspace = true } chrono = { workspace = true, default-features = false, features = ["clock"] } rust_decimal = "1.28.1" diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index a051dcf63c..5f4f7456af 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -3,7 +3,7 @@ use std::sync::{Arc, Weak}; use collab_database::rows::RowId; use lib_infra::box_any::BoxAny; use tokio::sync::oneshot; -use tracing::error; +use tracing::{error, trace}; use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::{af_spawn, data_result_ok, AFPluginData, AFPluginState, DataResult}; @@ -33,8 +33,17 @@ pub(crate) async fn get_database_data_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let view_id: DatabaseViewIdPB = data.into_inner(); - let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?; + let database_id = manager + .get_database_id_with_view_id(view_id.as_ref()) + .await?; + let database_editor = manager.get_database_editor(&database_id).await?; let data = database_editor.get_database_data(view_id.as_ref()).await?; + trace!( + "layout: {:?}, rows: {}, fields: {}", + data.layout_type, + data.rows.len(), + data.fields.len() + ); data_result_ok(data) } @@ -72,7 +81,9 @@ pub(crate) async fn get_database_setting_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let view_id: DatabaseViewIdPB = data.into_inner(); - let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?; + let database_editor = manager + .get_database_editor_with_view_id(view_id.as_ref()) + .await?; let data = database_editor .get_database_view_setting(view_id.as_ref()) .await?; @@ -86,7 +97,9 @@ pub(crate) async fn update_database_setting_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params = data.try_into_inner()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; if let Some(payload) = params.insert_filter { database_editor @@ -139,7 +152,9 @@ pub(crate) async fn get_all_filters_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let view_id: DatabaseViewIdPB = data.into_inner(); - let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?; + let database_editor = manager + .get_database_editor_with_view_id(view_id.as_ref()) + .await?; let filters = database_editor.get_all_filters(view_id.as_ref()).await; data_result_ok(filters) } @@ -151,7 +166,9 @@ pub(crate) async fn get_all_sorts_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let view_id: DatabaseViewIdPB = data.into_inner(); - let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?; + let database_editor = manager + .get_database_editor_with_view_id(view_id.as_ref()) + .await?; let sorts = database_editor.get_all_sorts(view_id.as_ref()).await; data_result_ok(sorts) } @@ -163,7 +180,9 @@ pub(crate) async fn delete_all_sorts_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let view_id: DatabaseViewIdPB = data.into_inner(); - let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?; + let database_editor = manager + .get_database_editor_with_view_id(view_id.as_ref()) + .await?; database_editor.delete_all_sorts(view_id.as_ref()).await; Ok(()) } @@ -175,9 +194,12 @@ pub(crate) async fn get_fields_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params: GetFieldParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; let fields = database_editor .get_fields(¶ms.view_id, params.field_ids) + .await .into_iter() .map(FieldPB::new) .collect::>() @@ -192,9 +214,10 @@ pub(crate) async fn get_primary_field_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let view_id = data.into_inner().value; - let database_editor = manager.get_database_with_view_id(&view_id).await?; + let database_editor = manager.get_database_editor_with_view_id(&view_id).await?; let mut fields = database_editor .get_fields(&view_id, None) + .await .into_iter() .filter(|field| field.is_primary) .map(FieldPB::new) @@ -221,7 +244,9 @@ pub(crate) async fn update_field_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: FieldChangesetParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; database_editor.update_field(params).await?; Ok(()) } @@ -233,8 +258,10 @@ pub(crate) async fn update_field_type_option_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: TypeOptionChangesetParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; - if let Some(old_field) = database_editor.get_field(¶ms.field_id) { + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; + if let Some(old_field) = database_editor.get_field(¶ms.field_id).await { let field_type = FieldType::from(old_field.field_type); let type_option_data = type_option_data_from_pb(params.type_option_data, &field_type)?; database_editor @@ -251,7 +278,9 @@ pub(crate) async fn delete_field_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: FieldIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; database_editor.delete_field(¶ms.field_id).await?; Ok(()) } @@ -263,7 +292,9 @@ pub(crate) async fn clear_field_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: FieldIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; database_editor .clear_field(¶ms.view_id, ¶ms.field_id) .await?; @@ -277,14 +308,17 @@ pub(crate) async fn switch_to_field_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: EditFieldParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; - let old_field = database_editor.get_field(¶ms.field_id); + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; + let old_field = database_editor.get_field(¶ms.field_id).await; database_editor .switch_to_field_type(¶ms.field_id, params.field_type) .await?; if let Some(new_type_option) = database_editor .get_field(¶ms.field_id) + .await .map(|field| field.get_any_type_option(field.field_type)) { match (old_field, new_type_option) { @@ -308,7 +342,9 @@ pub(crate) async fn duplicate_field_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: DuplicateFieldPayloadPB = data.into_inner(); - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; database_editor .duplicate_field(¶ms.view_id, ¶ms.field_id) .await?; @@ -323,7 +359,9 @@ pub(crate) async fn create_field_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params: CreateFieldParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; let data = database_editor .create_field_with_type_option(params) .await?; @@ -338,7 +376,9 @@ pub(crate) async fn move_field_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: MoveFieldParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; database_editor.move_field(params).await?; Ok(()) } @@ -350,21 +390,42 @@ pub(crate) async fn get_row_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params: RowIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; let row = database_editor .get_row(¶ms.view_id, ¶ms.row_id) + .await .map(RowPB::from); data_result_ok(OptionalRowPB { row }) } +pub(crate) async fn init_row_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let manager = upgrade_manager(manager)?; + let params: RowIdParams = data.into_inner().try_into()?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; + database_editor.init_database_row(¶ms.row_id).await?; + Ok(()) +} + pub(crate) async fn get_row_meta_handler( data: AFPluginData, manager: AFPluginState>, ) -> DataResult { let manager = upgrade_manager(manager)?; let params: RowIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; - match database_editor.get_row_meta(¶ms.view_id, ¶ms.row_id) { + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; + match database_editor + .get_row_meta(¶ms.view_id, ¶ms.row_id) + .await + { None => Err(FlowyError::record_not_found()), Some(row) => data_result_ok(row), } @@ -376,7 +437,9 @@ pub(crate) async fn update_row_meta_handler( ) -> FlowyResult<()> { let manager = upgrade_manager(manager)?; let params: UpdateRowMetaParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; let row_id = RowId::from(params.id.clone()); database_editor .update_row_meta(&row_id.clone(), params) @@ -391,7 +454,9 @@ pub(crate) async fn delete_rows_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: RepeatedRowIdPB = data.into_inner(); - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; let row_ids = params .row_ids .into_iter() @@ -408,7 +473,9 @@ pub(crate) async fn duplicate_row_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: RowIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; database_editor .duplicate_row(¶ms.view_id, ¶ms.row_id) .await?; @@ -422,7 +489,9 @@ pub(crate) async fn move_row_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: MoveRowParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; database_editor .move_row(¶ms.view_id, params.from_row_id, params.to_row_id) .await?; @@ -436,7 +505,9 @@ pub(crate) async fn create_row_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params = data.try_into_inner()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; match database_editor.create_row(params).await? { Some(row) => data_result_ok(RowMetaPB::from(row)), @@ -451,7 +522,9 @@ pub(crate) async fn get_cell_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params: CellIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; let cell = database_editor .get_cell_pb(¶ms.field_id, ¶ms.row_id) .await @@ -466,7 +539,9 @@ pub(crate) async fn update_cell_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: CellChangesetPB = data.into_inner(); - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; database_editor .update_cell_with_changeset( ¶ms.view_id, @@ -485,7 +560,9 @@ pub(crate) async fn new_select_option_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params: CreateSelectOptionParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; let result = database_editor .create_select_option(¶ms.field_id, params.option_name) .await; @@ -505,7 +582,9 @@ pub(crate) async fn insert_or_update_select_option_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params = data.into_inner(); - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; database_editor .insert_select_options( ¶ms.view_id, @@ -524,7 +603,9 @@ pub(crate) async fn delete_select_option_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params = data.into_inner(); - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; database_editor .delete_select_options( ¶ms.view_id, @@ -544,7 +625,7 @@ pub(crate) async fn update_select_option_cell_handler( let manager = upgrade_manager(manager)?; let params: SelectOptionCellChangesetParams = data.into_inner().try_into()?; let database_editor = manager - .get_database_with_view_id(¶ms.cell_identifier.view_id) + .get_database_editor_with_view_id(¶ms.cell_identifier.view_id) .await?; let changeset = SelectOptionCellChangeset { insert_option_ids: params.insert_option_ids, @@ -568,7 +649,9 @@ pub(crate) async fn update_checklist_cell_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: ChecklistCellDataChangesetParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; let changeset = ChecklistCellChangeset { insert_options: params .insert_options @@ -609,7 +692,9 @@ pub(crate) async fn update_date_cell_handler( reminder_id: data.reminder_id, }; - let database_editor = manager.get_database_with_view_id(&cell_id.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(&cell_id.view_id) + .await?; database_editor .update_cell_with_changeset( &cell_id.view_id, @@ -628,7 +713,9 @@ pub(crate) async fn get_groups_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params: DatabaseViewIdPB = data.into_inner(); - let database_editor = manager.get_database_with_view_id(params.as_ref()).await?; + let database_editor = manager + .get_database_editor_with_view_id(params.as_ref()) + .await?; let groups = database_editor.load_groups(params.as_ref()).await?; data_result_ok(groups) } @@ -640,7 +727,9 @@ pub(crate) async fn get_group_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params: DatabaseGroupIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; let group = database_editor .get_group(¶ms.view_id, ¶ms.group_id) .await?; @@ -654,7 +743,9 @@ pub(crate) async fn set_group_by_field_handler( ) -> FlowyResult<()> { let manager = upgrade_manager(manager)?; let params: GroupByFieldParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; database_editor .set_group_by_field(¶ms.view_id, ¶ms.field_id, params.setting_content) .await?; @@ -669,17 +760,11 @@ pub(crate) async fn update_group_handler( let manager = upgrade_manager(manager)?; let params: UpdateGroupParams = data.into_inner().try_into()?; let view_id = params.view_id.clone(); - let database_editor = manager.get_database_with_view_id(&view_id).await?; + let database_editor = manager.get_database_editor_with_view_id(&view_id).await?; let group_changeset = GroupChangeset::from(params); - let (tx, rx) = oneshot::channel(); - af_spawn(async move { - let result = database_editor - .update_group(&view_id, vec![group_changeset]) - .await; - let _ = tx.send(result); - }); - - let _ = rx.await?; + database_editor + .update_group(&view_id, vec![group_changeset]) + .await?; Ok(()) } @@ -690,7 +775,9 @@ pub(crate) async fn move_group_handler( ) -> FlowyResult<()> { let manager = upgrade_manager(manager)?; let params: MoveGroupParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; database_editor .move_group(¶ms.view_id, ¶ms.from_group_id, ¶ms.to_group_id) .await?; @@ -704,7 +791,9 @@ pub(crate) async fn move_group_row_handler( ) -> FlowyResult<()> { let manager = upgrade_manager(manager)?; let params: MoveGroupRowParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; database_editor .move_group_row( ¶ms.view_id, @@ -724,7 +813,9 @@ pub(crate) async fn create_group_handler( ) -> FlowyResult<()> { let manager = upgrade_manager(manager)?; let params: CreateGroupParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; database_editor .create_group(¶ms.view_id, ¶ms.name) .await?; @@ -738,7 +829,9 @@ pub(crate) async fn delete_group_handler( ) -> FlowyResult<()> { let manager = upgrade_manager(manager)?; let params: DeleteGroupParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; database_editor.delete_group(params).await?; Ok(()) } @@ -792,7 +885,7 @@ pub(crate) async fn set_layout_setting_handler( let changeset = data.into_inner(); let view_id = changeset.view_id.clone(); let params: LayoutSettingChangeset = changeset.try_into()?; - let database_editor = manager.get_database_with_view_id(&view_id).await?; + let database_editor = manager.get_database_editor_with_view_id(&view_id).await?; database_editor.set_layout_setting(&view_id, params).await?; Ok(()) } @@ -803,7 +896,9 @@ pub(crate) async fn get_layout_setting_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params: DatabaseLayoutMeta = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; let layout_setting_pb = database_editor .get_layout_setting(¶ms.view_id, params.layout) .await @@ -819,7 +914,9 @@ pub(crate) async fn get_calendar_events_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params: CalendarEventRequestParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; let events = database_editor .get_all_calendar_events(¶ms.view_id) .await; @@ -833,7 +930,9 @@ pub(crate) async fn get_no_date_calendar_events_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params: CalendarEventRequestParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; let _events = database_editor .get_all_no_date_calendar_events(¶ms.view_id) .await; @@ -847,7 +946,9 @@ pub(crate) async fn get_calendar_event_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params: RowIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; let event = database_editor .get_calendar_event(¶ms.view_id, params.row_id) .await; @@ -869,7 +970,9 @@ pub(crate) async fn move_calendar_event_handler( date: Some(data.timestamp), ..Default::default() }; - let database_editor = manager.get_database_with_view_id(&cell_id.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(&cell_id.view_id) + .await?; database_editor .update_cell_with_changeset( &cell_id.view_id, @@ -897,7 +1000,7 @@ pub(crate) async fn export_csv_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let view_id = data.into_inner().value; - let database = manager.get_database_with_view_id(&view_id).await?; + let database = manager.get_database_editor_with_view_id(&view_id).await?; let data = database.export_csv(CSVFormat::Original).await?; data_result_ok(DatabaseExportDataPB { export_type: DatabaseExportDataType::CSV, @@ -923,7 +1026,7 @@ pub(crate) async fn get_field_settings_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let (view_id, field_ids) = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(&view_id).await?; + let database_editor = manager.get_database_editor_with_view_id(&view_id).await?; let field_settings = database_editor .get_field_settings(&view_id, field_ids.clone()) @@ -944,7 +1047,9 @@ pub(crate) async fn get_all_field_settings_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let view_id = data.into_inner(); - let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?; + let database_editor = manager + .get_database_editor_with_view_id(view_id.as_ref()) + .await?; let field_settings = database_editor .get_all_field_settings(view_id.as_ref()) @@ -965,7 +1070,9 @@ pub(crate) async fn update_field_settings_handler( ) -> FlowyResult<()> { let manager = upgrade_manager(manager)?; let params = data.try_into_inner()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; database_editor .update_field_settings_with_changeset(params) .await?; @@ -979,7 +1086,9 @@ pub(crate) async fn get_all_calculations_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let view_id = data.into_inner(); - let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?; + let database_editor = manager + .get_database_editor_with_view_id(view_id.as_ref()) + .await?; let calculations = database_editor.get_all_calculations(view_id.as_ref()).await; @@ -993,7 +1102,9 @@ pub(crate) async fn update_calculation_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: UpdateCalculationChangesetPB = data.into_inner(); - let editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; editor.update_calculation(params).await?; @@ -1007,7 +1118,9 @@ pub(crate) async fn remove_calculation_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: RemoveCalculationChangesetPB = data.into_inner(); - let editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let editor = manager + .get_database_editor_with_view_id(¶ms.view_id) + .await?; editor.remove_calculation(params).await?; @@ -1041,7 +1154,7 @@ pub(crate) async fn update_relation_cell_handler( removed_row_ids: params.removed_row_ids.into_iter().map(Into::into).collect(), }; - let database_editor = manager.get_database_with_view_id(&view_id).await?; + let database_editor = manager.get_database_editor_with_view_id(&view_id).await?; // // get the related database // let related_database_id = database_editor @@ -1072,7 +1185,7 @@ pub(crate) async fn get_related_row_datas_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params: GetRelatedRowDataPB = data.into_inner(); - let database_editor = manager.get_database(¶ms.database_id).await?; + let database_editor = manager.get_database_editor(¶ms.database_id).await?; let row_datas = database_editor .get_related_rows(Some(¶ms.row_ids)) .await?; @@ -1086,7 +1199,7 @@ pub(crate) async fn get_related_database_rows_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let database_id = data.into_inner().value; - let database_editor = manager.get_database(&database_id).await?; + let database_editor = manager.get_database_editor(&database_id).await?; let row_datas = database_editor.get_related_rows(None).await?; data_result_ok(RepeatedRelatedRowDataPB { rows: row_datas }) diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index 02c64da785..03f263d16d 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -13,85 +13,86 @@ pub fn init(database_manager: Weak) -> AFPlugin { .name(env!("CARGO_PKG_NAME")) .state(database_manager); plugin - .event(DatabaseEvent::GetDatabase, get_database_data_handler) - .event(DatabaseEvent::GetDatabaseData, get_database_data_handler) - .event(DatabaseEvent::GetDatabaseId, get_database_id_handler) - .event(DatabaseEvent::GetDatabaseSetting, get_database_setting_handler) - .event(DatabaseEvent::UpdateDatabaseSetting, update_database_setting_handler) - .event(DatabaseEvent::GetAllFilters, get_all_filters_handler) - .event(DatabaseEvent::GetAllSorts, get_all_sorts_handler) - .event(DatabaseEvent::DeleteAllSorts, delete_all_sorts_handler) - // Field - .event(DatabaseEvent::GetFields, get_fields_handler) - .event(DatabaseEvent::GetPrimaryField, get_primary_field_handler) - .event(DatabaseEvent::UpdateField, update_field_handler) - .event(DatabaseEvent::UpdateFieldTypeOption, update_field_type_option_handler) - .event(DatabaseEvent::DeleteField, delete_field_handler) - .event(DatabaseEvent::ClearField, clear_field_handler) - .event(DatabaseEvent::UpdateFieldType, switch_to_field_handler) - .event(DatabaseEvent::DuplicateField, duplicate_field_handler) - .event(DatabaseEvent::MoveField, move_field_handler) - .event(DatabaseEvent::CreateField, create_field_handler) - // Row - .event(DatabaseEvent::CreateRow, create_row_handler) - .event(DatabaseEvent::GetRow, get_row_handler) - .event(DatabaseEvent::GetRowMeta, get_row_meta_handler) - .event(DatabaseEvent::UpdateRowMeta, update_row_meta_handler) - .event(DatabaseEvent::DeleteRows, delete_rows_handler) - .event(DatabaseEvent::DuplicateRow, duplicate_row_handler) - .event(DatabaseEvent::MoveRow, move_row_handler) - // Cell - .event(DatabaseEvent::GetCell, get_cell_handler) - .event(DatabaseEvent::UpdateCell, update_cell_handler) - // SelectOption - .event(DatabaseEvent::CreateSelectOption, new_select_option_handler) - .event(DatabaseEvent::InsertOrUpdateSelectOption, insert_or_update_select_option_handler) - .event(DatabaseEvent::DeleteSelectOption, delete_select_option_handler) - .event(DatabaseEvent::UpdateSelectOptionCell, update_select_option_cell_handler) - // Checklist - .event(DatabaseEvent::UpdateChecklistCell, update_checklist_cell_handler) - // Date - .event(DatabaseEvent::UpdateDateCell, update_date_cell_handler) - // Group - .event(DatabaseEvent::SetGroupByField, set_group_by_field_handler) - .event(DatabaseEvent::MoveGroup, move_group_handler) - .event(DatabaseEvent::MoveGroupRow, move_group_row_handler) - .event(DatabaseEvent::GetGroups, get_groups_handler) - .event(DatabaseEvent::GetGroup, get_group_handler) - .event(DatabaseEvent::UpdateGroup, update_group_handler) - .event(DatabaseEvent::CreateGroup, create_group_handler) - .event(DatabaseEvent::DeleteGroup, delete_group_handler) - // Database - .event(DatabaseEvent::GetDatabaseMeta, get_database_meta_handler) - .event(DatabaseEvent::GetDatabases, get_databases_handler) - // Calendar - .event(DatabaseEvent::GetAllCalendarEvents, get_calendar_events_handler) - .event(DatabaseEvent::GetNoDateCalendarEvents, get_no_date_calendar_events_handler) - .event(DatabaseEvent::GetCalendarEvent, get_calendar_event_handler) - .event(DatabaseEvent::MoveCalendarEvent, move_calendar_event_handler) - // Layout setting - .event(DatabaseEvent::SetLayoutSetting, set_layout_setting_handler) - .event(DatabaseEvent::GetLayoutSetting, get_layout_setting_handler) - .event(DatabaseEvent::CreateDatabaseView, create_database_view) - // Export - .event(DatabaseEvent::ExportCSV, export_csv_handler) - .event(DatabaseEvent::GetDatabaseSnapshots, get_snapshots_handler) - // Field settings - .event(DatabaseEvent::GetFieldSettings, get_field_settings_handler) - .event(DatabaseEvent::GetAllFieldSettings, get_all_field_settings_handler) - .event(DatabaseEvent::UpdateFieldSettings, update_field_settings_handler) - // Calculations - .event(DatabaseEvent::GetAllCalculations, get_all_calculations_handler) - .event(DatabaseEvent::UpdateCalculation, update_calculation_handler) - .event(DatabaseEvent::RemoveCalculation, remove_calculation_handler) - // Relation - .event(DatabaseEvent::GetRelatedDatabaseIds, get_related_database_ids_handler) - .event(DatabaseEvent::UpdateRelationCell, update_relation_cell_handler) - .event(DatabaseEvent::GetRelatedRowDatas, get_related_row_datas_handler) - .event(DatabaseEvent::GetRelatedDatabaseRows, get_related_database_rows_handler) - // AI - .event(DatabaseEvent::SummarizeRow, summarize_row_handler) - .event(DatabaseEvent::TranslateRow, translate_row_handler) + .event(DatabaseEvent::GetDatabase, get_database_data_handler) + .event(DatabaseEvent::GetDatabaseData, get_database_data_handler) + .event(DatabaseEvent::GetDatabaseId, get_database_id_handler) + .event(DatabaseEvent::GetDatabaseSetting, get_database_setting_handler) + .event(DatabaseEvent::UpdateDatabaseSetting, update_database_setting_handler) + .event(DatabaseEvent::GetAllFilters, get_all_filters_handler) + .event(DatabaseEvent::GetAllSorts, get_all_sorts_handler) + .event(DatabaseEvent::DeleteAllSorts, delete_all_sorts_handler) + // Field + .event(DatabaseEvent::GetFields, get_fields_handler) + .event(DatabaseEvent::GetPrimaryField, get_primary_field_handler) + .event(DatabaseEvent::UpdateField, update_field_handler) + .event(DatabaseEvent::UpdateFieldTypeOption, update_field_type_option_handler) + .event(DatabaseEvent::DeleteField, delete_field_handler) + .event(DatabaseEvent::ClearField, clear_field_handler) + .event(DatabaseEvent::UpdateFieldType, switch_to_field_handler) + .event(DatabaseEvent::DuplicateField, duplicate_field_handler) + .event(DatabaseEvent::MoveField, move_field_handler) + .event(DatabaseEvent::CreateField, create_field_handler) + // Row + .event(DatabaseEvent::CreateRow, create_row_handler) + .event(DatabaseEvent::GetRow, get_row_handler) + .event(DatabaseEvent::InitRow, init_row_handler) + .event(DatabaseEvent::GetRowMeta, get_row_meta_handler) + .event(DatabaseEvent::UpdateRowMeta, update_row_meta_handler) + .event(DatabaseEvent::DeleteRows, delete_rows_handler) + .event(DatabaseEvent::DuplicateRow, duplicate_row_handler) + .event(DatabaseEvent::MoveRow, move_row_handler) + // Cell + .event(DatabaseEvent::GetCell, get_cell_handler) + .event(DatabaseEvent::UpdateCell, update_cell_handler) + // SelectOption + .event(DatabaseEvent::CreateSelectOption, new_select_option_handler) + .event(DatabaseEvent::InsertOrUpdateSelectOption, insert_or_update_select_option_handler) + .event(DatabaseEvent::DeleteSelectOption, delete_select_option_handler) + .event(DatabaseEvent::UpdateSelectOptionCell, update_select_option_cell_handler) + // Checklist + .event(DatabaseEvent::UpdateChecklistCell, update_checklist_cell_handler) + // Date + .event(DatabaseEvent::UpdateDateCell, update_date_cell_handler) + // Group + .event(DatabaseEvent::SetGroupByField, set_group_by_field_handler) + .event(DatabaseEvent::MoveGroup, move_group_handler) + .event(DatabaseEvent::MoveGroupRow, move_group_row_handler) + .event(DatabaseEvent::GetGroups, get_groups_handler) + .event(DatabaseEvent::GetGroup, get_group_handler) + .event(DatabaseEvent::UpdateGroup, update_group_handler) + .event(DatabaseEvent::CreateGroup, create_group_handler) + .event(DatabaseEvent::DeleteGroup, delete_group_handler) + // Database + .event(DatabaseEvent::GetDatabaseMeta, get_database_meta_handler) + .event(DatabaseEvent::GetDatabases, get_databases_handler) + // Calendar + .event(DatabaseEvent::GetAllCalendarEvents, get_calendar_events_handler) + .event(DatabaseEvent::GetNoDateCalendarEvents, get_no_date_calendar_events_handler) + .event(DatabaseEvent::GetCalendarEvent, get_calendar_event_handler) + .event(DatabaseEvent::MoveCalendarEvent, move_calendar_event_handler) + // Layout setting + .event(DatabaseEvent::SetLayoutSetting, set_layout_setting_handler) + .event(DatabaseEvent::GetLayoutSetting, get_layout_setting_handler) + .event(DatabaseEvent::CreateDatabaseView, create_database_view) + // Export + .event(DatabaseEvent::ExportCSV, export_csv_handler) + .event(DatabaseEvent::GetDatabaseSnapshots, get_snapshots_handler) + // Field settings + .event(DatabaseEvent::GetFieldSettings, get_field_settings_handler) + .event(DatabaseEvent::GetAllFieldSettings, get_all_field_settings_handler) + .event(DatabaseEvent::UpdateFieldSettings, update_field_settings_handler) + // Calculations + .event(DatabaseEvent::GetAllCalculations, get_all_calculations_handler) + .event(DatabaseEvent::UpdateCalculation, update_calculation_handler) + .event(DatabaseEvent::RemoveCalculation, remove_calculation_handler) + // Relation + .event(DatabaseEvent::GetRelatedDatabaseIds, get_related_database_ids_handler) + .event(DatabaseEvent::UpdateRelationCell, update_relation_cell_handler) + .event(DatabaseEvent::GetRelatedRowDatas, get_related_row_datas_handler) + .event(DatabaseEvent::GetRelatedDatabaseRows, get_related_database_rows_handler) + // AI + .event(DatabaseEvent::SummarizeRow, summarize_row_handler) + .event(DatabaseEvent::TranslateRow, translate_row_handler) } /// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf) @@ -377,4 +378,7 @@ pub enum DatabaseEvent { #[event(input = "TranslateRowPB")] TranslateRow = 175, + + #[event(input = "RowIdPB")] + InitRow = 176, } diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 4aa3f643fc..c37321fc92 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -1,22 +1,28 @@ use anyhow::anyhow; +use arc_swap::ArcSwapOption; +use async_trait::async_trait; +use std::borrow::BorrowMut; use std::collections::HashMap; use std::sync::{Arc, Weak}; -use collab::core::collab::{DataSource, MutexCollab}; -use collab_database::database::{DatabaseData, MutexDatabase}; +use collab::core::collab::DataSource; +use collab::preclude::Collab; +use collab_database::database::{Database, DatabaseData}; use collab_database::error::DatabaseError; use collab_database::rows::RowId; use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLayout}; use collab_database::workspace_database::{ - CollabDocStateByOid, CollabFuture, DatabaseCollabService, DatabaseMeta, WorkspaceDatabase, + DatabaseCollabService, DatabaseMeta, EncodeCollabByOid, WorkspaceDatabase, }; use collab_entity::{CollabType, EncodedCollab}; use collab_plugins::local_storage::kv::KVTransactionDB; use tokio::sync::{Mutex, RwLock}; use tracing::{event, instrument, trace}; -use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; -use collab_integrate::{CollabKVAction, CollabKVDB, CollabPersistenceConfig}; +use collab_integrate::collab_builder::{ + AppFlowyCollabBuilder, CollabBuilderConfig, KVDBCollabPersistenceImpl, +}; +use collab_integrate::{CollabKVAction, CollabKVDB}; use flowy_database_pub::cloud::{ DatabaseAIService, DatabaseCloudService, SummaryRowContent, TranslateItem, TranslateRowContent, }; @@ -42,7 +48,7 @@ pub trait DatabaseUser: Send + Sync { pub struct DatabaseManager { user: Arc, - workspace_database: Arc>>>, + workspace_database: ArcSwapOption>, task_scheduler: Arc>, editors: Mutex>>, collab_builder: Arc, @@ -89,10 +95,10 @@ impl DatabaseManager { } self.editors.lock().await.clear(); // 3. Clear the workspace database - if let Some(old_workspace_database) = self.workspace_database.write().await.take() { - old_workspace_database.close(); + if let Some(old_workspace_database) = self.workspace_database.swap(None) { + let wdb = old_workspace_database.read().await; + wdb.close(); } - *self.workspace_database.write().await = None; let collab_db = self.user.collab_db(uid)?; let collab_builder = UserDatabaseCollabServiceImpl { @@ -100,30 +106,27 @@ impl DatabaseManager { collab_builder: self.collab_builder.clone(), cloud_service: self.cloud_service.clone(), }; - let config = CollabPersistenceConfig::new().snapshot_per_update(100); let workspace_id = self.user.workspace_id()?; let workspace_database_object_id = self.user.workspace_database_object_id()?; - let mut workspace_database_doc_state = DataSource::Disk; + let mut workspace_database_doc_state = + KVDBCollabPersistenceImpl::new(collab_db.clone(), uid).into_data_source(); // If the workspace database not exist in disk, try to fetch from remote. if !self.is_collab_exist(uid, &collab_db, &workspace_database_object_id) { trace!("workspace database not exist, try to fetch from remote"); match self .cloud_service - .get_database_object_doc_state( + .get_database_encode_collab( &workspace_database_object_id, CollabType::WorkspaceDatabase, &workspace_id, ) .await { - Ok(doc_state) => match doc_state { - Some(doc_state) => { - workspace_database_doc_state = DataSource::DocStateV1(doc_state); - }, - None => { - workspace_database_doc_state = DataSource::Disk; - }, + Ok(value) => { + if let Some(encode_collab) = value { + workspace_database_doc_state = DataSource::from(encode_collab); + } }, Err(err) => { return Err(FlowyError::record_not_found().with_context(format!( @@ -140,20 +143,64 @@ impl DatabaseManager { "open aggregate database views object: {}", &workspace_database_object_id ); - let collab = collab_builder.build_collab_with_config( + + let workspace_id = self + .user + .workspace_id() + .map_err(|err| DatabaseError::Internal(err.into()))?; + let collab_object = self.collab_builder.collab_object( + &workspace_id, uid, &workspace_database_object_id, CollabType::WorkspaceDatabase, - collab_db.clone(), - workspace_database_doc_state, - config.clone(), )?; - let workspace_database = - WorkspaceDatabase::open(uid, collab, collab_db, config, collab_builder); - *self.workspace_database.write().await = Some(Arc::new(workspace_database)); + let workspace_database = self.collab_builder.create_workspace_database( + collab_object, + workspace_database_doc_state, + collab_db, + CollabBuilderConfig::default().sync_enable(true), + collab_builder, + )?; + self.workspace_database.store(Some(workspace_database)); Ok(()) } + //FIXME: we need to initialize sync plugin for newly created collabs + #[allow(dead_code)] + fn initialize_plugins( + &self, + uid: i64, + object_id: &str, + collab_type: CollabType, + collab: Arc>, + ) -> FlowyResult>> + where + T: BorrowMut + Send + Sync + 'static, + { + //FIXME: unfortunately UserDatabaseCollabService::build_collab_with_config is broken by + // design as it assumes that we can split collab building process, which we cannot because: + // 1. We should not be able to run plugins ie. SyncPlugin over not-fully initialized collab, + // and that's what originally build_collab_with_config did. + // 2. We cannot fully initialize collab from UserDatabaseCollabService, because + // WorkspaceDatabase itself requires UserDatabaseCollabService as constructor parameter. + // Ideally we should never need to initialize plugins that require collab instance as part of + // that collab construction process itself - it means that we should redesign SyncPlugin to only + // be fired once a collab is fully initialized. + let workspace_id = self + .user + .workspace_id() + .map_err(|err| DatabaseError::Internal(err.into()))?; + let object = self + .collab_builder + .collab_object(&workspace_id, uid, object_id, collab_type)?; + let collab = self.collab_builder.finalize( + object, + CollabBuilderConfig::default().sync_enable(true), + collab, + )?; + Ok(collab) + } + #[instrument( name = "database_initialize_with_new_user", level = "debug", @@ -166,19 +213,24 @@ impl DatabaseManager { } pub async fn get_database_inline_view_id(&self, database_id: &str) -> FlowyResult { - let wdb = self.get_database_indexer().await?; - let database_collab = wdb.get_database(database_id).await.ok_or_else(|| { - FlowyError::record_not_found().with_context(format!("The database:{} not found", database_id)) - })?; - - let lock_guard = database_collab.lock(); + let lock = self.workspace_database()?; + let wdb = lock.read().await; + let database_collab = wdb + .get_or_create_database(database_id) + .await + .ok_or_else(|| { + FlowyError::record_not_found() + .with_context(format!("The database:{} not found", database_id)) + })?; + let lock_guard = database_collab.read().await; Ok(lock_guard.get_inline_view_id()) } pub async fn get_all_databases_meta(&self) -> Vec { let mut items = vec![]; - if let Ok(wdb) = self.get_database_indexer().await { + if let Some(lock) = self.workspace_database.load_full() { + let wdb = lock.read().await; items = wdb.get_all_database_meta() } items @@ -188,7 +240,8 @@ impl DatabaseManager { &self, view_ids_by_database_id: HashMap>, ) -> FlowyResult<()> { - let wdb = self.get_database_indexer().await?; + let lock = self.workspace_database()?; + let mut wdb = lock.write().await; view_ids_by_database_id .into_iter() .for_each(|(database_id, view_ids)| { @@ -197,13 +250,9 @@ impl DatabaseManager { Ok(()) } - pub async fn get_database_with_view_id(&self, view_id: &str) -> FlowyResult> { - let database_id = self.get_database_id_with_view_id(view_id).await?; - self.get_database(&database_id).await - } - pub async fn get_database_id_with_view_id(&self, view_id: &str) -> FlowyResult { - let wdb = self.get_database_indexer().await?; + let lock = self.workspace_database()?; + let wdb = lock.read().await; wdb.get_database_id_with_view_id(view_id).ok_or_else(|| { FlowyError::record_not_found() .with_context(format!("The database for view id: {} not found", view_id)) @@ -211,28 +260,44 @@ impl DatabaseManager { } pub async fn get_database_row_ids_with_view_id(&self, view_id: &str) -> FlowyResult> { - let database = self.get_database_with_view_id(view_id).await?; - Ok(database.get_row_ids()) + let database = self.get_database_editor_with_view_id(view_id).await?; + Ok(database.get_row_ids().await) } - pub async fn get_database(&self, database_id: &str) -> FlowyResult> { + pub async fn get_database_editor_with_view_id( + &self, + view_id: &str, + ) -> FlowyResult> { + let database_id = self.get_database_id_with_view_id(view_id).await?; + self.get_database_editor(&database_id).await + } + + pub async fn get_database_editor(&self, database_id: &str) -> FlowyResult> { if let Some(editor) = self.editors.lock().await.get(database_id).cloned() { return Ok(editor); } - // TODO(nathan): refactor the get_database that split the database creation and database opening. self.open_database(database_id).await } pub async fn open_database(&self, database_id: &str) -> FlowyResult> { trace!("open database editor:{}", database_id); - let database = self - .get_database_indexer() - .await? - .get_database(database_id) + let lock = self.workspace_database()?; + let database = lock + .read() + .await + .get_or_create_database(database_id) .await .ok_or_else(|| FlowyError::collab_not_sync().with_context("open database error"))?; - let editor = Arc::new(DatabaseEditor::new(database, self.task_scheduler.clone()).await?); + let editor = Arc::new( + DatabaseEditor::new( + self.user.clone(), + database, + self.task_scheduler.clone(), + self.collab_builder.clone(), + ) + .await?, + ); self .editors .lock() @@ -241,17 +306,14 @@ impl DatabaseManager { Ok(editor) } + /// Open the database view pub async fn open_database_view>(&self, view_id: T) -> FlowyResult<()> { let view_id = view_id.as_ref(); - let wdb = self.get_database_indexer().await?; - if let Some(database_id) = wdb.get_database_id_with_view_id(view_id) { - if let Some(database) = wdb.open_database(&database_id) { - if let Some(lock_database) = database.try_lock() { - if let Some(lock_collab) = lock_database.get_collab().try_lock() { - trace!("{} database start init sync", view_id); - lock_collab.start_init_sync(); - } - } + let lock = self.workspace_database()?; + let workspace_database = lock.read().await; + if let Some(database_id) = workspace_database.get_database_id_with_view_id(view_id) { + if self.editors.lock().await.get(&database_id).is_none() { + self.open_database(&database_id).await?; } } Ok(()) @@ -259,20 +321,23 @@ impl DatabaseManager { pub async fn close_database_view>(&self, view_id: T) -> FlowyResult<()> { let view_id = view_id.as_ref(); - let wdb = self.get_database_indexer().await?; - let database_id = wdb.get_database_id_with_view_id(view_id); + let lock = self.workspace_database()?; + let workspace_database = lock.read().await; + let database_id = workspace_database.get_database_id_with_view_id(view_id); if let Some(database_id) = database_id { let mut editors = self.editors.lock().await; let mut should_remove = false; + if let Some(editor) = editors.get(&database_id) { editor.close_view(view_id).await; - should_remove = editor.num_views().await == 0; + // when there is no opening views, mark the database to be removed. + should_remove = editor.num_of_opening_views().await == 0; } if should_remove { trace!("remove database editor:{}", database_id); editors.remove(&database_id); - wdb.close_database(&database_id); + workspace_database.close_database(&database_id); } } @@ -280,13 +345,14 @@ impl DatabaseManager { } pub async fn delete_database_view(&self, view_id: &str) -> FlowyResult<()> { - let database = self.get_database_with_view_id(view_id).await?; + let database = self.get_database_editor_with_view_id(view_id).await?; let _ = database.delete_database_view(view_id).await?; Ok(()) } pub async fn duplicate_database(&self, view_id: &str) -> FlowyResult> { - let wdb = self.get_database_indexer().await?; + let lock = self.workspace_database()?; + let wdb = lock.read().await; let data = wdb.get_database_data(view_id).await?; let json_bytes = data.to_json_bytes()?; Ok(json_bytes) @@ -313,12 +379,12 @@ impl DatabaseManager { create_view_params.view_id = view_id.to_string(); } - let wdb = self.get_database_indexer().await?; + let lock = self.workspace_database()?; + let mut wdb = lock.write().await; let database = wdb.create_database(create_database_params)?; let encoded_collab = database - .lock() - .get_collab() - .lock() + .read() + .await .encode_collab_v1(|collab| CollabType::Database.validate_require_data(collab))?; Ok(encoded_collab) } @@ -326,9 +392,11 @@ impl DatabaseManager { pub async fn create_database_with_params( &self, params: CreateDatabaseParams, - ) -> FlowyResult> { - let wdb = self.get_database_indexer().await?; + ) -> FlowyResult>> { + let lock = self.workspace_database()?; + let mut wdb = lock.write().await; let database = wdb.create_database(params)?; + Ok(database) } @@ -342,12 +410,14 @@ impl DatabaseManager { database_view_id: String, database_parent_view_id: String, ) -> FlowyResult<()> { - let wdb = self.get_database_indexer().await?; + let lock = self.workspace_database()?; + let mut wdb = lock.write().await; let mut params = CreateViewParams::new(database_id.clone(), database_view_id, name, layout); - if let Some(database) = wdb.get_database(&database_id).await { + if let Some(database) = wdb.get_or_create_database(&database_id).await { let (field, layout_setting, field_settings_map) = DatabaseLayoutDepsResolver::new(database, layout) - .resolve_deps_when_create_database_linked_view(&database_parent_view_id); + .resolve_deps_when_create_database_linked_view(&database_parent_view_id) + .await; if let Some(field) = field { params = params.with_deps_fields(vec![field], vec![default_field_settings_by_layout_map()]); } @@ -374,18 +444,12 @@ impl DatabaseManager { .await .map_err(internal_error)??; - // Currently, we only support importing up to 500 rows. We can support more rows in the future. - if !cfg!(debug_assertions) && params.rows.len() > 500 { - return Err(FlowyError::internal().with_context("The number of rows exceeds the limit")); - } - let view_id = params.inline_view_id.clone(); let database_id = params.database_id.clone(); let database = self.create_database_with_params(params).await?; let encoded_collab = database - .lock() - .get_collab() - .lock() + .read() + .await .encode_collab_v1(|collab| CollabType::Database.validate_require_data(collab))?; let result = ImportResult { database_id, @@ -405,7 +469,7 @@ impl DatabaseManager { } pub async fn export_csv(&self, view_id: &str, style: CSVFormat) -> FlowyResult { - let database = self.get_database_with_view_id(view_id).await?; + let database = self.get_database_editor_with_view_id(view_id).await?; database.export_csv(style).await } @@ -414,7 +478,7 @@ impl DatabaseManager { view_id: &str, layout: DatabaseLayoutPB, ) -> FlowyResult<()> { - let database = self.get_database_with_view_id(view_id).await?; + let database = self.get_database_editor_with_view_id(view_id).await?; database.update_view_layout(view_id, layout.into()).await } @@ -440,14 +504,11 @@ impl DatabaseManager { Ok(snapshots) } - /// Return the database indexer. - /// Each workspace has itw own Database indexer that manages all the databases and database views - async fn get_database_indexer(&self) -> FlowyResult> { - let database = self.workspace_database.read().await; - match &*database { - None => Err(FlowyError::internal().with_context("Workspace database not initialized")), - Some(user_database) => Ok(user_database.clone()), - } + fn workspace_database(&self) -> FlowyResult>> { + self + .workspace_database + .load_full() + .ok_or_else(|| FlowyError::internal().with_context("Workspace database not initialized")) } #[instrument(level = "debug", skip_all)] @@ -457,10 +518,10 @@ impl DatabaseManager { row_id: RowId, field_id: String, ) -> FlowyResult<()> { - let database = self.get_database_with_view_id(&view_id).await?; + let database = self.get_database_editor_with_view_id(&view_id).await?; let mut summary_row_content = SummaryRowContent::new(); - if let Some(row) = database.get_row(&view_id, &row_id) { - let fields = database.get_fields(&view_id, None); + if let Some(row) = database.get_row(&view_id, &row_id).await { + let fields = database.get_fields(&view_id, None).await; for field in fields { // When summarizing a row, skip the content in the "AI summary" cell; it does not need to // be summarized. @@ -501,12 +562,12 @@ impl DatabaseManager { row_id: RowId, field_id: String, ) -> FlowyResult<()> { - let database = self.get_database_with_view_id(&view_id).await?; + let database = self.get_database_editor_with_view_id(&view_id).await?; let mut translate_row_content = TranslateRowContent::new(); let mut language = "english".to_string(); - if let Some(row) = database.get_row(&view_id, &row_id) { - let fields = database.get_fields(&view_id, None); + if let Some(row) = database.get_row(&view_id, &row_id).await { + let fields = database.get_fields(&view_id, None).await; for field in fields { // When translate a row, skip the content in the "AI Translate" cell; it does not need to // be translated. @@ -582,79 +643,73 @@ struct UserDatabaseCollabServiceImpl { cloud_service: Arc, } +#[async_trait] impl DatabaseCollabService for UserDatabaseCollabServiceImpl { - fn get_collab_doc_state( + async fn get_encode_collab( &self, object_id: &str, object_ty: CollabType, - ) -> CollabFuture> { + ) -> Result, DatabaseError> { let workspace_id = self.user.workspace_id().unwrap(); let object_id = object_id.to_string(); let weak_cloud_service = Arc::downgrade(&self.cloud_service); - Box::pin(async move { - match weak_cloud_service.upgrade() { - None => Err(DatabaseError::Internal(anyhow!("Cloud service is dropped"))), - Some(cloud_service) => { - let doc_state = cloud_service - .get_database_object_doc_state(&object_id, object_ty, &workspace_id) - .await?; - match doc_state { - None => Ok(DataSource::Disk), - Some(doc_state) => Ok(DataSource::DocStateV1(doc_state)), - } - }, - } - }) + + match weak_cloud_service.upgrade() { + None => Err(DatabaseError::Internal(anyhow!("Cloud service is dropped"))), + Some(cloud_service) => { + let encode_collab = cloud_service + .get_database_encode_collab(&object_id, object_ty, &workspace_id) + .await?; + Ok(encode_collab) + }, + } } - fn batch_get_collab_update( + async fn batch_get_encode_collab( &self, object_ids: Vec, object_ty: CollabType, - ) -> CollabFuture> { + ) -> Result { let cloned_user = self.user.clone(); let weak_cloud_service = Arc::downgrade(&self.cloud_service); - Box::pin(async move { - let workspace_id = cloned_user - .workspace_id() - .map_err(|err| DatabaseError::Internal(err.into()))?; - match weak_cloud_service.upgrade() { - None => { - tracing::warn!("Cloud service is dropped"); - Ok(CollabDocStateByOid::default()) - }, - Some(cloud_service) => { - let updates = cloud_service - .batch_get_database_object_doc_state(object_ids, object_ty, &workspace_id) - .await?; - Ok(updates) - }, - } - }) + + let workspace_id = cloned_user + .workspace_id() + .map_err(|err| DatabaseError::Internal(err.into()))?; + match weak_cloud_service.upgrade() { + None => { + tracing::warn!("Cloud service is dropped"); + Ok(EncodeCollabByOid::default()) + }, + Some(cloud_service) => { + let updates = cloud_service + .batch_get_database_encode_collab(object_ids, object_ty, &workspace_id) + .await?; + Ok(updates) + }, + } } - fn build_collab_with_config( + ///NOTE: this method doesn't initialize plugins, however it is passed into WorkspaceDatabase, + /// therefore all Database/DatabaseRow creation methods must initialize plugins thmselves. + fn build_collab( &self, uid: i64, object_id: &str, object_type: CollabType, collab_db: Weak, - collab_raw_data: DataSource, - _persistence_config: CollabPersistenceConfig, - ) -> Result, DatabaseError> { + data_source: DataSource, + ) -> Result { let workspace_id = self .user .workspace_id() .map_err(|err| DatabaseError::Internal(err.into()))?; - let collab = self.collab_builder.build_with_config( - &workspace_id, - uid, - object_id, - object_type.clone(), - collab_db.clone(), - collab_raw_data, - CollabBuilderConfig::default().sync_enable(true), - )?; + let object = self + .collab_builder + .collab_object(&workspace_id, uid, object_id, object_type)?; + let collab = self + .collab_builder + .build_collab(&object, &collab_db, data_source)?; Ok(collab) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/calculations/cache.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/cache.rs index d406c88f04..4b6307b095 100644 --- a/frontend/rust-lib/flowy-database2/src/services/calculations/cache.rs +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/cache.rs @@ -1,6 +1,5 @@ -use parking_lot::RwLock; use std::sync::Arc; use crate::utils::cache::AnyTypeCache; -pub type CalculationsByFieldIdCache = Arc>>; +pub type CalculationsByFieldIdCache = Arc>; diff --git a/frontend/rust-lib/flowy-database2/src/services/calculations/controller.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/controller.rs index 5e199b84ad..ad6cb71e6d 100644 --- a/frontend/rust-lib/flowy-database2/src/services/calculations/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/controller.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use std::str::FromStr; use std::sync::Arc; @@ -7,7 +8,6 @@ use flowy_error::FlowyResult; use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; -use lib_infra::future::Fut; use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatcher}; use crate::entities::{ @@ -19,13 +19,14 @@ use crate::utils::cache::AnyTypeCache; use super::{Calculation, CalculationChangeset, CalculationsService}; +#[async_trait] pub trait CalculationsDelegate: Send + Sync + 'static { - fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut>>; - fn get_field(&self, field_id: &str) -> Option; - fn get_calculation(&self, view_id: &str, field_id: &str) -> Fut>>; - fn get_all_calculations(&self, view_id: &str) -> Fut>>>; - fn update_calculation(&self, view_id: &str, calculation: Calculation); - fn remove_calculation(&self, view_id: &str, calculation_id: &str); + async fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Vec>; + async fn get_field(&self, field_id: &str) -> Option; + async fn get_calculation(&self, view_id: &str, field_id: &str) -> Option>; + async fn get_all_calculations(&self, view_id: &str) -> Arc>>; + async fn update_calculation(&self, view_id: &str, calculation: Calculation); + async fn remove_calculation(&self, view_id: &str, calculation_id: &str); } pub struct CalculationsController { @@ -45,7 +46,7 @@ impl Drop for CalculationsController { } impl CalculationsController { - pub async fn new( + pub fn new( view_id: &str, handler_id: &str, delegate: T, @@ -65,7 +66,7 @@ impl CalculationsController { calculations_service: CalculationsService::new(), notifier, }; - this.update_cache(calculations).await; + this.update_cache(calculations); this } @@ -130,7 +131,8 @@ impl CalculationsController { if let Some(calculation) = calculation { self .delegate - .remove_calculation(&self.view_id, &calculation.id); + .remove_calculation(&self.view_id, &calculation.id) + .await; let notification = CalculationChangesetNotificationPB::from_delete( &self.view_id, @@ -165,7 +167,8 @@ impl CalculationsController { if !calc_type.is_allowed(new_field_type) { self .delegate - .remove_calculation(&self.view_id, &calculation.id); + .remove_calculation(&self.view_id, &calculation.id) + .await; let notification = CalculationChangesetNotificationPB::from_delete( &self.view_id, @@ -201,7 +204,8 @@ impl CalculationsController { if let Some(update) = update { self .delegate - .update_calculation(&self.view_id, update.clone()); + .update_calculation(&self.view_id, update.clone()) + .await; let notification = CalculationChangesetNotificationPB::from_update( &self.view_id, @@ -238,7 +242,10 @@ impl CalculationsController { let update = self.get_updated_calculation(calculation.clone()).await; if let Some(update) = update { updates.push(CalculationPB::from(&update)); - self.delegate.update_calculation(&self.view_id, update); + self + .delegate + .update_calculation(&self.view_id, update) + .await; } } } @@ -252,7 +259,10 @@ impl CalculationsController { if let Some(update) = update { updates.push(CalculationPB::from(&update)); - self.delegate.update_calculation(&self.view_id, update); + self + .delegate + .update_calculation(&self.view_id, update) + .await; } } } @@ -273,7 +283,7 @@ impl CalculationsController { .delegate .get_cells_for_field(&self.view_id, &calculation.field_id) .await; - let field = self.delegate.get_field(&calculation.field_id)?; + let field = self.delegate.get_field(&calculation.field_id).await?; let value = self @@ -299,7 +309,7 @@ impl CalculationsController { .get_cells_for_field(&self.view_id, &insert.field_id) .await; - let field = self.delegate.get_field(&insert.field_id)?; + let field = self.delegate.get_field(&insert.field_id).await?; let value = self .calculations_service @@ -331,12 +341,11 @@ impl CalculationsController { notification } - async fn update_cache(&self, calculations: Vec>) { + fn update_cache(&self, calculations: Vec>) { for calculation in calculations { let field_id = &calculation.field_id; self .calculations_by_field_cache - .write() .insert(field_id, calculation.clone()); } } diff --git a/frontend/rust-lib/flowy-database2/src/services/calculations/entities.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/entities.rs index f4502020ac..2a2613230d 100644 --- a/frontend/rust-lib/flowy-database2/src/services/calculations/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/entities.rs @@ -1,14 +1,17 @@ -use anyhow::bail; -use collab::core::any_map::AnyMapExtension; +use collab::preclude::encoding::serde::from_any; +use collab::preclude::Any; use collab_database::views::{CalculationMap, CalculationMapBuilder}; +use serde::Deserialize; use crate::entities::CalculationPB; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize)] pub struct Calculation { pub id: String, pub field_id: String, + #[serde(default, rename = "ty")] pub calculation_type: i64, + #[serde(default, rename = "calculation_value")] pub value: String, } @@ -19,12 +22,12 @@ const CALCULATION_VALUE: &str = "calculation_value"; impl From for CalculationMap { fn from(data: Calculation) -> Self { - CalculationMapBuilder::new() - .insert_str_value(CALCULATION_ID, data.id) - .insert_str_value(FIELD_ID, data.field_id) - .insert_i64_value(CALCULATION_TYPE, data.calculation_type) - .insert_str_value(CALCULATION_VALUE, data.value) - .build() + CalculationMapBuilder::from([ + (CALCULATION_ID.into(), data.id.into()), + (FIELD_ID.into(), data.field_id.into()), + (CALCULATION_TYPE.into(), data.calculation_type.into()), + (CALCULATION_VALUE.into(), data.value.into()), + ]) } } @@ -45,29 +48,7 @@ impl TryFrom for Calculation { type Error = anyhow::Error; fn try_from(calculation: CalculationMap) -> Result { - match ( - calculation.get_str_value(CALCULATION_ID), - calculation.get_str_value(FIELD_ID), - ) { - (Some(id), Some(field_id)) => { - let value = calculation - .get_str_value(CALCULATION_VALUE) - .unwrap_or_default(); - let calculation_type = calculation - .get_i64_value(CALCULATION_TYPE) - .unwrap_or_default(); - - Ok(Calculation { - id, - field_id, - calculation_type, - value, - }) - }, - _ => { - bail!("Invalid calculation data") - }, - } + from_any(&Any::from(calculation)).map_err(|e| e.into()) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/cell_data_cache.rs b/frontend/rust-lib/flowy-database2/src/services/cell/cell_data_cache.rs index 07864351d4..b7606fedbd 100644 --- a/frontend/rust-lib/flowy-database2/src/services/cell/cell_data_cache.rs +++ b/frontend/rust-lib/flowy-database2/src/services/cell/cell_data_cache.rs @@ -1,6 +1,5 @@ -use parking_lot::RwLock; use std::sync::Arc; use crate::utils::cache::AnyTypeCache; -pub type CellCache = Arc>>; +pub type CellCache = Arc>; diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 40ce8db243..bf913db50c 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -5,7 +5,7 @@ use crate::services::cell::{apply_cell_changeset, get_cell_protobuf, CellCache}; use crate::services::database::database_observe::*; use crate::services::database::util::database_view_setting_pb_from_view; use crate::services::database_view::{ - DatabaseViewChanged, DatabaseViewEditor, DatabaseViewOperation, DatabaseViews, EditorByViewId, + DatabaseViewChanged, DatabaseViewOperation, DatabaseViews, EditorByViewId, }; use crate::services::field::{ default_type_option_data_from_type, select_type_option_from_field, transform_type_option, @@ -19,42 +19,48 @@ use crate::services::group::{default_group_setting, GroupChangeset, GroupSetting use crate::services::share::csv::{CSVExport, CSVFormat}; use crate::services::sort::Sort; use crate::utils::cache::AnyTypeCache; -use collab_database::database::MutexDatabase; +use crate::DatabaseUser; +use async_trait::async_trait; +use collab_database::database::Database; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{Cell, Cells, Row, RowCell, RowDetail, RowId}; use collab_database::views::{ DatabaseLayout, DatabaseView, FilterMap, LayoutSetting, OrderObjectPosition, }; +use collab_entity::CollabType; +use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; use flowy_notification::DebounceNotificationSender; use lib_infra::box_any::BoxAny; -use lib_infra::future::{to_fut, Fut, FutureResult}; use lib_infra::priority_task::TaskDispatcher; use lib_infra::util::timestamp; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::{broadcast, RwLock}; -use tracing::{event, instrument, warn}; +use tracing::{error, event, instrument, trace, warn}; #[derive(Clone)] pub struct DatabaseEditor { - database: Arc, + pub(crate) database: Arc>, pub cell_cache: CellCache, database_views: Arc, #[allow(dead_code)] /// Used to send notification to the frontend. notification_sender: Arc, + user: Arc, + collab_builder: Arc, } impl DatabaseEditor { pub async fn new( - database: Arc, + user: Arc, + database: Arc>, task_scheduler: Arc>, + collab_builder: Arc, ) -> FlowyResult { let notification_sender = Arc::new(DebounceNotificationSender::new(200)); let cell_cache = AnyTypeCache::::new(); - let database_id = database.lock().get_database_id(); - + let database_id = database.read().await.get_database_id(); // Receive database sync state and send to frontend via the notification observe_sync_state(&database_id, &database).await; // observe_view_change(&database_id, &database).await; @@ -81,11 +87,26 @@ impl DatabaseEditor { .await?, ); + let collab_object = collab_builder.collab_object( + &user.workspace_id()?, + user.user_id()?, + &database_id, + CollabType::Database, + )?; + + let database = collab_builder.finalize( + collab_object, + CollabBuilderConfig::default(), + database.clone(), + )?; + Ok(Self { + user, database, cell_cache, database_views, notification_sender, + collab_builder, }) } @@ -93,18 +114,19 @@ impl DatabaseEditor { self.database_views.close_view(view_id).await; } - pub fn get_row_ids(&self) -> Vec { + pub async fn get_row_ids(&self) -> Vec { self .database - .lock() - .block - .rows - .iter() - .map(|entry| entry.key().clone()) + .read() + .await + .get_database_rows() + .await + .into_iter() + .map(|entry| entry.id) .collect() } - pub async fn num_views(&self) -> usize { + pub async fn num_of_opening_views(&self) -> usize { self.database_views.num_editors().await } @@ -143,8 +165,8 @@ impl DatabaseEditor { Ok(view_editor.notifier.subscribe()) } - pub fn get_field(&self, field_id: &str) -> Option { - self.database.lock().fields.get_field(field_id) + pub async fn get_field(&self, field_id: &str) -> Option { + self.database.read().await.get_field(field_id) } pub async fn set_group_by_field( @@ -156,15 +178,15 @@ impl DatabaseEditor { let old_group_settings: Vec; let mut setting_content = "".to_string(); { - let database = self.database.lock(); - let field = database.fields.get_field(field_id); + let mut database = self.database.write().await; + let field = database.get_field(field_id); old_group_settings = database.get_all_group_setting(view_id); if let Some(field) = field { let field_type = FieldType::from(field.field_type); setting_content = group_config_pb_to_json_str(data, &field_type)?; let mut group_setting = default_group_setting(&field); group_setting.content = setting_content.clone(); - database.views.update_database_view(view_id, |view| { + database.update_database_view(view_id, |view| { view.set_groups(vec![group_setting.into()]); }); } @@ -201,7 +223,7 @@ impl DatabaseEditor { /// will be the reference view ids and the inline view id. Otherwise, the return value will /// be the view id. pub async fn delete_database_view(&self, view_id: &str) -> FlowyResult> { - Ok(self.database.lock().delete_view(view_id)) + Ok(self.database.write().await.delete_view(view_id)) } pub async fn update_group( @@ -295,11 +317,10 @@ impl DatabaseEditor { /// Returns a list of fields of the view. /// If `field_ids` is not provided, all the fields will be returned in the order of the field that /// defined in the view. Otherwise, the fields will be returned in the order of the `field_ids`. - pub fn get_fields(&self, view_id: &str, field_ids: Option>) -> Vec { - let database = self.database.lock(); + pub async fn get_fields(&self, view_id: &str, field_ids: Option>) -> Vec { + let database = self.database.read().await; let field_ids = field_ids.unwrap_or_else(|| { database - .fields .get_all_field_orders() .into_iter() .map(|field| field.id) @@ -309,22 +330,19 @@ impl DatabaseEditor { } pub async fn update_field(&self, params: FieldChangesetParams) -> FlowyResult<()> { - self - .database - .lock() - .fields - .update_field(¶ms.field_id, |update| { - update.set_name_if_not_none(params.name); - }); - notify_did_update_database_field(&self.database, ¶ms.field_id)?; + let mut database = self.database.write().await; + database.update_field(¶ms.field_id, |update| { + update.set_name_if_not_none(params.name); + }); + notify_did_update_database_field(&database, ¶ms.field_id)?; Ok(()) } pub async fn delete_field(&self, field_id: &str) -> FlowyResult<()> { let is_primary = self .database - .lock() - .fields + .write() + .await .get_field(field_id) .map(|field| field.is_primary) .unwrap_or(false); @@ -337,7 +355,7 @@ impl DatabaseEditor { } let database_id = { - let database = self.database.lock(); + let mut database = self.database.write().await; database.delete_field(field_id); database.get_database_id() }; @@ -355,6 +373,7 @@ impl DatabaseEditor { pub async fn clear_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> { let field_type: FieldType = self .get_field(field_id) + .await .map(|field| field.field_type.into()) .unwrap_or_default(); @@ -385,8 +404,17 @@ impl DatabaseEditor { old_field: Field, ) -> FlowyResult<()> { let view_editors = self.database_views.editors().await; - update_field_type_option_fn(&self.database, &view_editors, type_option_data, old_field).await?; + { + let mut database = self.database.write().await; + update_field_type_option_fn(&mut database, type_option_data, &old_field).await?; + drop(database); + } + for view_editor in view_editors { + view_editor + .v_did_update_field_type_option(&old_field) + .await?; + } Ok(()) } @@ -395,7 +423,8 @@ impl DatabaseEditor { field_id: &str, new_field_type: FieldType, ) -> FlowyResult<()> { - let field = self.database.lock().fields.get_field(field_id); + let mut database = self.database.write().await; + let field = database.get_field(field_id); match field { None => {}, Some(field) => { @@ -418,15 +447,11 @@ impl DatabaseEditor { old_type_option_data, new_type_option_data, ); - self - .database - .lock() - .fields - .update_field(field_id, |update| { - update - .set_field_type(new_field_type.into()) - .set_type_option(new_field_type.into(), Some(transformed_type_option)); - }); + database.update_field(field_id, |update| { + update + .set_field_type(new_field_type.into()) + .set_type_option(new_field_type.into(), Some(transformed_type_option)); + }); for view in self.database_views.editors().await { view.v_did_update_field_type(field_id, new_field_type).await; @@ -434,15 +459,13 @@ impl DatabaseEditor { }, } - notify_did_update_database_field(&self.database, field_id)?; + notify_did_update_database_field(&database, field_id)?; Ok(()) } pub async fn duplicate_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> { - let is_primary = self - .database - .lock() - .fields + let mut database = self.database.write().await; + let is_primary = database .get_field(field_id) .map(|field| field.is_primary) .unwrap_or(false); @@ -454,10 +477,10 @@ impl DatabaseEditor { )); } - let value = self - .database - .lock() - .duplicate_field(view_id, field_id, |field| format!("{} (copy)", field.name)); + let value = + database.duplicate_field(view_id, field_id, |field| format!("{} (copy)", field.name)); + drop(database); + if let Some((index, duplicated_field)) = value { let _ = self .notify_did_insert_database_field(duplicated_field.clone(), index) @@ -478,20 +501,16 @@ impl DatabaseEditor { pub async fn duplicate_row(&self, view_id: &str, row_id: &RowId) -> FlowyResult<()> { let (row_detail, index) = { - let database = self.database.lock(); + let mut database = self.database.write().await; let params = database .duplicate_row(row_id) + .await .ok_or_else(|| FlowyError::internal().with_context("error while copying row"))?; - let (index, row_order) = database - .create_row_in_view(view_id, params) - .ok_or_else(|| { - FlowyError::internal().with_context("error while inserting duplicated row") - })?; - + let (index, row_order) = database.create_row_in_view(view_id, params); tracing::trace!("duplicated row: {:?} at {}", row_order, index); - let row_detail = database.get_row_detail(&row_order.id); + let row_detail = database.get_row_detail(&row_order.id).await; (row_detail, index) }; @@ -511,14 +530,14 @@ impl DatabaseEditor { from_row_id: RowId, to_row_id: RowId, ) -> FlowyResult<()> { - let database = self.database.lock(); + let mut database = self.database.write().await; - let row_detail = database.get_row_detail(&from_row_id).ok_or_else(|| { + let row_detail = database.get_row_detail(&from_row_id).await.ok_or_else(|| { let msg = format!("Cannot find row {}", from_row_id); FlowyError::internal().with_context(msg) })?; - database.views.update_database_view(view_id, |view| { + database.update_database_view(view_id, |view| { view.move_row_order(&from_row_id, &to_row_id); }); @@ -546,20 +565,17 @@ impl DatabaseEditor { open_after_create: _, } = view_editor.v_will_create_row(params).await?; - let result = self - .database - .lock() - .create_row_in_view(&view_editor.view_id, collab_params); + let mut database = self.database.write().await; + let (index, order_id) = database.create_row_in_view(&view_editor.view_id, collab_params); + let row_detail = database.get_row_detail(&order_id.id).await; + drop(database); // Explicitly release the lock here - if let Some((index, row_order)) = result { - tracing::trace!("created row: {:?} at {}", row_order, index); - let row_detail = self.database.lock().get_row_detail(&row_order.id); - if let Some(row_detail) = row_detail { - for view in self.database_views.editors().await { - view.v_did_create_row(&row_detail, index).await; - } - return Ok(Some(row_detail)); + if let Some(row_detail) = row_detail { + trace!("created row: {:?} at {}", row_detail, index); + for view in self.database_views.editors().await { + view.v_did_create_row(&row_detail, index).await; } + return Ok(Some(row_detail)); } Ok(None) @@ -579,7 +595,7 @@ impl DatabaseEditor { .and_then(|data| type_option_data_from_pb(data, ¶ms.field_type).ok()) .unwrap_or(default_type_option_data_from_type(params.field_type)); - let (index, field) = self.database.lock().create_field_with_mut( + let (index, field) = self.database.write().await.create_field_with_mut( ¶ms.view_id, name, params.field_type.into(), @@ -601,21 +617,16 @@ impl DatabaseEditor { pub async fn move_field(&self, params: MoveFieldParams) -> FlowyResult<()> { let (field, new_index) = { - let database = self.database.lock(); + let mut database = self.database.write().await; - let field = database - .fields - .get_field(¶ms.from_field_id) - .ok_or_else(|| { - let msg = format!("Field with id: {} not found", ¶ms.from_field_id); - FlowyError::internal().with_context(msg) - })?; + let field = database.get_field(¶ms.from_field_id).ok_or_else(|| { + let msg = format!("Field with id: {} not found", ¶ms.from_field_id); + FlowyError::internal().with_context(msg) + })?; - database - .views - .update_database_view(¶ms.view_id, |view_update| { - view_update.move_field_order(¶ms.from_field_id, ¶ms.to_field_id); - }); + database.update_database_view(¶ms.view_id, |view_update| { + view_update.move_field_order(¶ms.from_field_id, ¶ms.to_field_id); + }); let new_index = database.index_of_field(¶ms.view_id, ¶ms.from_field_id); @@ -648,18 +659,49 @@ impl DatabaseEditor { Ok(view_editor.v_get_rows().await) } - pub fn get_row(&self, view_id: &str, row_id: &RowId) -> Option { - if self.database.lock().views.is_row_exist(view_id, row_id) { - Some(self.database.lock().get_row(row_id)) + pub async fn get_row(&self, view_id: &str, row_id: &RowId) -> Option { + let database = self.database.read().await; + if database.contains_row(view_id, row_id) { + Some(database.get_row(row_id).await) } else { None } } - pub fn get_row_meta(&self, view_id: &str, row_id: &RowId) -> Option { - if self.database.lock().views.is_row_exist(view_id, row_id) { - let row_meta = self.database.lock().get_row_meta(row_id)?; - let row_document_id = self.database.lock().get_row_document_id(row_id)?; + pub async fn init_database_row(&self, row_id: &RowId) -> FlowyResult<()> { + let database_row = self + .database + .read() + .await + .get_row_collab(row_id) + .ok_or_else(|| { + FlowyError::record_not_found() + .with_context(format!("The row:{} in database not found", row_id)) + })?; + + let collab_object = self.collab_builder.collab_object( + &self.user.workspace_id()?, + self.user.user_id()?, + row_id, + CollabType::DatabaseRow, + )?; + + if let Err(err) = + self + .collab_builder + .finalize(collab_object, CollabBuilderConfig::default(), database_row) + { + error!("Failed to init database row: {}", err); + } + + Ok(()) + } + + pub async fn get_row_meta(&self, view_id: &str, row_id: &RowId) -> Option { + let database = self.database.read().await; + if database.contains_row(view_id, row_id) { + let row_meta = database.get_row_meta(row_id).await?; + let row_document_id = database.get_row_document_id(row_id)?; Some(RowMetaPB { id: row_id.clone().into_inner(), document_id: row_document_id, @@ -673,9 +715,10 @@ impl DatabaseEditor { } } - pub fn get_row_detail(&self, view_id: &str, row_id: &RowId) -> Option { - if self.database.lock().views.is_row_exist(view_id, row_id) { - self.database.lock().get_row_detail(row_id) + pub async fn get_row_detail(&self, view_id: &str, row_id: &RowId) -> Option { + let database = self.database.read().await; + if database.contains_row(view_id, row_id) { + database.get_row_detail(row_id).await } else { warn!("the row:{} is exist in view:{}", row_id.as_str(), view_id); None @@ -683,7 +726,7 @@ impl DatabaseEditor { } pub async fn delete_rows(&self, row_ids: &[RowId]) { - let rows = self.database.lock().remove_rows(row_ids); + let rows = self.database.write().await.remove_rows(row_ids).await; for row in rows { tracing::trace!("Did delete row:{:?}", row); @@ -695,15 +738,20 @@ impl DatabaseEditor { #[tracing::instrument(level = "trace", skip_all)] pub async fn update_row_meta(&self, row_id: &RowId, changeset: UpdateRowMetaParams) { - self.database.lock().update_row_meta(row_id, |meta_update| { - meta_update - .insert_cover_if_not_none(changeset.cover_url) - .insert_icon_if_not_none(changeset.icon_url) - .update_is_document_empty_if_not_none(changeset.is_document_empty); - }); + let mut database = self.database.write().await; + database + .update_row_meta(row_id, |meta_update| { + meta_update + .insert_cover_if_not_none(changeset.cover_url) + .insert_icon_if_not_none(changeset.icon_url) + .update_is_document_empty_if_not_none(changeset.is_document_empty); + }) + .await; // Use the temporary row meta to get rid of the lock that not implement the `Send` or 'Sync' trait. - let row_detail = self.database.lock().get_row_detail(row_id); + let row_detail = database.get_row_detail(row_id).await; + drop(database); + if let Some(row_detail) = row_detail { for view in self.database_views.editors().await { view.v_did_update_row_meta(row_id, &row_detail).await; @@ -722,13 +770,13 @@ impl DatabaseEditor { } pub async fn get_cell(&self, field_id: &str, row_id: &RowId) -> Option { - let database = self.database.lock(); - let field = database.fields.get_field(field_id)?; + let database = self.database.read().await; + let field = database.get_field(field_id)?; let field_type = FieldType::from(field.field_type); // If the cell data is referenced, return the reference data. Otherwise, return an empty cell. match field_type { FieldType::LastEditedTime | FieldType::CreatedTime => { - let row = database.get_row(row_id); + let row = database.get_row(row_id).await; let wrapped_cell_data = if field_type.is_created_time() { TimestampCellDataWrapper::from((field_type, TimestampCellData::new(row.created_at))) } else { @@ -736,14 +784,14 @@ impl DatabaseEditor { }; Some(Cell::from(wrapped_cell_data)) }, - _ => database.get_cell(field_id, row_id).cell, + _ => database.get_cell(field_id, row_id).await.cell, } } pub async fn get_cell_pb(&self, field_id: &str, row_id: &RowId) -> Option { let (field, cell) = { let cell = self.get_cell(field_id, row_id).await?; - let field = self.database.lock().fields.get_field(field_id)?; + let field = self.database.read().await.get_field(field_id)?; (field, cell) }; @@ -758,12 +806,13 @@ impl DatabaseEditor { } pub async fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Vec { - let database = self.database.lock(); - if let Some(field) = database.fields.get_field(field_id) { + let database = self.database.read().await; + if let Some(field) = database.get_field(field_id) { let field_type = FieldType::from(field.field_type); match field_type { FieldType::LastEditedTime | FieldType::CreatedTime => database .get_rows_for_view(view_id) + .await .into_iter() .map(|row| { let data = if field_type.is_created_time() { @@ -777,7 +826,7 @@ impl DatabaseEditor { } }) .collect(), - _ => database.get_cells_for_field(view_id, field_id), + _ => database.get_cells_for_field(view_id, field_id).await, } } else { vec![] @@ -793,15 +842,15 @@ impl DatabaseEditor { cell_changeset: BoxAny, ) -> FlowyResult<()> { let (field, cell) = { - let database = self.database.lock(); - let field = match database.fields.get_field(field_id) { + let database = self.database.read().await; + let field = match database.get_field(field_id) { Some(field) => Ok(field), None => { let msg = format!("Field with id:{} not found", &field_id); Err(FlowyError::internal().with_context(msg)) }, }?; - (field, database.get_cell(field_id, row_id).cell) + (field, database.get_cell(field_id, row_id).await.cell) }; let new_cell = @@ -812,10 +861,12 @@ impl DatabaseEditor { async fn update_last_modified_time(&self, row_detail: RowDetail, view_id: &str) { self .database - .lock() - .update_row(&row_detail.row.id, |row_update| { + .write() + .await + .update_row(row_detail.row.id.clone(), |row_update| { row_update.set_last_modified(timestamp()); - }); + }) + .await; let editor = self.database_views.get_view_editor(view_id).await; if let Ok(editor) = editor { @@ -835,12 +886,17 @@ impl DatabaseEditor { new_cell: Cell, ) -> FlowyResult<()> { // Get the old row before updating the cell. It would be better to get the old cell - let old_row = { self.get_row_detail(view_id, row_id) }; - self.database.lock().update_row(row_id, |row_update| { - row_update.update_cells(|cell_update| { - cell_update.insert(field_id, new_cell); - }); - }); + let old_row = self.get_row_detail(view_id, row_id).await; + self + .database + .write() + .await + .update_row(row_id.clone(), |row_update| { + row_update.update_cells(|cell_update| { + cell_update.insert(field_id, new_cell); + }); + }) + .await; self .did_update_row(view_id, row_id, field_id, old_row) @@ -851,13 +907,18 @@ impl DatabaseEditor { pub async fn clear_cell(&self, view_id: &str, row_id: RowId, field_id: &str) -> FlowyResult<()> { // Get the old row before updating the cell. It would be better to get the old cell - let old_row = { self.get_row_detail(view_id, &row_id) }; + let old_row = self.get_row_detail(view_id, &row_id).await; - self.database.lock().update_row(&row_id, |row_update| { - row_update.update_cells(|cell_update| { - cell_update.clear(field_id); - }); - }); + self + .database + .write() + .await + .update_row(row_id.clone(), |row_update| { + row_update.update_cells(|cell_update| { + cell_update.clear(field_id); + }); + }) + .await; self .did_update_row(view_id, &row_id, field_id, old_row) @@ -873,7 +934,7 @@ impl DatabaseEditor { field_id: &str, old_row: Option, ) { - let option_row = self.get_row_detail(view_id, row_id); + let option_row = self.get_row_detail(view_id, row_id).await; if let Some(new_row_detail) = option_row { for view in self.database_views.editors().await { view @@ -883,14 +944,14 @@ impl DatabaseEditor { } } - pub fn get_auto_updated_fields_changesets( + pub async fn get_auto_updated_fields_changesets( &self, view_id: &str, row_id: RowId, ) -> Vec { // Get all auto updated fields. It will be used to notify the frontend // that the fields have been updated. - let auto_updated_fields = self.get_auto_updated_fields(view_id); + let auto_updated_fields = self.get_auto_updated_fields(view_id).await; // Collect all the updated field's id. Notify the frontend that all of them have been updated. let auto_updated_field_ids = auto_updated_fields @@ -913,7 +974,7 @@ impl DatabaseEditor { field_id: &str, option_name: String, ) -> Option { - let field = self.database.lock().fields.get_field(field_id)?; + let field = self.database.read().await.get_field(field_id)?; let type_option = select_type_option_from_field(&field).ok()?; let select_option = type_option.create_option(&option_name); Some(SelectOptionPB::from(select_option)) @@ -928,15 +989,10 @@ impl DatabaseEditor { row_id: RowId, options: Vec, ) -> FlowyResult<()> { - let field = self - .database - .lock() - .fields - .get_field(field_id) - .ok_or_else(|| { - FlowyError::record_not_found() - .with_context(format!("Field with id:{} not found", &field_id)) - })?; + let mut database = self.database.write().await; + let field = database.get_field(field_id).ok_or_else(|| { + FlowyError::record_not_found().with_context(format!("Field with id:{} not found", &field_id)) + })?; debug_assert!(FieldType::from(field.field_type).is_select_option()); let mut type_option = select_type_option_from_field(&field)?; @@ -950,13 +1006,12 @@ impl DatabaseEditor { // Update the field's type option let view_editors = self.database_views.editors().await; - update_field_type_option_fn( - &self.database, - &view_editors, - type_option.to_type_option_data(), - field.clone(), - ) - .await?; + update_field_type_option_fn(&mut database, type_option.to_type_option_data(), &field).await?; + drop(database); + + for view_editor in view_editors { + view_editor.v_did_update_field_type_option(&field).await?; + } // Insert the options into the cell self @@ -972,7 +1027,8 @@ impl DatabaseEditor { row_id: RowId, options: Vec, ) -> FlowyResult<()> { - let field = match self.database.lock().fields.get_field(field_id) { + let mut database = self.database.write().await; + let field = match database.get_field(field_id) { Some(field) => Ok(field), None => { let msg = format!("Field with id:{} not found", &field_id); @@ -990,13 +1046,14 @@ impl DatabaseEditor { } let view_editors = self.database_views.editors().await; - update_field_type_option_fn( - &self.database, - &view_editors, - type_option.to_type_option_data(), - field.clone(), - ) - .await?; + update_field_type_option_fn(&mut database, type_option.to_type_option_data(), &field).await?; + + // Drop the database write lock ASAP + drop(database); + + for view_editor in view_editors { + view_editor.v_did_update_field_type_option(&field).await?; + } self .update_cell_with_changeset(view_id, &row_id, field_id, BoxAny::new(cell_changeset)) @@ -1013,8 +1070,8 @@ impl DatabaseEditor { ) -> FlowyResult<()> { let field = self .database - .lock() - .fields + .read() + .await .get_field(field_id) .ok_or_else(|| { FlowyError::record_not_found() @@ -1068,7 +1125,7 @@ impl DatabaseEditor { from_row: RowId, to_row: Option, ) -> FlowyResult<()> { - let row_detail = self.get_row_detail(view_id, &from_row); + let row_detail = self.get_row_detail(view_id, &from_row).await; match row_detail { None => { warn!( @@ -1100,9 +1157,14 @@ impl DatabaseEditor { } tracing::trace!("Row data changed: {:?}", row_changeset); - self.database.lock().update_row(&row_detail.row.id, |row| { - row.set_cells(Cells::from(row_changeset.cell_by_field_id.clone())); - }); + self + .database + .write() + .await + .update_row(row_detail.row.id, |row| { + row.set_cells(Cells::from(row_changeset.cell_by_field_id.clone())); + }) + .await; }, } @@ -1170,7 +1232,7 @@ impl DatabaseEditor { #[tracing::instrument(level = "trace", skip_all, err)] async fn notify_did_insert_database_field(&self, field: Field, index: usize) -> FlowyResult<()> { - let database_id = self.database.lock().get_database_id(); + let database_id = self.database.read().await.get_database_id(); let index_field = IndexFieldPB { field: FieldPB::new(field), index: index as i32, @@ -1184,7 +1246,7 @@ impl DatabaseEditor { &self, changeset: DatabaseFieldChangesetPB, ) -> FlowyResult<()> { - let views = self.database.lock().get_all_database_views_meta(); + let views = self.database.read().await.get_all_database_views_meta(); for view in views { send_notification(&view.id, DatabaseNotification::DidUpdateFields) .payload(changeset.clone()) @@ -1198,10 +1260,12 @@ impl DatabaseEditor { &self, view_id: &str, ) -> FlowyResult { - let view = - self.database.lock().get_view(view_id).ok_or_else(|| { - FlowyError::record_not_found().with_context("Can't find the database view") - })?; + let view = self + .database + .read() + .await + .get_view(view_id) + .ok_or_else(|| FlowyError::record_not_found().with_context("Can't find the database view"))?; Ok(database_view_setting_pb_from_view(view)) } @@ -1213,10 +1277,9 @@ impl DatabaseEditor { .ok_or_else(FlowyError::record_not_found)?; let rows = database_view.v_get_rows().await; let (database_id, fields, is_linked) = { - let database = self.database.lock(); + let database = self.database.read().await; let database_id = database.get_database_id(); let fields = database - .fields .get_all_field_orders() .into_iter() .map(FieldIdPB::from) @@ -1240,13 +1303,11 @@ impl DatabaseEditor { pub async fn export_csv(&self, style: CSVFormat) -> FlowyResult { let database = self.database.clone(); - let csv = tokio::task::spawn_blocking(move || { - let database_guard = database.lock(); - let csv = CSVExport.export_database(&database_guard, style)?; - Ok::(csv) - }) - .await - .map_err(internal_error)??; + let database_guard = database.read().await; + let csv = CSVExport + .export_database(&database_guard, style) + .await + .map_err(internal_error)?; Ok(csv) } @@ -1269,6 +1330,7 @@ impl DatabaseEditor { pub async fn get_all_field_settings(&self, view_id: &str) -> FlowyResult> { let field_ids = self .get_fields(view_id, None) + .await .iter() .map(|field| field.id.clone()) .collect(); @@ -1289,7 +1351,8 @@ impl DatabaseEditor { pub async fn get_related_database_id(&self, field_id: &str) -> FlowyResult { let mut field = self .database - .lock() + .read() + .await .get_fields(Some(vec![field_id.to_string()])); let field = field.pop().ok_or(FlowyError::internal())?; @@ -1304,42 +1367,43 @@ impl DatabaseEditor { &self, row_ids: Option<&Vec>, ) -> FlowyResult> { - let primary_field = self.database.lock().fields.get_primary_field().unwrap(); + let database = self.database.read().await; + let primary_field = database.get_primary_field().unwrap(); let handler = TypeOptionCellExt::new(&primary_field, Some(self.cell_cache.clone())) .get_type_option_cell_data_handler_with_field_type(FieldType::RichText) .ok_or(FlowyError::internal())?; let row_data = { - let database = self.database.lock(); - let mut rows = database.get_database_rows(); + let mut rows = database.get_database_rows().await; if let Some(row_ids) = row_ids { rows.retain(|row| row_ids.contains(&row.id)); } - rows - .iter() - .map(|row| { - let title = database - .get_cell(&primary_field.id, &row.id) - .cell - .and_then(|cell| handler.handle_get_boxed_cell_data(&cell, &primary_field)) - .and_then(|cell_data| cell_data.unbox_or_none()) - .unwrap_or_else(|| StringCellData("".to_string())); + let mut row_data = vec![]; + for row in rows { + let title = database + .get_cell(&primary_field.id, &row.id) + .await + .cell + .and_then(|cell| handler.handle_get_boxed_cell_data(&cell, &primary_field)) + .and_then(|cell_data| cell_data.unbox_or_none()) + .unwrap_or_else(|| StringCellData("".to_string())); - RelatedRowDataPB { - row_id: row.id.to_string(), - name: title.0, - } + row_data.push(RelatedRowDataPB { + row_id: row.id.to_string(), + name: title.0, }) - .collect::>() + } + row_data }; Ok(row_data) } - fn get_auto_updated_fields(&self, view_id: &str) -> Vec { + async fn get_auto_updated_fields(&self, view_id: &str) -> Vec { self .database - .lock() + .read() + .await .get_fields_in_view(view_id, None) .into_iter() .filter(|f| FieldType::from(f.field_type).is_auto_update()) @@ -1348,45 +1412,48 @@ impl DatabaseEditor { /// Only expose this method for testing #[cfg(debug_assertions)] - pub fn get_mutex_database(&self) -> &MutexDatabase { + pub fn get_mutex_database(&self) -> &RwLock { &self.database } } struct DatabaseViewOperationImpl { - database: Arc, + database: Arc>, task_scheduler: Arc>, cell_cache: CellCache, editor_by_view_id: Arc>, } +#[async_trait] impl DatabaseViewOperation for DatabaseViewOperationImpl { - fn get_database(&self) -> Arc { + fn get_database(&self) -> Arc> { self.database.clone() } - fn get_view(&self, view_id: &str) -> Fut> { - let view = self.database.lock().get_view(view_id); - to_fut(async move { view }) + async fn get_view(&self, view_id: &str) -> Option { + self.database.read().await.get_view(view_id) } - fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut> { - let fields = self.database.lock().get_fields_in_view(view_id, field_ids); - to_fut(async move { fields }) + async fn get_fields(&self, view_id: &str, field_ids: Option>) -> Vec { + self + .database + .read() + .await + .get_fields_in_view(view_id, field_ids) } - fn get_field(&self, field_id: &str) -> Option { - self.database.lock().fields.get_field(field_id) + async fn get_field(&self, field_id: &str) -> Option { + self.database.read().await.get_field(field_id) } - fn create_field( + async fn create_field( &self, view_id: &str, name: &str, field_type: FieldType, type_option_data: TypeOptionData, - ) -> Fut { - let (_, field) = self.database.lock().create_field_with_mut( + ) -> Field { + let (_, field) = self.database.write().await.create_field_with_mut( view_id, name.to_string(), field_type.into(), @@ -1398,199 +1465,219 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { }, default_field_settings_by_layout_map(), ); - to_fut(async move { field }) + field } - fn update_field( + async fn update_field( &self, type_option_data: TypeOptionData, old_field: Field, - ) -> FutureResult<(), FlowyError> { - let weak_editor_by_view_id = Arc::downgrade(&self.editor_by_view_id); - let weak_database = Arc::downgrade(&self.database); - FutureResult::new(async move { - if let (Some(database), Some(editor_by_view_id)) = - (weak_database.upgrade(), weak_editor_by_view_id.upgrade()) - { - let view_editors = editor_by_view_id.read().await.values().cloned().collect(); - let _ = - update_field_type_option_fn(&database, &view_editors, type_option_data, old_field).await; - } - Ok(()) - }) - } - - fn get_primary_field(&self) -> Fut>> { - let field = self - .database - .lock() - .fields - .get_primary_field() - .map(Arc::new); - to_fut(async move { field }) - } - - fn index_of_row(&self, view_id: &str, row_id: &RowId) -> Fut> { - let index = self.database.lock().index_of_row(view_id, row_id); - to_fut(async move { index }) - } - - fn get_row(&self, view_id: &str, row_id: &RowId) -> Fut)>> { - let index = self.database.lock().index_of_row(view_id, row_id); - let row_detail = self.database.lock().get_row_detail(row_id); - to_fut(async move { - match (index, row_detail) { - (Some(index), Some(row_detail)) => Some((index, Arc::new(row_detail))), - _ => None, - } - }) - } - - fn get_rows(&self, view_id: &str) -> Fut>> { - let database = self.database.clone(); - let view_id = view_id.to_string(); - to_fut(async move { - let cloned_database = database.clone(); - // offloads the blocking operation to a thread where blocking is acceptable. This prevents - // blocking the main asynchronous runtime - let row_orders = tokio::task::spawn_blocking(move || { - cloned_database.lock().get_row_orders_for_view(&view_id) - }) + ) -> Result<(), FlowyError> { + let view_editors = self + .editor_by_view_id + .read() .await - .unwrap_or_default(); - tokio::task::yield_now().await; + .values() + .cloned() + .collect::>(); - let mut all_rows = vec![]; + // + { + let mut database = self.database.write().await; + let _ = update_field_type_option_fn(&mut database, type_option_data, &old_field).await; + drop(database); + } - // Loading the rows in chunks of 10 rows in order to prevent blocking the main asynchronous runtime - for chunk in row_orders.chunks(10) { - let cloned_database = database.clone(); - let chunk = chunk.to_vec(); - let rows = tokio::task::spawn_blocking(move || { - let orders = cloned_database.lock().get_rows_from_row_orders(&chunk); - let lock_guard = cloned_database.lock(); - orders - .into_iter() - .flat_map(|row| lock_guard.get_row_detail(&row.id)) - .collect::>() - }) - .await - .unwrap_or_default(); + for view_editor in view_editors { + view_editor + .v_did_update_field_type_option(&old_field) + .await?; + } + Ok(()) + } - all_rows.extend(rows); - tokio::task::yield_now().await; + async fn get_primary_field(&self) -> Option> { + self.database.read().await.get_primary_field().map(Arc::new) + } + + async fn index_of_row(&self, view_id: &str, row_id: &RowId) -> Option { + self.database.read().await.index_of_row(view_id, row_id) + } + + async fn get_row(&self, view_id: &str, row_id: &RowId) -> Option<(usize, Arc)> { + let database = self.database.read().await; + let index = database.index_of_row(view_id, row_id); + let row_detail = database.get_row_detail(row_id).await; + match (index, row_detail) { + (Some(index), Some(row_detail)) => Some((index, Arc::new(row_detail))), + _ => None, + } + } + + async fn get_rows(&self, view_id: &str) -> Vec> { + let view_id = view_id.to_string(); + let row_orders = self.database.read().await.get_row_orders_for_view(&view_id); + trace!("total row orders: {}", row_orders.len()); + + let mut row_details_list = vec![]; + // Loading the rows in chunks of 10 rows in order to prevent blocking the main asynchronous runtime + const CHUNK_SIZE: usize = 10; + for chunk in row_orders.chunks(CHUNK_SIZE) { + let database_read_guard = self.database.read().await; + let chunk = chunk.to_vec(); + let rows = database_read_guard.get_rows_from_row_orders(&chunk).await; + for row in rows { + match database_read_guard.get_row_detail(&row.id).await { + None => warn!("Failed to get row detail for row: {}", row.id.as_str()), + Some(row_details) => { + row_details_list.push(row_details); + }, + } } - - all_rows.into_iter().map(Arc::new).collect() - }) + drop(database_read_guard); + tokio::task::yield_now().await; + } + trace!("total row details: {}", row_details_list.len()); + row_details_list.into_iter().map(Arc::new).collect() } - fn remove_row(&self, row_id: &RowId) -> Option { - self.database.lock().remove_row(row_id) + async fn remove_row(&self, row_id: &RowId) -> Option { + self.database.write().await.remove_row(row_id).await } - fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut>> { - let cells = self.database.lock().get_cells_for_field(view_id, field_id); - to_fut(async move { cells.into_iter().map(Arc::new).collect() }) + async fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Vec> { + let cells = self + .database + .read() + .await + .get_cells_for_field(view_id, field_id) + .await; + cells.into_iter().map(Arc::new).collect() } - fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Fut> { - let cell = self.database.lock().get_cell(field_id, row_id); - to_fut(async move { Arc::new(cell) }) + async fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Arc { + let cell = self.database.read().await.get_cell(field_id, row_id).await; + cell.into() } - fn get_layout_for_view(&self, view_id: &str) -> DatabaseLayout { - self.database.lock().views.get_database_view_layout(view_id) + async fn get_layout_for_view(&self, view_id: &str) -> DatabaseLayout { + self.database.read().await.get_database_view_layout(view_id) } - fn get_group_setting(&self, view_id: &str) -> Vec { - self.database.lock().get_all_group_setting(view_id) + async fn get_group_setting(&self, view_id: &str) -> Vec { + self.database.read().await.get_all_group_setting(view_id) } - fn insert_group_setting(&self, view_id: &str, setting: GroupSetting) { - self.database.lock().insert_group_setting(view_id, setting); - } - - fn get_sort(&self, view_id: &str, sort_id: &str) -> Option { - self.database.lock().get_sort::(view_id, sort_id) - } - - fn insert_sort(&self, view_id: &str, sort: Sort) { - self.database.lock().insert_sort(view_id, sort); - } - - fn move_sort(&self, view_id: &str, from_sort_id: &str, to_sort_id: &str) { + async fn insert_group_setting(&self, view_id: &str, setting: GroupSetting) { self .database - .lock() + .write() + .await + .insert_group_setting(view_id, setting); + } + + async fn get_sort(&self, view_id: &str, sort_id: &str) -> Option { + self + .database + .read() + .await + .get_sort::(view_id, sort_id) + } + + async fn insert_sort(&self, view_id: &str, sort: Sort) { + self.database.write().await.insert_sort(view_id, sort); + } + + async fn move_sort(&self, view_id: &str, from_sort_id: &str, to_sort_id: &str) { + self + .database + .write() + .await .move_sort(view_id, from_sort_id, to_sort_id); } - fn remove_sort(&self, view_id: &str, sort_id: &str) { - self.database.lock().remove_sort(view_id, sort_id); + async fn remove_sort(&self, view_id: &str, sort_id: &str) { + self.database.write().await.remove_sort(view_id, sort_id); } - fn get_all_sorts(&self, view_id: &str) -> Vec { - self.database.lock().get_all_sorts::(view_id) + async fn get_all_sorts(&self, view_id: &str) -> Vec { + self.database.read().await.get_all_sorts::(view_id) } - fn remove_all_sorts(&self, view_id: &str) { - self.database.lock().remove_all_sorts(view_id); + async fn remove_all_sorts(&self, view_id: &str) { + self.database.write().await.remove_all_sorts(view_id); } - fn get_all_calculations(&self, view_id: &str) -> Vec> { + async fn get_all_calculations(&self, view_id: &str) -> Vec> { self .database - .lock() + .read() + .await .get_all_calculations(view_id) .into_iter() .map(Arc::new) .collect() } - fn get_calculation(&self, view_id: &str, field_id: &str) -> Option { + async fn get_calculation(&self, view_id: &str, field_id: &str) -> Option { self .database - .lock() + .read() + .await .get_calculation::(view_id, field_id) } - fn get_all_filters(&self, view_id: &str) -> Vec { + async fn get_all_filters(&self, view_id: &str) -> Vec { self .database - .lock() + .read() + .await .get_all_filters(view_id) .into_iter() .collect() } - fn delete_filter(&self, view_id: &str, filter_id: &str) { - self.database.lock().remove_filter(view_id, filter_id); - } - - fn insert_filter(&self, view_id: &str, filter: Filter) { - self.database.lock().insert_filter(view_id, &filter); - } - - fn save_filters(&self, view_id: &str, filters: &[Filter]) { + async fn delete_filter(&self, view_id: &str, filter_id: &str) { self .database - .lock() + .write() + .await + .remove_filter(view_id, filter_id); + } + + async fn insert_filter(&self, view_id: &str, filter: Filter) { + self.database.write().await.insert_filter(view_id, &filter); + } + + async fn save_filters(&self, view_id: &str, filters: &[Filter]) { + self + .database + .write() + .await .save_filters::(view_id, filters); } - fn get_filter(&self, view_id: &str, filter_id: &str) -> Option { + async fn get_filter(&self, view_id: &str, filter_id: &str) -> Option { self .database - .lock() + .read() + .await .get_filter::(view_id, filter_id) } - fn get_layout_setting(&self, view_id: &str, layout_ty: &DatabaseLayout) -> Option { - self.database.lock().get_layout_setting(view_id, layout_ty) + async fn get_layout_setting( + &self, + view_id: &str, + layout_ty: &DatabaseLayout, + ) -> Option { + self + .database + .read() + .await + .get_layout_setting(view_id, layout_ty) } - fn insert_layout_setting( + async fn insert_layout_setting( &self, view_id: &str, layout_ty: &DatabaseLayout, @@ -1598,14 +1685,16 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { ) { self .database - .lock() + .write() + .await .insert_layout_setting(view_id, layout_ty, layout_setting); } - fn update_layout_type(&self, view_id: &str, layout_type: &DatabaseLayout) { + async fn update_layout_type(&self, view_id: &str, layout_type: &DatabaseLayout) { self .database - .lock() + .write() + .await .update_layout_type(view_id, layout_type); } @@ -1620,14 +1709,14 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { TypeOptionCellExt::new(field, Some(self.cell_cache.clone())).get_type_option_cell_data_handler() } - fn get_field_settings( + async fn get_field_settings( &self, view_id: &str, field_ids: &[String], ) -> HashMap { let (layout_type, field_settings_map) = { - let database = self.database.lock(); - let layout_type = database.views.get_database_view_layout(view_id); + let database = self.database.read().await; + let layout_type = database.get_database_view_layout(view_id); let field_settings_map = database.get_field_settings(view_id, Some(field_ids)); (layout_type, field_settings_map) }; @@ -1658,19 +1747,20 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { field_settings } - fn update_field_settings(&self, params: FieldSettingsChangesetPB) { - let field_settings_map = self.get_field_settings(¶ms.view_id, &[params.field_id.clone()]); + async fn update_field_settings(&self, params: FieldSettingsChangesetPB) { + let field_settings_map = self + .get_field_settings(¶ms.view_id, &[params.field_id.clone()]) + .await; - let field_settings = field_settings_map - .get(¶ms.field_id) - .cloned() - .unwrap_or_else(|| { - let layout_type = self.get_layout_for_view(¶ms.view_id); + let field_settings = match field_settings_map.get(¶ms.field_id).cloned() { + Some(field_settings) => field_settings, + None => { + let layout_type = self.get_layout_for_view(¶ms.view_id).await; let default_field_settings = default_field_settings_by_layout_map(); let default_field_settings = default_field_settings.get(&layout_type).unwrap(); - FieldSettings::from_any_map(¶ms.field_id, layout_type, default_field_settings) - }); + }, + }; let new_field_settings = FieldSettings { visibility: params @@ -1683,7 +1773,7 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { ..field_settings }; - self.database.lock().update_field_settings( + self.database.write().await.update_field_settings( ¶ms.view_id, Some(vec![params.field_id]), new_field_settings.clone(), @@ -1697,70 +1787,59 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { .send() } - fn update_calculation(&self, view_id: &str, calculation: Calculation) { + async fn update_calculation(&self, view_id: &str, calculation: Calculation) { self .database - .lock() + .write() + .await .update_calculation(view_id, calculation) } - fn remove_calculation(&self, view_id: &str, field_id: &str) { - self.database.lock().remove_calculation(view_id, field_id) + async fn remove_calculation(&self, view_id: &str, field_id: &str) { + self + .database + .write() + .await + .remove_calculation(view_id, field_id) } } #[tracing::instrument(level = "trace", skip_all, err)] pub async fn update_field_type_option_fn( - database: &Arc, - view_editors: &Vec>, + database: &mut Database, type_option_data: TypeOptionData, - old_field: Field, + old_field: &Field, ) -> FlowyResult<()> { if type_option_data.is_empty() { warn!("Update type option with empty data"); return Ok(()); } let field_type = FieldType::from(old_field.field_type); - database - .lock() - .fields - .update_field(&old_field.id, |update| { - if old_field.is_primary { - warn!("Cannot update primary field type"); - } else { - update.update_type_options(|type_options_update| { - event!( - tracing::Level::TRACE, - "insert type option to field type: {:?}, {:?}", - field_type, - type_option_data - ); - type_options_update.insert(&field_type.to_string(), type_option_data); - }); - } - }); + database.update_field(&old_field.id, |update| { + if old_field.is_primary { + warn!("Cannot update primary field type"); + } else { + update.update_type_options(|type_options_update| { + event!( + tracing::Level::TRACE, + "insert type option to field type: {:?}, {:?}", + field_type, + type_option_data + ); + type_options_update.insert(&field_type.to_string(), type_option_data); + }); + } + }); let _ = notify_did_update_database_field(database, &old_field.id); - for view_editor in view_editors { - view_editor - .v_did_update_field_type_option(&old_field) - .await?; - } - Ok(()) } #[tracing::instrument(level = "trace", skip_all, err)] -fn notify_did_update_database_field( - database: &Arc, - field_id: &str, -) -> FlowyResult<()> { +fn notify_did_update_database_field(database: &Database, field_id: &str) -> FlowyResult<()> { let (database_id, field, views) = { - let database = database - .try_lock() - .ok_or(FlowyError::internal().with_context("fail to acquire the lock of database"))?; let database_id = database.get_database_id(); - let field = database.fields.get_field(field_id); + let field = database.get_field(field_id); let views = database.get_all_database_views_meta(); (database_id, field, views) }; diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs index 682001948d..b25d365ab0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs @@ -2,7 +2,7 @@ use crate::entities::{DatabaseSyncStatePB, DidFetchRowPB, RowsChangePB}; use crate::notification::{send_notification, DatabaseNotification, DATABASE_OBSERVABLE_SOURCE}; use crate::services::database::UpdatedRow; use collab_database::blocks::BlockEvent; -use collab_database::database::MutexDatabase; +use collab_database::database::Database; use collab_database::fields::FieldChange; use collab_database::rows::{RowChange, RowId}; use collab_database::views::DatabaseViewChange; @@ -10,11 +10,12 @@ use flowy_notification::{DebounceNotificationSender, NotificationBuilder}; use futures::StreamExt; use lib_dispatch::prelude::af_spawn; use std::sync::Arc; +use tokio::sync::RwLock; use tracing::{trace, warn}; -pub(crate) async fn observe_sync_state(database_id: &str, database: &Arc) { +pub(crate) async fn observe_sync_state(database_id: &str, database: &Arc>) { let weak_database = Arc::downgrade(database); - let mut sync_state = database.lock().subscribe_sync_state(); + let mut sync_state = database.read().await.subscribe_sync_state(); let database_id = database_id.to_string(); af_spawn(async move { while let Some(sync_state) = sync_state.next().await { @@ -35,13 +36,13 @@ pub(crate) async fn observe_sync_state(database_id: &str, database: &Arc, + database: &Arc>, notification_sender: &Arc, ) { let notification_sender = notification_sender.clone(); let database_id = database_id.to_string(); let weak_database = Arc::downgrade(database); - let mut row_change = database.lock().subscribe_row_change(); + let mut row_change = database.read().await.subscribe_row_change(); af_spawn(async move { while let Ok(row_change) = row_change.recv().await { if let Some(database) = weak_database.upgrade() { @@ -59,7 +60,7 @@ pub(crate) async fn observe_rows_change( let cell_id = format!("{}:{}", row_id, field_id); notify_cell(¬ification_sender, &cell_id); - let views = database.lock().get_all_database_views_meta(); + let views = database.read().await.get_all_database_views_meta(); for view in views { notify_row(¬ification_sender, &view.id, &field_id, &row_id); } @@ -75,10 +76,10 @@ pub(crate) async fn observe_rows_change( }); } #[allow(dead_code)] -pub(crate) async fn observe_field_change(database_id: &str, database: &Arc) { +pub(crate) async fn observe_field_change(database_id: &str, database: &Arc>) { let database_id = database_id.to_string(); let weak_database = Arc::downgrade(database); - let mut field_change = database.lock().subscribe_field_change(); + let mut field_change = database.read().await.subscribe_field_change(); af_spawn(async move { while let Ok(field_change) = field_change.recv().await { if weak_database.upgrade().is_none() { @@ -100,10 +101,10 @@ pub(crate) async fn observe_field_change(database_id: &str, database: &Arc) { +pub(crate) async fn observe_view_change(database_id: &str, database: &Arc>) { let database_id = database_id.to_string(); let weak_database = Arc::downgrade(database); - let mut view_change = database.lock().subscribe_view_change(); + let mut view_change = database.read().await.subscribe_view_change(); af_spawn(async move { while let Ok(view_change) = view_change.recv().await { if weak_database.upgrade().is_none() { @@ -136,10 +137,10 @@ pub(crate) async fn observe_view_change(database_id: &str, database: &Arc) { +pub(crate) async fn observe_block_event(database_id: &str, database: &Arc>) { let database_id = database_id.to_string(); let weak_database = Arc::downgrade(database); - let mut block_event_rx = database.lock().subscribe_block_event(); + let mut block_event_rx = database.read().await.subscribe_block_event(); af_spawn(async move { while let Ok(event) = block_event_rx.recv().await { if weak_database.upgrade().is_none() { diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs index 33a4dd8a4e..d337c5002e 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs @@ -1,9 +1,10 @@ -use collab_database::database::{gen_field_id, MutexDatabase}; +use collab_database::database::{gen_field_id, Database}; use collab_database::fields::Field; use collab_database::views::{ DatabaseLayout, FieldSettingsByFieldIdMap, LayoutSetting, OrderObjectPosition, }; use std::sync::Arc; +use tokio::sync::RwLock; use crate::entities::FieldType; use crate::services::field::{DateTypeOption, SingleSelectTypeOption}; @@ -15,20 +16,20 @@ use crate::services::setting::{BoardLayoutSetting, CalendarLayoutSetting}; /// view depends on a field that can be used to group rows while a calendar view /// depends on a date field. pub struct DatabaseLayoutDepsResolver { - pub database: Arc, + pub database: Arc>, /// The new database layout. pub database_layout: DatabaseLayout, } impl DatabaseLayoutDepsResolver { - pub fn new(database: Arc, database_layout: DatabaseLayout) -> Self { + pub fn new(database: Arc>, database_layout: DatabaseLayout) -> Self { Self { database, database_layout, } } - pub fn resolve_deps_when_create_database_linked_view( + pub async fn resolve_deps_when_create_database_linked_view( &self, view_id: &str, ) -> ( @@ -41,9 +42,8 @@ impl DatabaseLayoutDepsResolver { DatabaseLayout::Board => { let layout_settings = BoardLayoutSetting::new().into(); - let field = if !self - .database - .lock() + let database = self.database.read().await; + let field = if !database .get_fields(None) .into_iter() .any(|field| FieldType::from(field.field_type).can_be_group()) @@ -53,7 +53,7 @@ impl DatabaseLayoutDepsResolver { None }; - let field_settings_map = self.database.lock().get_field_settings(view_id, None); + let field_settings_map = database.get_field_settings(view_id, None); tracing::info!( "resolve_deps_when_create_database_linked_view {:?}", field_settings_map @@ -68,7 +68,8 @@ impl DatabaseLayoutDepsResolver { DatabaseLayout::Calendar => { match self .database - .lock() + .read() + .await .get_fields(None) .into_iter() .find(|field| FieldType::from(field.field_type) == FieldType::DateTime) @@ -89,13 +90,20 @@ impl DatabaseLayoutDepsResolver { /// If the new layout type is a calendar and there is not date field in the database, it will add /// a new date field to the database and create the corresponding layout setting. - pub fn resolve_deps_when_update_layout_type(&self, view_id: &str) { - let fields = self.database.lock().get_fields(None); + pub async fn resolve_deps_when_update_layout_type(&self, view_id: &str) { + let mut database = self.database.write().await; + let fields = database.get_fields(None); // Insert the layout setting if it's not exist match &self.database_layout { DatabaseLayout::Grid => {}, DatabaseLayout::Board => { - self.create_board_layout_setting_if_need(view_id); + if database + .get_layout_setting::(view_id, &self.database_layout) + .is_none() + { + let layout_setting = BoardLayoutSetting::new(); + database.insert_layout_setting(view_id, &self.database_layout, layout_setting); + } }, DatabaseLayout::Calendar => { let date_field_id = match fields @@ -106,7 +114,7 @@ impl DatabaseLayoutDepsResolver { tracing::trace!("Create a new date field after layout type change"); let field = self.create_date_field(); let field_id = field.id.clone(); - self.database.lock().create_field( + database.create_field( None, field, &OrderObjectPosition::End, @@ -116,41 +124,17 @@ impl DatabaseLayoutDepsResolver { }, Some(date_field) => date_field.id, }; - self.create_calendar_layout_setting_if_need(view_id, &date_field_id); + if database + .get_layout_setting::(view_id, &self.database_layout) + .is_none() + { + let layout_setting = CalendarLayoutSetting::new(date_field_id); + database.insert_layout_setting(view_id, &self.database_layout, layout_setting); + } }, } } - fn create_board_layout_setting_if_need(&self, view_id: &str) { - if self - .database - .lock() - .get_layout_setting::(view_id, &self.database_layout) - .is_none() - { - let layout_setting = BoardLayoutSetting::new(); - self - .database - .lock() - .insert_layout_setting(view_id, &self.database_layout, layout_setting); - } - } - - fn create_calendar_layout_setting_if_need(&self, view_id: &str, field_id: &str) { - if self - .database - .lock() - .get_layout_setting::(view_id, &self.database_layout) - .is_none() - { - let layout_setting = CalendarLayoutSetting::new(field_id.to_string()); - self - .database - .lock() - .insert_layout_setting(view_id, &self.database_layout, layout_setting); - } - } - fn create_date_field(&self) -> Field { let field_type = FieldType::DateTime; let default_date_type_option = DateTypeOption::default(); diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_calculations.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_calculations.rs index 32ddecc667..e6f5da1134 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_calculations.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_calculations.rs @@ -1,8 +1,8 @@ +use async_trait::async_trait; use collab_database::fields::Field; use std::sync::Arc; use collab_database::rows::RowCell; -use lib_infra::future::{to_fut, Fut}; use crate::services::calculations::{ Calculation, CalculationsController, CalculationsDelegate, CalculationsTaskHandler, @@ -17,7 +17,7 @@ pub async fn make_calculations_controller( delegate: Arc, notifier: DatabaseViewChangedNotifier, ) -> Arc { - let calculations = delegate.get_all_calculations(view_id); + let calculations = delegate.get_all_calculations(view_id).await; let task_scheduler = delegate.get_task_scheduler(); let calculations_delegate = DatabaseViewCalculationsDelegateImpl(delegate.clone()); let handler_id = gen_handler_id(); @@ -29,8 +29,7 @@ pub async fn make_calculations_controller( calculations, task_scheduler.clone(), notifier, - ) - .await; + ); let calculations_controller = Arc::new(calculations_controller); task_scheduler @@ -45,30 +44,33 @@ pub async fn make_calculations_controller( struct DatabaseViewCalculationsDelegateImpl(Arc); +#[async_trait] impl CalculationsDelegate for DatabaseViewCalculationsDelegateImpl { - fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut>> { - self.0.get_cells_for_field(view_id, field_id) + async fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Vec> { + self.0.get_cells_for_field(view_id, field_id).await } - fn get_field(&self, field_id: &str) -> Option { - self.0.get_field(field_id) + async fn get_field(&self, field_id: &str) -> Option { + self.0.get_field(field_id).await } - fn get_calculation(&self, view_id: &str, field_id: &str) -> Fut>> { - let calculation = self.0.get_calculation(view_id, field_id).map(Arc::new); - to_fut(async move { calculation }) + async fn get_calculation(&self, view_id: &str, field_id: &str) -> Option> { + self + .0 + .get_calculation(view_id, field_id) + .await + .map(Arc::new) } - fn update_calculation(&self, view_id: &str, calculation: Calculation) { - self.0.update_calculation(view_id, calculation) + async fn update_calculation(&self, view_id: &str, calculation: Calculation) { + self.0.update_calculation(view_id, calculation).await } - fn remove_calculation(&self, view_id: &str, calculation_id: &str) { - self.0.remove_calculation(view_id, calculation_id) + async fn remove_calculation(&self, view_id: &str, calculation_id: &str) { + self.0.remove_calculation(view_id, calculation_id).await } - fn get_all_calculations(&self, view_id: &str) -> Fut>>> { - let calculations = Arc::new(self.0.get_all_calculations(view_id)); - to_fut(async move { calculations }) + async fn get_all_calculations(&self, view_id: &str) -> Arc>> { + self.0.get_all_calculations(view_id).await.into() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index 1d5d8cf1b4..aafc78e10e 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -156,6 +156,7 @@ impl DatabaseViewEditor { let field = self .delegate .get_field(controller.get_grouping_field_id()) + .await .ok_or_else(|| FlowyError::internal().with_context("Failed to get grouping field"))?; controller.will_create_row(&mut cells, &field, &group_id); } @@ -249,7 +250,10 @@ impl DatabaseViewEditor { field_id: Option, ) { if let Some(controller) = self.group_controller.write().await.as_mut() { - let field = self.delegate.get_field(controller.get_grouping_field_id()); + let field = self + .delegate + .get_field(controller.get_grouping_field_id()) + .await; if let Some(field) = field { let mut row_details = vec![Arc::new(row_detail.clone())]; @@ -413,8 +417,11 @@ impl DatabaseViewEditor { pub async fn v_create_group(&self, name: &str) -> FlowyResult<()> { let mut old_field: Option = None; let result = if let Some(controller) = self.group_controller.write().await.as_mut() { - let create_group_results = controller.create_group(name.to_string())?; - old_field = self.delegate.get_field(controller.get_grouping_field_id()); + let create_group_results = controller.create_group(name.to_string()).await?; + old_field = self + .delegate + .get_field(controller.get_grouping_field_id()) + .await; create_group_results } else { (None, None) @@ -447,20 +454,22 @@ impl DatabaseViewEditor { None => return Ok(RowsChangePB::default()), }; - let old_field = self.delegate.get_field(controller.get_grouping_field_id()); - let (row_ids, type_option_data) = controller.delete_group(group_id)?; + let old_field = self + .delegate + .get_field(controller.get_grouping_field_id()) + .await; + let (row_ids, type_option_data) = controller.delete_group(group_id).await?; drop(group_controller); let mut changes = RowsChangePB::default(); if let Some(field) = old_field { - let deleted_rows = row_ids - .iter() - .filter_map(|row_id| self.delegate.remove_row(row_id)) - .map(|row| row.id.into_inner()); - - changes.deleted_rows.extend(deleted_rows); + for row_id in row_ids { + if let Some(row) = self.delegate.remove_row(&row_id).await { + changes.deleted_rows.push(row.id.into_inner()); + } + } if let Some(type_option) = type_option_data { self.delegate.update_field(type_option, field).await?; @@ -478,19 +487,23 @@ impl DatabaseViewEditor { pub async fn v_update_group(&self, changeset: Vec) -> FlowyResult<()> { let mut type_option_data = None; - let (old_field, updated_groups) = - if let Some(controller) = self.group_controller.write().await.as_mut() { - let old_field = self.delegate.get_field(controller.get_grouping_field_id()); - let (updated_groups, new_type_option) = controller.apply_group_changeset(&changeset)?; + let (old_field, updated_groups) = if let Some(controller) = + self.group_controller.write().await.as_mut() + { + let old_field = self + .delegate + .get_field(controller.get_grouping_field_id()) + .await; + let (updated_groups, new_type_option) = controller.apply_group_changeset(&changeset).await?; - if new_type_option.is_some() { - type_option_data = new_type_option; - } + if new_type_option.is_some() { + type_option_data = new_type_option; + } - (old_field, updated_groups) - } else { - (None, vec![]) - }; + (old_field, updated_groups) + } else { + (None, vec![]) + }; if let Some(old_field) = old_field { if let Some(type_option_data) = type_option_data { @@ -511,7 +524,7 @@ impl DatabaseViewEditor { } pub async fn v_get_all_sorts(&self) -> Vec { - self.delegate.get_all_sorts(&self.view_id) + self.delegate.get_all_sorts(&self.view_id).await } #[tracing::instrument(level = "trace", skip(self), err)] @@ -528,7 +541,7 @@ impl DatabaseViewEditor { condition: params.condition.into(), }; - self.delegate.insert_sort(&self.view_id, sort.clone()); + self.delegate.insert_sort(&self.view_id, sort.clone()).await; let mut sort_controller = self.sort_controller.write().await; @@ -549,7 +562,8 @@ impl DatabaseViewEditor { pub async fn v_reorder_sort(&self, params: ReorderSortPayloadPB) -> FlowyResult<()> { self .delegate - .move_sort(&self.view_id, ¶ms.from_sort_id, ¶ms.to_sort_id); + .move_sort(&self.view_id, ¶ms.from_sort_id, ¶ms.to_sort_id) + .await; let notification = self .sort_controller @@ -573,7 +587,10 @@ impl DatabaseViewEditor { .apply_changeset(SortChangeset::from_delete(params.sort_id.clone())) .await; - self.delegate.remove_sort(&self.view_id, ¶ms.sort_id); + self + .delegate + .remove_sort(&self.view_id, ¶ms.sort_id) + .await; notify_did_update_sort(notification).await; Ok(()) @@ -583,7 +600,7 @@ impl DatabaseViewEditor { let all_sorts = self.v_get_all_sorts().await; self.sort_controller.write().await.delete_all_sorts().await; - self.delegate.remove_all_sorts(&self.view_id); + self.delegate.remove_all_sorts(&self.view_id).await; let mut notification = SortChangesetNotificationPB::new(self.view_id.clone()); notification.delete_sorts = all_sorts.into_iter().map(SortPB::from).collect(); notify_did_update_sort(notification).await; @@ -591,7 +608,7 @@ impl DatabaseViewEditor { } pub async fn v_get_all_calculations(&self) -> Vec> { - self.delegate.get_all_calculations(&self.view_id) + self.delegate.get_all_calculations(&self.view_id).await } pub async fn v_update_calculations( @@ -620,7 +637,8 @@ impl DatabaseViewEditor { let calculation: Calculation = Calculation::from(&insert); self .delegate - .update_calculation(¶ms.view_id, calculation); + .update_calculation(¶ms.view_id, calculation) + .await; } } @@ -636,7 +654,8 @@ impl DatabaseViewEditor { ) -> FlowyResult<()> { self .delegate - .remove_calculation(¶ms.view_id, ¶ms.calculation_id); + .remove_calculation(¶ms.view_id, ¶ms.calculation_id) + .await; let calculation = Calculation::none(params.calculation_id, params.field_id, None); @@ -653,11 +672,11 @@ impl DatabaseViewEditor { } pub async fn v_get_all_filters(&self) -> Vec { - self.delegate.get_all_filters(&self.view_id) + self.delegate.get_all_filters(&self.view_id).await } pub async fn v_get_filter(&self, filter_id: &str) -> Option { - self.delegate.get_filter(&self.view_id, filter_id) + self.delegate.get_filter(&self.view_id, filter_id).await } #[tracing::instrument(level = "trace", skip(self), err)] @@ -686,15 +705,23 @@ impl DatabaseViewEditor { match layout_ty { DatabaseLayout::Grid => {}, DatabaseLayout::Board => { - if let Some(value) = self.delegate.get_layout_setting(&self.view_id, layout_ty) { + if let Some(value) = self + .delegate + .get_layout_setting(&self.view_id, layout_ty) + .await + { layout_setting.board = Some(value.into()); } }, DatabaseLayout::Calendar => { - if let Some(value) = self.delegate.get_layout_setting(&self.view_id, layout_ty) { + if let Some(value) = self + .delegate + .get_layout_setting(&self.view_id, layout_ty) + .await + { let calendar_setting = CalendarLayoutSetting::from(value); // Check the field exist or not - if let Some(field) = self.delegate.get_field(&calendar_setting.field_id) { + if let Some(field) = self.delegate.get_field(&calendar_setting.field_id).await { let field_type = FieldType::from(field.field_type); // Check the type of field is Datetime or not @@ -723,27 +750,33 @@ impl DatabaseViewEditor { DatabaseLayout::Board => { let layout_setting = params.board.unwrap(); - self.delegate.insert_layout_setting( - &self.view_id, - ¶ms.layout_type, - layout_setting.clone().into(), - ); + self + .delegate + .insert_layout_setting( + &self.view_id, + ¶ms.layout_type, + layout_setting.clone().into(), + ) + .await; Some(DatabaseLayoutSettingPB::from_board(layout_setting)) }, DatabaseLayout::Calendar => { let layout_setting = params.calendar.unwrap(); - if let Some(field) = self.delegate.get_field(&layout_setting.field_id) { + if let Some(field) = self.delegate.get_field(&layout_setting.field_id).await { if FieldType::from(field.field_type) != FieldType::DateTime { return Err(FlowyError::unexpect_calendar_field_type()); } - self.delegate.insert_layout_setting( - &self.view_id, - ¶ms.layout_type, - layout_setting.clone().into(), - ); + self + .delegate + .insert_layout_setting( + &self.view_id, + ¶ms.layout_type, + layout_setting.clone().into(), + ) + .await; Some(DatabaseLayoutSettingPB::from_calendar(layout_setting)) } else { @@ -769,10 +802,10 @@ impl DatabaseViewEditor { let notification = self.filter_controller.apply_changeset(changeset).await; notify_did_update_filter(notification).await; - let sorts = self.delegate.get_all_sorts(&self.view_id); + let sorts = self.delegate.get_all_sorts(&self.view_id).await; if let Some(sort) = sorts.iter().find(|sort| sort.field_id == deleted_field_id) { - self.delegate.remove_sort(&self.view_id, &sort.id); + self.delegate.remove_sort(&self.view_id, &sort.id).await; let notification = self .sort_controller .write() @@ -810,7 +843,7 @@ impl DatabaseViewEditor { pub async fn v_did_update_field_type_option(&self, old_field: &Field) -> FlowyResult<()> { let field_id = &old_field.id; - if let Some(field) = self.delegate.get_field(field_id) { + if let Some(field) = self.delegate.get_field(field_id).await { self .sort_controller .read() @@ -839,7 +872,7 @@ impl DatabaseViewEditor { /// Called when a grouping field is updated. #[tracing::instrument(level = "debug", skip_all, err)] pub async fn v_group_by_field(&self, field_id: &str) -> FlowyResult<()> { - if let Some(field) = self.delegate.get_field(field_id) { + if let Some(field) = self.delegate.get_field(field_id).await { tracing::trace!("create new group controller"); let new_group_controller = new_group_controller( @@ -890,7 +923,7 @@ impl DatabaseViewEditor { let text_cell = get_cell_for_row(self.delegate.clone(), &primary_field.id, &row_id).await?; // Date - let date_field = self.delegate.get_field(&calendar_setting.field_id)?; + let date_field = self.delegate.get_field(&calendar_setting.field_id).await?; let date_cell = get_cell_for_row(self.delegate.clone(), &date_field.id, &row_id).await?; let title = text_cell @@ -981,20 +1014,23 @@ impl DatabaseViewEditor { } pub async fn v_get_layout_type(&self) -> DatabaseLayout { - self.delegate.get_layout_for_view(&self.view_id) + self.delegate.get_layout_for_view(&self.view_id).await } #[tracing::instrument(level = "trace", skip_all)] pub async fn v_update_layout_type(&self, new_layout_type: DatabaseLayout) -> FlowyResult<()> { self .delegate - .update_layout_type(&self.view_id, &new_layout_type); + .update_layout_type(&self.view_id, &new_layout_type) + .await; // using the {} brackets to denote the lifetime of the resolver. Because the DatabaseLayoutDepsResolver // is not sync and send, so we can't pass it to the async block. { let resolver = DatabaseLayoutDepsResolver::new(self.delegate.get_database(), new_layout_type); - resolver.resolve_deps_when_update_layout_type(&self.view_id); + resolver + .resolve_deps_when_update_layout_type(&self.view_id) + .await; } // initialize the group controller if the current layout support grouping @@ -1034,12 +1070,14 @@ impl DatabaseViewEditor { } pub async fn v_get_field_settings(&self, field_ids: &[String]) -> HashMap { - self.delegate.get_field_settings(&self.view_id, field_ids) + self + .delegate + .get_field_settings(&self.view_id, field_ids) + .await } pub async fn v_update_field_settings(&self, params: FieldSettingsChangesetPB) -> FlowyResult<()> { - self.delegate.update_field_settings(params); - + self.delegate.update_field_settings(params).await; Ok(()) } @@ -1053,7 +1091,7 @@ impl DatabaseViewEditor { .await .as_ref() .map(|controller| controller.get_grouping_field_id().to_owned())?; - let field = self.delegate.get_field(&group_field_id)?; + let field = self.delegate.get_field(&group_field_id).await?; let mut write_guard = self.group_controller.write().await; if let Some(group_controller) = &mut *write_guard { f(group_controller, field).ok() diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs index f710144e60..994041aea9 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs @@ -1,10 +1,9 @@ +use async_trait::async_trait; use std::sync::Arc; use collab_database::fields::Field; use collab_database::rows::{RowDetail, RowId}; -use lib_infra::future::Fut; - use crate::services::cell::CellCache; use crate::services::database_view::{ gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewOperation, @@ -43,28 +42,29 @@ pub async fn make_filter_controller( struct DatabaseViewFilterDelegateImpl(Arc); +#[async_trait] impl FilterDelegate for DatabaseViewFilterDelegateImpl { - fn get_field(&self, field_id: &str) -> Option { - self.0.get_field(field_id) + async fn get_field(&self, field_id: &str) -> Option { + self.0.get_field(field_id).await } - fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut> { - self.0.get_fields(view_id, field_ids) + async fn get_fields(&self, view_id: &str, field_ids: Option>) -> Vec { + self.0.get_fields(view_id, field_ids).await } - fn get_rows(&self, view_id: &str) -> Fut>> { - self.0.get_rows(view_id) + async fn get_rows(&self, view_id: &str) -> Vec> { + self.0.get_rows(view_id).await } - fn get_row(&self, view_id: &str, rows_id: &RowId) -> Fut)>> { - self.0.get_row(view_id, rows_id) + async fn get_row(&self, view_id: &str, rows_id: &RowId) -> Option<(usize, Arc)> { + self.0.get_row(view_id, rows_id).await } - fn get_all_filters(&self, view_id: &str) -> Vec { - self.0.get_all_filters(view_id) + async fn get_all_filters(&self, view_id: &str) -> Vec { + self.0.get_all_filters(view_id).await } - fn save_filters(&self, view_id: &str, filters: &[Filter]) { - self.0.save_filters(view_id, filters) + async fn save_filters(&self, view_id: &str, filters: &[Filter]) { + self.0.save_filters(view_id, filters).await } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs index 504511608a..b180904d6e 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs @@ -1,10 +1,10 @@ +use async_trait::async_trait; use std::sync::Arc; use collab_database::fields::Field; use collab_database::rows::{RowDetail, RowId}; use flowy_error::FlowyResult; -use lib_infra::future::{to_fut, Fut}; use crate::entities::FieldType; use crate::services::database_view::DatabaseViewOperation; @@ -21,7 +21,7 @@ pub async fn new_group_controller( filter_controller: Arc, grouping_field: Option, ) -> FlowyResult>> { - if !delegate.get_layout_for_view(&view_id).is_board() { + if !delegate.get_layout_for_view(&view_id).await.is_board() { return Ok(None); } @@ -61,45 +61,45 @@ pub(crate) struct GroupControllerDelegateImpl { filter_controller: Arc, } +#[async_trait] impl GroupContextDelegate for GroupControllerDelegateImpl { - fn get_group_setting(&self, view_id: &str) -> Fut>> { - let mut settings = self.delegate.get_group_setting(view_id); - to_fut(async move { - if settings.is_empty() { - None - } else { - Some(Arc::new(settings.remove(0))) - } - }) + async fn get_group_setting(&self, view_id: &str) -> Option> { + let mut settings = self.delegate.get_group_setting(view_id).await; + if settings.is_empty() { + None + } else { + Some(Arc::new(settings.remove(0))) + } } - fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Fut> { - let field_id = field_id.to_owned(); - let view_id = view_id.to_owned(); + async fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Vec { let delegate = self.delegate.clone(); - to_fut(async move { get_cells_for_field(delegate, &view_id, &field_id).await }) + get_cells_for_field(delegate, view_id, field_id).await } - fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut> { - self.delegate.insert_group_setting(view_id, group_setting); - to_fut(async move { Ok(()) }) + async fn save_configuration( + &self, + view_id: &str, + group_setting: GroupSetting, + ) -> FlowyResult<()> { + self + .delegate + .insert_group_setting(view_id, group_setting) + .await; + Ok(()) } } +#[async_trait] impl GroupControllerDelegate for GroupControllerDelegateImpl { - fn get_field(&self, field_id: &str) -> Option { - self.delegate.get_field(field_id) + async fn get_field(&self, field_id: &str) -> Option { + self.delegate.get_field(field_id).await } - fn get_all_rows(&self, view_id: &str) -> Fut>> { - let view_id = view_id.to_string(); - let delegate = self.delegate.clone(); - let filter_controller = self.filter_controller.clone(); - to_fut(async move { - let mut row_details = delegate.get_rows(&view_id).await; - filter_controller.filter_rows(&mut row_details).await; - row_details - }) + async fn get_all_rows(&self, view_id: &str) -> Vec> { + let mut row_details = self.delegate.get_rows(view_id).await; + self.filter_controller.filter_rows(&mut row_details).await; + row_details } } @@ -108,7 +108,7 @@ pub(crate) async fn get_cell_for_row( field_id: &str, row_id: &RowId, ) -> Option { - let field = delegate.get_field(field_id)?; + let field = delegate.get_field(field_id).await?; let row_cell = delegate.get_cell_in_row(field_id, row_id).await; let field_type = FieldType::from(field.field_type); let handler = delegate.get_type_option_cell_handler(&field)?; @@ -131,7 +131,7 @@ pub(crate) async fn get_cells_for_field( view_id: &str, field_id: &str, ) -> Vec { - if let Some(field) = delegate.get_field(field_id) { + if let Some(field) = delegate.get_field(field_id).await { let field_type = FieldType::from(field.field_type); if let Some(handler) = delegate.get_type_option_cell_handler(&field) { let cells = delegate.get_cells_for_field(view_id, field_id).await; diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs index 3a912646cd..4681566ebd 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs @@ -1,14 +1,14 @@ +use async_trait::async_trait; +use collab_database::database::Database; use std::collections::HashMap; use std::sync::Arc; -use collab_database::database::MutexDatabase; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{Row, RowCell, RowDetail, RowId}; use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting}; use tokio::sync::RwLock; use flowy_error::FlowyError; -use lib_infra::future::{Fut, FutureResult}; use lib_infra::priority_task::TaskDispatcher; use crate::entities::{FieldSettingsChangesetPB, FieldType}; @@ -20,97 +20,102 @@ use crate::services::group::GroupSetting; use crate::services::sort::Sort; /// Defines the operation that can be performed on a database view +#[async_trait] pub trait DatabaseViewOperation: Send + Sync + 'static { /// Get the database that the view belongs to - fn get_database(&self) -> Arc; + fn get_database(&self) -> Arc>; /// Get the view of the database with the view_id - fn get_view(&self, view_id: &str) -> Fut>; + async fn get_view(&self, view_id: &str) -> Option; /// If the field_ids is None, then it will return all the field revisions - fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>; + async fn get_fields(&self, view_id: &str, field_ids: Option>) -> Vec; /// Returns the field with the field_id - fn get_field(&self, field_id: &str) -> Option; + async fn get_field(&self, field_id: &str) -> Option; - fn create_field( + async fn create_field( &self, view_id: &str, name: &str, field_type: FieldType, type_option_data: TypeOptionData, - ) -> Fut; + ) -> Field; - fn update_field( + async fn update_field( &self, type_option_data: TypeOptionData, old_field: Field, - ) -> FutureResult<(), FlowyError>; + ) -> Result<(), FlowyError>; - fn get_primary_field(&self) -> Fut>>; + async fn get_primary_field(&self) -> Option>; /// Returns the index of the row with row_id - fn index_of_row(&self, view_id: &str, row_id: &RowId) -> Fut>; + async fn index_of_row(&self, view_id: &str, row_id: &RowId) -> Option; /// Returns the `index` and `RowRevision` with row_id - fn get_row(&self, view_id: &str, row_id: &RowId) -> Fut)>>; + async fn get_row(&self, view_id: &str, row_id: &RowId) -> Option<(usize, Arc)>; /// Returns all the rows in the view - fn get_rows(&self, view_id: &str) -> Fut>>; + async fn get_rows(&self, view_id: &str) -> Vec>; - fn remove_row(&self, row_id: &RowId) -> Option; + async fn remove_row(&self, row_id: &RowId) -> Option; - fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut>>; + async fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Vec>; - fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Fut>; + async fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Arc; /// Return the database layout type for the view with given view_id /// The default layout type is [DatabaseLayout::Grid] - fn get_layout_for_view(&self, view_id: &str) -> DatabaseLayout; + async fn get_layout_for_view(&self, view_id: &str) -> DatabaseLayout; - fn get_group_setting(&self, view_id: &str) -> Vec; + async fn get_group_setting(&self, view_id: &str) -> Vec; - fn insert_group_setting(&self, view_id: &str, setting: GroupSetting); + async fn insert_group_setting(&self, view_id: &str, setting: GroupSetting); - fn get_sort(&self, view_id: &str, sort_id: &str) -> Option; + async fn get_sort(&self, view_id: &str, sort_id: &str) -> Option; - fn insert_sort(&self, view_id: &str, sort: Sort); + async fn insert_sort(&self, view_id: &str, sort: Sort); - fn move_sort(&self, view_id: &str, from_sort_id: &str, to_sort_id: &str); + async fn move_sort(&self, view_id: &str, from_sort_id: &str, to_sort_id: &str); - fn remove_sort(&self, view_id: &str, sort_id: &str); + async fn remove_sort(&self, view_id: &str, sort_id: &str); - fn get_all_sorts(&self, view_id: &str) -> Vec; + async fn get_all_sorts(&self, view_id: &str) -> Vec; - fn remove_all_sorts(&self, view_id: &str); + async fn remove_all_sorts(&self, view_id: &str); - fn get_all_calculations(&self, view_id: &str) -> Vec>; + async fn get_all_calculations(&self, view_id: &str) -> Vec>; - fn get_calculation(&self, view_id: &str, field_id: &str) -> Option; + async fn get_calculation(&self, view_id: &str, field_id: &str) -> Option; - fn update_calculation(&self, view_id: &str, calculation: Calculation); + async fn update_calculation(&self, view_id: &str, calculation: Calculation); - fn remove_calculation(&self, view_id: &str, calculation_id: &str); + async fn remove_calculation(&self, view_id: &str, calculation_id: &str); - fn get_all_filters(&self, view_id: &str) -> Vec; + async fn get_all_filters(&self, view_id: &str) -> Vec; - fn get_filter(&self, view_id: &str, filter_id: &str) -> Option; + async fn get_filter(&self, view_id: &str, filter_id: &str) -> Option; - fn delete_filter(&self, view_id: &str, filter_id: &str); + async fn delete_filter(&self, view_id: &str, filter_id: &str); - fn insert_filter(&self, view_id: &str, filter: Filter); + async fn insert_filter(&self, view_id: &str, filter: Filter); - fn save_filters(&self, view_id: &str, filters: &[Filter]); + async fn save_filters(&self, view_id: &str, filters: &[Filter]); - fn get_layout_setting(&self, view_id: &str, layout_ty: &DatabaseLayout) -> Option; + async fn get_layout_setting( + &self, + view_id: &str, + layout_ty: &DatabaseLayout, + ) -> Option; - fn insert_layout_setting( + async fn insert_layout_setting( &self, view_id: &str, layout_ty: &DatabaseLayout, layout_setting: LayoutSetting, ); - fn update_layout_type(&self, view_id: &str, layout_type: &DatabaseLayout); + async fn update_layout_type(&self, view_id: &str, layout_type: &DatabaseLayout); /// Returns a `TaskDispatcher` used to poll a `Task` fn get_task_scheduler(&self) -> Arc>; @@ -120,11 +125,11 @@ pub trait DatabaseViewOperation: Send + Sync + 'static { field: &Field, ) -> Option>; - fn get_field_settings( + async fn get_field_settings( &self, view_id: &str, field_ids: &[String], ) -> HashMap; - fn update_field_settings(&self, params: FieldSettingsChangesetPB); + async fn update_field_settings(&self, params: FieldSettingsChangesetPB); } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs index 0397526b66..a719590e09 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs @@ -1,11 +1,10 @@ +use async_trait::async_trait; use std::sync::Arc; use collab_database::fields::Field; use collab_database::rows::RowDetail; use tokio::sync::RwLock; -use lib_infra::future::{to_fut, Fut}; - use crate::services::cell::CellCache; use crate::services::database_view::{ gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewOperation, @@ -23,6 +22,7 @@ pub(crate) async fn make_sort_controller( let handler_id = gen_handler_id(); let sorts = delegate .get_all_sorts(view_id) + .await .into_iter() .map(Arc::new) .collect(); @@ -53,38 +53,31 @@ struct DatabaseViewSortDelegateImpl { filter_controller: Arc, } +#[async_trait] impl SortDelegate for DatabaseViewSortDelegateImpl { - fn get_sort(&self, view_id: &str, sort_id: &str) -> Fut>> { - let sort = self.delegate.get_sort(view_id, sort_id).map(Arc::new); - to_fut(async move { sort }) + async fn get_sort(&self, view_id: &str, sort_id: &str) -> Option> { + self.delegate.get_sort(view_id, sort_id).await.map(Arc::new) } - fn get_rows(&self, view_id: &str) -> Fut>> { + async fn get_rows(&self, view_id: &str) -> Vec> { let view_id = view_id.to_string(); - let delegate = self.delegate.clone(); - let filter_controller = self.filter_controller.clone(); - to_fut(async move { - let mut row_details = delegate.get_rows(&view_id).await; - filter_controller.filter_rows(&mut row_details).await; - row_details - }) + let mut row_details = self.delegate.get_rows(&view_id).await; + self.filter_controller.filter_rows(&mut row_details).await; + row_details } - fn filter_row(&self, row_detail: &RowDetail) -> Fut { - let filter_controller = self.filter_controller.clone(); + async fn filter_row(&self, row_detail: &RowDetail) -> bool { let row_detail = row_detail.clone(); - to_fut(async move { - let mut row_details = vec![Arc::new(row_detail)]; - filter_controller.filter_rows(&mut row_details).await; - !row_details.is_empty() - }) + let mut row_details = vec![Arc::new(row_detail)]; + self.filter_controller.filter_rows(&mut row_details).await; + !row_details.is_empty() } - fn get_field(&self, field_id: &str) -> Option { - self.delegate.get_field(field_id) + async fn get_field(&self, field_id: &str) -> Option { + self.delegate.get_field(field_id).await } - fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut> { - self.delegate.get_fields(view_id, field_ids) + async fn get_fields(&self, view_id: &str, field_ids: Option>) -> Vec { + self.delegate.get_fields(view_id, field_ids).await } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs index 132b480123..445257a8b9 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs @@ -1,11 +1,11 @@ +use collab_database::database::Database; use std::collections::HashMap; use std::sync::Arc; -use collab_database::database::MutexDatabase; use nanoid::nanoid; use tokio::sync::{broadcast, RwLock}; -use flowy_error::{FlowyError, FlowyResult}; +use flowy_error::FlowyResult; use crate::services::cell::CellCache; use crate::services::database::DatabaseRowEvent; @@ -17,7 +17,7 @@ pub type EditorByViewId = HashMap>; pub struct DatabaseViews { #[allow(dead_code)] - database: Arc, + database: Arc>, cell_cache: CellCache, view_operation: Arc, view_editors: Arc>, @@ -25,7 +25,7 @@ pub struct DatabaseViews { impl DatabaseViews { pub async fn new( - database: Arc, + database: Arc>, cell_cache: CellCache, view_operation: Arc, view_editors: Arc>, @@ -59,13 +59,10 @@ impl DatabaseViews { return Ok(editor.clone()); } - let mut editor_map = self.view_editors.try_write().map_err(|err| { - FlowyError::internal().with_context(format!( - "fail to acquire the lock of editor_by_view_id: {}", - err - )) - })?; - let database_id = self.database.lock().get_database_id(); + //FIXME: not thread-safe + let mut editor_map = self.view_editors.write().await; + let database_id = self.database.read().await.get_database_id(); + //FIXME: that method below is not Send+Sync let editor = Arc::new( DatabaseViewEditor::new( database_id, diff --git a/frontend/rust-lib/flowy-database2/src/services/field/field_operation.rs b/frontend/rust-lib/flowy-database2/src/services/field/field_operation.rs index e9db74358f..758b32dfa5 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/field_operation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/field_operation.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use flowy_error::FlowyResult; +use flowy_error::{FlowyError, FlowyResult}; use crate::entities::FieldType; use crate::services::database::DatabaseEditor; @@ -11,14 +11,15 @@ pub async fn edit_field_type_option( editor: Arc, action: impl FnOnce(&mut T), ) -> FlowyResult<()> { - let get_type_option = async { - let field = editor.get_field(field_id)?; - let field_type = FieldType::from(field.field_type); - field.get_type_option::(field_type) - }; + let field = editor + .get_field(field_id) + .await + .ok_or_else(FlowyError::field_record_not_found)?; + let field_type = FieldType::from(field.field_type); + let get_type_option = field.get_type_option::(field_type); - if let Some(mut type_option) = get_type_option.await { - if let Some(old_field) = editor.get_field(field_id) { + if let Some(mut type_option) = get_type_option { + if let Some(old_field) = editor.get_field(field_id).await { action(&mut type_option); let type_option_data = type_option.into(); editor diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs index de95ba058c..ea448d76aa 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs @@ -35,7 +35,7 @@ impl From for CheckboxTypeOption { impl From for TypeOptionData { fn from(_data: CheckboxTypeOption) -> Self { - TypeOptionDataBuilder::new().build() + TypeOptionDataBuilder::new() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs index 35de68136b..8b93382ac3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use bytes::Bytes; -use collab::core::any_map::AnyMapExtension; +use collab::util::AnyMapExt; use collab_database::rows::{new_cell_builder, Cell}; use flowy_error::{FlowyError, FlowyResult}; @@ -21,16 +21,16 @@ impl TypeOptionCellData for CheckboxCellDataPB { impl From<&Cell> for CheckboxCellDataPB { fn from(cell: &Cell) -> Self { - let value = cell.get_str_value(CELL_DATA).unwrap_or_default(); + let value: String = cell.get_as(CELL_DATA).unwrap_or_default(); CheckboxCellDataPB::from_str(&value).unwrap_or_default() } } impl From for Cell { fn from(data: CheckboxCellDataPB) -> Self { - new_cell_builder(FieldType::Checkbox) - .insert_str_value(CELL_DATA, data.to_string()) - .build() + let mut cell = new_cell_builder(FieldType::Checkbox); + cell.insert(CELL_DATA.into(), data.to_string().into()); + cell } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist.rs index ceddeadce6..c800bf1104 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist.rs @@ -31,7 +31,7 @@ impl From for ChecklistTypeOption { impl From for TypeOptionData { fn from(_data: ChecklistTypeOption) -> Self { - TypeOptionDataBuilder::new().build() + TypeOptionDataBuilder::new() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_entities.rs index 12b3e07527..ef8a5720e1 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_entities.rs @@ -1,6 +1,6 @@ use crate::entities::FieldType; use crate::services::field::{SelectOption, TypeOptionCellData, CELL_DATA}; -use collab::core::any_map::AnyMapExtension; +use collab::util::AnyMapExt; use collab_database::rows::{new_cell_builder, Cell}; use serde::{Deserialize, Serialize}; use std::fmt::Debug; @@ -64,7 +64,7 @@ impl ChecklistCellData { impl From<&Cell> for ChecklistCellData { fn from(cell: &Cell) -> Self { cell - .get_str_value(CELL_DATA) + .get_as::(CELL_DATA) .map(|data| serde_json::from_str::(&data).unwrap_or_default()) .unwrap_or_default() } @@ -73,9 +73,9 @@ impl From<&Cell> for ChecklistCellData { impl From for Cell { fn from(cell_data: ChecklistCellData) -> Self { let data = serde_json::to_string(&cell_data).unwrap_or_default(); - new_cell_builder(FieldType::Checklist) - .insert_str_value(CELL_DATA, data) - .build() + let mut cell = new_cell_builder(FieldType::Checklist); + cell.insert(CELL_DATA.into(), data.into()); + cell } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs index 6214dc3f24..2a7a713b61 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, NaiveTime, Offset, TimeZone}; use chrono_tz::Tz; -use collab::core::any_map::AnyMapExtension; +use collab::util::AnyMapExt; use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; use serde::{Deserialize, Serialize}; @@ -36,14 +36,14 @@ impl TypeOption for DateTypeOption { impl From for DateTypeOption { fn from(data: TypeOptionData) -> Self { let date_format = data - .get_i64_value("date_format") + .get_as::("date_format") .map(DateFormat::from) .unwrap_or_default(); let time_format = data - .get_i64_value("time_format") + .get_as::("time_format") .map(TimeFormat::from) .unwrap_or_default(); - let timezone_id = data.get_str_value("timezone_id").unwrap_or_default(); + let timezone_id: String = data.get_as("timezone_id").unwrap_or_default(); Self { date_format, time_format, @@ -54,11 +54,11 @@ impl From for DateTypeOption { impl From for TypeOptionData { fn from(data: DateTypeOption) -> Self { - TypeOptionDataBuilder::new() - .insert_i64_value("date_format", data.date_format.value()) - .insert_i64_value("time_format", data.time_format.value()) - .insert_str_value("timezone_id", data.timezone_id) - .build() + TypeOptionDataBuilder::from([ + ("date_format".into(), data.date_format.value().into()), + ("time_format".into(), data.time_format.value().into()), + ("timezone_id".into(), data.timezone_id.into()), + ]) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs index c2b0259aff..b57185ce23 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs @@ -1,7 +1,7 @@ #![allow(clippy::upper_case_acronyms)] use bytes::Bytes; -use collab::core::any_map::AnyMapExtension; +use collab::util::AnyMapExt; use collab_database::rows::{new_cell_builder, Cell}; use serde::de::Visitor; use serde::{Deserialize, Serialize}; @@ -58,14 +58,14 @@ impl TypeOptionCellData for DateCellData { impl From<&Cell> for DateCellData { fn from(cell: &Cell) -> Self { let timestamp = cell - .get_str_value(CELL_DATA) + .get_as::(CELL_DATA) .and_then(|data| data.parse::().ok()); let end_timestamp = cell - .get_str_value("end_timestamp") + .get_as::("end_timestamp") .and_then(|data| data.parse::().ok()); - let include_time = cell.get_bool_value("include_time").unwrap_or_default(); - let is_range = cell.get_bool_value("is_range").unwrap_or_default(); - let reminder_id = cell.get_str_value("reminder_id").unwrap_or_default(); + let include_time: bool = cell.get_as("include_time").unwrap_or_default(); + let is_range: bool = cell.get_as("is_range").unwrap_or_default(); + let reminder_id: String = cell.get_as("reminder_id").unwrap_or_default(); Self { timestamp, @@ -101,13 +101,16 @@ impl From<&DateCellData> for Cell { }; // Most of the case, don't use these keys in other places. Otherwise, we should define // constants for them. - new_cell_builder(FieldType::DateTime) - .insert_str_value(CELL_DATA, timestamp_string) - .insert_str_value("end_timestamp", end_timestamp_string) - .insert_bool_value("include_time", cell_data.include_time) - .insert_bool_value("is_range", cell_data.is_range) - .insert_str_value("reminder_id", cell_data.reminder_id.to_owned()) - .build() + let mut cell = new_cell_builder(FieldType::DateTime); + cell.insert(CELL_DATA.into(), timestamp_string.into()); + cell.insert("end_timestamp".into(), end_timestamp_string.into()); + cell.insert("include_time".into(), cell_data.include_time.into()); + cell.insert("is_range".into(), cell_data.is_range.into()); + cell.insert( + "reminder_id".into(), + cell_data.reminder_id.to_owned().into(), + ); + cell } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs index 0fc7cd5920..eee660af86 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs @@ -1,14 +1,16 @@ +use collab::preclude::encoding::serde::from_any; +use collab::preclude::Any; +use collab::util::AnyMapExt; use std::cmp::Ordering; use std::default::Default; use std::str::FromStr; -use collab::core::any_map::AnyMapExtension; use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::{new_cell_builder, Cell}; use fancy_regex::Regex; use lazy_static::lazy_static; use rust_decimal::Decimal; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use flowy_error::FlowyResult; @@ -25,12 +27,24 @@ use crate::services::sort::SortCondition; // Number #[derive(Clone, Debug, Serialize, Deserialize)] pub struct NumberTypeOption { + #[serde(default, deserialize_with = "number_format_from_i64")] pub format: NumberFormat, + #[serde(default)] pub scale: u32, + #[serde(default)] pub symbol: String, + #[serde(default)] pub name: String, } +fn number_format_from_i64<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let value = i64::deserialize(deserializer)?; + Ok(NumberFormat::from(value)) +} + #[derive(Clone, Debug, Default)] pub struct NumberCellData(pub String); @@ -42,15 +56,15 @@ impl TypeOptionCellData for NumberCellData { impl From<&Cell> for NumberCellData { fn from(cell: &Cell) -> Self { - Self(cell.get_str_value(CELL_DATA).unwrap_or_default()) + Self(cell.get_as(CELL_DATA).unwrap_or_default()) } } impl From for Cell { fn from(data: NumberCellData) -> Self { - new_cell_builder(FieldType::Number) - .insert_str_value(CELL_DATA, data.0) - .build() + let mut cell = new_cell_builder(FieldType::Number); + cell.insert(CELL_DATA.into(), data.0.into()); + cell } } @@ -75,30 +89,18 @@ impl TypeOption for NumberTypeOption { impl From for NumberTypeOption { fn from(data: TypeOptionData) -> Self { - let format = data - .get_i64_value("format") - .map(NumberFormat::from) - .unwrap_or_default(); - let scale = data.get_i64_value("scale").unwrap_or_default() as u32; - let symbol = data.get_str_value("symbol").unwrap_or_default(); - let name = data.get_str_value("name").unwrap_or_default(); - Self { - format, - scale, - symbol, - name, - } + from_any(&Any::from(data)).unwrap() } } impl From for TypeOptionData { fn from(data: NumberTypeOption) -> Self { - TypeOptionDataBuilder::new() - .insert_i64_value("format", data.format.value()) - .insert_i64_value("scale", data.scale as i64) - .insert_str_value("name", data.name) - .insert_str_value("symbol", data.symbol) - .build() + TypeOptionDataBuilder::from([ + ("format".into(), data.format.value().into()), + ("scale".into(), data.scale.into()), + ("name".into(), data.name.into()), + ("symbol".into(), data.symbol.into()), + ]) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/relation.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/relation.rs index ac2548b89d..9806471cb2 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/relation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/relation.rs @@ -1,6 +1,6 @@ +use collab::util::AnyMapExt; use std::cmp::Ordering; -use collab::core::any_map::AnyMapExtension; use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; use flowy_error::FlowyResult; @@ -23,16 +23,14 @@ pub struct RelationTypeOption { impl From for RelationTypeOption { fn from(value: TypeOptionData) -> Self { - let database_id = value.get_str_value("database_id").unwrap_or_default(); + let database_id: String = value.get_as("database_id").unwrap_or_default(); Self { database_id } } } impl From for TypeOptionData { fn from(value: RelationTypeOption) -> Self { - TypeOptionDataBuilder::new() - .insert_str_value("database_id", value.database_id) - .build() + TypeOptionDataBuilder::from([("database_id".into(), value.database_id.into())]) } } @@ -57,7 +55,7 @@ impl CellDataChangeset for RelationTypeOption { return Ok(((&cell_data).into(), cell_data)); } - let cell_data: RelationCellData = cell.unwrap().as_ref().into(); + let cell_data: RelationCellData = cell.as_ref().unwrap().into(); let mut row_ids = cell_data.row_ids.clone(); for inserted in changeset.inserted_row_ids.iter() { if !row_ids.iter().any(|row_id| row_id == inserted) { diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/relation_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/relation_entities.rs index 97b18590af..c8911a2ffe 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/relation_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/relation_entities.rs @@ -40,9 +40,9 @@ impl From<&RelationCellData> for Cell { .map(|id| Any::String(Arc::from(id.to_string()))) .collect::>(), )); - new_cell_builder(FieldType::Relation) - .insert_any(CELL_DATA, data) - .build() + let mut cell = new_cell_builder(FieldType::Relation); + cell.insert(CELL_DATA.into(), data); + cell } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/multi_select_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/multi_select_type_option.rs index 8ebd0d1db4..850b383a45 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/multi_select_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/multi_select_type_option.rs @@ -1,6 +1,6 @@ +use collab::util::AnyMapExt; use std::cmp::Ordering; -use collab::core::any_map::AnyMapExtension; use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; use serde::{Deserialize, Serialize}; @@ -33,8 +33,8 @@ impl TypeOption for MultiSelectTypeOption { impl From for MultiSelectTypeOption { fn from(data: TypeOptionData) -> Self { data - .get_str_value("content") - .map(|s| serde_json::from_str::(&s).unwrap_or_default()) + .get_as::("content") + .map(|json| serde_json::from_str::(&json).unwrap_or_default()) .unwrap_or_default() } } @@ -42,9 +42,7 @@ impl From for MultiSelectTypeOption { impl From for TypeOptionData { fn from(data: MultiSelectTypeOption) -> Self { let content = serde_json::to_string(&data).unwrap_or_default(); - TypeOptionDataBuilder::new() - .insert_str_value("content", content) - .build() + TypeOptionDataBuilder::from([("content".into(), content.into())]) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_ids.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_ids.rs index c47738b788..c75730dfac 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_ids.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_ids.rs @@ -1,6 +1,6 @@ +use collab::util::AnyMapExt; use std::str::FromStr; -use collab::core::any_map::AnyMapExtension; use collab_database::rows::{new_cell_builder, Cell}; use flowy_error::FlowyError; @@ -26,9 +26,9 @@ impl SelectOptionIds { self.0 } pub fn to_cell_data(&self, field_type: FieldType) -> Cell { - new_cell_builder(field_type) - .insert_str_value(CELL_DATA, self.to_string()) - .build() + let mut cell = new_cell_builder(field_type); + cell.insert(CELL_DATA.into(), self.to_string().into()); + cell } } @@ -40,7 +40,7 @@ impl TypeOptionCellData for SelectOptionIds { impl From<&Cell> for SelectOptionIds { fn from(cell: &Cell) -> Self { - let value = cell.get_str_value(CELL_DATA).unwrap_or_default(); + let value: String = cell.get_as(CELL_DATA).unwrap_or_default(); Self::from_str(&value).unwrap_or_default() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/single_select_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/single_select_type_option.rs index fa0745133b..bbf131b64f 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/single_select_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/single_select_type_option.rs @@ -8,7 +8,7 @@ use crate::services::field::{ SelectOptionCellChangeset, SelectOptionIds, SelectTypeOptionSharedAction, }; use crate::services::sort::SortCondition; -use collab::core::any_map::AnyMapExtension; +use collab::util::AnyMapExt; use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; use flowy_error::FlowyResult; @@ -32,7 +32,7 @@ impl TypeOption for SingleSelectTypeOption { impl From for SingleSelectTypeOption { fn from(data: TypeOptionData) -> Self { data - .get_str_value("content") + .get_as::("content") .map(|s| serde_json::from_str::(&s).unwrap_or_default()) .unwrap_or_default() } @@ -41,9 +41,7 @@ impl From for SingleSelectTypeOption { impl From for TypeOptionData { fn from(data: SingleSelectTypeOption) -> Self { let content = serde_json::to_string(&data).unwrap_or_default(); - TypeOptionDataBuilder::new() - .insert_str_value("content", content) - .build() + TypeOptionDataBuilder::from([("content".into(), content.into())]) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary.rs index 920f76de8e..4d99e67dd3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary.rs @@ -7,7 +7,7 @@ use crate::services::field::{ TypeOptionCellDataSerde, TypeOptionTransform, }; use crate::services::sort::SortCondition; -use collab::core::any_map::AnyMapExtension; +use collab::util::AnyMapExt; use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; use flowy_error::FlowyResult; @@ -20,16 +20,14 @@ pub struct SummarizationTypeOption { impl From for SummarizationTypeOption { fn from(value: TypeOptionData) -> Self { - let auto_fill = value.get_bool_value("auto_fill").unwrap_or_default(); + let auto_fill: bool = value.get_as("auto_fill").unwrap_or_default(); Self { auto_fill } } } impl From for TypeOptionData { fn from(value: SummarizationTypeOption) -> Self { - TypeOptionDataBuilder::new() - .insert_bool_value("auto_fill", value.auto_fill) - .build() + TypeOptionDataBuilder::from([("auto_fill".into(), value.auto_fill.into())]) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary_entities.rs index 8d45578e38..ef41e2d0f5 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary_entities.rs @@ -1,6 +1,6 @@ use crate::entities::FieldType; use crate::services::field::{TypeOptionCellData, CELL_DATA}; -use collab::core::any_map::AnyMapExtension; +use collab::util::AnyMapExt; use collab_database::rows::{new_cell_builder, Cell}; #[derive(Default, Debug, Clone)] @@ -21,15 +21,15 @@ impl TypeOptionCellData for SummaryCellData { impl From<&Cell> for SummaryCellData { fn from(cell: &Cell) -> Self { - Self(cell.get_str_value(CELL_DATA).unwrap_or_default()) + Self(cell.get_as::(CELL_DATA).unwrap_or_default()) } } impl From for Cell { fn from(data: SummaryCellData) -> Self { - new_cell_builder(FieldType::Summary) - .insert_str_value(CELL_DATA, data.0) - .build() + let mut cell = new_cell_builder(FieldType::Summary); + cell.insert(CELL_DATA.into(), data.0.into()); + cell } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs index 5cb2875de5..d32c9f0e44 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs @@ -1,6 +1,6 @@ +use collab::util::AnyMapExt; use std::cmp::Ordering; -use collab::core::any_map::AnyMapExtension; use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::{new_cell_builder, Cell}; use serde::{Deserialize, Serialize}; @@ -33,16 +33,15 @@ impl TypeOption for RichTextTypeOption { impl From for RichTextTypeOption { fn from(data: TypeOptionData) -> Self { - let s = data.get_str_value(CELL_DATA).unwrap_or_default(); - Self { inner: s } + Self { + inner: data.get_as(CELL_DATA).unwrap_or_default(), + } } } impl From for TypeOptionData { fn from(data: RichTextTypeOption) -> Self { - TypeOptionDataBuilder::new() - .insert_str_value(CELL_DATA, data.inner) - .build() + TypeOptionDataBuilder::from([(CELL_DATA.into(), data.inner.into())]) } } @@ -164,15 +163,15 @@ impl TypeOptionCellData for StringCellData { impl From<&Cell> for StringCellData { fn from(cell: &Cell) -> Self { - Self(cell.get_str_value(CELL_DATA).unwrap_or_default()) + Self(cell.get_as(CELL_DATA).unwrap_or_default()) } } impl From for Cell { fn from(data: StringCellData) -> Self { - new_cell_builder(FieldType::RichText) - .insert_str_value(CELL_DATA, data.0) - .build() + let mut cell = new_cell_builder(FieldType::RichText); + cell.insert(CELL_DATA.into(), data.0.into()); + cell } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time.rs index 0b7c141cb8..2125eb8f88 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time.rs @@ -29,7 +29,7 @@ impl From for TimeTypeOption { impl From for TypeOptionData { fn from(_data: TimeTypeOption) -> Self { - TypeOptionDataBuilder::new().build() + TypeOptionDataBuilder::new() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_entities.rs index 6084c80b5f..f07babeda0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_entities.rs @@ -1,6 +1,6 @@ use crate::entities::FieldType; use crate::services::field::{TypeOptionCellData, CELL_DATA}; -use collab::core::any_map::AnyMapExtension; +use collab::util::AnyMapExt; use collab_database::rows::{new_cell_builder, Cell}; #[derive(Clone, Debug, Default)] @@ -16,7 +16,7 @@ impl From<&Cell> for TimeCellData { fn from(cell: &Cell) -> Self { Self( cell - .get_str_value(CELL_DATA) + .get_as::(CELL_DATA) .and_then(|data| data.parse::().ok()), ) } @@ -40,8 +40,8 @@ impl ToString for TimeCellData { impl From<&TimeCellData> for Cell { fn from(data: &TimeCellData) -> Self { - new_cell_builder(FieldType::Time) - .insert_str_value(CELL_DATA, data.to_string()) - .build() + let mut cell = new_cell_builder(FieldType::Time); + cell.insert(CELL_DATA.into(), data.to_string().into()); + cell } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs index 17b9f54dd3..116bd1a0a4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs @@ -1,7 +1,7 @@ use std::cmp::Ordering; use chrono::{DateTime, Local, Offset}; -use collab::core::any_map::AnyMapExtension; +use collab::util::AnyMapExt; use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; @@ -44,16 +44,16 @@ impl TypeOption for TimestampTypeOption { impl From for TimestampTypeOption { fn from(data: TypeOptionData) -> Self { let date_format = data - .get_i64_value("date_format") + .get_as::("date_format") .map(DateFormat::from) .unwrap_or_default(); let time_format = data - .get_i64_value("time_format") + .get_as::("time_format") .map(TimeFormat::from) .unwrap_or_default(); - let include_time = data.get_bool_value("include_time").unwrap_or_default(); + let include_time = data.get_as::("include_time").unwrap_or_default(); let field_type = data - .get_i64_value("field_type") + .get_as::("field_type") .map(FieldType::from) .unwrap_or(FieldType::LastEditedTime); Self { @@ -67,12 +67,12 @@ impl From for TimestampTypeOption { impl From for TypeOptionData { fn from(option: TimestampTypeOption) -> Self { - TypeOptionDataBuilder::new() - .insert_i64_value("date_format", option.date_format.value()) - .insert_i64_value("time_format", option.time_format.value()) - .insert_bool_value("include_time", option.include_time) - .insert_i64_value("field_type", option.field_type.value()) - .build() + TypeOptionDataBuilder::from([ + ("date_format".into(), option.date_format.value().into()), + ("time_format".into(), option.time_format.value().into()), + ("include_time".into(), option.include_time.into()), + ("field_type".into(), option.field_type.value().into()), + ]) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option_entities.rs index 307b7637b8..a1e416d688 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option_entities.rs @@ -1,4 +1,4 @@ -use collab::core::any_map::AnyMapExtension; +use collab::util::AnyMapExt; use collab_database::rows::{new_cell_builder, Cell}; use serde::Serialize; @@ -23,7 +23,7 @@ impl TimestampCellData { impl From<&Cell> for TimestampCellData { fn from(cell: &Cell) -> Self { let timestamp = cell - .get_str_value(CELL_DATA) + .get_as::(CELL_DATA) .and_then(|data| data.parse::().ok()); Self { timestamp } } @@ -45,11 +45,11 @@ impl From<(FieldType, TimestampCellData)> for TimestampCellDataWrapper { impl From for Cell { fn from(wrapper: TimestampCellDataWrapper) -> Self { let (field_type, data) = (wrapper.field_type, wrapper.data); - let timestamp_string = data.timestamp.unwrap_or_default(); + let timestamp_string = data.timestamp.unwrap_or_default().to_string(); - new_cell_builder(field_type) - .insert_str_value(CELL_DATA, timestamp_string) - .build() + let mut cell = new_cell_builder(field_type); + cell.insert(CELL_DATA.into(), timestamp_string.into()); + cell } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate.rs index 5403782387..ff84213a15 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate.rs @@ -7,16 +7,20 @@ use crate::services::field::{ TypeOptionCellDataSerde, TypeOptionTransform, }; use crate::services::sort::SortCondition; -use collab::core::any_map::AnyMapExtension; +use collab::preclude::encoding::serde::from_any; +use collab::preclude::Any; use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; use flowy_error::FlowyResult; +use serde::Deserialize; use std::cmp::Ordering; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize)] pub struct TranslateTypeOption { + #[serde(default)] pub auto_fill: bool, /// Use [TranslateTypeOption::language_from_type] to get the language name + #[serde(default, rename = "language")] pub language_type: i64, } @@ -48,21 +52,16 @@ impl Default for TranslateTypeOption { impl From for TranslateTypeOption { fn from(value: TypeOptionData) -> Self { - let auto_fill = value.get_bool_value("auto_fill").unwrap_or_default(); - let language = value.get_i64_value("language").unwrap_or_default(); - Self { - auto_fill, - language_type: language, - } + from_any(&Any::from(value)).unwrap() } } impl From for TypeOptionData { fn from(value: TranslateTypeOption) -> Self { - TypeOptionDataBuilder::new() - .insert_bool_value("auto_fill", value.auto_fill) - .insert_i64_value("language", value.language_type) - .build() + TypeOptionDataBuilder::from([ + ("auto_fill".into(), value.auto_fill.into()), + ("language".into(), value.language_type.into()), + ]) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate_entities.rs index b52b746ab5..eefbf873da 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate_entities.rs @@ -1,6 +1,6 @@ use crate::entities::FieldType; use crate::services::field::{TypeOptionCellData, CELL_DATA}; -use collab::core::any_map::AnyMapExtension; +use collab::util::AnyMapExt; use collab_database::rows::{new_cell_builder, Cell}; #[derive(Default, Debug, Clone)] @@ -21,15 +21,15 @@ impl TypeOptionCellData for TranslateCellData { impl From<&Cell> for TranslateCellData { fn from(cell: &Cell) -> Self { - Self(cell.get_str_value(CELL_DATA).unwrap_or_default()) + Self(cell.get_as(CELL_DATA).unwrap_or_default()) } } impl From for Cell { fn from(data: TranslateCellData) -> Self { - new_cell_builder(FieldType::Translate) - .insert_str_value(CELL_DATA, data.0) - .build() + let mut cell = new_cell_builder(FieldType::Translate); + cell.insert(CELL_DATA.into(), data.0.into()); + cell } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs index 415f694164..9e33d874be 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs @@ -1,5 +1,7 @@ +use collab::preclude::Any; use std::cmp::Ordering; use std::collections::hash_map::DefaultHasher; +use std::collections::HashMap; use std::hash::{Hash, Hasher}; use collab_database::fields::{Field, TypeOptionData}; @@ -96,15 +98,40 @@ impl CellDataCacheKey { pub fn new(field_rev: &Field, decoded_field_type: FieldType, cell: &Cell) -> Self { let mut hasher = DefaultHasher::new(); if let Some(type_option_data) = field_rev.get_any_type_option(decoded_field_type) { - type_option_data.hash(&mut hasher); + map_hash(&type_option_data, &mut hasher); } hasher.write(field_rev.id.as_bytes()); hasher.write_u8(decoded_field_type as u8); - cell.hash(&mut hasher); + map_hash(cell, &mut hasher); Self(hasher.finish()) } } +fn any_hash(any: &Any, hasher: &mut H) { + //FIXME: this is very bad idea for hash calculation + match any { + Any::Null | Any::Undefined => hasher.write_u8(0), + Any::Bool(v) => v.hash(hasher), + Any::Number(v) => v.to_be_bytes().hash(hasher), + Any::BigInt(v) => v.hash(hasher), + Any::String(v) => v.hash(hasher), + Any::Buffer(v) => v.hash(hasher), + Any::Array(v) => { + for v in v.iter() { + any_hash(v, hasher); + } + }, + Any::Map(v) => map_hash(v, hasher), + } +} + +fn map_hash(map: &HashMap, hasher: &mut H) { + for (k, v) in map.iter() { + k.hash(hasher); + any_hash(v, hasher); + } +} + impl AsRef for CellDataCacheKey { fn as_ref(&self) -> &u64 { &self.0 @@ -159,9 +186,10 @@ where fn get_cell_data_from_cache(&self, cell: &Cell, field: &Field) -> Option { let key = self.get_cell_data_cache_key(cell, field); - let cell_data_cache = self.cell_data_cache.as_ref()?.read(); + let cell_data_cache = self.cell_data_cache.as_ref()?; - cell_data_cache.get(key.as_ref()).cloned() + let cell = cell_data_cache.get::(key.as_ref())?; + Some(cell.value().clone()) } fn set_cell_data_in_cache(&self, cell: &Cell, cell_data: T::CellData, field: &Field) { @@ -174,7 +202,7 @@ where cell, cell_data ); - cell_data_cache.write().insert(key.as_ref(), cell_data); + cell_data_cache.insert(key.as_ref(), cell_data); } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option.rs index 3a95c6bae0..f167284014 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option.rs @@ -1,6 +1,7 @@ +use collab::preclude::encoding::serde::from_any; +use collab::preclude::Any; use std::cmp::Ordering; -use collab::core::any_map::AnyMapExtension; use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; use flowy_error::FlowyResult; @@ -16,7 +17,9 @@ use crate::services::sort::SortCondition; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct URLTypeOption { + #[serde(default)] pub url: String, + #[serde(default)] pub content: String, } @@ -29,18 +32,16 @@ impl TypeOption for URLTypeOption { impl From for URLTypeOption { fn from(data: TypeOptionData) -> Self { - let url = data.get_str_value("url").unwrap_or_default(); - let content = data.get_str_value("content").unwrap_or_default(); - Self { url, content } + from_any(&Any::from(data)).unwrap() } } impl From for TypeOptionData { fn from(data: URLTypeOption) -> Self { - TypeOptionDataBuilder::new() - .insert_str_value("url", data.url) - .insert_str_value("content", data.content) - .build() + TypeOptionDataBuilder::from([ + ("url".into(), data.url.into()), + ("content".into(), data.content.into()), + ]) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option_entities.rs index 2b286e0604..dd351fcc2a 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option_entities.rs @@ -1,5 +1,5 @@ use bytes::Bytes; -use collab::core::any_map::AnyMapExtension; +use collab::util::AnyMapExt; use collab_database::rows::{new_cell_builder, Cell}; use serde::{Deserialize, Serialize}; @@ -34,16 +34,17 @@ impl TypeOptionCellData for URLCellData { impl From<&Cell> for URLCellData { fn from(cell: &Cell) -> Self { - let data = cell.get_str_value(CELL_DATA).unwrap_or_default(); - Self { data } + Self { + data: cell.get_as(CELL_DATA).unwrap_or_default(), + } } } impl From for Cell { fn from(data: URLCellData) -> Self { - new_cell_builder(FieldType::URL) - .insert_str_value(CELL_DATA, data.data) - .build() + let mut cell = new_cell_builder(FieldType::URL); + cell.insert(CELL_DATA.into(), data.data.into()); + cell } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs b/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs index 9f9e82311f..1fb7cde207 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs @@ -1,4 +1,4 @@ -use collab::core::any_map::AnyMapExtension; +use collab::util::AnyMapExt; use collab_database::views::{DatabaseLayout, FieldSettingsMap, FieldSettingsMapBuilder}; use crate::entities::FieldVisibility; @@ -25,16 +25,11 @@ impl FieldSettings { field_settings: &FieldSettingsMap, ) -> Self { let visibility = field_settings - .get_i64_value(VISIBILITY) + .get_as::(VISIBILITY) .map(Into::into) .unwrap_or_else(|| default_field_visibility(layout_type)); - let width = field_settings - .get_i64_value(WIDTH) - .map(|value| value as i32) - .unwrap_or(DEFAULT_WIDTH); - let wrap_cell_content = field_settings - .get_bool_value(WRAP_CELL_CONTENT) - .unwrap_or(true); + let width = field_settings.get_as::(WIDTH).unwrap_or(DEFAULT_WIDTH); + let wrap_cell_content: bool = field_settings.get_as(WRAP_CELL_CONTENT).unwrap_or(true); Self { field_id: field_id.to_string(), @@ -47,10 +42,16 @@ impl FieldSettings { impl From for FieldSettingsMap { fn from(field_settings: FieldSettings) -> Self { - FieldSettingsMapBuilder::new() - .insert_i64_value(VISIBILITY, field_settings.visibility.into()) - .insert_i64_value(WIDTH, field_settings.width as i64) - .insert_bool_value(WRAP_CELL_CONTENT, field_settings.wrap_cell_content) - .build() + FieldSettingsMapBuilder::from([ + ( + VISIBILITY.into(), + i64::from(field_settings.visibility).into(), + ), + (WIDTH.into(), field_settings.width.into()), + ( + WRAP_CELL_CONTENT.into(), + field_settings.wrap_cell_content.into(), + ), + ]) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs b/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs index 7602224acd..7f9ed6b2c0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs @@ -86,9 +86,8 @@ pub fn default_field_settings_by_layout_map() -> HashMap Option; - fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>; - fn get_rows(&self, view_id: &str) -> Fut>>; - fn get_row(&self, view_id: &str, rows_id: &RowId) -> Fut)>>; - fn get_all_filters(&self, view_id: &str) -> Vec; - fn save_filters(&self, view_id: &str, filters: &[Filter]); + async fn get_field(&self, field_id: &str) -> Option; + async fn get_fields(&self, view_id: &str, field_ids: Option>) -> Vec; + async fn get_rows(&self, view_id: &str) -> Vec>; + async fn get_row(&self, view_id: &str, rows_id: &RowId) -> Option<(usize, Arc)>; + async fn get_all_filters(&self, view_id: &str) -> Vec; + async fn save_filters(&self, view_id: &str, filters: &[Filter]); } pub trait PreFillCellsWithFilter { @@ -72,7 +73,7 @@ impl FilterController { let mut need_save = false; - let mut filters = delegate.get_all_filters(view_id); + let mut filters = delegate.get_all_filters(view_id).await; let mut filtering_field_ids: HashMap> = HashMap::new(); for filter in filters.iter() { @@ -93,7 +94,7 @@ impl FilterController { } if need_save { - delegate.save_filters(view_id, &filters); + delegate.save_filters(view_id, &filters).await; } Self { @@ -231,7 +232,7 @@ impl FilterController { }, } - self.delegate.save_filters(&self.view_id, &filters); + self.delegate.save_filters(&self.view_id, &filters).await; self .gen_task(FilterEvent::FilterDidChanged, QualityOfService::Background) diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs index 718d062fbb..adb5ef8c5b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs @@ -1,8 +1,10 @@ use std::collections::HashMap; use std::mem; +use std::ops::Deref; use anyhow::bail; -use collab::core::any_map::AnyMapExtension; +use collab::preclude::Any; +use collab::util::AnyMapExt; use collab_database::database::gen_database_filter_id; use collab_database::rows::RowId; use collab_database::views::{FilterMap, FilterMapBuilder}; @@ -316,13 +318,20 @@ const FILTER_DATA_INDEX: i64 = 2; impl<'a> From<&'a Filter> for FilterMap { fn from(filter: &'a Filter) -> Self { - let mut builder = FilterMapBuilder::new() - .insert_str_value(FILTER_ID, &filter.id) - .insert_i64_value(FILTER_TYPE, filter.inner.get_int_repr()); + let mut builder = FilterMapBuilder::from([ + (FILTER_ID.into(), filter.id.as_str().into()), + (FILTER_TYPE.into(), filter.inner.get_int_repr().into()), + ]); builder = match &filter.inner { FilterInner::And { children } | FilterInner::Or { children } => { - builder.insert_maps(FILTER_CHILDREN, children.iter().collect::>()) + let mut vec = Vec::with_capacity(children.len()); + for child in children.iter() { + let any: Any = FilterMap::from(child).into(); + vec.push(any); + } + builder.insert(FILTER_CHILDREN.into(), Any::from(vec)); + builder }, FilterInner::Data { field_id, @@ -387,15 +396,15 @@ impl<'a> From<&'a Filter> for FilterMap { Default::default() }); + builder.insert(FIELD_ID.into(), field_id.as_str().into()); + builder.insert(FIELD_TYPE.into(), i64::from(field_type).into()); + builder.insert(FILTER_CONDITION.into(), (condition as i64).into()); + builder.insert(FILTER_CONTENT.into(), content.into()); builder - .insert_str_value(FIELD_ID, field_id) - .insert_i64_value(FIELD_TYPE, field_type.into()) - .insert_i64_value(FILTER_CONDITION, condition as i64) - .insert_str_value(FILTER_CONTENT, content) }, }; - builder.build() + builder } } @@ -403,32 +412,30 @@ impl TryFrom for Filter { type Error = anyhow::Error; fn try_from(filter_map: FilterMap) -> Result { - let filter_id = filter_map - .get_str_value(FILTER_ID) + let filter_id: String = filter_map + .get_as(FILTER_ID) .ok_or_else(|| anyhow::anyhow!("invalid filter data"))?; - let filter_type = filter_map - .get_i64_value(FILTER_TYPE) - .unwrap_or(FILTER_DATA_INDEX); + let filter_type: i64 = filter_map.get_as(FILTER_TYPE).unwrap_or(FILTER_DATA_INDEX); let filter = Filter { id: filter_id, inner: match filter_type { FILTER_AND_INDEX => FilterInner::And { - children: filter_map.try_get_array(FILTER_CHILDREN), + children: get_children(filter_map), }, FILTER_OR_INDEX => FilterInner::Or { - children: filter_map.try_get_array(FILTER_CHILDREN), + children: get_children(filter_map), }, FILTER_DATA_INDEX => { - let field_id = filter_map - .get_str_value(FIELD_ID) + let field_id: String = filter_map + .get_as(FIELD_ID) .ok_or_else(|| anyhow::anyhow!("invalid filter data"))?; let field_type = filter_map - .get_i64_value(FIELD_TYPE) + .get_as::(FIELD_TYPE) .map(FieldType::from) .unwrap_or_default(); - let condition = filter_map.get_i64_value(FILTER_CONDITION).unwrap_or(0); - let content = filter_map.get_str_value(FILTER_CONTENT).unwrap_or_default(); + let condition: i64 = filter_map.get_as(FILTER_CONDITION).unwrap_or_default(); + let content: String = filter_map.get_as(FILTER_CONTENT).unwrap_or_default(); FilterInner::new_data(field_id, field_type, condition, content) }, @@ -440,6 +447,22 @@ impl TryFrom for Filter { } } +fn get_children(filter_map: FilterMap) -> Vec { + //TODO: this method wouldn't be necessary if we could make Filters serializable in backward + // compatible way + let mut result = Vec::new(); + if let Some(Any::Array(children)) = filter_map.get(FILTER_CHILDREN) { + for child in children.iter() { + if let Any::Map(child_map) = child { + if let Ok(filter) = Filter::try_from(child_map.deref().clone()) { + result.push(filter); + } + } + } + } + result +} + #[derive(Debug)] pub enum FilterChangeset { Insert { diff --git a/frontend/rust-lib/flowy-database2/src/services/group/action.rs b/frontend/rust-lib/flowy-database2/src/services/group/action.rs index b540fb5fa3..cf4b8ae5eb 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/action.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/action.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{Cell, Cells, Row, RowDetail, RowId}; @@ -10,7 +11,7 @@ use crate::services::group::{GroupChangeset, GroupData, MoveGroupRowContext}; /// [GroupCustomize] is implemented by parameterized `BaseGroupController`s to provide different /// behaviors. This allows the BaseGroupController to call these actions indescriminantly using /// polymorphism. -/// +#[async_trait] pub trait GroupCustomize: Send + Sync { type GroupTypeOption: TypeOption; /// Returns the a value of the cell if the cell data is not exist. @@ -67,14 +68,14 @@ pub trait GroupCustomize: Send + Sync { None } - fn create_group( + async fn create_group( &mut self, _name: String, ) -> FlowyResult<(Option, Option)> { Ok((None, None)) } - fn delete_group(&mut self, group_id: &str) -> FlowyResult>; + async fn delete_group(&mut self, group_id: &str) -> FlowyResult>; fn update_type_option_when_update_group( &mut self, @@ -95,7 +96,7 @@ pub trait GroupCustomize: Send + Sync { /// or a `DefaultGroupController` may be the actual object that provides the functionality of /// this trait. For example, a `Single-Select` group controller will be a `BaseGroupController`, /// while a `URL` group controller will be a `DefaultGroupController`. -/// +#[async_trait] pub trait GroupController: Send + Sync { /// Returns the id of field that is being used to group the rows fn get_grouping_field_id(&self) -> &str; @@ -119,7 +120,7 @@ pub trait GroupController: Send + Sync { /// Returns a new type option data for the grouping field if it's altered. /// /// * `name`: name of the new group - fn create_group( + async fn create_group( &mut self, name: String, ) -> FlowyResult<(Option, Option)>; @@ -179,7 +180,10 @@ pub trait GroupController: Send + Sync { /// successful. /// /// * `group_id`: the id of the group to be deleted - fn delete_group(&mut self, group_id: &str) -> FlowyResult<(Vec, Option)>; + async fn delete_group( + &mut self, + group_id: &str, + ) -> FlowyResult<(Vec, Option)>; /// Updates the name and/or visibility of groups. /// @@ -187,7 +191,7 @@ pub trait GroupController: Send + Sync { /// in the field type option data. /// /// * `changesets`: list of changesets to be made to one or more groups - fn apply_group_changeset( + async fn apply_group_changeset( &mut self, changesets: &[GroupChangeset], ) -> FlowyResult<(Vec, Option)>; diff --git a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs index 980fee21b2..ba949de745 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use std::fmt::Formatter; use std::marker::PhantomData; use std::sync::Arc; @@ -10,7 +11,6 @@ use tracing::event; use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::af_spawn; -use lib_infra::future::Fut; use crate::entities::{GroupChangesPB, GroupPB, InsertedGroupPB}; use crate::services::field::RowSingleCellData; @@ -18,12 +18,14 @@ use crate::services::group::{ default_group_setting, GeneratedGroups, Group, GroupChangeset, GroupData, GroupSetting, }; +#[async_trait] pub trait GroupContextDelegate: Send + Sync + 'static { - fn get_group_setting(&self, view_id: &str) -> Fut>>; + async fn get_group_setting(&self, view_id: &str) -> Option>; - fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Fut>; + async fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Vec; - fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut>; + async fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) + -> FlowyResult<()>; } impl std::fmt::Display for GroupControllerContext { diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs index a918e7f7c2..bad911bcde 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs @@ -1,10 +1,10 @@ +use async_trait::async_trait; use std::marker::PhantomData; use std::sync::Arc; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{Cells, Row, RowDetail, RowId}; use futures::executor::block_on; -use lib_infra::future::Fut; use serde::de::DeserializeOwned; use serde::Serialize; @@ -23,10 +23,11 @@ use crate::services::group::configuration::GroupControllerContext; use crate::services::group::entities::GroupData; use crate::services::group::{GroupChangeset, GroupsBuilder, MoveGroupRowContext}; +#[async_trait] pub trait GroupControllerDelegate: Send + Sync + 'static { - fn get_field(&self, field_id: &str) -> Option; + async fn get_field(&self, field_id: &str) -> Option; - fn get_all_rows(&self, view_id: &str) -> Fut>>; + async fn get_all_rows(&self, view_id: &str) -> Vec>; } /// [BaseGroupController] is a generic group controller that provides customized implementations @@ -75,10 +76,11 @@ where }) } - pub fn get_grouping_field_type_option(&self) -> Option { + pub async fn get_grouping_field_type_option(&self) -> Option { self .delegate .get_field(&self.grouping_field_id) + .await .and_then(|field| field.get_type_option::(FieldType::from(field.field_type))) } @@ -154,6 +156,7 @@ where } } +#[async_trait] impl GroupController for BaseGroupController where P: CellProtobufBlobParser::CellProtobufType>, @@ -215,11 +218,11 @@ where Ok(()) } - fn create_group( + async fn create_group( &mut self, name: String, ) -> FlowyResult<(Option, Option)> { - ::create_group(self, name) + ::create_group(self, name).await } fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> { @@ -373,7 +376,10 @@ where Ok(None) } - fn delete_group(&mut self, group_id: &str) -> FlowyResult<(Vec, Option)> { + async fn delete_group( + &mut self, + group_id: &str, + ) -> FlowyResult<(Vec, Option)> { let group = if group_id != self.get_grouping_field_id() { self.get_group(group_id) } else { @@ -387,14 +393,14 @@ where .iter() .map(|row| row.row.id.clone()) .collect(); - let type_option_data = ::delete_group(self, group_id)?; + let type_option_data = ::delete_group(self, group_id).await?; Ok((row_ids, type_option_data)) }, None => Ok((vec![], None)), } } - fn apply_group_changeset( + async fn apply_group_changeset( &mut self, changeset: &[GroupChangeset], ) -> FlowyResult<(Vec, Option)> { @@ -404,7 +410,7 @@ where } // update group name - let type_option = self.get_grouping_field_type_option().ok_or_else(|| { + let type_option = self.get_grouping_field_type_option().await.ok_or_else(|| { FlowyError::internal().with_context("Failed to get grouping field type option") })?; diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs index a3057b24a0..62896e6a29 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs @@ -25,14 +25,14 @@ pub type CheckboxGroupController = BaseGroupController; pub type CheckboxGroupControllerContext = GroupControllerContext; + +#[async_trait] impl GroupCustomize for CheckboxGroupController { type GroupTypeOption = CheckboxTypeOption; fn placeholder_cell(&self) -> Option { - Some( - new_cell_builder(FieldType::Checkbox) - .insert_str_value("data", UNCHECK) - .build(), - ) + let mut cell = new_cell_builder(FieldType::Checkbox); + cell.insert("data".into(), UNCHECK.into()); + Some(cell) } fn can_group( @@ -129,7 +129,7 @@ impl GroupCustomize for CheckboxGroupController { group_changeset } - fn delete_group(&mut self, _group_id: &str) -> FlowyResult> { + async fn delete_group(&mut self, _group_id: &str) -> FlowyResult> { Ok(None) } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs index 1402793264..9644f918a3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs @@ -53,15 +53,14 @@ pub type DateGroupController = pub type DateGroupControllerContext = GroupControllerContext; +#[async_trait] impl GroupCustomize for DateGroupController { type GroupTypeOption = DateTypeOption; fn placeholder_cell(&self) -> Option { - Some( - new_cell_builder(FieldType::DateTime) - .insert_str_value("data", "") - .build(), - ) + let mut cell = new_cell_builder(FieldType::DateTime); + cell.insert("data".into(), "".into()); + Some(cell) } fn can_group( @@ -214,7 +213,7 @@ impl GroupCustomize for DateGroupController { deleted_group } - fn delete_group(&mut self, group_id: &str) -> FlowyResult> { + async fn delete_group(&mut self, group_id: &str) -> FlowyResult> { self.context.delete_group(group_id)?; Ok(None) } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs index bcfd48bc09..a652e7e24c 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use std::sync::Arc; use collab_database::fields::{Field, TypeOptionData}; @@ -38,6 +39,7 @@ impl DefaultGroupController { } } +#[async_trait] impl GroupController for DefaultGroupController { fn get_grouping_field_id(&self) -> &str { &self.field_id @@ -58,7 +60,7 @@ impl GroupController for DefaultGroupController { Ok(()) } - fn create_group( + async fn create_group( &mut self, _name: String, ) -> FlowyResult<(Option, Option)> { @@ -125,11 +127,14 @@ impl GroupController for DefaultGroupController { Ok(None) } - fn delete_group(&mut self, _group_id: &str) -> FlowyResult<(Vec, Option)> { + async fn delete_group( + &mut self, + _group_id: &str, + ) -> FlowyResult<(Vec, Option)> { Ok((vec![], None)) } - fn apply_group_changeset( + async fn apply_group_changeset( &mut self, _changeset: &[GroupChangeset], ) -> FlowyResult<(Vec, Option)> { diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index cae19109f6..752679ae50 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -31,6 +31,7 @@ pub type MultiSelectGroupController = BaseGroupController< SelectOptionCellDataParser, >; +#[async_trait] impl GroupCustomize for MultiSelectGroupController { type GroupTypeOption = MultiSelectTypeOption; @@ -43,11 +44,9 @@ impl GroupCustomize for MultiSelectGroupController { } fn placeholder_cell(&self) -> Option { - Some( - new_cell_builder(FieldType::MultiSelect) - .insert_str_value("data", "") - .build(), - ) + let mut cell = new_cell_builder(FieldType::MultiSelect); + cell.insert("data".into(), "".into()); + Some(cell) } fn add_or_remove_row_when_cell_changed( @@ -88,11 +87,11 @@ impl GroupCustomize for MultiSelectGroupController { group_changeset } - fn create_group( + async fn create_group( &mut self, name: String, ) -> FlowyResult<(Option, Option)> { - let mut new_type_option = self.get_grouping_field_type_option().ok_or_else(|| { + let mut new_type_option = self.get_grouping_field_type_option().await.ok_or_else(|| { FlowyError::internal().with_context("Failed to get grouping field type option") })?; let new_select_option = new_type_option.create_option(&name); @@ -104,8 +103,8 @@ impl GroupCustomize for MultiSelectGroupController { Ok((Some(new_type_option.into()), Some(inserted_group_pb))) } - fn delete_group(&mut self, group_id: &str) -> FlowyResult> { - let mut new_type_option = self.get_grouping_field_type_option().ok_or_else(|| { + async fn delete_group(&mut self, group_id: &str) -> FlowyResult> { + let mut new_type_option = self.get_grouping_field_type_option().await.ok_or_else(|| { FlowyError::internal().with_context("Failed to get grouping field type option") })?; if let Some(option_index) = new_type_option diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index d26ef50b70..b73606cbc4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -33,6 +33,7 @@ pub type SingleSelectGroupController = BaseGroupController< SelectOptionCellDataParser, >; +#[async_trait] impl GroupCustomize for SingleSelectGroupController { type GroupTypeOption = SingleSelectTypeOption; @@ -45,11 +46,9 @@ impl GroupCustomize for SingleSelectGroupController { } fn placeholder_cell(&self) -> Option { - Some( - new_cell_builder(FieldType::SingleSelect) - .insert_str_value("data", "") - .build(), - ) + let mut cell = new_cell_builder(FieldType::SingleSelect); + cell.insert("data".into(), "".into()); + Some(cell) } fn add_or_remove_row_when_cell_changed( @@ -90,11 +89,11 @@ impl GroupCustomize for SingleSelectGroupController { group_changeset } - fn create_group( + async fn create_group( &mut self, name: String, ) -> FlowyResult<(Option, Option)> { - let mut new_type_option = self.get_grouping_field_type_option().ok_or_else(|| { + let mut new_type_option = self.get_grouping_field_type_option().await.ok_or_else(|| { FlowyError::internal().with_context("Failed to get grouping field type option") })?; let new_select_option = new_type_option.create_option(&name); @@ -106,8 +105,8 @@ impl GroupCustomize for SingleSelectGroupController { Ok((Some(new_type_option.into()), Some(inserted_group_pb))) } - fn delete_group(&mut self, group_id: &str) -> FlowyResult> { - let mut new_type_option = self.get_grouping_field_type_option().ok_or_else(|| { + async fn delete_group(&mut self, group_id: &str) -> FlowyResult> { + let mut new_type_option = self.get_grouping_field_type_option().await.ok_or_else(|| { FlowyError::internal().with_context("Failed to get grouping field type option") })?; if let Some(option_index) = new_type_option diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs index 9d9a0468cb..de8e3f26ce 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs @@ -27,15 +27,14 @@ pub type URLGroupController = pub type URLGroupControllerContext = GroupControllerContext; +#[async_trait] impl GroupCustomize for URLGroupController { type GroupTypeOption = URLTypeOption; fn placeholder_cell(&self) -> Option { - Some( - new_cell_builder(FieldType::URL) - .insert_str_value("data", "") - .build(), - ) + let mut cell = new_cell_builder(FieldType::URL); + cell.insert("data".into(), "".into()); + Some(cell) } fn can_group( @@ -174,7 +173,7 @@ impl GroupCustomize for URLGroupController { deleted_group } - fn delete_group(&mut self, group_id: &str) -> FlowyResult> { + async fn delete_group(&mut self, group_id: &str) -> FlowyResult> { self.context.delete_group(group_id)?; Ok(None) } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs index 12692fd812..cfb5de588e 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs @@ -1,16 +1,20 @@ -use anyhow::bail; -use collab::core::any_map::AnyMapExtension; +use collab::preclude::encoding::serde::{from_any, to_any}; +use collab::preclude::Any; use collab_database::database::gen_database_group_id; use collab_database::rows::{RowDetail, RowId}; use collab_database::views::{GroupMap, GroupMapBuilder, GroupSettingBuilder, GroupSettingMap}; use serde::{Deserialize, Serialize}; +use std::sync::Arc; -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Deserialize)] pub struct GroupSetting { pub id: String, pub field_id: String, + #[serde(rename = "ty")] pub field_type: i64, + #[serde(default)] pub groups: Vec, + #[serde(default)] pub content: String, } @@ -44,38 +48,20 @@ impl TryFrom for GroupSetting { type Error = anyhow::Error; fn try_from(value: GroupSettingMap) -> Result { - match ( - value.get_str_value(GROUP_ID), - value.get_str_value(FIELD_ID), - value.get_i64_value(FIELD_TYPE), - ) { - (Some(id), Some(field_id), Some(field_type)) => { - let content = value.get_str_value(CONTENT).unwrap_or_default(); - let groups = value.try_get_array(GROUPS); - Ok(Self { - id, - field_id, - field_type, - groups, - content, - }) - }, - _ => { - bail!("Invalid group setting data") - }, - } + from_any(&Any::from(value)).map_err(|e| e.into()) } } impl From for GroupSettingMap { fn from(setting: GroupSetting) -> Self { - GroupSettingBuilder::new() - .insert_str_value(GROUP_ID, setting.id) - .insert_str_value(FIELD_ID, setting.field_id) - .insert_i64_value(FIELD_TYPE, setting.field_type) - .insert_maps(GROUPS, setting.groups) - .insert_str_value(CONTENT, setting.content) - .build() + let groups = to_any(&setting.groups).unwrap_or_else(|_| Any::Array(Arc::from([]))); + GroupSettingBuilder::from([ + (GROUP_ID.into(), setting.id.into()), + (FIELD_ID.into(), setting.field_id.into()), + (FIELD_TYPE.into(), setting.field_type.into()), + (GROUPS.into(), groups), + (CONTENT.into(), setting.content.into()), + ]) } } @@ -90,22 +76,16 @@ impl TryFrom for Group { type Error = anyhow::Error; fn try_from(value: GroupMap) -> Result { - match value.get_str_value("id") { - None => bail!("Invalid group data"), - Some(id) => { - let visible = value.get_bool_value("visible").unwrap_or_default(); - Ok(Self { id, visible }) - }, - } + from_any(&Any::from(value)).map_err(|e| e.into()) } } impl From for GroupMap { fn from(group: Group) -> Self { - GroupMapBuilder::new() - .insert_str_value("id", group.id) - .insert_bool_value("visible", group.visible) - .build() + GroupMapBuilder::from([ + ("id".into(), group.id.into()), + ("visible".into(), group.visible.into()), + ]) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs b/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs index 7cfe093725..5a71b58127 100644 --- a/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs @@ -1,50 +1,41 @@ -use collab::core::any_map::AnyMapExtension; +use collab::preclude::encoding::serde::from_any; +use collab::preclude::Any; use collab_database::views::{LayoutSetting, LayoutSettingBuilder}; use serde::{Deserialize, Serialize}; use serde_repr::*; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CalendarLayoutSetting { + #[serde(default)] pub layout_ty: CalendarLayout, + #[serde(default)] pub first_day_of_week: i32, + #[serde(default)] pub show_weekends: bool, + #[serde(default)] pub show_week_numbers: bool, + #[serde(default)] pub field_id: String, } impl From for CalendarLayoutSetting { fn from(setting: LayoutSetting) -> Self { - let layout_ty = setting - .get_i64_value("layout_ty") - .map(CalendarLayout::from) - .unwrap_or_default(); - let first_day_of_week = setting - .get_i64_value("first_day_of_week") - .unwrap_or(DEFAULT_FIRST_DAY_OF_WEEK as i64) as i32; - let show_weekends = setting.get_bool_value("show_weekends").unwrap_or_default(); - let show_week_numbers = setting - .get_bool_value("show_week_numbers") - .unwrap_or_default(); - let field_id = setting.get_str_value("field_id").unwrap_or_default(); - Self { - layout_ty, - first_day_of_week, - show_weekends, - show_week_numbers, - field_id, - } + from_any(&Any::from(setting)).unwrap() } } impl From for LayoutSetting { fn from(setting: CalendarLayoutSetting) -> Self { - LayoutSettingBuilder::new() - .insert_i64_value("layout_ty", setting.layout_ty.value()) - .insert_i64_value("first_day_of_week", setting.first_day_of_week as i64) - .insert_bool_value("show_week_numbers", setting.show_week_numbers) - .insert_bool_value("show_weekends", setting.show_weekends) - .insert_str_value("field_id", setting.field_id) - .build() + LayoutSettingBuilder::from([ + ("layout_ty".into(), setting.layout_ty.value().into()), + ( + "first_day_of_week".into(), + (setting.first_day_of_week as i64).into(), + ), + ("show_week_numbers".into(), setting.show_week_numbers.into()), + ("show_weekends".into(), setting.show_weekends.into()), + ("field_id".into(), setting.field_id.into()), + ]) } } @@ -90,9 +81,11 @@ pub const DEFAULT_FIRST_DAY_OF_WEEK: i32 = 0; pub const DEFAULT_SHOW_WEEKENDS: bool = true; pub const DEFAULT_SHOW_WEEK_NUMBERS: bool = true; -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Deserialize)] pub struct BoardLayoutSetting { + #[serde(default)] pub hide_ungrouped_column: bool, + #[serde(default)] pub collapse_hidden_groups: bool, } @@ -104,22 +97,21 @@ impl BoardLayoutSetting { impl From for BoardLayoutSetting { fn from(setting: LayoutSetting) -> Self { - Self { - hide_ungrouped_column: setting - .get_bool_value("hide_ungrouped_column") - .unwrap_or_default(), - collapse_hidden_groups: setting - .get_bool_value("collapse_hidden_groups") - .unwrap_or_default(), - } + from_any(&Any::from(setting)).unwrap() } } impl From for LayoutSetting { fn from(setting: BoardLayoutSetting) -> Self { - LayoutSettingBuilder::new() - .insert_bool_value("hide_ungrouped_column", setting.hide_ungrouped_column) - .insert_bool_value("collapse_hidden_groups", setting.collapse_hidden_groups) - .build() + LayoutSettingBuilder::from([ + ( + "hide_ungrouped_column".into(), + setting.hide_ungrouped_column.into(), + ), + ( + "collapse_hidden_groups".into(), + setting.collapse_hidden_groups.into(), + ), + ]) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs b/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs index 8cb59a1872..3a3a63249b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs +++ b/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs @@ -21,7 +21,11 @@ pub enum CSVFormat { pub struct CSVExport; impl CSVExport { - pub fn export_database(&self, database: &Database, style: CSVFormat) -> FlowyResult { + pub async fn export_database( + &self, + database: &Database, + style: CSVFormat, + ) -> FlowyResult { let mut wtr = csv::Writer::from_writer(vec![]); let inline_view_id = database.get_inline_view_id(); let fields = database.get_fields_in_view(&inline_view_id, None); @@ -43,7 +47,7 @@ impl CSVExport { fields.into_iter().for_each(|field| { field_by_field_id.insert(field.id.clone(), field); }); - let rows = database.get_rows_for_view(&inline_view_id); + let rows = database.get_rows_for_view(&inline_view_id).await; let stringify = |cell: &Cell, field: &Field, style: CSVFormat| match style { CSVFormat::Original => stringify_cell(cell, field), diff --git a/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs b/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs index 531401ea87..8cf6aa4e23 100644 --- a/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs +++ b/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs @@ -109,17 +109,18 @@ fn database_from_fields_and_rows( let field_type = FieldType::from(field.field_type); // Make the cell based on the style. - let cell = match format { - CSVFormat::Original => new_cell_builder(field_type) - .insert_str_value(CELL_DATA, cell_content.to_string()) - .build(), - CSVFormat::META => match serde_json::from_str::(cell_content) { - Ok(cell) => cell, - Err(_) => new_cell_builder(field_type) - .insert_str_value(CELL_DATA, "".to_string()) - .build(), + let mut cell = new_cell_builder(field_type); + match format { + CSVFormat::Original => { + cell.insert(CELL_DATA.into(), cell_content.as_str().into()); }, - }; + CSVFormat::META => match serde_json::from_str::(cell_content) { + Ok(cell_json) => cell = cell_json, + Err(_) => { + cell.insert(CELL_DATA.into(), "".into()); + }, + }, + } params.cells.insert(field.id.clone(), cell); } } diff --git a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs index 330f46f7f7..ebdb715e32 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use std::cmp::Ordering; use std::collections::HashMap; use std::str::FromStr; @@ -10,7 +11,6 @@ use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use flowy_error::FlowyResult; -use lib_infra::future::Fut; use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatcher}; use crate::entities::SortChangesetNotificationPB; @@ -24,13 +24,14 @@ use crate::services::sort::{ InsertRowResult, ReorderAllRowsResult, ReorderSingleRowResult, Sort, SortChangeset, SortCondition, }; +#[async_trait] pub trait SortDelegate: Send + Sync { - fn get_sort(&self, view_id: &str, sort_id: &str) -> Fut>>; + async fn get_sort(&self, view_id: &str, sort_id: &str) -> Option>; /// Returns all the rows after applying grid's filter - fn get_rows(&self, view_id: &str) -> Fut>>; - fn filter_row(&self, row_detail: &RowDetail) -> Fut; - fn get_field(&self, field_id: &str) -> Option; - fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>; + async fn get_rows(&self, view_id: &str) -> Vec>; + async fn filter_row(&self, row_detail: &RowDetail) -> bool; + async fn get_field(&self, field_id: &str) -> Option; + async fn get_fields(&self, view_id: &str, field_ids: Option>) -> Vec; } pub struct SortController { diff --git a/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs b/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs index 9f9d37d4fb..9b5608761a 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs @@ -1,7 +1,7 @@ use std::cmp::Ordering; use anyhow::bail; -use collab::core::any_map::AnyMapExtension; +use collab::util::AnyMapExt; use collab_database::rows::{RowDetail, RowId}; use collab_database::views::{SortMap, SortMapBuilder}; @@ -20,10 +20,13 @@ impl TryFrom for Sort { type Error = anyhow::Error; fn try_from(value: SortMap) -> Result { - match (value.get_str_value(SORT_ID), value.get_str_value(FIELD_ID)) { + match ( + value.get_as::(SORT_ID), + value.get_as::(FIELD_ID), + ) { (Some(id), Some(field_id)) => { let condition = value - .get_i64_value(SORT_CONDITION) + .get_as::(SORT_CONDITION) .map(SortCondition::from) .unwrap_or_default(); Ok(Self { @@ -41,11 +44,11 @@ impl TryFrom for Sort { impl From for SortMap { fn from(data: Sort) -> Self { - SortMapBuilder::new() - .insert_str_value(SORT_ID, data.id) - .insert_str_value(FIELD_ID, data.field_id) - .insert_i64_value(SORT_CONDITION, data.condition.value()) - .build() + SortMapBuilder::from([ + (SORT_ID.into(), data.id.into()), + (FIELD_ID.into(), data.field_id.into()), + (SORT_CONDITION.into(), data.condition.value().into()), + ]) } } diff --git a/frontend/rust-lib/flowy-database2/src/utils/cache.rs b/frontend/rust-lib/flowy-database2/src/utils/cache.rs index 5f9bda50c9..840bdbb1b4 100644 --- a/frontend/rust-lib/flowy-database2/src/utils/cache.rs +++ b/frontend/rust-lib/flowy-database2/src/utils/cache.rs @@ -1,23 +1,25 @@ -use parking_lot::RwLock; +use dashmap::mapref::one::{MappedRef, MappedRefMut}; +use dashmap::DashMap; use std::any::{type_name, Any}; -use std::collections::HashMap; use std::fmt::Debug; use std::hash::Hash; use std::sync::Arc; #[derive(Default, Debug)] /// The better option is use LRU cache -pub struct AnyTypeCache(HashMap); - -impl AnyTypeCache +pub struct AnyTypeCache(DashMap) where - TypeValueKey: Clone + Hash + Eq, + K: Clone + Hash + Eq; + +impl AnyTypeCache +where + K: Clone + Hash + Eq, { - pub fn new() -> Arc>> { - Arc::new(RwLock::new(AnyTypeCache(HashMap::default()))) + pub fn new() -> Arc> { + Arc::new(AnyTypeCache(DashMap::default())) } - pub fn insert(&mut self, key: &TypeValueKey, val: T) -> Option + pub fn insert(&self, key: &K, val: T) -> Option where T: 'static + Send + Sync, { @@ -27,31 +29,27 @@ where .and_then(downcast_owned) } - pub fn remove(&mut self, key: &TypeValueKey) { + pub fn remove(&self, key: &K) { self.0.remove(key); } - pub fn get(&self, key: &TypeValueKey) -> Option<&T> + pub fn get(&self, key: &K) -> Option> where T: 'static + Send + Sync, { - self - .0 - .get(key) - .and_then(|type_value| type_value.boxed.downcast_ref()) + let cell = self.0.get(key)?; + cell.try_map(|v| v.boxed.downcast_ref()).ok() } - pub fn get_mut(&mut self, key: &TypeValueKey) -> Option<&mut T> + pub fn get_mut(&self, key: &K) -> Option> where T: 'static + Send + Sync, { - self - .0 - .get_mut(key) - .and_then(|type_value| type_value.boxed.downcast_mut()) + let cell = self.0.get_mut(key)?; + cell.try_map(|v| v.boxed.downcast_mut()).ok() } - pub fn contains(&self, key: &TypeValueKey) -> bool { + pub fn contains(&self, key: &K) -> bool { self.0.contains_key(key) } @@ -65,7 +63,7 @@ fn downcast_owned(type_value: TypeValue) -> Option } #[derive(Debug)] -struct TypeValue { +pub struct TypeValue { boxed: Box, #[allow(dead_code)] ty: &'static str, diff --git a/frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs b/frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs index 648de5edc7..7ce0c31a4b 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs @@ -18,7 +18,7 @@ async fn created_at_field_test() { // Get created time of the new row. let row_detail = test.get_rows().await.last().cloned().unwrap(); - let updated_at_field = test.get_first_field(FieldType::CreatedTime); + let updated_at_field = test.get_first_field(FieldType::CreatedTime).await; let cell = test .editor .get_cell(&updated_at_field.id, &row_detail.row.id) @@ -35,7 +35,7 @@ async fn created_at_field_test() { async fn update_at_field_test() { let mut test = DatabaseRowTest::new().await; let row_detail = test.get_rows().await.remove(0); - let last_edit_field = test.get_first_field(FieldType::LastEditedTime); + let last_edit_field = test.get_first_field(FieldType::LastEditedTime).await; let cell = test .editor .get_cell(&last_edit_field.id, &row_detail.row.id) @@ -53,7 +53,7 @@ async fn update_at_field_test() { // Get the updated time of the row. let row_detail = test.get_rows().await.remove(0); - let last_edit_field = test.get_first_field(FieldType::LastEditedTime); + let last_edit_field = test.get_first_field(FieldType::LastEditedTime).await; let cell = test .editor .get_cell(&last_edit_field.id, &row_detail.row.id) diff --git a/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs index 1c1f633e47..8f1e52c7c1 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs @@ -14,7 +14,7 @@ use crate::database::cell_test::script::DatabaseCellTest; #[tokio::test] async fn grid_cell_update() { let mut test = DatabaseCellTest::new().await; - let fields = test.get_fields(); + let fields = test.get_fields().await; let rows = &test.row_details; let mut scripts = vec![]; @@ -76,7 +76,7 @@ async fn grid_cell_update() { #[tokio::test] async fn text_cell_data_test() { let test = DatabaseCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText); + let text_field = test.get_first_field(FieldType::RichText).await; let cells = test .editor @@ -100,7 +100,7 @@ async fn text_cell_data_test() { #[tokio::test] async fn url_cell_data_test() { let test = DatabaseCellTest::new().await; - let url_field = test.get_first_field(FieldType::URL); + let url_field = test.get_first_field(FieldType::URL).await; let cells = test .editor .get_cells_for_field(&test.view_id, &url_field.id) @@ -122,7 +122,7 @@ async fn url_cell_data_test() { #[tokio::test] async fn update_updated_at_field_on_other_cell_update() { let mut test = DatabaseCellTest::new().await; - let updated_at_field = test.get_first_field(FieldType::LastEditedTime); + let updated_at_field = test.get_first_field(FieldType::LastEditedTime).await; let text_field = test .fields @@ -204,7 +204,7 @@ async fn update_updated_at_field_on_other_cell_update() { #[tokio::test] async fn time_cell_data_test() { let test = DatabaseCellTest::new().await; - let time_field = test.get_first_field(FieldType::Time); + let time_field = test.get_first_field(FieldType::Time).await; let cells = test .editor .get_cells_for_field(&test.view_id, &time_field.id) diff --git a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs index 2d087cce00..c18fef66a2 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs @@ -76,11 +76,12 @@ impl DatabaseEditorTest { pub async fn new(sdk: EventIntegrationTest, test: ViewTest) -> Self { let editor = sdk .database_manager - .get_database_with_view_id(&test.child_view.id) + .get_database_editor_with_view_id(&test.child_view.id) .await .unwrap(); let fields = editor .get_fields(&test.child_view.id, None) + .await .into_iter() .map(Arc::new) .collect(); @@ -111,10 +112,11 @@ impl DatabaseEditorTest { self.editor.get_rows(&self.view_id).await.unwrap() } - pub fn get_field(&self, field_id: &str, field_type: FieldType) -> Field { + pub async fn get_field(&self, field_id: &str, field_type: FieldType) -> Field { self .editor .get_fields(&self.view_id, None) + .await .into_iter() .filter(|field| { let t_field_type = FieldType::from(field.field_type); @@ -127,10 +129,11 @@ impl DatabaseEditorTest { /// returns the first `Field` in the build-in test grid. /// Not support duplicate `FieldType` in test grid yet. - pub fn get_first_field(&self, field_type: FieldType) -> Field { + pub async fn get_first_field(&self, field_type: FieldType) -> Field { self .editor .get_fields(&self.view_id, None) + .await .into_iter() .filter(|field| { let t_field_type = FieldType::from(field.field_type); @@ -141,22 +144,22 @@ impl DatabaseEditorTest { .unwrap() } - pub fn get_fields(&self) -> Vec { - self.editor.get_fields(&self.view_id, None) + pub async fn get_fields(&self) -> Vec { + self.editor.get_fields(&self.view_id, None).await } - pub fn get_multi_select_type_option(&self, field_id: &str) -> Vec { + pub async fn get_multi_select_type_option(&self, field_id: &str) -> Vec { let field_type = FieldType::MultiSelect; - let field = self.get_field(field_id, field_type); + let field = self.get_field(field_id, field_type).await; let type_option = field .get_type_option::(field_type) .unwrap(); type_option.options } - pub fn get_single_select_type_option(&self, field_id: &str) -> Vec { + pub async fn get_single_select_type_option(&self, field_id: &str) -> Vec { let field_type = FieldType::SingleSelect; - let field = self.get_field(field_id, field_type); + let field = self.get_field(field_id, field_type).await; let type_option = field .get_type_option::(field_type) .unwrap(); @@ -164,18 +167,18 @@ impl DatabaseEditorTest { } #[allow(dead_code)] - pub fn get_checklist_type_option(&self, field_id: &str) -> ChecklistTypeOption { + pub async fn get_checklist_type_option(&self, field_id: &str) -> ChecklistTypeOption { let field_type = FieldType::Checklist; - let field = self.get_field(field_id, field_type); + let field = self.get_field(field_id, field_type).await; field .get_type_option::(field_type) .unwrap() } #[allow(dead_code)] - pub fn get_checkbox_type_option(&self, field_id: &str) -> CheckboxTypeOption { + pub async fn get_checkbox_type_option(&self, field_id: &str) -> CheckboxTypeOption { let field_type = FieldType::Checkbox; - let field = self.get_field(field_id, field_type); + let field = self.get_field(field_id, field_type).await; field .get_type_option::(field_type) .unwrap() @@ -190,6 +193,7 @@ impl DatabaseEditorTest { let field = self .editor .get_fields(&self.view_id, None) + .await .into_iter() .find(|field| field.id == field_id) .unwrap(); @@ -204,6 +208,7 @@ impl DatabaseEditorTest { let field = self .editor .get_fields(&self.view_id, None) + .await .iter() .find(|field| { let field_type = FieldType::from(field.field_type); @@ -225,6 +230,7 @@ impl DatabaseEditorTest { let field = self .editor .get_fields(&self.view_id, None) + .await .iter() .find(|field| { let field_type = FieldType::from(field.field_type); @@ -250,6 +256,7 @@ impl DatabaseEditorTest { let field = self .editor .get_fields(&self.view_id, None) + .await .iter() .find(|field| { let field_type = FieldType::from(field.field_type); @@ -277,7 +284,7 @@ impl DatabaseEditorTest { self .sdk .database_manager - .get_database(database_id) + .get_database_editor(database_id) .await .ok() } diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/test.rs index b550567699..a378f4d90e 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/test.rs @@ -20,11 +20,12 @@ async fn get_default_board_field_settings() { let mut test = FieldSettingsTest::new_board().await; let non_primary_field_ids: Vec = test .get_fields() + .await .into_iter() .filter(|field| !field.is_primary) .map(|field| field.id) .collect(); - let primary_field_id = test.get_first_field(FieldType::RichText).id; + let primary_field_id = test.get_first_field(FieldType::RichText).await.id; test .assert_field_settings( non_primary_field_ids.clone(), @@ -47,11 +48,12 @@ async fn get_default_calendar_field_settings() { let mut test = FieldSettingsTest::new_calendar().await; let non_primary_field_ids: Vec = test .get_fields() + .await .into_iter() .filter(|field| !field.is_primary) .map(|field| field.id) .collect(); - let primary_field_id = test.get_first_field(FieldType::RichText).id; + let primary_field_id = test.get_first_field(FieldType::RichText).await.id; test .assert_field_settings( non_primary_field_ids.clone(), @@ -74,11 +76,12 @@ async fn update_field_settings_test() { let mut test = FieldSettingsTest::new_board().await; let non_primary_field_ids: Vec = test .get_fields() + .await .into_iter() .filter(|field| !field.is_primary) .map(|field| field.id) .collect(); - let primary_field_id = test.get_first_field(FieldType::RichText).id; + let primary_field_id = test.get_first_field(FieldType::RichText).await.id; test .assert_field_settings( diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs index 554b5a7b21..bf93c130f8 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs @@ -64,19 +64,19 @@ impl DatabaseFieldTest { FieldScript::CreateField { params } => { self.field_count += 1; let _ = self.editor.create_field_with_type_option(params).await; - let fields = self.editor.get_fields(&self.view_id, None); + let fields = self.editor.get_fields(&self.view_id, None).await; assert_eq!(self.field_count, fields.len()); }, FieldScript::UpdateField { changeset: change } => { self.editor.update_field(change).await.unwrap(); }, FieldScript::DeleteField { field } => { - if self.editor.get_field(&field.id).is_some() { + if self.editor.get_field(&field.id).await.is_some() { self.field_count -= 1; } self.editor.delete_field(&field.id).await.unwrap(); - let fields = self.editor.get_fields(&self.view_id, None); + let fields = self.editor.get_fields(&self.view_id, None).await; assert_eq!(self.field_count, fields.len()); }, FieldScript::SwitchToField { @@ -95,7 +95,7 @@ impl DatabaseFieldTest { type_option, } => { // - let old_field = self.editor.get_field(&field_id).unwrap(); + let old_field = self.editor.get_field(&field_id).await.unwrap(); self .editor .update_field_type_option(&field_id, type_option, old_field) @@ -103,13 +103,13 @@ impl DatabaseFieldTest { .unwrap(); }, FieldScript::AssertFieldCount(count) => { - assert_eq!(self.get_fields().len(), count); + assert_eq!(self.get_fields().await.len(), count); }, FieldScript::AssertFieldTypeOptionEqual { field_index, expected_type_option_data, } => { - let fields = self.get_fields(); + let fields = self.get_fields().await; let field = &fields[field_index]; let type_option_data = field.get_any_type_option(field.field_type).unwrap(); assert_eq!(type_option_data, expected_type_option_data); @@ -119,7 +119,7 @@ impl DatabaseFieldTest { row_index, expected_content, } => { - let field = self.editor.get_field(&field_id).unwrap(); + let field = self.editor.get_field(&field_id).await.unwrap(); let rows = self.editor.get_rows(&self.view_id()).await.unwrap(); let row_detail = rows.get(row_index).unwrap(); diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs index 7cd9f9f3d1..9e949b4965 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs @@ -85,7 +85,7 @@ async fn grid_update_field_with_empty_change() { let scripts = vec![CreateField { params }]; test.run_scripts(scripts).await; - let field = test.get_fields().pop().unwrap().clone(); + let field = test.get_fields().await.pop().unwrap().clone(); let changeset = FieldChangesetParams { field_id: field.id.clone(), view_id: test.view_id(), @@ -110,7 +110,7 @@ async fn grid_delete_field() { let scripts = vec![CreateField { params }]; test.run_scripts(scripts).await; - let field = test.get_fields().pop().unwrap(); + let field = test.get_fields().await.pop().unwrap(); let scripts = vec![ DeleteField { field }, AssertFieldCount(original_field_count), @@ -121,10 +121,10 @@ async fn grid_delete_field() { #[tokio::test] async fn grid_switch_from_select_option_to_checkbox_test() { let mut test = DatabaseFieldTest::new().await; - let field = test.get_first_field(FieldType::SingleSelect); + let field = test.get_first_field(FieldType::SingleSelect).await; // Update the type option data of single select option - let mut options = test.get_single_select_type_option(&field.id); + let mut options = test.get_single_select_type_option(&field.id).await; options.clear(); // Add a new option with name CHECK options.push(SelectOption { @@ -159,7 +159,7 @@ async fn grid_switch_from_select_option_to_checkbox_test() { #[tokio::test] async fn grid_switch_from_checkbox_to_select_option_test() { let mut test = DatabaseFieldTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox).clone(); + let checkbox_field = test.get_first_field(FieldType::Checkbox).await.clone(); let scripts = vec![ // switch to single-select field type SwitchToField { @@ -181,7 +181,7 @@ async fn grid_switch_from_checkbox_to_select_option_test() { ]; test.run_scripts(scripts).await; - let options = test.get_single_select_type_option(&checkbox_field.id); + let options = test.get_single_select_type_option(&checkbox_field.id).await; assert_eq!(options.len(), 2); assert!(options.iter().any(|option| option.name == UNCHECK)); assert!(options.iter().any(|option| option.name == CHECK)); @@ -194,9 +194,9 @@ async fn grid_switch_from_checkbox_to_select_option_test() { #[tokio::test] async fn grid_switch_from_multi_select_to_text_test() { let mut test = DatabaseFieldTest::new().await; - let field_rev = test.get_first_field(FieldType::MultiSelect).clone(); + let field_rev = test.get_first_field(FieldType::MultiSelect).await.clone(); - let multi_select_type_option = test.get_multi_select_type_option(&field_rev.id); + let multi_select_type_option = test.get_multi_select_type_option(&field_rev.id).await; let script_switch_field = vec![SwitchToField { field_id: field_rev.id.clone(), @@ -225,7 +225,7 @@ async fn grid_switch_from_multi_select_to_text_test() { #[tokio::test] async fn grid_switch_from_checkbox_to_text_test() { let mut test = DatabaseFieldTest::new().await; - let field_rev = test.get_first_field(FieldType::Checkbox); + let field_rev = test.get_first_field(FieldType::Checkbox).await; let scripts = vec![ SwitchToField { @@ -252,7 +252,7 @@ async fn grid_switch_from_checkbox_to_text_test() { #[tokio::test] async fn grid_switch_from_date_to_text_test() { let mut test = DatabaseFieldTest::new().await; - let field = test.get_first_field(FieldType::DateTime).clone(); + let field = test.get_first_field(FieldType::DateTime).await.clone(); let scripts = vec![ SwitchToField { field_id: field.id.clone(), @@ -278,7 +278,7 @@ async fn grid_switch_from_date_to_text_test() { #[tokio::test] async fn grid_switch_from_number_to_text_test() { let mut test = DatabaseFieldTest::new().await; - let field = test.get_first_field(FieldType::Number).clone(); + let field = test.get_first_field(FieldType::Number).await.clone(); let scripts = vec![ SwitchToField { @@ -304,7 +304,7 @@ async fn grid_switch_from_number_to_text_test() { #[tokio::test] async fn grid_switch_from_checklist_to_text_test() { let mut test = DatabaseFieldTest::new().await; - let field_rev = test.get_first_field(FieldType::Checklist); + let field_rev = test.get_first_field(FieldType::Checklist).await; let scripts = vec![ SwitchToField { diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs index 3da9cab5a2..3cc8452462 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs @@ -61,7 +61,7 @@ async fn grid_filter_checklist_is_complete_test() { } async fn get_checklist_cell_options(test: &DatabaseFilterTest) -> Vec { - let field = test.get_first_field(FieldType::Checklist); + let field = test.get_first_field(FieldType::Checklist).await; let row_cell = test .editor .get_cell(&field.id, &test.row_details[0].row.id) diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs index f2b58070e7..2f429752d6 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs @@ -194,7 +194,7 @@ impl DatabaseFilterTest { } => { self.subscribe_view_changed().await; self.assert_future_changed(changed).await; - let field = self.get_first_field(field_type); + let field = self.get_first_field(field_type).await; let params = FilterChangeset::Insert { parent_filter_id, data: FilterInner::Data { diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs index eb808d0bc3..6cda4669f7 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs @@ -43,8 +43,8 @@ async fn grid_filter_multi_select_is_not_empty_test() { #[tokio::test] async fn grid_filter_multi_select_is_test() { let mut test = DatabaseFilterTest::new().await; - let field = test.get_first_field(FieldType::MultiSelect); - let mut options = test.get_multi_select_type_option(&field.id); + let field = test.get_first_field(FieldType::MultiSelect).await; + let mut options = test.get_multi_select_type_option(&field.id).await; let scripts = vec![ CreateDataFilter { parent_filter_id: None, @@ -63,8 +63,8 @@ async fn grid_filter_multi_select_is_test() { #[tokio::test] async fn grid_filter_multi_select_is_test2() { let mut test = DatabaseFilterTest::new().await; - let field = test.get_first_field(FieldType::MultiSelect); - let mut options = test.get_multi_select_type_option(&field.id); + let field = test.get_first_field(FieldType::MultiSelect).await; + let mut options = test.get_multi_select_type_option(&field.id).await; let scripts = vec![ CreateDataFilter { parent_filter_id: None, @@ -106,8 +106,8 @@ async fn grid_filter_single_select_is_empty_test() { #[tokio::test] async fn grid_filter_single_select_is_test() { let mut test = DatabaseFilterTest::new().await; - let field = test.get_first_field(FieldType::SingleSelect); - let mut options = test.get_single_select_type_option(&field.id); + let field = test.get_first_field(FieldType::SingleSelect).await; + let mut options = test.get_single_select_type_option(&field.id).await; let expected = 2; let row_count = test.row_details.len(); let scripts = vec![ @@ -131,9 +131,9 @@ async fn grid_filter_single_select_is_test() { #[tokio::test] async fn grid_filter_single_select_is_test2() { let mut test = DatabaseFilterTest::new().await; - let field = test.get_first_field(FieldType::SingleSelect); + let field = test.get_first_field(FieldType::SingleSelect).await; let row_details = test.get_rows().await; - let mut options = test.get_single_select_type_option(&field.id); + let mut options = test.get_single_select_type_option(&field.id).await; let option = options.remove(0); let row_count = test.row_details.len(); @@ -173,8 +173,8 @@ async fn grid_filter_single_select_is_test2() { #[tokio::test] async fn grid_filter_multi_select_contains_test() { let mut test = DatabaseFilterTest::new().await; - let field = test.get_first_field(FieldType::MultiSelect); - let mut options = test.get_multi_select_type_option(&field.id); + let field = test.get_first_field(FieldType::MultiSelect).await; + let mut options = test.get_multi_select_type_option(&field.id).await; let scripts = vec![ CreateDataFilter { parent_filter_id: None, @@ -193,8 +193,8 @@ async fn grid_filter_multi_select_contains_test() { #[tokio::test] async fn grid_filter_multi_select_contains_test2() { let mut test = DatabaseFilterTest::new().await; - let field = test.get_first_field(FieldType::MultiSelect); - let mut options = test.get_multi_select_type_option(&field.id); + let field = test.get_first_field(FieldType::MultiSelect).await; + let mut options = test.get_multi_select_type_option(&field.id).await; let scripts = vec![ CreateDataFilter { parent_filter_id: None, diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs index 1fe883e041..d5312b2985 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs @@ -159,7 +159,7 @@ impl DatabaseGroupTest { let from_group = self.group_at_index(from_group_index).await; let to_group = self.group_at_index(to_group_index).await; let field_id = from_group.field_id; - let field = self.editor.get_field(&field_id).unwrap(); + let field = self.editor.get_field(&field_id).await.unwrap(); let field_type = FieldType::from(field.field_type); let cell = if to_group.is_default { @@ -203,7 +203,7 @@ impl DatabaseGroupTest { } => { let from_group = self.group_at_index(from_group_index).await; let field_id = from_group.field_id; - let field = self.editor.get_field(&field_id).unwrap(); + let field = self.editor.get_field(&field_id).await.unwrap(); let field_type = FieldType::from(field.field_type); let cell = match field_type { FieldType::URL => insert_url_cell(cell_data, &field), @@ -309,6 +309,7 @@ impl DatabaseGroupTest { self .inner .get_fields() + .await .into_iter() .find(|field| { let ft = FieldType::from(field.field_type); diff --git a/frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs index 6800a7e4db..57f17b9870 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs @@ -36,7 +36,10 @@ impl DatabaseLayoutTest { } pub async fn get_first_date_field(&self) -> Field { - self.database_test.get_first_field(FieldType::DateTime) + self + .database_test + .get_first_field(FieldType::DateTime) + .await } async fn get_layout_setting( diff --git a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_according_to_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_according_to_filter_test.rs index b47bf2e99b..a15814f13d 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_according_to_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_according_to_filter_test.rs @@ -17,7 +17,7 @@ use crate::database::pre_fill_cell_test::script::{ async fn according_to_text_contains_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText); + let text_field = test.get_first_field(FieldType::RichText).await; let scripts = vec![ InsertFilter { @@ -60,7 +60,7 @@ async fn according_to_text_contains_filter_test() { async fn according_to_empty_text_contains_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText); + let text_field = test.get_first_field(FieldType::RichText).await; let scripts = vec![ InsertFilter { @@ -95,7 +95,7 @@ async fn according_to_empty_text_contains_filter_test() { async fn according_to_text_is_not_empty_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText); + let text_field = test.get_first_field(FieldType::RichText).await; let scripts = vec![ AssertRowCount(7), @@ -125,7 +125,7 @@ async fn according_to_text_is_not_empty_filter_test() { async fn according_to_checkbox_is_unchecked_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox); + let checkbox_field = test.get_first_field(FieldType::Checkbox).await; let scripts = vec![ AssertRowCount(7), @@ -162,7 +162,7 @@ async fn according_to_checkbox_is_unchecked_filter_test() { async fn according_to_checkbox_is_checked_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox); + let checkbox_field = test.get_first_field(FieldType::Checkbox).await; let scripts = vec![ AssertRowCount(7), @@ -207,7 +207,7 @@ async fn according_to_checkbox_is_checked_filter_test() { async fn according_to_date_time_is_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let datetime_field = test.get_first_field(FieldType::DateTime); + let datetime_field = test.get_first_field(FieldType::DateTime).await; let scripts = vec![ AssertRowCount(7), @@ -254,7 +254,7 @@ async fn according_to_date_time_is_filter_test() { async fn according_to_invalid_date_time_is_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let datetime_field = test.get_first_field(FieldType::DateTime); + let datetime_field = test.get_first_field(FieldType::DateTime).await; let scripts = vec![ AssertRowCount(7), @@ -290,8 +290,10 @@ async fn according_to_invalid_date_time_is_filter_test() { async fn according_to_select_option_is_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let multi_select_field = test.get_first_field(FieldType::MultiSelect); - let options = test.get_multi_select_type_option(&multi_select_field.id); + let multi_select_field = test.get_first_field(FieldType::MultiSelect).await; + let options = test + .get_multi_select_type_option(&multi_select_field.id) + .await; let filtering_options = [options[1].clone(), options[2].clone()]; let ids = filtering_options @@ -343,8 +345,10 @@ async fn according_to_select_option_is_filter_test() { async fn according_to_select_option_contains_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let multi_select_field = test.get_first_field(FieldType::MultiSelect); - let options = test.get_multi_select_type_option(&multi_select_field.id); + let multi_select_field = test.get_first_field(FieldType::MultiSelect).await; + let options = test + .get_multi_select_type_option(&multi_select_field.id) + .await; let filtering_options = [options[1].clone(), options[2].clone()]; let ids = filtering_options @@ -392,8 +396,10 @@ async fn according_to_select_option_contains_filter_test() { async fn according_to_select_option_is_not_empty_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let multi_select_field = test.get_first_field(FieldType::MultiSelect); - let options = test.get_multi_select_type_option(&multi_select_field.id); + let multi_select_field = test.get_first_field(FieldType::MultiSelect).await; + let options = test + .get_multi_select_type_option(&multi_select_field.id) + .await; let stringified_expected = options.first().unwrap().name.clone(); diff --git a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_with_payload_test.rs b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_with_payload_test.rs index a67bad48f3..1cb004f5a3 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_with_payload_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_with_payload_test.rs @@ -16,7 +16,7 @@ use crate::database::pre_fill_cell_test::script::{ async fn row_data_payload_with_empty_hashmap_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText); + let text_field = test.get_first_field(FieldType::RichText).await; let scripts = vec![ CreateRowWithPayload { @@ -47,7 +47,7 @@ async fn row_data_payload_with_empty_hashmap_test() { async fn row_data_payload_with_unknown_field_id_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText); + let text_field = test.get_first_field(FieldType::RichText).await; let malformed_field_id = "this_field_id_will_never_exist"; let scripts = vec![ @@ -87,7 +87,7 @@ async fn row_data_payload_with_unknown_field_id_test() { async fn row_data_payload_with_empty_string_text_data_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText); + let text_field = test.get_first_field(FieldType::RichText).await; let cell_data = ""; let scripts = vec![ @@ -119,7 +119,7 @@ async fn row_data_payload_with_empty_string_text_data_test() { async fn row_data_payload_with_text_data_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText); + let text_field = test.get_first_field(FieldType::RichText).await; let cell_data = "sample cell data"; let scripts = vec![ @@ -151,9 +151,9 @@ async fn row_data_payload_with_text_data_test() { async fn row_data_payload_with_multi_text_data_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText); - let number_field = test.get_first_field(FieldType::Number); - let url_field = test.get_first_field(FieldType::URL); + let text_field = test.get_first_field(FieldType::RichText).await; + let number_field = test.get_first_field(FieldType::Number).await; + let url_field = test.get_first_field(FieldType::URL).await; let text_cell_data = "sample cell data"; let number_cell_data = "1234"; @@ -214,7 +214,7 @@ async fn row_data_payload_with_multi_text_data_test() { async fn row_data_payload_with_date_time_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let date_field = test.get_first_field(FieldType::DateTime); + let date_field = test.get_first_field(FieldType::DateTime).await; let cell_data = "1710510086"; let scripts = vec![ @@ -246,7 +246,7 @@ async fn row_data_payload_with_date_time_test() { async fn row_data_payload_with_invalid_date_time_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let date_field = test.get_first_field(FieldType::DateTime); + let date_field = test.get_first_field(FieldType::DateTime).await; let cell_data = DateCellData { timestamp: Some(1710510086), ..Default::default() @@ -276,7 +276,7 @@ async fn row_data_payload_with_invalid_date_time_test() { async fn row_data_payload_with_checkbox_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox); + let checkbox_field = test.get_first_field(FieldType::Checkbox).await; let cell_data = "Yes"; let scripts = vec![ @@ -308,8 +308,10 @@ async fn row_data_payload_with_checkbox_test() { async fn row_data_payload_with_select_option_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let multi_select_field = test.get_first_field(FieldType::MultiSelect); - let options = test.get_multi_select_type_option(&multi_select_field.id); + let multi_select_field = test.get_first_field(FieldType::MultiSelect).await; + let options = test + .get_multi_select_type_option(&multi_select_field.id) + .await; let ids = options .iter() @@ -352,8 +354,10 @@ async fn row_data_payload_with_select_option_test() { async fn row_data_payload_with_invalid_select_option_id_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let multi_select_field = test.get_first_field(FieldType::MultiSelect); - let mut options = test.get_multi_select_type_option(&multi_select_field.id); + let multi_select_field = test.get_first_field(FieldType::MultiSelect).await; + let mut options = test + .get_multi_select_type_option(&multi_select_field.id) + .await; let first_id = options.swap_remove(0).id; let ids = [first_id.clone(), "nonsense".to_string()].join(SELECTION_IDS_SEPARATOR); @@ -386,8 +390,10 @@ async fn row_data_payload_with_invalid_select_option_id_test() { async fn row_data_payload_with_too_many_select_option_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let single_select_field = test.get_first_field(FieldType::SingleSelect); - let mut options = test.get_single_select_type_option(&single_select_field.id); + let single_select_field = test.get_first_field(FieldType::SingleSelect).await; + let mut options = test + .get_single_select_type_option(&single_select_field.id) + .await; let ids = options .iter() diff --git a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs index e41e42207e..6b524fdf15 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs @@ -106,7 +106,7 @@ impl DatabasePreFillRowCellTest { row_index, expected_content, } => { - let field = self.editor.get_field(&field_id).unwrap(); + let field = self.editor.get_field(&field_id).await.unwrap(); let rows = self.editor.get_rows(&self.view_id).await.unwrap(); let row_detail = rows.get(row_index).unwrap(); diff --git a/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs b/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs index 3fbb0aafe2..02f4f135ca 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs @@ -32,7 +32,7 @@ async fn export_and_then_import_meta_csv_test() { let result = test.import(csv_1.clone(), format).await; let database = test.get_database(&result.database_id).await.unwrap(); - let fields = database.get_fields(&result.view_id, None); + let fields = database.get_fields(&result.view_id, None).await; let rows = database.get_rows(&result.view_id).await.unwrap(); assert_eq!(fields[0].field_type, 0); assert_eq!(fields[1].field_type, 1); @@ -111,7 +111,7 @@ async fn history_database_import_test() { let result = test.import(csv.to_string(), format).await; let database = test.get_database(&result.database_id).await.unwrap(); - let fields = database.get_fields(&result.view_id, None); + let fields = database.get_fields(&result.view_id, None).await; let rows = database.get_rows(&result.view_id).await.unwrap(); assert_eq!(fields[0].field_type, 0); assert_eq!(fields[1].field_type, 1); diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/multi_sort_test.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/multi_sort_test.rs index 7fe1874984..d7fe529d13 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/sort_test/multi_sort_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/multi_sort_test.rs @@ -7,8 +7,8 @@ use crate::database::sort_test::script::SortScript::*; #[tokio::test] async fn sort_checkbox_and_then_text_by_descending_test() { let mut test = DatabaseSortTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox); - let text_field = test.get_first_field(FieldType::RichText); + let checkbox_field = test.get_first_field(FieldType::Checkbox).await; + let text_field = test.get_first_field(FieldType::RichText).await; let scripts = vec![ AssertCellContentOrder { field_id: checkbox_field.id.clone(), @@ -51,8 +51,8 @@ async fn sort_checkbox_and_then_text_by_descending_test() { #[tokio::test] async fn reorder_sort_test() { let mut test = DatabaseSortTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox); - let text_field = test.get_first_field(FieldType::RichText); + let checkbox_field = test.get_first_field(FieldType::Checkbox).await; + let text_field = test.get_first_field(FieldType::RichText).await; // Use the same sort set up as above let scripts = vec![ AssertCellContentOrder { diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs index a6b99dc99c..e95deaa187 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs @@ -118,7 +118,7 @@ impl DatabaseSortTest { SortScript::AssertCellContentOrder { field_id, orders } => { let mut cells = vec![]; let rows = self.editor.get_rows(&self.view_id).await.unwrap(); - let field = self.editor.get_field(&field_id).unwrap(); + let field = self.editor.get_field(&field_id).await.unwrap(); for row_detail in rows { if let Some(cell) = row_detail.row.cells.get(&field_id) { let content = stringify_cell(cell, &field); diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs index 63f3b08422..77ce14458f 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs @@ -6,7 +6,7 @@ use crate::database::sort_test::script::{DatabaseSortTest, SortScript::*}; #[tokio::test] async fn sort_text_by_ascending_test() { let mut test = DatabaseSortTest::new().await; - let text_field = test.get_first_field(FieldType::RichText); + let text_field = test.get_first_field(FieldType::RichText).await; let scripts = vec![ AssertCellContentOrder { field_id: text_field.id.clone(), @@ -27,7 +27,7 @@ async fn sort_text_by_ascending_test() { #[tokio::test] async fn sort_text_by_descending_test() { let mut test = DatabaseSortTest::new().await; - let text_field = test.get_first_field(FieldType::RichText); + let text_field = test.get_first_field(FieldType::RichText).await; let scripts = vec![ AssertCellContentOrder { field_id: text_field.id.clone(), @@ -48,7 +48,7 @@ async fn sort_text_by_descending_test() { #[tokio::test] async fn sort_change_notification_by_update_text_test() { let mut test = DatabaseSortTest::new().await; - let text_field = test.get_first_field(FieldType::RichText).clone(); + let text_field = test.get_first_field(FieldType::RichText).await.clone(); let scripts = vec![ AssertCellContentOrder { field_id: text_field.id.clone(), @@ -84,7 +84,7 @@ async fn sort_change_notification_by_update_text_test() { #[tokio::test] async fn sort_after_new_row_test() { let mut test = DatabaseSortTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox); + let checkbox_field = test.get_first_field(FieldType::Checkbox).await; let scripts = vec![ AssertCellContentOrder { field_id: checkbox_field.id.clone(), @@ -110,7 +110,7 @@ async fn sort_after_new_row_test() { #[tokio::test] async fn sort_text_by_ascending_and_delete_sort_test() { let mut test = DatabaseSortTest::new().await; - let text_field = test.get_first_field(FieldType::RichText); + let text_field = test.get_first_field(FieldType::RichText).await; let scripts = vec![ InsertSort { field: text_field.clone(), @@ -137,7 +137,7 @@ async fn sort_text_by_ascending_and_delete_sort_test() { #[tokio::test] async fn sort_checkbox_by_ascending_test() { let mut test = DatabaseSortTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox); + let checkbox_field = test.get_first_field(FieldType::Checkbox).await; let scripts = vec![ AssertCellContentOrder { field_id: checkbox_field.id.clone(), @@ -158,7 +158,7 @@ async fn sort_checkbox_by_ascending_test() { #[tokio::test] async fn sort_checkbox_by_descending_test() { let mut test = DatabaseSortTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox); + let checkbox_field = test.get_first_field(FieldType::Checkbox).await; let scripts = vec![ AssertCellContentOrder { field_id: checkbox_field.id.clone(), @@ -179,7 +179,7 @@ async fn sort_checkbox_by_descending_test() { #[tokio::test] async fn sort_date_by_ascending_test() { let mut test = DatabaseSortTest::new().await; - let date_field = test.get_first_field(FieldType::DateTime); + let date_field = test.get_first_field(FieldType::DateTime).await; let scripts = vec![ AssertCellContentOrder { field_id: date_field.id.clone(), @@ -216,7 +216,7 @@ async fn sort_date_by_ascending_test() { #[tokio::test] async fn sort_date_by_descending_test() { let mut test = DatabaseSortTest::new().await; - let date_field = test.get_first_field(FieldType::DateTime); + let date_field = test.get_first_field(FieldType::DateTime).await; let scripts = vec![ AssertCellContentOrder { field_id: date_field.id.clone(), @@ -253,7 +253,7 @@ async fn sort_date_by_descending_test() { #[tokio::test] async fn sort_number_by_ascending_test() { let mut test = DatabaseSortTest::new().await; - let number_field = test.get_first_field(FieldType::Number); + let number_field = test.get_first_field(FieldType::Number).await; let scripts = vec![ AssertCellContentOrder { field_id: number_field.id.clone(), @@ -274,7 +274,7 @@ async fn sort_number_by_ascending_test() { #[tokio::test] async fn sort_number_by_descending_test() { let mut test = DatabaseSortTest::new().await; - let number_field = test.get_first_field(FieldType::Number); + let number_field = test.get_first_field(FieldType::Number).await; let scripts = vec![ AssertCellContentOrder { field_id: number_field.id.clone(), @@ -295,7 +295,7 @@ async fn sort_number_by_descending_test() { #[tokio::test] async fn sort_single_select_by_ascending_test() { let mut test = DatabaseSortTest::new().await; - let single_select = test.get_first_field(FieldType::SingleSelect); + let single_select = test.get_first_field(FieldType::SingleSelect).await; let scripts = vec![ AssertCellContentOrder { field_id: single_select.id.clone(), @@ -316,7 +316,7 @@ async fn sort_single_select_by_ascending_test() { #[tokio::test] async fn sort_single_select_by_descending_test() { let mut test = DatabaseSortTest::new().await; - let single_select = test.get_first_field(FieldType::SingleSelect); + let single_select = test.get_first_field(FieldType::SingleSelect).await; let scripts = vec![ AssertCellContentOrder { field_id: single_select.id.clone(), @@ -337,7 +337,7 @@ async fn sort_single_select_by_descending_test() { #[tokio::test] async fn sort_multi_select_by_ascending_test() { let mut test = DatabaseSortTest::new().await; - let multi_select = test.get_first_field(FieldType::MultiSelect); + let multi_select = test.get_first_field(FieldType::MultiSelect).await; let scripts = vec![ AssertCellContentOrder { field_id: multi_select.id.clone(), @@ -374,7 +374,7 @@ async fn sort_multi_select_by_ascending_test() { #[tokio::test] async fn sort_multi_select_by_descending_test() { let mut test = DatabaseSortTest::new().await; - let multi_select = test.get_first_field(FieldType::MultiSelect); + let multi_select = test.get_first_field(FieldType::MultiSelect).await; let scripts = vec![ AssertCellContentOrder { field_id: multi_select.id.clone(), @@ -411,7 +411,7 @@ async fn sort_multi_select_by_descending_test() { #[tokio::test] async fn sort_checklist_by_ascending_test() { let mut test = DatabaseSortTest::new().await; - let checklist_field = test.get_first_field(FieldType::Checklist); + let checklist_field = test.get_first_field(FieldType::Checklist).await; let scripts = vec![ AssertCellContentOrder { field_id: checklist_field.id.clone(), @@ -448,7 +448,7 @@ async fn sort_checklist_by_ascending_test() { #[tokio::test] async fn sort_checklist_by_descending_test() { let mut test = DatabaseSortTest::new().await; - let checklist_field = test.get_first_field(FieldType::Checklist); + let checklist_field = test.get_first_field(FieldType::Checklist).await; let scripts = vec![ AssertCellContentOrder { field_id: checklist_field.id.clone(), diff --git a/frontend/rust-lib/flowy-document-pub/src/cloud.rs b/frontend/rust-lib/flowy-document-pub/src/cloud.rs index 2f4da1bd37..18e40691a1 100644 --- a/frontend/rust-lib/flowy-document-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-document-pub/src/cloud.rs @@ -2,30 +2,31 @@ use anyhow::Error; pub use collab_document::blocks::DocumentData; use flowy_error::FlowyError; -use lib_infra::future::FutureResult; +use lib_infra::async_trait::async_trait; /// A trait for document cloud service. /// Each kind of server should implement this trait. Check out the [AppFlowyServerProvider] of /// [flowy-server] crate for more information. +#[async_trait] pub trait DocumentCloudService: Send + Sync + 'static { - fn get_document_doc_state( + async fn get_document_doc_state( &self, document_id: &str, workspace_id: &str, - ) -> FutureResult, FlowyError>; + ) -> Result, FlowyError>; - fn get_document_snapshots( + async fn get_document_snapshots( &self, document_id: &str, limit: usize, workspace_id: &str, - ) -> FutureResult, Error>; + ) -> Result, Error>; - fn get_document_data( + async fn get_document_data( &self, document_id: &str, workspace_id: &str, - ) -> FutureResult, Error>; + ) -> Result, Error>; } pub struct DocumentSnapshot { diff --git a/frontend/rust-lib/flowy-document/Cargo.toml b/frontend/rust-lib/flowy-document/Cargo.toml index f64c960b12..6fe59c0c52 100644 --- a/frontend/rust-lib/flowy-document/Cargo.toml +++ b/frontend/rust-lib/flowy-document/Cargo.toml @@ -24,7 +24,6 @@ validator = { version = "0.16.0", features = ["derive"] } protobuf.workspace = true bytes.workspace = true nanoid = "0.4.0" -parking_lot.workspace = true strum_macros = "0.21" serde.workspace = true serde_json.workspace = true diff --git a/frontend/rust-lib/flowy-document/src/document.rs b/frontend/rust-lib/flowy-document/src/document.rs index 6ec018f171..7e1c1d143b 100644 --- a/frontend/rust-lib/flowy-document/src/document.rs +++ b/frontend/rust-lib/flowy-document/src/document.rs @@ -2,86 +2,28 @@ use crate::entities::{ DocEventPB, DocumentAwarenessStatesPB, DocumentSnapshotStatePB, DocumentSyncStatePB, }; use crate::notification::{send_notification, DocumentNotification}; -use collab::core::collab::MutexCollab; -use collab_document::document::DocumentIndexContent; -use collab_document::{blocks::DocumentData, document::Document}; -use flowy_error::FlowyResult; +use collab::preclude::Collab; +use collab_document::document::Document; use futures::StreamExt; use lib_dispatch::prelude::af_spawn; -use parking_lot::Mutex; -use std::{ - ops::{Deref, DerefMut}, - sync::Arc, -}; -use tracing::{instrument, warn}; -/// This struct wrap the document::Document -#[derive(Clone)] -pub struct MutexDocument(Arc>); - -impl MutexDocument { - /// Open a document with the given collab. - /// # Arguments - /// * `collab` - the identifier of the collaboration instance - /// - /// # Returns - /// * `Result` - a Result containing either a new Document object or an Error if the document creation failed - pub fn open(doc_id: &str, collab: Arc) -> FlowyResult { - #[allow(clippy::arc_with_non_send_sync)] - let document = Document::open(collab.clone()).map(|inner| Self(Arc::new(Mutex::new(inner))))?; - subscribe_document_changed(doc_id, &document); - subscribe_document_snapshot_state(&collab); - subscribe_document_sync_state(&collab); - Ok(document) - } - - /// Creates and returns a new Document object with initial data. - /// # Arguments - /// * `collab` - the identifier of the collaboration instance - /// * `data` - the initial data to include in the document - /// - /// # Returns - /// * `Result` - a Result containing either a new Document object or an Error if the document creation failed - pub fn create_with_data(collab: Arc, data: DocumentData) -> FlowyResult { - #[allow(clippy::arc_with_non_send_sync)] - let document = - Document::create_with_data(collab, data).map(|inner| Self(Arc::new(Mutex::new(inner))))?; - Ok(document) - } - - #[instrument(level = "debug", skip_all)] - pub fn start_init_sync(&self) { - if let Some(document) = self.0.try_lock() { - if let Some(collab) = document.get_collab().try_lock() { - collab.start_init_sync(); - } else { - warn!("Failed to start init sync, collab is locked"); - } - } else { - warn!("Failed to start init sync, document is locked"); - } - } -} - -fn subscribe_document_changed(doc_id: &str, document: &MutexDocument) { +pub fn subscribe_document_changed(doc_id: &str, document: &mut Document) { let doc_id_clone_for_block_changed = doc_id.to_owned(); - document - .lock() - .subscribe_block_changed(move |events, is_remote| { - #[cfg(feature = "verbose_log")] - tracing::trace!("subscribe_document_changed: {:?}", events); + document.subscribe_block_changed("key", move |events, is_remote| { + #[cfg(feature = "verbose_log")] + tracing::trace!("subscribe_document_changed: {:?}", events); - // send notification to the client. - send_notification( - &doc_id_clone_for_block_changed, - DocumentNotification::DidReceiveUpdate, - ) - .payload::((events, is_remote, None).into()) - .send(); - }); + // send notification to the client. + send_notification( + &doc_id_clone_for_block_changed, + DocumentNotification::DidReceiveUpdate, + ) + .payload::((events, is_remote, None).into()) + .send(); + }); let doc_id_clone_for_awareness_state = doc_id.to_owned(); - document.lock().subscribe_awareness_state(move |events| { + document.subscribe_awareness_state("key", move |events| { #[cfg(feature = "verbose_log")] tracing::trace!("subscribe_awareness_state: {:?}", events); send_notification( @@ -93,9 +35,9 @@ fn subscribe_document_changed(doc_id: &str, document: &MutexDocument) { }); } -fn subscribe_document_snapshot_state(collab: &Arc) { - let document_id = collab.lock().object_id.clone(); - let mut snapshot_state = collab.lock().subscribe_snapshot_state(); +pub fn subscribe_document_snapshot_state(collab: &Collab) { + let document_id = collab.object_id().to_string(); + let mut snapshot_state = collab.subscribe_snapshot_state(); af_spawn(async move { while let Some(snapshot_state) = snapshot_state.next().await { if let Some(new_snapshot_id) = snapshot_state.snapshot_id() { @@ -111,9 +53,9 @@ fn subscribe_document_snapshot_state(collab: &Arc) { }); } -fn subscribe_document_sync_state(collab: &Arc) { - let document_id = collab.lock().object_id.clone(); - let mut sync_state_stream = collab.lock().subscribe_sync_state(); +pub fn subscribe_document_sync_state(collab: &Collab) { + let document_id = collab.object_id().to_string(); + let mut sync_state_stream = collab.subscribe_sync_state(); af_spawn(async move { while let Some(sync_state) = sync_state_stream.next().await { send_notification( @@ -125,27 +67,3 @@ fn subscribe_document_sync_state(collab: &Arc) { } }); } - -unsafe impl Sync for MutexDocument {} -unsafe impl Send for MutexDocument {} - -impl Deref for MutexDocument { - type Target = Arc>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for MutexDocument { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl From<&MutexDocument> for DocumentIndexContent { - fn from(doc: &MutexDocument) -> Self { - let doc = doc.lock(); - DocumentIndexContent::from(&*doc) - } -} diff --git a/frontend/rust-lib/flowy-document/src/event_handler.rs b/frontend/rust-lib/flowy-document/src/event_handler.rs index 66a98e3105..2efde86a89 100644 --- a/frontend/rust-lib/flowy-document/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document/src/event_handler.rs @@ -42,7 +42,7 @@ pub(crate) async fn get_encode_collab_handler( let manager = upgrade_document(manager)?; let params: OpenDocumentParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let state = manager.get_encoded_collab_with_view_id(&doc_id).await?; + let state = manager.get_encoded_collab_with_view_id(&doc_id)?; data_result_ok(EncodedCollabPB { state_vector: Vec::from(state.state_vector), doc_state: Vec::from(state.doc_state), @@ -74,8 +74,8 @@ pub(crate) async fn open_document_handler( let doc_id = params.document_id; manager.open_document(&doc_id).await?; - let document = manager.get_opened_document(&doc_id).await?; - let document_data = document.lock().get_document_data()?; + let document = manager.editable_document(&doc_id).await?; + let document_data = document.read().await.get_document_data()?; data_result_ok(DocumentDataPB::from(document_data)) } @@ -122,12 +122,12 @@ pub(crate) async fn apply_action_handler( let manager = upgrade_document(manager)?; let params: ApplyActionParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.get_opened_document(&doc_id).await?; + let document = manager.editable_document(&doc_id).await?; let actions = params.actions; if cfg!(feature = "verbose_log") { tracing::trace!("{} applying actions: {:?}", doc_id, actions); } - document.lock().apply_action(actions); + document.write().await.apply_action(actions)?; Ok(()) } @@ -139,9 +139,9 @@ pub(crate) async fn create_text_handler( let manager = upgrade_document(manager)?; let params: TextDeltaParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.get_opened_document(&doc_id).await?; - let document = document.lock(); - document.create_text(¶ms.text_id, params.delta); + let document = manager.editable_document(&doc_id).await?; + let mut document = document.write().await; + document.apply_text_delta(¶ms.text_id, params.delta); Ok(()) } @@ -153,10 +153,10 @@ pub(crate) async fn apply_text_delta_handler( let manager = upgrade_document(manager)?; let params: TextDeltaParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.get_opened_document(&doc_id).await?; + let document = manager.editable_document(&doc_id).await?; let text_id = params.text_id; let delta = params.delta; - let document = document.lock(); + let mut document = document.write().await; if cfg!(feature = "verbose_log") { tracing::trace!("{} applying delta: {:?}", doc_id, delta); } @@ -194,8 +194,8 @@ pub(crate) async fn redo_handler( let manager = upgrade_document(manager)?; let params: DocumentRedoUndoParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.get_opened_document(&doc_id).await?; - let document = document.lock(); + let document = manager.editable_document(&doc_id).await?; + let mut document = document.write().await; let redo = document.redo(); let can_redo = document.can_redo(); let can_undo = document.can_undo(); @@ -213,8 +213,8 @@ pub(crate) async fn undo_handler( let manager = upgrade_document(manager)?; let params: DocumentRedoUndoParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.get_opened_document(&doc_id).await?; - let document = document.lock(); + let document = manager.editable_document(&doc_id).await?; + let mut document = document.write().await; let undo = document.undo(); let can_redo = document.can_redo(); let can_undo = document.can_undo(); @@ -232,11 +232,10 @@ pub(crate) async fn can_undo_redo_handler( let manager = upgrade_document(manager)?; let params: DocumentRedoUndoParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.get_opened_document(&doc_id).await?; - let document = document.lock(); + let document = manager.editable_document(&doc_id).await?; + let document = document.read().await; let can_redo = document.can_redo(); let can_undo = document.can_undo(); - drop(document); data_result_ok(DocumentRedoUndoResponsePB { can_redo, can_undo, @@ -388,8 +387,7 @@ pub async fn convert_document_handler( let manager = upgrade_document(manager)?; let params: ConvertDocumentParams = data.into_inner().try_into()?; - let document = manager.get_opened_document(¶ms.document_id).await?; - let document_data = document.lock().get_document_data()?; + let document_data = manager.get_document_data(¶ms.document_id).await?; let parser = DocumentDataParser::new(Arc::new(document_data), params.range); if !params.parse_types.any_enabled() { diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index 5ea5aeb2de..bf503b0cfc 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use std::sync::Weak; -use collab::core::collab::{DataSource, MutexCollab}; +use collab::core::collab::DataSource; use collab::core::origin::CollabOrigin; use collab::entity::EncodedCollab; use collab::preclude::Collab; @@ -12,19 +12,26 @@ use collab_document::document_awareness::DocumentAwarenessState; use collab_document::document_awareness::DocumentAwarenessUser; use collab_document::document_data::default_document_data; use collab_entity::CollabType; +use collab_plugins::local_storage::kv::doc::CollabKVAction; +use collab_plugins::local_storage::kv::KVTransactionDB; use collab_plugins::CollabKVDB; use dashmap::DashMap; use lib_infra::util::timestamp; +use tokio::sync::RwLock; use tracing::trace; use tracing::{event, instrument}; -use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; +use crate::document::{ + subscribe_document_changed, subscribe_document_snapshot_state, subscribe_document_sync_state, +}; +use collab_integrate::collab_builder::{ + AppFlowyCollabBuilder, CollabBuilderConfig, KVDBCollabPersistenceImpl, +}; use flowy_document_pub::cloud::DocumentCloudService; use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; use flowy_storage_pub::storage::{CreatedUpload, StorageService}; use lib_dispatch::prelude::af_spawn; -use crate::document::MutexDocument; use crate::entities::UpdateDocumentAwarenessStatePB; use crate::entities::{ DocumentSnapshotData, DocumentSnapshotMeta, DocumentSnapshotMetaPB, DocumentSnapshotPB, @@ -49,8 +56,8 @@ pub trait DocumentSnapshotService: Send + Sync { pub struct DocumentManager { pub user_service: Arc, collab_builder: Arc, - documents: Arc>>, - removing_documents: Arc>>, + documents: Arc>>>, + removing_documents: Arc>>>, cloud_service: Arc, storage_service: Weak, snapshot_service: Arc, @@ -76,17 +83,17 @@ impl DocumentManager { } /// Get the encoded collab of the document. - pub async fn get_encoded_collab_with_view_id(&self, doc_id: &str) -> FlowyResult { - let doc_state = DataSource::Disk; + pub fn get_encoded_collab_with_view_id(&self, doc_id: &str) -> FlowyResult { let uid = self.user_service.user_id()?; - let collab = self - .collab_for_document(uid, doc_id, doc_state, false) - .await?; - - let collab = collab.lock(); - collab + let doc_state = + KVDBCollabPersistenceImpl::new(self.user_service.collab_db(uid)?, uid).into_data_source(); + let collab = self.collab_for_document(uid, doc_id, doc_state, false)?; + let encoded_collab = collab + .try_read() + .unwrap() .encode_collab_v1(|collab| CollabType::Document.validate_require_data(collab)) - .map_err(internal_error) + .map_err(internal_error)?; + Ok(encoded_collab) } pub async fn initialize(&self, _uid: i64) -> FlowyResult<()> { @@ -132,27 +139,56 @@ impl DocumentManager { format!("document {} already exists", doc_id), )) } else { + let db = self + .user_service + .collab_db(uid)? + .upgrade() + .ok_or_else(|| FlowyError::internal().with_context("Failed to get collab db"))?; let encoded_collab = doc_state_from_document_data( doc_id, data.unwrap_or_else(|| default_document_data(doc_id)), ) .await?; - let doc_state = encoded_collab.doc_state.to_vec(); - let collab = self - .collab_for_document( + + db.with_write_txn(|write_txn| { + write_txn.flush_doc( uid, doc_id, - DataSource::DocStateV1(doc_state.clone()), - false, - ) - .await?; - collab.lock().flush(); + encoded_collab.state_vector.to_vec(), + encoded_collab.doc_state.to_vec(), + )?; + Ok(()) + })?; Ok(encoded_collab) } } - pub async fn get_opened_document(&self, doc_id: &str) -> FlowyResult> { + fn collab_for_document( + &self, + uid: i64, + doc_id: &str, + data_source: DataSource, + sync_enable: bool, + ) -> FlowyResult>> { + let db = self.user_service.collab_db(uid)?; + let workspace_id = self.user_service.workspace_id()?; + let collab_object = + self + .collab_builder + .collab_object(&workspace_id, uid, doc_id, CollabType::Document)?; + let document = self.collab_builder.create_document( + collab_object, + data_source, + db, + CollabBuilderConfig::default().sync_enable(sync_enable), + None, + )?; + Ok(document) + } + + /// Return a document instance if the document is already opened. + pub async fn editable_document(&self, doc_id: &str) -> FlowyResult>> { if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) { return Ok(doc); } @@ -160,6 +196,7 @@ impl DocumentManager { if let Some(doc) = self.restore_document_from_removing(doc_id) { return Ok(doc); } + Err(FlowyError::internal().with_context("Call open document first")) } @@ -167,12 +204,14 @@ impl DocumentManager { /// If the document does not exist in local disk, try get the doc state from the cloud. /// If the document exists, open the document and cache it #[tracing::instrument(level = "info", skip(self), err)] - async fn init_document_instance(&self, doc_id: &str) -> FlowyResult> { - if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) { - return Ok(doc); - } - - let mut doc_state = DataSource::Disk; + async fn create_document_instance( + &self, + doc_id: &str, + enable_sync: bool, + ) -> FlowyResult>> { + let uid = self.user_service.user_id()?; + let mut doc_state = + KVDBCollabPersistenceImpl::new(self.user_service.collab_db(uid)?, uid).into_data_source(); // If the document does not exist in local disk, try get the doc state from the cloud. This happens // When user_device_a create a document and user_device_b open the document. if !self.is_doc_exist(doc_id).await? { @@ -192,21 +231,25 @@ impl DocumentManager { } } - let uid = self.user_service.user_id()?; event!( tracing::Level::DEBUG, "Initialize document: {}, workspace_id: {:?}", doc_id, self.user_service.workspace_id() ); - let collab = self - .collab_for_document(uid, doc_id, doc_state, true) - .await?; - - match MutexDocument::open(doc_id, collab) { + let result = self.collab_for_document(uid, doc_id, doc_state, enable_sync); + match result { Ok(document) => { - let document = Arc::new(document); - self.documents.insert(doc_id.to_string(), document.clone()); + // Only push the document to the cache if the sync is enabled. + if enable_sync { + { + let mut lock = document.write().await; + subscribe_document_changed(doc_id, &mut lock); + subscribe_document_snapshot_state(&lock); + subscribe_document_sync_state(&lock); + } + self.documents.insert(doc_id.to_string(), document.clone()); + } Ok(document) }, Err(err) => { @@ -222,47 +265,52 @@ impl DocumentManager { pub async fn get_document_data(&self, doc_id: &str) -> FlowyResult { let document = self.get_document(doc_id).await?; + let document = document.read().await; document.get_document_data().map_err(internal_error) } pub async fn get_document_text(&self, doc_id: &str) -> FlowyResult { let document = self.get_document(doc_id).await?; - let text = convert_document_to_plain_text(document)?; + let document = document.read().await; + let text = convert_document_to_plain_text(&document)?; Ok(text) } - async fn get_document(&self, doc_id: &str) -> FlowyResult { - let mut doc_state = DataSource::Disk; - if !self.is_doc_exist(doc_id).await? { - doc_state = DataSource::DocStateV1( - self - .cloud_service - .get_document_doc_state(doc_id, &self.user_service.workspace_id()?) - .await?, - ); + /// Return a document instance. + /// The returned document might or might not be able to sync with the cloud. + async fn get_document(&self, doc_id: &str) -> FlowyResult>> { + if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) { + return Ok(doc); } - let uid = self.user_service.user_id()?; - let collab = self - .collab_for_document(uid, doc_id, doc_state, false) - .await?; - let document = Document::open(collab)?; + + if let Some(doc) = self.restore_document_from_removing(doc_id) { + return Ok(doc); + } + + let document = self.create_document_instance(doc_id, false).await?; Ok(document) } pub async fn open_document(&self, doc_id: &str) -> FlowyResult<()> { if let Some(mutex_document) = self.restore_document_from_removing(doc_id) { - mutex_document.start_init_sync(); + let lock = mutex_document.read().await; + lock.start_init_sync(); } - let _ = self.init_document_instance(doc_id).await?; + if self.documents.contains_key(doc_id) { + return Ok(()); + } + + let _ = self.create_document_instance(doc_id, true).await?; Ok(()) } pub async fn close_document(&self, doc_id: &str) -> FlowyResult<()> { if let Some((doc_id, document)) = self.documents.remove(doc_id) { - if let Some(doc) = document.try_lock() { + { // clear the awareness state when close the document - doc.clean_awareness_local_state(); - let _ = doc.flush(); + let mut lock = document.write().await; + lock.clean_awareness_local_state(); + lock.flush(); } let clone_doc_id = doc_id.clone(); trace!("move document to removing_documents: {}", doc_id); @@ -300,20 +348,19 @@ impl DocumentManager { ) -> FlowyResult { let uid = self.user_service.user_id()?; let device_id = self.user_service.device_id()?; - if let Ok(doc) = self.get_opened_document(doc_id).await { - if let Some(doc) = doc.try_lock() { - let user = DocumentAwarenessUser { uid, device_id }; - let selection = state.selection.map(|s| s.into()); - let state = DocumentAwarenessState { - version: 1, - user, - selection, - metadata: state.metadata, - timestamp: timestamp(), - }; - doc.set_awareness_local_state(state); - return Ok(true); - } + if let Ok(doc) = self.editable_document(doc_id).await { + let mut doc = doc.write().await; + let user = DocumentAwarenessUser { uid, device_id }; + let selection = state.selection.map(|s| s.into()); + let state = DocumentAwarenessState { + version: 1, + user, + selection, + metadata: state.metadata, + timestamp: timestamp(), + }; + doc.set_awareness_local_state(state); + return Ok(true); } Ok(false) } @@ -376,27 +423,6 @@ impl DocumentManager { Ok(()) } - async fn collab_for_document( - &self, - uid: i64, - doc_id: &str, - doc_state: DataSource, - sync_enable: bool, - ) -> FlowyResult> { - let db = self.user_service.collab_db(uid)?; - let workspace_id = self.user_service.workspace_id()?; - let collab = self.collab_builder.build_with_config( - &workspace_id, - uid, - doc_id, - CollabType::Document, - db, - doc_state, - CollabBuilderConfig::default().sync_enable(sync_enable), - )?; - Ok(collab) - } - async fn is_doc_exist(&self, doc_id: &str) -> FlowyResult { let uid = self.user_service.user_id()?; if let Some(collab_db) = self.user_service.collab_db(uid)?.upgrade() { @@ -425,7 +451,7 @@ impl DocumentManager { &self.storage_service } - fn restore_document_from_removing(&self, doc_id: &str) -> Option> { + fn restore_document_from_removing(&self, doc_id: &str) -> Option>> { let (doc_id, doc) = self.removing_documents.remove(doc_id)?; trace!( "move document {} from removing_documents to documents", @@ -443,13 +469,8 @@ async fn doc_state_from_document_data( let doc_id = doc_id.to_string(); // spawn_blocking is used to avoid blocking the tokio thread pool if the document is large. let encoded_collab = tokio::task::spawn_blocking(move || { - let collab = Arc::new(MutexCollab::new(Collab::new_with_origin( - CollabOrigin::Empty, - doc_id, - vec![], - false, - ))); - let document = Document::create_with_data(collab.clone(), data).map_err(internal_error)?; + let collab = Collab::new_with_origin(CollabOrigin::Empty, doc_id, vec![], false); + let document = Document::open_with(collab, Some(data)).map_err(internal_error)?; let encode_collab = document.encode_collab()?; Ok::<_, FlowyError>(encode_collab) }) diff --git a/frontend/rust-lib/flowy-document/tests/document/document_insert_test.rs b/frontend/rust-lib/flowy-document/tests/document/document_insert_test.rs index 1181395cae..28c02641e8 100644 --- a/frontend/rust-lib/flowy-document/tests/document/document_insert_test.rs +++ b/frontend/rust-lib/flowy-document/tests/document/document_insert_test.rs @@ -31,9 +31,13 @@ async fn document_apply_insert_block_with_empty_parent_id() { text_id: None, }, }; - document.lock().apply_action(vec![insert_text_action]); + document + .write() + .await + .apply_action(vec![insert_text_action]) + .unwrap(); // read the text block and it's parent id should be the page id - let block = document.lock().get_block(&text_block_id).unwrap(); + let block = document.read().await.get_block(&text_block_id).unwrap(); assert_eq!(block.parent, page_id); } diff --git a/frontend/rust-lib/flowy-document/tests/document/document_redo_undo_test.rs b/frontend/rust-lib/flowy-document/tests/document/document_redo_undo_test.rs index ce97aa0bdd..b11cd2ecde 100644 --- a/frontend/rust-lib/flowy-document/tests/document/document_redo_undo_test.rs +++ b/frontend/rust-lib/flowy-document/tests/document/document_redo_undo_test.rs @@ -23,8 +23,8 @@ async fn undo_redo_test() { // open a document test.open_document(&doc_id).await.unwrap(); - let document = test.get_opened_document(&doc_id).await.unwrap(); - let document = document.lock(); + let document = test.editable_document(&doc_id).await.unwrap(); + let mut document = document.write().await; let page_block = document.get_block(&data.page_id).unwrap(); let page_id = page_block.id; let text_block_id = gen_id(); @@ -49,7 +49,7 @@ async fn undo_redo_test() { text_id: None, }, }; - document.apply_action(vec![insert_text_action]); + document.apply_action(vec![insert_text_action]).unwrap(); let can_undo = document.can_undo(); assert!(can_undo); diff --git a/frontend/rust-lib/flowy-document/tests/document/document_test.rs b/frontend/rust-lib/flowy-document/tests/document/document_test.rs index 8c57d94346..d7906bc114 100644 --- a/frontend/rust-lib/flowy-document/tests/document/document_test.rs +++ b/frontend/rust-lib/flowy-document/tests/document/document_test.rs @@ -23,10 +23,11 @@ async fn restore_document() { test.open_document(&doc_id).await.unwrap(); let data_b = test - .get_opened_document(&doc_id) + .editable_document(&doc_id) .await .unwrap() - .lock() + .read() + .await .get_document_data() .unwrap(); // close a document @@ -37,10 +38,11 @@ async fn restore_document() { _ = test.create_document(uid, &doc_id, Some(data.clone())).await; // open a document let data_b = test - .get_opened_document(&doc_id) + .editable_document(&doc_id) .await .unwrap() - .lock() + .read() + .await .get_document_data() .unwrap(); // close a document @@ -61,8 +63,9 @@ async fn document_apply_insert_action() { // open a document test.open_document(&doc_id).await.unwrap(); - let document = test.get_opened_document(&doc_id).await.unwrap(); - let page_block = document.lock().get_block(&data.page_id).unwrap(); + let document = test.editable_document(&doc_id).await.unwrap(); + let mut document = document.write().await; + let page_block = document.get_block(&data.page_id).unwrap(); // insert a text block let text_block = Block { @@ -84,17 +87,19 @@ async fn document_apply_insert_action() { text_id: None, }, }; - document.lock().apply_action(vec![insert_text_action]); - let data_a = document.lock().get_document_data().unwrap(); + document.apply_action(vec![insert_text_action]).unwrap(); + let data_a = document.get_document_data().unwrap(); + drop(document); // close the original document _ = test.close_document(&doc_id).await; // re-open the document let data_b = test - .get_opened_document(&doc_id) + .editable_document(&doc_id) .await .unwrap() - .lock() + .read() + .await .get_document_data() .unwrap(); // close a document @@ -115,8 +120,9 @@ async fn document_apply_update_page_action() { // open a document test.open_document(&doc_id).await.unwrap(); - let document = test.get_opened_document(&doc_id).await.unwrap(); - let page_block = document.lock().get_block(&data.page_id).unwrap(); + let document = test.editable_document(&doc_id).await.unwrap(); + let mut document = document.write().await; + let page_block = document.get_block(&data.page_id).unwrap(); let mut page_block_clone = page_block; page_block_clone.data = HashMap::new(); @@ -136,13 +142,14 @@ async fn document_apply_update_page_action() { }; let actions = vec![action]; tracing::trace!("{:?}", &actions); - document.lock().apply_action(actions); - let page_block_old = document.lock().get_block(&data.page_id).unwrap(); + document.apply_action(actions).unwrap(); + let page_block_old = document.get_block(&data.page_id).unwrap(); + drop(document); _ = test.close_document(&doc_id).await; // re-open the document - let document = test.get_opened_document(&doc_id).await.unwrap(); - let page_block_new = document.lock().get_block(&data.page_id).unwrap(); + let document = test.editable_document(&doc_id).await.unwrap(); + let page_block_new = document.read().await.get_block(&data.page_id).unwrap(); assert_eq!(page_block_old, page_block_new); assert!(page_block_new.data.contains_key("delta")); } @@ -159,8 +166,9 @@ async fn document_apply_update_action() { // open a document test.open_document(&doc_id).await.unwrap(); - let document = test.get_opened_document(&doc_id).await.unwrap(); - let page_block = document.lock().get_block(&data.page_id).unwrap(); + let document = test.editable_document(&doc_id).await.unwrap(); + let mut document = document.write().await; + let page_block = document.get_block(&data.page_id).unwrap(); // insert a text block let text_block_id = gen_id(); @@ -183,10 +191,10 @@ async fn document_apply_update_action() { text_id: None, }, }; - document.lock().apply_action(vec![insert_text_action]); + document.apply_action(vec![insert_text_action]).unwrap(); // update the text block - let existing_text_block = document.lock().get_block(&text_block_id).unwrap(); + let existing_text_block = document.get_block(&text_block_id).unwrap(); let mut updated_text_block_data = HashMap::new(); updated_text_block_data.insert("delta".to_string(), Value::String("delta".to_string())); let updated_text_block = Block { @@ -208,13 +216,14 @@ async fn document_apply_update_action() { text_id: None, }, }; - document.lock().apply_action(vec![update_text_action]); + document.apply_action(vec![update_text_action]).unwrap(); + drop(document); // close the original document _ = test.close_document(&doc_id).await; // re-open the document - let document = test.get_opened_document(&doc_id).await.unwrap(); - let block = document.lock().get_block(&text_block_id).unwrap(); + let document = test.editable_document(&doc_id).await.unwrap(); + let block = document.read().await.get_block(&text_block_id).unwrap(); assert_eq!(block.data, updated_text_block_data); // close a document _ = test.close_document(&doc_id).await; diff --git a/frontend/rust-lib/flowy-document/tests/document/util.rs b/frontend/rust-lib/flowy-document/tests/document/util.rs index 58663abd14..2bc2f9d7bb 100644 --- a/frontend/rust-lib/flowy-document/tests/document/util.rs +++ b/frontend/rust-lib/flowy-document/tests/document/util.rs @@ -1,13 +1,14 @@ use std::ops::Deref; -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; use anyhow::Error; use collab::preclude::CollabPlugin; use collab_document::blocks::DocumentData; +use collab_document::document::Document; use collab_document::document_data::default_document_data; use nanoid::nanoid; -use parking_lot::Once; use tempfile::TempDir; +use tokio::sync::RwLock; use tracing_subscriber::{fmt::Subscriber, util::SubscriberInitExt, EnvFilter}; use collab_integrate::collab_builder::{ @@ -15,7 +16,6 @@ use collab_integrate::collab_builder::{ CollabPluginProviderType, WorkspaceCollabIntegrate, }; use collab_integrate::CollabKVDB; -use flowy_document::document::MutexDocument; use flowy_document::entities::{DocumentSnapshotData, DocumentSnapshotMeta}; use flowy_document::manager::{DocumentManager, DocumentSnapshotService, DocumentUserService}; use flowy_document_pub::cloud::*; @@ -24,7 +24,6 @@ use flowy_storage_pub::chunked_byte::ChunkedBytes; use flowy_storage_pub::storage::{CreatedUpload, FileProgressReceiver, StorageService}; use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; -use lib_infra::future::FutureResult; pub struct DocumentTest { inner: DocumentManager, @@ -103,8 +102,8 @@ impl DocumentUserService for FakeUser { } pub fn setup_log() { - static START: Once = Once::new(); - START.call_once(|| { + static START: OnceLock<()> = OnceLock::new(); + START.get_or_init(|| { std::env::set_var("RUST_LOG", "collab_persistence=trace"); let subscriber = Subscriber::builder() .with_env_filter(EnvFilter::from_default_env()) @@ -114,7 +113,7 @@ pub fn setup_log() { }); } -pub async fn create_and_open_empty_document() -> (DocumentTest, Arc, String) { +pub async fn create_and_open_empty_document() -> (DocumentTest, Arc>, String) { let test = DocumentTest::new(); let doc_id: String = gen_document_id(); let data = default_document_data(&doc_id); @@ -126,7 +125,7 @@ pub async fn create_and_open_empty_document() -> (DocumentTest, Arc String { } pub struct LocalTestDocumentCloudServiceImpl(); + +#[async_trait] impl DocumentCloudService for LocalTestDocumentCloudServiceImpl { - fn get_document_doc_state( + async fn get_document_doc_state( &self, document_id: &str, _workspace_id: &str, - ) -> FutureResult, FlowyError> { + ) -> Result, FlowyError> { let document_id = document_id.to_string(); - FutureResult::new(async move { - Err(FlowyError::new( - ErrorCode::RecordNotFound, - format!("Document {} not found", document_id), - )) - }) + Err(FlowyError::new( + ErrorCode::RecordNotFound, + format!("Document {} not found", document_id), + )) } - fn get_document_snapshots( + async fn get_document_snapshots( &self, _document_id: &str, _limit: usize, _workspace_id: &str, - ) -> FutureResult, Error> { - FutureResult::new(async move { Ok(vec![]) }) + ) -> Result, Error> { + Ok(vec![]) } - fn get_document_data( + async fn get_document_data( &self, _document_id: &str, _workspace_id: &str, - ) -> FutureResult, Error> { - FutureResult::new(async move { Ok(None) }) + ) -> Result, Error> { + Ok(None) } } diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index fc12ed4606..93fe6a88d6 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -307,6 +307,10 @@ pub enum ErrorCode { #[error("Invalid Request")] InvalidRequest = 106, + + #[error("In progress")] + // when client receives InProgress, it should retry + InProgress = 107, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs index 4747a4eb51..19c3422984 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs @@ -3,7 +3,6 @@ pub use anyhow::Error; use collab_entity::CollabType; pub use collab_folder::{Folder, FolderData, Workspace}; use lib_infra::async_trait::async_trait; -use lib_infra::future::FutureResult; use uuid::Uuid; /// [FolderCloudService] represents the cloud service for folder. @@ -11,59 +10,59 @@ use uuid::Uuid; pub trait FolderCloudService: Send + Sync + 'static { /// Creates a new workspace for the user. /// Returns error if the cloud service doesn't support multiple workspaces - fn create_workspace(&self, uid: i64, name: &str) -> FutureResult; + async fn create_workspace(&self, uid: i64, name: &str) -> Result; - fn open_workspace(&self, workspace_id: &str) -> FutureResult<(), Error>; + async fn open_workspace(&self, workspace_id: &str) -> Result<(), Error>; /// Returns all workspaces of the user. /// Returns vec![] if the cloud service doesn't support multiple workspaces - fn get_all_workspace(&self) -> FutureResult, Error>; + async fn get_all_workspace(&self) -> Result, Error>; - fn get_folder_data( + async fn get_folder_data( &self, workspace_id: &str, uid: &i64, - ) -> FutureResult, Error>; + ) -> Result, Error>; - fn get_folder_snapshots( + async fn get_folder_snapshots( &self, workspace_id: &str, limit: usize, - ) -> FutureResult, Error>; + ) -> Result, Error>; - fn get_folder_doc_state( + async fn get_folder_doc_state( &self, workspace_id: &str, uid: i64, collab_type: CollabType, object_id: &str, - ) -> FutureResult, Error>; + ) -> Result, Error>; - fn batch_create_folder_collab_objects( + async fn batch_create_folder_collab_objects( &self, workspace_id: &str, objects: Vec, - ) -> FutureResult<(), Error>; + ) -> Result<(), Error>; fn service_name(&self) -> String; - fn publish_view( + async fn publish_view( &self, workspace_id: &str, payload: Vec, - ) -> FutureResult<(), Error>; + ) -> Result<(), Error>; - fn unpublish_views(&self, workspace_id: &str, view_ids: Vec) -> FutureResult<(), Error>; + async fn unpublish_views(&self, workspace_id: &str, view_ids: Vec) -> Result<(), Error>; - fn get_publish_info(&self, view_id: &str) -> FutureResult; + async fn get_publish_info(&self, view_id: &str) -> Result; - fn set_publish_namespace( + async fn set_publish_namespace( &self, workspace_id: &str, new_namespace: &str, - ) -> FutureResult<(), Error>; + ) -> Result<(), Error>; - fn get_publish_namespace(&self, workspace_id: &str) -> FutureResult; + async fn get_publish_namespace(&self, workspace_id: &str) -> Result; } #[derive(Debug)] diff --git a/frontend/rust-lib/flowy-folder/Cargo.toml b/frontend/rust-lib/flowy-folder/Cargo.toml index e0327a5044..20a131cf6c 100644 --- a/frontend/rust-lib/flowy-folder/Cargo.toml +++ b/frontend/rust-lib/flowy-folder/Cargo.toml @@ -17,7 +17,7 @@ flowy-search-pub = { workspace = true } flowy-sqlite = { workspace = true } flowy-derive.workspace = true flowy-notification = { workspace = true } -parking_lot.workspace = true +arc-swap.workspace = true unicode-segmentation = "1.10" tracing.workspace = true flowy-error = { path = "../flowy-error", features = [ diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index e3426db8a8..30b6566862 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -107,7 +107,7 @@ pub(crate) async fn create_view_handler( let set_as_current = params.set_as_current; let (view, _) = folder.create_view_with_params(params, true).await?; if set_as_current { - let _ = folder.set_current_view(&view.id).await; + let _ = folder.set_current_view(view.id.clone()).await; } data_result_ok(view_pb_without_child_views(view)) } @@ -121,7 +121,7 @@ pub(crate) async fn create_orphan_view_handler( let set_as_current = params.set_as_current; let view = folder.create_orphan_view_with_params(params).await?; if set_as_current { - let _ = folder.set_current_view(&view.id).await; + let _ = folder.set_current_view(view.id.clone()).await; } data_result_ok(view_pb_without_child_views(view)) } @@ -226,7 +226,7 @@ pub(crate) async fn set_latest_view_handler( ) -> Result<(), FlowyError> { let folder = upgrade_folder(folder)?; let view_id: ViewIdPB = data.into_inner(); - let _ = folder.set_current_view(&view_id.value).await; + let _ = folder.set_current_view(view_id.value.clone()).await; Ok(()) } @@ -400,7 +400,9 @@ pub(crate) async fn update_view_visibility_status_handler( ) -> Result<(), FlowyError> { let folder = upgrade_folder(folder)?; let params = data.into_inner(); - folder.set_views_visibility(params.view_ids, params.is_public); + folder + .set_views_visibility(params.view_ids, params.is_public) + .await; Ok(()) } diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index f9034bf481..bc691ab7dc 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -20,14 +20,16 @@ use crate::util::{ use crate::view_operation::{ create_view, EncodedCollabWrapper, FolderOperationHandler, FolderOperationHandlers, }; -use collab::core::collab::{DataSource, MutexCollab}; +use arc_swap::ArcSwapOption; +use collab::core::collab::DataSource; use collab_entity::{CollabType, EncodedCollab}; -use collab_folder::error::FolderError; use collab_folder::{ - Folder, FolderNotify, Section, SectionItem, TrashInfo, UserId, View, ViewLayout, ViewUpdate, + Folder, FolderData, FolderNotify, Section, SectionItem, TrashInfo, View, ViewLayout, ViewUpdate, Workspace, }; -use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; +use collab_integrate::collab_builder::{ + AppFlowyCollabBuilder, CollabBuilderConfig, KVDBCollabPersistenceImpl, +}; use collab_integrate::CollabKVDB; use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; use flowy_folder_pub::cloud::{gen_view_id, FolderCloudService, FolderCollabParams}; @@ -39,22 +41,27 @@ use flowy_folder_pub::folder_builder::ParentChildViews; use flowy_search_pub::entities::FolderIndexManager; use flowy_sqlite::kv::KVStorePreferences; use futures::future; -use parking_lot::RwLock; use std::collections::HashMap; use std::fmt::{Display, Formatter}; -use std::ops::Deref; use std::sync::{Arc, Weak}; +use tokio::sync::RwLock; use tracing::{error, info, instrument}; pub trait FolderUser: Send + Sync { fn user_id(&self) -> Result; fn workspace_id(&self) -> Result; fn collab_db(&self, uid: i64) -> Result, FlowyError>; + + fn is_folder_exist_on_disk(&self, uid: i64, workspace_id: &str) -> FlowyResult; } pub struct FolderManager { + //FIXME: there's no sense in having a mutex_folder behind an RwLock. It's being obtained multiple + // times in the same function. FolderManager itself should be hidden behind RwLock if necessary. + // Unfortunately, this would require a changing the SyncPlugin architecture which requires access + // to Arc>>. Eventually SyncPlugin should be refactored. /// MutexFolder is the folder that is used to store the data. - pub(crate) mutex_folder: Arc, + pub(crate) mutex_folder: ArcSwapOption>, pub(crate) collab_builder: Arc, pub(crate) user: Arc, pub(crate) operation_handlers: FolderOperationHandlers, @@ -72,10 +79,9 @@ impl FolderManager { folder_indexer: Arc, store_preferences: Arc, ) -> FlowyResult { - let mutex_folder = Arc::new(MutexFolder::default()); let manager = Self { user, - mutex_folder, + mutex_folder: Default::default(), collab_builder, operation_handlers, cloud_service, @@ -89,12 +95,14 @@ impl FolderManager { #[instrument(level = "debug", skip(self), err)] pub async fn get_current_workspace(&self) -> FlowyResult { let workspace_id = self.user.workspace_id()?; - self.with_folder( - || { + + match self.mutex_folder.load_full() { + None => { let uid = self.user.user_id()?; Err(workspace_data_not_sync_error(uid, &workspace_id)) }, - |folder| { + Some(lock) => { + let folder = lock.read().await; let workspace_pb_from_workspace = |workspace: Workspace, folder: &Folder| { let views = get_workspace_public_view_pbs(&workspace_id, folder); let workspace: WorkspacePB = (workspace, views).into(); @@ -103,10 +111,10 @@ impl FolderManager { match folder.get_workspace_info(&workspace_id) { None => Err(FlowyError::record_not_found().with_context("Can not find the workspace")), - Some(workspace) => workspace_pb_from_workspace(workspace, folder), + Some(workspace) => workspace_pb_from_workspace(workspace, &folder), } }, - ) + } } /// Return a list of views of the current workspace. @@ -118,16 +126,24 @@ impl FolderManager { pub async fn get_workspace_public_views(&self) -> FlowyResult> { let workspace_id = self.user.workspace_id()?; - Ok(self.with_folder(Vec::new, |folder| { - get_workspace_public_view_pbs(&workspace_id, folder) - })) + match self.mutex_folder.load_full() { + None => Ok(Vec::default()), + Some(lock) => { + let folder = lock.read().await; + Ok(get_workspace_public_view_pbs(&workspace_id, &folder)) + }, + } } pub async fn get_workspace_private_views(&self) -> FlowyResult> { let workspace_id = self.user.workspace_id()?; - Ok(self.with_folder(Vec::new, |folder| { - get_workspace_private_view_pbs(&workspace_id, folder) - })) + match self.mutex_folder.load_full() { + None => Ok(Vec::default()), + Some(folder) => { + let folder = folder.read().await; + Ok(get_workspace_private_view_pbs(&workspace_id, &folder)) + }, + } } #[instrument(level = "trace", skip_all, err)] @@ -136,59 +152,48 @@ impl FolderManager { uid: i64, workspace_id: &str, collab_db: Weak, - doc_state: DataSource, + data_source: Option, folder_notifier: T, - ) -> Result { + ) -> Result>, FlowyError> { let folder_notifier = folder_notifier.into(); // only need the check the workspace id when the doc state is not from the disk. - let should_check_workspace_id = !matches!(doc_state, DataSource::Disk); - let should_auto_initialize = !should_check_workspace_id; let config = CollabBuilderConfig::default() .sync_enable(true) - .auto_initialize(should_auto_initialize); + .auto_initialize(true); + + let data_source = data_source + .unwrap_or_else(|| KVDBCollabPersistenceImpl::new(collab_db.clone(), uid).into_data_source()); let object_id = workspace_id; - let collab = self.collab_builder.build_with_config( - workspace_id, - uid, - object_id, - CollabType::Folder, + let collab_object = + self + .collab_builder + .collab_object(workspace_id, uid, object_id, CollabType::Folder)?; + let result = self.collab_builder.create_folder( + collab_object, + data_source, collab_db, - doc_state, config, - )?; - let (should_clear, err) = match Folder::open(UserId::from(uid), collab.clone(), folder_notifier) - { - Ok(folder) => { - if should_check_workspace_id { - // check the workspace id in the folder is matched with the workspace id. Just in case the folder - // is overwritten by another workspace. - let folder_workspace_id = folder.get_workspace_id(); - if folder_workspace_id != workspace_id { - error!( - "expect workspace_id: {}, actual workspace_id: {}", - workspace_id, folder_workspace_id - ); - return Err(FlowyError::workspace_data_not_match()); - } - // Initialize the folder manually - collab.lock().initialize(); - } - return Ok(folder); - }, - Err(err) => (matches!(err, FolderError::NoRequiredData(_)), err), - }; + folder_notifier, + None, + ); // If opening the folder fails due to missing required data (indicated by a `FolderError::NoRequiredData`), // the function logs an informational message and attempts to clear the folder data by deleting its // document from the collaborative database. It then returns the encountered error. - if should_clear { - info!("Clear the folder data and try to open the folder again"); - if let Some(db) = self.user.collab_db(uid).ok().and_then(|a| a.upgrade()) { - let _ = db.delete_doc(uid, workspace_id).await; - } + match result { + Ok(folder) => Ok(folder), + Err(err) => { + info!( + "Clear the folder data and try to open the folder again due to: {}", + err + ); + if let Some(db) = self.user.collab_db(uid).ok().and_then(|a| a.upgrade()) { + let _ = db.delete_doc(uid, workspace_id).await; + } + Err(err.into()) + }, } - Err(err.into()) } pub(crate) async fn create_empty_collab( @@ -196,18 +201,25 @@ impl FolderManager { uid: i64, workspace_id: &str, collab_db: Weak, - ) -> Result, FlowyError> { + notifier: Option, + folder_data: Option, + ) -> Result>, FlowyError> { let object_id = workspace_id; - let collab = self.collab_builder.build_with_config( - workspace_id, - uid, - object_id, - CollabType::Folder, + let collab_object = + self + .collab_builder + .collab_object(workspace_id, uid, object_id, CollabType::Folder)?; + + let doc_state = KVDBCollabPersistenceImpl::new(collab_db.clone(), uid).into_data_source(); + let folder = self.collab_builder.create_folder( + collab_object, + doc_state, collab_db, - DataSource::Disk, CollabBuilderConfig::default().sync_enable(true), + notifier, + folder_data, )?; - Ok(collab) + Ok(folder) } /// Initialize the folder with the given workspace id. @@ -216,21 +228,12 @@ impl FolderManager { pub async fn initialize_with_workspace_id(&self, user_id: i64) -> FlowyResult<()> { let workspace_id = self.user.workspace_id()?; let object_id = &workspace_id; - let folder_doc_state = self - .cloud_service - .get_folder_doc_state(&workspace_id, user_id, CollabType::Folder, object_id) - .await?; - if let Err(err) = self - .initialize( - user_id, - &workspace_id, - FolderInitDataSource::Cloud(folder_doc_state), - ) - .await - { - // If failed to open folder with remote data, open from local disk. After open from the local - // disk. the data will be synced to the remote server. - error!("initialize folder with error {:?}, fallback local", err); + + let is_exist = self + .user + .is_folder_exist_on_disk(user_id, &workspace_id) + .unwrap_or(false); + if is_exist { self .initialize( user_id, @@ -240,7 +243,34 @@ impl FolderManager { }, ) .await?; + } else { + let folder_doc_state = self + .cloud_service + .get_folder_doc_state(&workspace_id, user_id, CollabType::Folder, object_id) + .await?; + if let Err(err) = self + .initialize( + user_id, + &workspace_id, + FolderInitDataSource::Cloud(folder_doc_state), + ) + .await + { + // If failed to open folder with remote data, open from local disk. After open from the local + // disk. the data will be synced to the remote server. + error!("initialize folder with error {:?}, fallback local", err); + self + .initialize( + user_id, + &workspace_id, + FolderInitDataSource::LocalDisk { + create_if_not_exist: false, + }, + ) + .await?; + } } + Ok(()) } @@ -322,36 +352,34 @@ impl FolderManager { &self, views: Vec, ) -> Result<(), FlowyError> { - self.with_folder( - || Err(FlowyError::internal().with_context("The folder is not initialized")), - |folder| { + match self.mutex_folder.load_full() { + None => Err(FlowyError::internal().with_context("The folder is not initialized")), + Some(lock) => { + let mut folder = lock.write().await; for view in views { - insert_parent_child_views(folder, view); + insert_parent_child_views(&mut folder, view); } Ok(()) }, - )?; - - Ok(()) + } } pub async fn get_workspace_pb(&self) -> FlowyResult { let workspace_id = self.user.workspace_id()?; - let guard = self.mutex_folder.write(); - let folder = guard - .as_ref() - .ok_or(FlowyError::internal().with_context("folder is not initialized"))?; + let lock = self + .mutex_folder + .load_full() + .ok_or_else(|| FlowyError::internal().with_context("folder is not initialized"))?; + let folder = lock.read().await; let workspace = folder .get_workspace_info(&workspace_id) .ok_or_else(|| FlowyError::record_not_found().with_context("Can not find the workspace"))?; let views = folder - .views .get_views_belong_to(&workspace.id) .into_iter() .map(|view| view_pb_without_child_views(view.as_ref().clone())) .collect::>(); - drop(guard); Ok(WorkspacePB { id: workspace.id, @@ -361,25 +389,6 @@ impl FolderManager { }) } - /// This function acquires a lock on the `mutex_folder` and checks its state. - /// If the folder is `None`, it invokes the `none_callback`, otherwise, it passes the folder to the `f2` callback. - /// - /// # Parameters - /// - /// * `none_callback`: A callback function that is invoked when `mutex_folder` contains `None`. - /// * `f2`: A callback function that is invoked when `mutex_folder` contains a `Some` value. The contained folder is passed as an argument to this callback. - fn with_folder(&self, none_callback: F1, f2: F2) -> Output - where - F1: FnOnce() -> Output, - F2: FnOnce(&Folder) -> Output, - { - let folder = self.mutex_folder.write(); - match &*folder { - None => none_callback(), - Some(folder) => f2(folder), - } - } - /// Asynchronously creates a view with provided parameters and notifies the workspace if update is needed. /// /// Commonly, the notify_workspace_update parameter is set to true when the view is created in the workspace. @@ -412,20 +421,14 @@ impl FolderManager { let section = params.section.clone().unwrap_or(ViewSectionPB::Public); let is_private = section == ViewSectionPB::Private; let view = create_view(self.user.user_id()?, params, view_layout); - self.with_folder( - || (), - |folder| { - folder.insert_view(view.clone(), index); - if is_private { - folder.add_private_view_ids(vec![view.id.clone()]); - } - }, - ); - - if notify_workspace_update { - let folder = &self.mutex_folder.read(); - if let Some(folder) = folder.as_ref() { - notify_did_update_workspace(&workspace_id, folder); + if let Some(lock) = self.mutex_folder.load_full() { + let mut folder = lock.write().await; + folder.insert_view(view.clone(), index); + if is_private { + folder.add_private_view_ids(vec![view.id.clone()]); + } + if notify_workspace_update { + notify_did_update_workspace(&workspace_id, &folder); } } @@ -448,20 +451,24 @@ impl FolderManager { .await?; let view = create_view(self.user.user_id()?, params, view_layout); - self.with_folder( - || (), - |folder| { - folder.insert_view(view.clone(), None); - }, - ); + if let Some(lock) = self.mutex_folder.load_full() { + let mut folder = lock.write().await; + folder.insert_view(view.clone(), None); + } Ok(view) } #[tracing::instrument(level = "debug", skip(self), err)] pub(crate) async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> { - if let Some(view) = self.with_folder(|| None, |folder| folder.views.get_view(view_id)) { - let handler = self.get_handler(&view.layout)?; - handler.close_view(view_id).await?; + if let Some(lock) = self.mutex_folder.load_full() { + let folder = lock.read().await; + if let Some(view) = folder.get_view(view_id) { + // Drop the folder lock explicitly to avoid deadlock when following calls contains 'self' + drop(folder); + + let handler = self.get_handler(&view.layout)?; + handler.close_view(view_id).await?; + } } Ok(()) } @@ -477,11 +484,14 @@ impl FolderManager { pub async fn get_view_pb(&self, view_id: &str) -> FlowyResult { let view_id = view_id.to_string(); - let folder = self.mutex_folder.read(); - let folder = folder.as_ref().ok_or_else(folder_not_init_error)?; + let lock = self + .mutex_folder + .load_full() + .ok_or_else(folder_not_init_error)?; + let folder = lock.read().await; // trash views and other private views should not be accessed - let view_ids_should_be_filtered = self.get_view_ids_should_be_filtered(folder); + let view_ids_should_be_filtered = Self::get_view_ids_should_be_filtered(&folder); if view_ids_should_be_filtered.contains(&view_id) { return Err(FlowyError::new( @@ -490,14 +500,13 @@ impl FolderManager { )); } - match folder.views.get_view(&view_id) { + match folder.get_view(&view_id) { None => { error!("Can't find the view with id: {}", view_id); Err(FlowyError::record_not_found()) }, Some(view) => { let child_views = folder - .views .get_views_belong_to(&view.id) .into_iter() .filter(|view| !view_ids_should_be_filtered.contains(&view.id)) @@ -520,11 +529,14 @@ impl FolderManager { &self, view_ids: Vec, ) -> FlowyResult> { - let folder = self.mutex_folder.read(); - let folder = folder.as_ref().ok_or_else(folder_not_init_error)?; + let lock = self + .mutex_folder + .load_full() + .ok_or_else(folder_not_init_error)?; // trash views and other private views should not be accessed - let view_ids_should_be_filtered = self.get_view_ids_should_be_filtered(folder); + let folder = lock.read().await; + let view_ids_should_be_filtered = Self::get_view_ids_should_be_filtered(&folder); let views = view_ids .into_iter() @@ -532,7 +544,7 @@ impl FolderManager { if view_ids_should_be_filtered.contains(&view_id) { return None; } - folder.views.get_view(&view_id) + folder.get_view(&view_id) }) .map(view_pb_without_child_views_from_arc) .collect::>(); @@ -548,13 +560,16 @@ impl FolderManager { /// #[tracing::instrument(level = "debug", skip(self))] pub async fn get_all_views_pb(&self) -> FlowyResult> { - let folder = self.mutex_folder.read(); - let folder = folder.as_ref().ok_or_else(folder_not_init_error)?; + let lock = self + .mutex_folder + .load_full() + .ok_or_else(folder_not_init_error)?; // trash views and other private views should not be accessed - let view_ids_should_be_filtered = self.get_view_ids_should_be_filtered(folder); + let folder = lock.read().await; + let view_ids_should_be_filtered = Self::get_view_ids_should_be_filtered(&folder); - let all_views = folder.views.get_all_views(); + let all_views = folder.get_all_views(); let views = all_views .into_iter() .filter(|view| !view_ids_should_be_filtered.contains(&view.id)) @@ -576,17 +591,18 @@ impl FolderManager { pub async fn get_view_ancestors_pb(&self, view_id: &str) -> FlowyResult> { let mut ancestors = vec![]; let mut parent_view_id = view_id.to_string(); - while let Some(view) = - self.with_folder(|| None, |folder| folder.views.get_view(&parent_view_id)) - { - // If the view is already in the ancestors list, then break the loop - if ancestors.iter().any(|v: &ViewPB| v.id == view.id) { - break; + if let Some(lock) = self.mutex_folder.load_full() { + let folder = lock.read().await; + while let Some(view) = folder.get_view(&parent_view_id) { + // If the view is already in the ancestors list, then break the loop + if ancestors.iter().any(|v: &ViewPB| v.id == view.id) { + break; + } + ancestors.push(view_pb_without_child_views(view.as_ref().clone())); + parent_view_id = view.parent_view_id.clone(); } - ancestors.push(view_pb_without_child_views(view.as_ref().clone())); - parent_view_id = view.parent_view_id.clone(); + ancestors.reverse(); } - ancestors.reverse(); Ok(ancestors) } @@ -595,34 +611,34 @@ impl FolderManager { /// All the favorite views being trashed will be unfavorited first to remove it from favorites list as well. The process of unfavoriting concerned view is handled by `unfavorite_view_and_decendants()` #[tracing::instrument(level = "debug", skip(self), err)] pub async fn move_view_to_trash(&self, view_id: &str) -> FlowyResult<()> { - self.with_folder( - || (), - |folder| { - if let Some(view) = folder.views.get_view(view_id) { - self.unfavorite_view_and_decendants(view.clone(), folder); - folder.add_trash_view_ids(vec![view_id.to_string()]); - // notify the parent view that the view is moved to trash - send_notification(view_id, FolderNotification::DidMoveViewToTrash) - .payload(DeletedViewPB { - view_id: view_id.to_string(), - index: None, - }) - .send(); + if let Some(lock) = self.mutex_folder.load_full() { + let mut folder = lock.write().await; + if let Some(view) = folder.get_view(view_id) { + Self::unfavorite_view_and_decendants(view.clone(), &mut folder); + folder.add_trash_view_ids(vec![view_id.to_string()]); + drop(folder); - notify_child_views_changed( - view_pb_without_child_views(view.as_ref().clone()), - ChildViewChangeReason::Delete, - ); - } - }, - ); + // notify the parent view that the view is moved to trash + send_notification(view_id, FolderNotification::DidMoveViewToTrash) + .payload(DeletedViewPB { + view_id: view_id.to_string(), + index: None, + }) + .send(); + + notify_child_views_changed( + view_pb_without_child_views(view.as_ref().clone()), + ChildViewChangeReason::Delete, + ); + } + } Ok(()) } - fn unfavorite_view_and_decendants(&self, view: Arc, folder: &Folder) { + fn unfavorite_view_and_decendants(view: Arc, folder: &mut Folder) { let mut all_descendant_views: Vec> = vec![view.clone()]; - all_descendant_views.extend(folder.views.get_views_belong_to(&view.id)); + all_descendant_views.extend(folder.get_views_belong_to(&view.id)); let favorite_descendant_views: Vec = all_descendant_views .iter() @@ -672,25 +688,18 @@ impl FolderManager { let to_section = params.to_section; let view = self.get_view_pb(&view_id).await?; let old_parent_id = view.parent_view_id; - self.with_folder( - || (), - |folder| { - folder.move_nested_view(&view_id, &new_parent_id, prev_view_id); - - if from_section != to_section { - if to_section == Some(ViewSectionPB::Private) { - folder.add_private_view_ids(vec![view_id.clone()]); - } else { - folder.delete_private_view_ids(vec![view_id.clone()]); - } + if let Some(lock) = self.mutex_folder.load_full() { + let mut folder = lock.write().await; + folder.move_nested_view(&view_id, &new_parent_id, prev_view_id); + if from_section != to_section { + if to_section == Some(ViewSectionPB::Private) { + folder.add_private_view_ids(vec![view_id.clone()]); + } else { + folder.delete_private_view_ids(vec![view_id.clone()]); } - }, - ); - notify_parent_view_did_change( - &workspace_id, - self.mutex_folder.clone(), - vec![new_parent_id, old_parent_id], - ); + } + notify_parent_view_did_change(&workspace_id, &folder, vec![new_parent_id, old_parent_id]); + } Ok(()) } @@ -731,17 +740,11 @@ impl FolderManager { if let (Some(actual_from_index), Some(actual_to_index)) = (actual_from_index, actual_to_index) { - self.with_folder( - || (), - |folder| { - folder.move_view(view_id, actual_from_index as u32, actual_to_index as u32); - }, - ); - notify_parent_view_did_change( - &workspace_id, - self.mutex_folder.clone(), - vec![parent_view_id], - ); + if let Some(lock) = self.mutex_folder.load_full() { + let mut folder = lock.write().await; + folder.move_view(view_id, actual_from_index as u32, actual_to_index as u32); + notify_parent_view_did_change(&workspace_id, &folder, vec![parent_view_id]); + } } } } @@ -751,10 +754,10 @@ impl FolderManager { /// Return a list of views that belong to the given parent view id. #[tracing::instrument(level = "debug", skip(self, parent_view_id), err)] pub async fn get_views_belong_to(&self, parent_view_id: &str) -> FlowyResult>> { - let views = self.with_folder(Vec::new, |folder| { - folder.views.get_views_belong_to(parent_view_id) - }); - Ok(views) + match self.mutex_folder.load_full() { + Some(folder) => Ok(folder.read().await.get_views_belong_to(parent_view_id)), + None => Ok(Vec::default()), + } } /// Update the view with the given params. @@ -791,9 +794,18 @@ impl FolderManager { /// Including the view data (icon, cover, extra) and the child views. #[tracing::instrument(level = "debug", skip(self), err)] pub(crate) async fn duplicate_view(&self, params: DuplicateViewParams) -> Result<(), FlowyError> { - let view = self - .with_folder(|| None, |folder| folder.views.get_view(¶ms.view_id)) + let lock = self + .mutex_folder + .load_full() .ok_or_else(|| FlowyError::record_not_found().with_context("Can't duplicate the view"))?; + let folder = lock.read().await; + let view = folder + .get_view(¶ms.view_id) + .ok_or_else(|| FlowyError::record_not_found().with_context("Can't duplicate the view"))?; + + // Explicitly drop the folder lock to avoid deadlock when following calls contains 'self' + drop(folder); + let parent_view_id = params .parent_view_id .clone() @@ -831,9 +843,13 @@ impl FolderManager { } // filter the view ids that in the trash or private section - let filtered_view_ids = self.with_folder(Vec::new, |folder| { - self.get_view_ids_should_be_filtered(folder) - }); + let filtered_view_ids = match self.mutex_folder.load_full() { + None => Vec::default(), + Some(lock) => { + let folder = lock.read().await; + Self::get_view_ids_should_be_filtered(&folder) + }, + }; // only apply the `open_after_duplicated` and the `include_children` to the first view let mut is_source_view = true; @@ -842,9 +858,20 @@ impl FolderManager { let mut objects = vec![]; let suffix = suffix.unwrap_or(" (copy)".to_string()); + let lock = match self.mutex_folder.load_full() { + None => { + return Err( + FlowyError::record_not_found() + .with_context(format!("Can't duplicate the view({})", view_id)), + ) + }, + Some(lock) => lock, + }; while let Some((current_view_id, current_parent_id)) = stack.pop() { - let view = self - .with_folder(|| None, |folder| folder.views.get_view(¤t_view_id)) + let view = lock + .read() + .await + .get_view(¤t_view_id) .ok_or_else(|| { FlowyError::record_not_found() .with_context(format!("Can't duplicate the view({})", view_id)) @@ -864,16 +891,14 @@ impl FolderManager { .map(|i| i as u32) }); - let section = self.with_folder( - || ViewSectionPB::Private, - |folder| { - if folder.is_view_in_section(Section::Private, &view.id) { - ViewSectionPB::Private - } else { - ViewSectionPB::Public - } - }, - ); + let section = { + let folder = lock.read().await; + if folder.is_view_in_section(Section::Private, &view.id) { + ViewSectionPB::Private + } else { + ViewSectionPB::Public + } + }; let name = if is_source_view { format!("{}{}", &view.name, suffix) @@ -946,32 +971,28 @@ impl FolderManager { } // notify the update here - notify_parent_view_did_change( - workspace_id, - self.mutex_folder.clone(), - vec![parent_view_id.to_string()], - ); + let folder = lock.read().await; + notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id.to_string()]); Ok(()) } #[tracing::instrument(level = "trace", skip(self), err)] - pub(crate) async fn set_current_view(&self, view_id: &str) -> Result<(), FlowyError> { - self.with_folder( - || Err(FlowyError::record_not_found()), - |folder| { - folder.set_current_view(view_id); - folder.add_recent_view_ids(vec![view_id.to_string()]); - Ok(()) - }, - )?; + pub(crate) async fn set_current_view(&self, view_id: String) -> Result<(), FlowyError> { + if let Some(lock) = self.mutex_folder.load_full() { + let mut folder = lock.write().await; + folder.set_current_view(view_id.clone()); + folder.add_recent_view_ids(vec![view_id.clone()]); + } else { + return Err(FlowyError::record_not_found()); + } let view = self.get_current_view().await; if let Some(view) = &view { let view_layout: ViewLayout = view.layout.clone().into(); if let Some(handle) = self.operation_handlers.get(&view_layout) { info!("Open view: {}", view.id); - if let Err(err) = handle.open_view(view_id).await { + if let Err(err) = handle.open_view(&view_id).await { error!("Open view error: {:?}", err); } } @@ -988,25 +1009,29 @@ impl FolderManager { #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn get_current_view(&self) -> Option { - let view_id = self.with_folder(|| None, |folder| folder.get_current_view())?; + let view_id = { + let lock = self.mutex_folder.load_full()?; + let folder = lock.read().await; + let view = folder.get_current_view()?; + drop(folder); + view + }; self.get_view_pb(&view_id).await.ok() } /// Toggles the favorite status of a view identified by `view_id`If the view is not a favorite, it will be added to the favorites list; otherwise, it will be removed from the list. #[tracing::instrument(level = "debug", skip(self), err)] pub async fn toggle_favorites(&self, view_id: &str) -> FlowyResult<()> { - self.with_folder( - || (), - |folder| { - if let Some(old_view) = folder.views.get_view(view_id) { - if old_view.is_favorite { - folder.delete_favorite_view_ids(vec![view_id.to_string()]); - } else { - folder.add_favorite_view_ids(vec![view_id.to_string()]); - } + if let Some(lock) = self.mutex_folder.load_full() { + let mut folder = lock.write().await; + if let Some(old_view) = folder.get_view(view_id) { + if old_view.is_favorite { + folder.delete_favorite_view_ids(vec![view_id.to_string()]); + } else { + folder.add_favorite_view_ids(vec![view_id.to_string()]); } - }, - ); + } + } self.send_toggle_favorite_notification(view_id).await; Ok(()) } @@ -1014,12 +1039,10 @@ impl FolderManager { /// Add the view to the recent view list / history. #[tracing::instrument(level = "debug", skip(self), err)] pub async fn add_recent_views(&self, view_ids: Vec) -> FlowyResult<()> { - self.with_folder( - || (), - |folder| { - folder.add_recent_view_ids(view_ids); - }, - ); + if let Some(lock) = self.mutex_folder.load_full() { + let mut folder = lock.write().await; + folder.add_recent_view_ids(view_ids); + } self.send_update_recent_views_notification().await; Ok(()) } @@ -1027,12 +1050,10 @@ impl FolderManager { /// Add the view to the recent view list / history. #[tracing::instrument(level = "debug", skip(self), err)] pub async fn remove_recent_views(&self, view_ids: Vec) -> FlowyResult<()> { - self.with_folder( - || (), - |folder| { - folder.delete_recent_view_ids(view_ids); - }, - ); + if let Some(lock) = self.mutex_folder.load_full() { + let mut folder = lock.write().await; + folder.delete_recent_view_ids(view_ids); + } self.send_update_recent_views_notification().await; Ok(()) } @@ -1047,12 +1068,22 @@ impl FolderManager { publish_name: Option, selected_view_ids: Option>, ) -> FlowyResult<()> { - let view = self - .with_folder(|| None, |folder| folder.views.get_view(view_id)) - .ok_or_else(|| { + let view = { + let lock = match self.mutex_folder.load_full() { + None => { + return Err( + FlowyError::record_not_found() + .with_context(format!("Can't find the view with ID: {}", view_id)), + ) + }, + Some(lock) => lock, + }; + let read_guard = lock.read().await; + read_guard.get_view(view_id).ok_or_else(|| { FlowyError::record_not_found() .with_context(format!("Can't find the view with ID: {}", view_id)) - })?; + })? + }; if view.layout == ViewLayout::Chat { return Err(FlowyError::new( @@ -1311,52 +1342,57 @@ impl FolderManager { #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn get_all_favorites(&self) -> Vec { - self.get_sections(Section::Favorite) + self.get_sections(Section::Favorite).await } #[tracing::instrument(level = "debug", skip(self))] pub(crate) async fn get_my_recent_sections(&self) -> Vec { - self.get_sections(Section::Recent) + self.get_sections(Section::Recent).await } #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn get_my_trash_info(&self) -> Vec { - self.with_folder(Vec::new, |folder| folder.get_my_trash_info()) + match self.mutex_folder.load_full() { + None => Vec::default(), + Some(folder) => folder.read().await.get_my_trash_info(), + } } #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn restore_all_trash(&self) { - self.with_folder( - || (), - |folder| { - folder.remove_all_my_trash_sections(); - }, - ); - send_notification("trash", FolderNotification::DidUpdateTrash) - .payload(RepeatedTrashPB { items: vec![] }) - .send(); + if let Some(lock) = self.mutex_folder.load_full() { + let mut folder = lock.write().await; + folder.remove_all_my_trash_sections(); + send_notification("trash", FolderNotification::DidUpdateTrash) + .payload(RepeatedTrashPB { items: vec![] }) + .send(); + } } #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn restore_trash(&self, trash_id: &str) { - self.with_folder( - || (), - |folder| { - folder.delete_trash_view_ids(vec![trash_id.to_string()]); - }, - ); + if let Some(lock) = self.mutex_folder.load_full() { + let mut folder = lock.write().await; + folder.delete_trash_view_ids(vec![trash_id.to_string()]); + } } /// Delete all the trash permanently. #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn delete_my_trash(&self) { - let deleted_trash = self.with_folder(Vec::new, |folder| folder.get_my_trash_info()); - for trash in deleted_trash { - let _ = self.delete_trash(&trash.id).await; + if let Some(lock) = self.mutex_folder.load_full() { + let deleted_trash = lock.read().await.get_my_trash_info(); + + // Explicitly drop the folder lock to avoid deadlock when following calls contains 'self' + drop(lock); + + for trash in deleted_trash { + let _ = self.delete_trash(&trash.id).await; + } + send_notification("trash", FolderNotification::DidUpdateTrash) + .payload(RepeatedTrashPB { items: vec![] }) + .send(); } - send_notification("trash", FolderNotification::DidUpdateTrash) - .payload(RepeatedTrashPB { items: vec![] }) - .send(); } /// Delete the trash permanently. @@ -1364,17 +1400,19 @@ impl FolderManager { /// is a database view. Then the database will be deleted as well. #[tracing::instrument(level = "debug", skip(self, view_id), err)] pub async fn delete_trash(&self, view_id: &str) -> FlowyResult<()> { - let view = self.with_folder(|| None, |folder| folder.views.get_view(view_id)); - self.with_folder( - || (), - |folder| { + if let Some(lock) = self.mutex_folder.load_full() { + let view = { + let mut folder = lock.write().await; + let view = folder.get_view(view_id); folder.delete_trash_view_ids(vec![view_id.to_string()]); - folder.views.delete_views(vec![view_id]); - }, - ); - if let Some(view) = view { - if let Ok(handler) = self.get_handler(&view.layout) { - handler.delete_view(view_id).await?; + folder.delete_views(vec![view_id]); + view + }; + + if let Some(view) = view { + if let Ok(handler) = self.get_handler(&view.layout) { + handler.delete_view(view_id).await?; + } } } Ok(()) @@ -1416,7 +1454,6 @@ impl FolderManager { // Import data from file path if available if let Some(file_path) = import_data.file_path { - // TODO(Lucas): return the collab handler .import_from_file_path(&view_id, &import_data.name, file_path) .await?; @@ -1440,12 +1477,10 @@ impl FolderManager { let view = create_view(self.user.user_id()?, params, import_data.view_layout); // Insert the new view into the folder - self.with_folder( - || (), - |folder| { - folder.insert_view(view.clone(), None); - }, - ); + if let Some(lock) = self.mutex_folder.load_full() { + let mut folder = lock.write().await; + folder.insert_view(view.clone(), None); + } Ok((view, encoded_collab)) } @@ -1493,11 +1528,10 @@ impl FolderManager { } // Notify that the parent view has changed - notify_parent_view_did_change( - &workspace_id, - self.mutex_folder.clone(), - vec![import_data.parent_view_id], - ); + if let Some(lock) = self.mutex_folder.load_full() { + let folder = lock.read().await; + notify_parent_view_did_change(&workspace_id, &folder, vec![import_data.parent_view_id]); + } Ok(RepeatedViewPB { items: views }) } @@ -1508,15 +1542,16 @@ impl FolderManager { F: FnOnce(ViewUpdate) -> Option, { let workspace_id = self.user.workspace_id()?; - let value = self.with_folder( - || None, - |folder| { - let old_view = folder.views.get_view(view_id); - let new_view = folder.views.update_view(view_id, f); + let value = match self.mutex_folder.load_full() { + None => None, + Some(lock) => { + let mut folder = lock.write().await; + let old_view = folder.get_view(view_id); + let new_view = folder.update_view(view_id, f); Some((old_view, new_view)) }, - ); + }; if let Some((Some(old_view), Some(new_view))) = value { if let Ok(handler) = self.get_handler(&old_view.layout) { @@ -1529,9 +1564,9 @@ impl FolderManager { .payload(view_pb) .send(); - let folder = &self.mutex_folder.read(); - if let Some(folder) = folder.as_ref() { - notify_did_update_workspace(&workspace_id, folder); + if let Some(lock) = self.mutex_folder.load_full() { + let folder = lock.read().await; + notify_did_update_workspace(&workspace_id, &folder); } } @@ -1574,37 +1609,34 @@ impl FolderManager { /// child view ids of the view. async fn get_view_relation(&self, view_id: &str) -> Option<(bool, String, Vec)> { let workspace_id = self.user.workspace_id().ok()?; - self.with_folder( - || None, - |folder| { - let view = folder.views.get_view(view_id)?; - match folder.views.get_view(&view.parent_view_id) { - None => folder.get_workspace_info(&workspace_id).map(|workspace| { - ( - true, - workspace.id, - workspace - .child_views - .items - .into_iter() - .map(|view| view.id) - .collect::>(), - ) - }), - Some(parent_view) => Some(( - false, - parent_view.id.clone(), - parent_view - .children - .items - .clone() - .into_iter() - .map(|view| view.id) - .collect::>(), - )), - } - }, - ) + let lock = self.mutex_folder.load_full()?; + let folder = lock.read().await; + let view = folder.get_view(view_id)?; + match folder.get_view(&view.parent_view_id) { + None => folder.get_workspace_info(&workspace_id).map(|workspace| { + ( + true, + workspace.id, + workspace + .child_views + .items + .into_iter() + .map(|view| view.id) + .collect::>(), + ) + }), + Some(parent_view) => Some(( + false, + parent_view.id.clone(), + parent_view + .children + .items + .clone() + .into_iter() + .map(|view| view.id) + .collect::>(), + )), + } } pub async fn get_folder_snapshots( @@ -1628,39 +1660,41 @@ impl FolderManager { Ok(snapshots) } - pub fn set_views_visibility(&self, view_ids: Vec, is_public: bool) { - self.with_folder( - || (), - |folder| { - if is_public { - folder.delete_private_view_ids(view_ids); - } else { - folder.add_private_view_ids(view_ids); - } - }, - ); + pub async fn set_views_visibility(&self, view_ids: Vec, is_public: bool) { + if let Some(lock) = self.mutex_folder.load_full() { + let mut folder = lock.write().await; + if is_public { + folder.delete_private_view_ids(view_ids); + } else { + folder.add_private_view_ids(view_ids); + } + } } /// Only support getting the Favorite and Recent sections. - fn get_sections(&self, section_type: Section) -> Vec { - self.with_folder(Vec::new, |folder| { - let views = match section_type { - Section::Favorite => folder.get_my_favorite_sections(), - Section::Recent => folder.get_my_recent_sections(), - _ => vec![], - }; - let view_ids_should_be_filtered = self.get_view_ids_should_be_filtered(folder); - views - .into_iter() - .filter(|view| !view_ids_should_be_filtered.contains(&view.id)) - .collect() - }) + async fn get_sections(&self, section_type: Section) -> Vec { + match self.mutex_folder.load_full() { + None => Vec::default(), + Some(lock) => { + let folder = lock.read().await; + let views = match section_type { + Section::Favorite => folder.get_my_favorite_sections(), + Section::Recent => folder.get_my_recent_sections(), + _ => vec![], + }; + let view_ids_should_be_filtered = Self::get_view_ids_should_be_filtered(&folder); + views + .into_iter() + .filter(|view| !view_ids_should_be_filtered.contains(&view.id)) + .collect() + }, + } } /// Get all the view that are in the trash, including the child views of the child views. /// For example, if A view which is in the trash has a child view B, this function will return /// both A and B. - fn get_all_trash_ids(&self, folder: &Folder) -> Vec { + fn get_all_trash_ids(folder: &Folder) -> Vec { let trash_ids = folder .get_all_trash_sections() .into_iter() @@ -1674,13 +1708,13 @@ impl FolderManager { } /// Filter the views that are in the trash and belong to the other private sections. - fn get_view_ids_should_be_filtered(&self, folder: &Folder) -> Vec { - let trash_ids = self.get_all_trash_ids(folder); - let other_private_view_ids = self.get_other_private_view_ids(folder); + fn get_view_ids_should_be_filtered(folder: &Folder) -> Vec { + let trash_ids = Self::get_all_trash_ids(folder); + let other_private_view_ids = Self::get_other_private_view_ids(folder); [trash_ids, other_private_view_ids].concat() } - fn get_other_private_view_ids(&self, folder: &Folder) -> Vec { + fn get_other_private_view_ids(folder: &Folder) -> Vec { let my_private_view_ids = folder .get_my_private_sections() .into_iter() @@ -1724,7 +1758,7 @@ pub(crate) fn get_workspace_public_view_pbs(workspace_id: &str, folder: &Folder) .map(|view| view.id) .collect::>(); - let mut views = folder.views.get_views_belong_to(workspace_id); + let mut views = folder.get_views_belong_to(workspace_id); // filter the views that are in the trash and all the private views views.retain(|view| !trash_ids.contains(&view.id) && !private_view_ids.contains(&view.id)); @@ -1732,11 +1766,8 @@ pub(crate) fn get_workspace_public_view_pbs(workspace_id: &str, folder: &Folder) .into_iter() .map(|view| { // Get child views - let mut child_views: Vec> = folder - .views - .get_views_belong_to(&view.id) - .into_iter() - .collect(); + let mut child_views: Vec> = + folder.get_views_belong_to(&view.id).into_iter().collect(); child_views.retain(|view| !trash_ids.contains(&view.id)); view_pb_with_child_views(view, child_views) }) @@ -1746,7 +1777,6 @@ pub(crate) fn get_workspace_public_view_pbs(workspace_id: &str, folder: &Folder) /// Get all the child views belong to the view id, including the child views of the child views. fn get_all_child_view_ids(folder: &Folder, view_id: &str) -> Vec { let child_view_ids = folder - .views .get_views_belong_to(view_id) .into_iter() .map(|view| view.id.clone()) @@ -1774,7 +1804,7 @@ pub(crate) fn get_workspace_private_view_pbs(workspace_id: &str, folder: &Folder .map(|view| view.id) .collect::>(); - let mut views = folder.views.get_views_belong_to(workspace_id); + let mut views = folder.get_views_belong_to(workspace_id); // filter the views that are in the trash and not in the private view ids views.retain(|view| !trash_ids.contains(&view.id) && private_view_ids.contains(&view.id)); @@ -1782,30 +1812,14 @@ pub(crate) fn get_workspace_private_view_pbs(workspace_id: &str, folder: &Folder .into_iter() .map(|view| { // Get child views - let mut child_views: Vec> = folder - .views - .get_views_belong_to(&view.id) - .into_iter() - .collect(); + let mut child_views: Vec> = + folder.get_views_belong_to(&view.id).into_iter().collect(); child_views.retain(|view| !trash_ids.contains(&view.id)); view_pb_with_child_views(view, child_views) }) .collect() } -/// The MutexFolder is a wrapper of the [Folder] that is used to share the folder between different -/// threads. -#[derive(Clone, Default)] -pub struct MutexFolder(Arc>>); -impl Deref for MutexFolder { - type Target = Arc>>; - fn deref(&self) -> &Self::Target { - &self.0 - } -} -unsafe impl Sync for MutexFolder {} -unsafe impl Send for MutexFolder {} - #[allow(clippy::large_enum_variant)] pub enum FolderInitDataSource { /// It means using the data stored on local disk to initialize the folder diff --git a/frontend/rust-lib/flowy-folder/src/manager_init.rs b/frontend/rust-lib/flowy-folder/src/manager_init.rs index d1266a146e..cd1c45882e 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_init.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_init.rs @@ -3,10 +3,11 @@ use crate::manager_observer::*; use crate::user_default::DefaultFolderBuilder; use collab::core::collab::DataSource; use collab_entity::{CollabType, EncodedCollab}; -use collab_folder::{Folder, FolderNotify, UserId}; +use collab_folder::{Folder, FolderNotify}; use collab_integrate::CollabKVDB; use flowy_error::{FlowyError, FlowyResult}; use std::sync::{Arc, Weak}; +use tokio::sync::RwLock; use tokio::task::spawn_blocking; use tracing::{event, info, Level}; @@ -27,9 +28,13 @@ impl FolderManager { initial_data ); - if let Some(old_folder) = self.mutex_folder.write().take() { + if let Some(old_folder) = self.mutex_folder.swap(None) { + let old_folder = old_folder.read().await; old_folder.close(); - info!("remove old folder: {}", old_folder.get_workspace_id()); + info!( + "remove old folder: {}", + old_folder.get_workspace_id().unwrap_or_default() + ); } let workspace_id = workspace_id.to_string(); @@ -47,18 +52,15 @@ impl FolderManager { FolderInitDataSource::LocalDisk { create_if_not_exist, } => { - let is_exist = self.is_workspace_exist_in_local(uid, &workspace_id).await; + let is_exist = self + .user + .is_folder_exist_on_disk(uid, &workspace_id) + .unwrap_or(false); // 1. if the folder exists, open it from local disk if is_exist { event!(Level::INFO, "Init folder from local disk"); self - .make_folder( - uid, - &workspace_id, - collab_db, - DataSource::Disk, - folder_notifier, - ) + .make_folder(uid, &workspace_id, collab_db, None, folder_notifier) .await? } else if create_if_not_exist { // 2. if the folder doesn't exist and create_if_not_exist is true, create a default folder @@ -80,7 +82,7 @@ impl FolderManager { uid, &workspace_id, collab_db.clone(), - DataSource::DocStateV1(doc_state), + Some(DataSource::DocStateV1(doc_state)), folder_notifier.clone(), ) .await? @@ -90,13 +92,7 @@ impl FolderManager { if doc_state.is_empty() { event!(Level::ERROR, "remote folder data is empty, open from local"); self - .make_folder( - uid, - &workspace_id, - collab_db, - DataSource::Disk, - folder_notifier, - ) + .make_folder(uid, &workspace_id, collab_db, None, folder_notifier) .await? } else { event!(Level::INFO, "Restore folder from remote data"); @@ -105,7 +101,7 @@ impl FolderManager { uid, &workspace_id, collab_db.clone(), - DataSource::DocStateV1(doc_state), + Some(DataSource::DocStateV1(doc_state)), folder_notifier.clone(), ) .await? @@ -113,16 +109,20 @@ impl FolderManager { }, }; - let folder_state_rx = folder.subscribe_sync_state(); - let index_content_rx = folder.subscribe_index_content(); - self - .folder_indexer - .set_index_content_receiver(index_content_rx, workspace_id.clone()); - self.handle_index_folder(workspace_id.clone(), &folder); + let folder_state_rx = { + let folder = folder.read().await; + let folder_state_rx = folder.subscribe_sync_state(); + let index_content_rx = folder.subscribe_index_content(); + self + .folder_indexer + .set_index_content_receiver(index_content_rx, workspace_id.clone()); + self.handle_index_folder(workspace_id.clone(), &folder); + folder_state_rx + }; - *self.mutex_folder.write() = Some(folder); + self.mutex_folder.store(Some(folder.clone())); - let weak_mutex_folder = Arc::downgrade(&self.mutex_folder); + let weak_mutex_folder = Arc::downgrade(&folder); subscribe_folder_sync_state_changed( workspace_id.clone(), folder_state_rx, @@ -130,41 +130,32 @@ impl FolderManager { ); subscribe_folder_snapshot_state_changed( workspace_id.clone(), - &weak_mutex_folder, + weak_mutex_folder.clone(), Arc::downgrade(&self.user), ); subscribe_folder_trash_changed( workspace_id.clone(), section_change_rx, - &weak_mutex_folder, + weak_mutex_folder.clone(), Arc::downgrade(&self.user), ); subscribe_folder_view_changed( workspace_id.clone(), view_rx, - &weak_mutex_folder, + weak_mutex_folder.clone(), Arc::downgrade(&self.user), ); Ok(()) } - async fn is_workspace_exist_in_local(&self, uid: i64, workspace_id: &str) -> bool { - if let Ok(weak_collab) = self.user.collab_db(uid) { - if let Some(collab_db) = weak_collab.upgrade() { - return collab_db.is_exist(uid, workspace_id).await.unwrap_or(false); - } - } - false - } - async fn create_default_folder( &self, uid: i64, workspace_id: &str, collab_db: Weak, folder_notifier: FolderNotify, - ) -> Result { + ) -> Result>, FlowyError> { event!( Level::INFO, "Create folder:{} with default folder builder", @@ -172,15 +163,16 @@ impl FolderManager { ); let folder_data = DefaultFolderBuilder::build(uid, workspace_id.to_string(), &self.operation_handlers).await; - let collab = self - .create_empty_collab(uid, workspace_id, collab_db) + let folder = self + .create_empty_collab( + uid, + workspace_id, + collab_db, + Some(folder_notifier), + Some(folder_data), + ) .await?; - Ok(Folder::create( - UserId::from(uid), - collab, - Some(folder_notifier), - folder_data, - )) + Ok(folder) } fn handle_index_folder(&self, workspace_id: String, folder: &Folder) { @@ -194,7 +186,7 @@ impl FolderManager { if let Ok(changes) = folder.calculate_view_changes(encoded_collab) { let folder_indexer = self.folder_indexer.clone(); - let views = folder.views.get_all_views(); + let views = folder.get_all_views(); let wid = workspace_id.clone(); if !changes.is_empty() && !views.is_empty() { @@ -208,7 +200,7 @@ impl FolderManager { } if index_all { - let views = folder.views.get_all_views(); + let views = folder.get_all_views(); let folder_indexer = self.folder_indexer.clone(); let wid = workspace_id.clone(); @@ -226,12 +218,12 @@ impl FolderManager { } fn save_collab_to_preferences(&self, folder: &Folder) { - let encoded_collab = folder.encode_collab_v1(); + if let Some(workspace_id) = folder.get_workspace_id() { + let encoded_collab = folder.encode_collab(); - if let Ok(encoded) = encoded_collab { - let _ = self - .store_preferences - .set_object(&folder.get_workspace_id(), encoded); + if let Ok(encoded) = encoded_collab { + let _ = self.store_preferences.set_object(&workspace_id, &encoded); + } } } } diff --git a/frontend/rust-lib/flowy-folder/src/manager_observer.rs b/frontend/rust-lib/flowy-folder/src/manager_observer.rs index 91bd450a70..e196f492f9 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_observer.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_observer.rs @@ -2,9 +2,7 @@ use crate::entities::{ view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, FolderSnapshotStatePB, FolderSyncStatePB, RepeatedTrashPB, RepeatedViewPB, SectionViewsPB, ViewPB, ViewSectionPB, }; -use crate::manager::{ - get_workspace_private_view_pbs, get_workspace_public_view_pbs, FolderUser, MutexFolder, -}; +use crate::manager::{get_workspace_private_view_pbs, get_workspace_public_view_pbs, FolderUser}; use crate::notification::{send_notification, FolderNotification}; use collab::core::collab_state::SyncState; use collab_folder::{ @@ -13,7 +11,8 @@ use collab_folder::{ }; use lib_dispatch::prelude::af_spawn; use std::collections::HashSet; -use std::sync::{Arc, Weak}; +use std::sync::Weak; +use tokio::sync::RwLock; use tokio_stream::wrappers::WatchStream; use tokio_stream::StreamExt; use tracing::{event, trace, Level}; @@ -22,10 +21,9 @@ use tracing::{event, trace, Level}; pub(crate) fn subscribe_folder_view_changed( workspace_id: String, mut rx: ViewChangeReceiver, - weak_mutex_folder: &Weak, + weak_mutex_folder: Weak>, user: Weak, ) { - let weak_mutex_folder = weak_mutex_folder.clone(); af_spawn(async move { while let Ok(value) = rx.recv().await { if let Some(user) = user.upgrade() { @@ -38,7 +36,7 @@ pub(crate) fn subscribe_folder_view_changed( } } - if let Some(folder) = weak_mutex_folder.upgrade() { + if let Some(lock) = weak_mutex_folder.upgrade() { tracing::trace!("Did receive view change: {:?}", value); match value { ViewChange::DidCreateView { view } => { @@ -46,7 +44,8 @@ pub(crate) fn subscribe_folder_view_changed( view_pb_without_child_views(view.clone()), ChildViewChangeReason::Create, ); - notify_parent_view_did_change(&workspace_id, folder.clone(), vec![view.parent_view_id]); + let folder = lock.read().await; + notify_parent_view_did_change(&workspace_id, &folder, vec![view.parent_view_id]); }, ViewChange::DidDeleteView { views } => { for view in views { @@ -62,11 +61,8 @@ pub(crate) fn subscribe_folder_view_changed( view_pb_without_child_views(view.clone()), ChildViewChangeReason::Update, ); - notify_parent_view_did_change( - &workspace_id, - folder.clone(), - vec![view.parent_view_id.clone()], - ); + let folder = lock.read().await; + notify_parent_view_did_change(&workspace_id, &folder, vec![view.parent_view_id]); }, }; } @@ -76,35 +72,30 @@ pub(crate) fn subscribe_folder_view_changed( pub(crate) fn subscribe_folder_snapshot_state_changed( workspace_id: String, - weak_mutex_folder: &Weak, + weak_mutex_folder: Weak>, user: Weak, ) { - let weak_mutex_folder = weak_mutex_folder.clone(); af_spawn(async move { - if let Some(mutex_folder) = weak_mutex_folder.upgrade() { - let stream = mutex_folder - .read() - .as_ref() - .map(|folder| folder.subscribe_snapshot_state()); - if let Some(mut state_stream) = stream { - while let Some(snapshot_state) = state_stream.next().await { - if let Some(user) = user.upgrade() { - if let Ok(actual_workspace_id) = user.workspace_id() { - if actual_workspace_id != workspace_id { - // break the loop when the workspace id is not matched. - break; - } + if let Some(folder) = weak_mutex_folder.upgrade() { + let mut state_stream = folder.read().await.subscribe_snapshot_state(); + + while let Some(snapshot_state) = state_stream.next().await { + if let Some(user) = user.upgrade() { + if let Ok(actual_workspace_id) = user.workspace_id() { + if actual_workspace_id != workspace_id { + // break the loop when the workspace id is not matched. + break; } } - if let Some(new_snapshot_id) = snapshot_state.snapshot_id() { - tracing::debug!("Did create folder remote snapshot: {}", new_snapshot_id); - send_notification( - &workspace_id, - FolderNotification::DidUpdateFolderSnapshotState, - ) - .payload(FolderSnapshotStatePB { new_snapshot_id }) - .send(); - } + } + if let Some(new_snapshot_id) = snapshot_state.snapshot_id() { + tracing::debug!("Did create folder remote snapshot: {}", new_snapshot_id); + send_notification( + &workspace_id, + FolderNotification::DidUpdateFolderSnapshotState, + ) + .payload(FolderSnapshotStatePB { new_snapshot_id }) + .send(); } } } @@ -138,10 +129,9 @@ pub(crate) fn subscribe_folder_sync_state_changed( pub(crate) fn subscribe_folder_trash_changed( workspace_id: String, mut rx: SectionChangeReceiver, - weak_mutex_folder: &Weak, + weak_mutex_folder: Weak>, user: Weak, ) { - let weak_mutex_folder = weak_mutex_folder.clone(); af_spawn(async move { while let Ok(value) = rx.recv().await { if let Some(user) = user.upgrade() { @@ -153,7 +143,7 @@ pub(crate) fn subscribe_folder_trash_changed( } } - if let Some(folder) = weak_mutex_folder.upgrade() { + if let Some(lock) = weak_mutex_folder.upgrade() { let mut unique_ids = HashSet::new(); tracing::trace!("Did receive trash change: {:?}", value); @@ -163,20 +153,19 @@ pub(crate) fn subscribe_folder_trash_changed( TrashSectionChange::TrashItemAdded { ids } => ids, TrashSectionChange::TrashItemRemoved { ids } => ids, }; - if let Some(folder) = folder.read().as_ref() { - let views = folder.views.get_views(&ids); - for view in views { - unique_ids.insert(view.parent_view_id.clone()); - } - - let repeated_trash: RepeatedTrashPB = folder.get_my_trash_info().into(); - send_notification("trash", FolderNotification::DidUpdateTrash) - .payload(repeated_trash) - .send(); + let folder = lock.read().await; + let views = folder.get_views(&ids); + for view in views { + unique_ids.insert(view.parent_view_id.clone()); } + let repeated_trash: RepeatedTrashPB = folder.get_my_trash_info().into(); + send_notification("trash", FolderNotification::DidUpdateTrash) + .payload(repeated_trash) + .send(); + let parent_view_ids = unique_ids.into_iter().collect(); - notify_parent_view_did_change(&workspace_id, folder.clone(), parent_view_ids); + notify_parent_view_did_change(&workspace_id, &folder, parent_view_ids); }, } } @@ -188,11 +177,9 @@ pub(crate) fn subscribe_folder_trash_changed( #[tracing::instrument(level = "debug", skip(folder, parent_view_ids))] pub(crate) fn notify_parent_view_did_change>( workspace_id: &str, - folder: Arc, + folder: &Folder, parent_view_ids: Vec, ) -> Option<()> { - let folder = folder.read(); - let folder = folder.as_ref()?; let trash_ids = folder .get_all_trash_sections() .into_iter() @@ -210,8 +197,8 @@ pub(crate) fn notify_parent_view_did_change>( } else { // Parent view can contain a list of child views. Currently, only get the first level // child views. - let parent_view = folder.views.get_view(parent_view_id)?; - let mut child_views = folder.views.get_views_belong_to(parent_view_id); + let parent_view = folder.get_view(parent_view_id)?; + let mut child_views = folder.get_views_belong_to(parent_view_id); child_views.retain(|view| !trash_ids.contains(&view.id)); event!(Level::DEBUG, child_views_count = child_views.len()); diff --git a/frontend/rust-lib/flowy-folder/src/manager_test_util.rs b/frontend/rust-lib/flowy-folder/src/manager_test_util.rs index 4280c788d9..37e86d5867 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_test_util.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_test_util.rs @@ -1,13 +1,15 @@ -use crate::manager::{FolderManager, FolderUser, MutexFolder}; +use crate::manager::{FolderManager, FolderUser}; use crate::view_operation::FolderOperationHandlers; +use collab_folder::Folder; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use flowy_folder_pub::cloud::FolderCloudService; use flowy_search_pub::entities::FolderIndexManager; use std::sync::Arc; +use tokio::sync::RwLock; impl FolderManager { - pub fn get_mutex_folder(&self) -> Arc { - self.mutex_folder.clone() + pub fn get_mutex_folder(&self) -> Option>> { + self.mutex_folder.load_full() } pub fn get_cloud_service(&self) -> Arc { diff --git a/frontend/rust-lib/flowy-folder/src/util.rs b/frontend/rust-lib/flowy-folder/src/util.rs index a56db33511..3466d7c527 100644 --- a/frontend/rust-lib/flowy-folder/src/util.rs +++ b/frontend/rust-lib/flowy-folder/src/util.rs @@ -16,7 +16,7 @@ pub(crate) fn workspace_data_not_sync_error(uid: i64, workspace_id: &str) -> Flo } #[instrument(level = "debug", skip(folder, view))] -pub(crate) fn insert_parent_child_views(folder: &Folder, view: ParentChildViews) { +pub(crate) fn insert_parent_child_views(folder: &mut Folder, view: ParentChildViews) { event!( tracing::Level::DEBUG, "Inserting view: {}, view children: {}", diff --git a/frontend/rust-lib/flowy-folder/src/view_operation.rs b/frontend/rust-lib/flowy-folder/src/view_operation.rs index fd5f90d206..5cafee0d25 100644 --- a/frontend/rust-lib/flowy-folder/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder/src/view_operation.rs @@ -40,7 +40,6 @@ pub struct DatabaseEncodedCollab { /// The handler will be used to handler the folder operation for a specific /// view layout. Each [ViewLayout] will have a handler. So when creating a new /// view, the [ViewLayout] will be used to get the handler. -/// #[async_trait] pub trait FolderOperationHandler { /// Create the view for the workspace of new user. diff --git a/frontend/rust-lib/flowy-server-pub/src/lib.rs b/frontend/rust-lib/flowy-server-pub/src/lib.rs index 4736587f4e..ee43b3c40c 100644 --- a/frontend/rust-lib/flowy-server-pub/src/lib.rs +++ b/frontend/rust-lib/flowy-server-pub/src/lib.rs @@ -28,15 +28,12 @@ if_wasm! { } } -pub mod supabase_config; - pub const CLOUT_TYPE_STR: &str = "APPFLOWY_CLOUD_ENV_CLOUD_TYPE"; #[derive(Deserialize_repr, Debug, Clone, PartialEq, Eq)] #[repr(u8)] pub enum AuthenticatorType { Local = 0, - Supabase = 1, AppFlowyCloud = 2, } @@ -50,7 +47,6 @@ impl AuthenticatorType { fn from_str(s: &str) -> Self { match s { "0" => AuthenticatorType::Local, - "1" => AuthenticatorType::Supabase, "2" => AuthenticatorType::AppFlowyCloud, _ => AuthenticatorType::Local, } diff --git a/frontend/rust-lib/flowy-server-pub/src/supabase_config.rs b/frontend/rust-lib/flowy-server-pub/src/supabase_config.rs deleted file mode 100644 index 90dbe39bc5..0000000000 --- a/frontend/rust-lib/flowy-server-pub/src/supabase_config.rs +++ /dev/null @@ -1,41 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use flowy_error::{ErrorCode, FlowyError}; - -pub const SUPABASE_URL: &str = "APPFLOWY_CLOUD_ENV_SUPABASE_URL"; -pub const SUPABASE_ANON_KEY: &str = "APPFLOWY_CLOUD_ENV_SUPABASE_ANON_KEY"; - -/// The configuration for the postgres database. It supports deserializing from the json string that -/// passed from the frontend application. [AppFlowyEnv::parser] -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -pub struct SupabaseConfiguration { - /// The url of the supabase server. - pub url: String, - /// The key of the supabase server. - pub anon_key: String, -} - -impl SupabaseConfiguration { - pub fn from_env() -> Result { - let url = std::env::var(SUPABASE_URL) - .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_URL"))?; - - let anon_key = std::env::var(SUPABASE_ANON_KEY) - .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_ANON_KEY"))?; - - if url.is_empty() || anon_key.is_empty() { - return Err(FlowyError::new( - ErrorCode::InvalidAuthConfig, - "Missing SUPABASE_URL or SUPABASE_ANON_KEY", - )); - } - - Ok(Self { url, anon_key }) - } - - /// Write the configuration to the environment variables. - pub fn write_env(&self) { - std::env::set_var(SUPABASE_URL, &self.url); - std::env::set_var(SUPABASE_ANON_KEY, &self.anon_key); - } -} diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index 76fcd99a32..e746aca35a 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -18,11 +18,12 @@ serde.workspace = true serde_json.workspace = true thiserror = "1.0" tokio = { workspace = true, features = ["sync"] } -parking_lot.workspace = true lazy_static = "1.4.0" bytes = { workspace = true, features = ["serde"] } tokio-retry = "0.3" anyhow.workspace = true +arc-swap.workspace = true +dashmap.workspace = true uuid.workspace = true chrono = { workspace = true, default-features = false, features = ["clock", "serde"] } collab = { workspace = true } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs index 57c97a3a67..7ecebac485 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs @@ -5,7 +5,6 @@ use client_api::entity::ai_dto::{ use client_api::entity::QueryCollabResult::{Failed, Success}; use client_api::entity::{QueryCollab, QueryCollabParams}; use client_api::error::ErrorCode::RecordNotFound; -use collab::core::collab::DataSource; use collab::entity::EncodedCollab; use collab_entity::CollabType; use serde_json::{Map, Value}; @@ -13,12 +12,11 @@ use std::sync::Arc; use tracing::{error, instrument}; use flowy_database_pub::cloud::{ - CollabDocStateByOid, DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, - SummaryRowContent, TranslateRowContent, TranslateRowResponse, + DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid, SummaryRowContent, + TranslateRowContent, TranslateRowResponse, }; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; -use lib_infra::future::FutureResult; use crate::af_cloud::define::ServerUser; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; @@ -35,93 +33,85 @@ where T: AFServer, { #[instrument(level = "debug", skip_all)] - fn get_database_object_doc_state( + async fn get_database_encode_collab( &self, object_id: &str, collab_type: CollabType, workspace_id: &str, - ) -> FutureResult>, Error> { + ) -> Result, Error> { let workspace_id = workspace_id.to_string(); let object_id = object_id.to_string(); let try_get_client = self.inner.try_get_client(); let cloned_user = self.user.clone(); - FutureResult::new(async move { - let params = QueryCollabParams { - workspace_id: workspace_id.clone(), - inner: QueryCollab::new(object_id.clone(), collab_type.clone()), - }; - match try_get_client?.get_collab(params).await { - Ok(data) => { - check_request_workspace_id_is_match( - &workspace_id, - &cloned_user, - format!("get database object: {}:{}", object_id, collab_type), - )?; - Ok(Some(data.encode_collab.doc_state.to_vec())) - }, - Err(err) => { - if err.code == RecordNotFound { - Ok(None) - } else { - Err(Error::new(err)) - } - }, - } - }) + let params = QueryCollabParams { + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(object_id.clone(), collab_type.clone()), + }; + match try_get_client?.get_collab(params).await { + Ok(data) => { + check_request_workspace_id_is_match( + &workspace_id, + &cloned_user, + format!("get database object: {}:{}", object_id, collab_type), + )?; + Ok(Some(data.encode_collab)) + }, + Err(err) => { + if err.code == RecordNotFound { + Ok(None) + } else { + Err(Error::new(err)) + } + }, + } } #[instrument(level = "debug", skip_all)] - fn batch_get_database_object_doc_state( + async fn batch_get_database_encode_collab( &self, object_ids: Vec, object_ty: CollabType, workspace_id: &str, - ) -> FutureResult { + ) -> Result { let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); let cloned_user = self.user.clone(); - FutureResult::new(async move { - let client = try_get_client?; - let params = object_ids + let client = try_get_client?; + let params = object_ids + .into_iter() + .map(|object_id| QueryCollab::new(object_id, object_ty.clone())) + .collect(); + let results = client.batch_get_collab(&workspace_id, params).await?; + check_request_workspace_id_is_match(&workspace_id, &cloned_user, "batch get database object")?; + Ok( + results + .0 .into_iter() - .map(|object_id| QueryCollab::new(object_id, object_ty.clone())) - .collect(); - let results = client.batch_get_collab(&workspace_id, params).await?; - check_request_workspace_id_is_match( - &workspace_id, - &cloned_user, - "batch get database object", - )?; - Ok( - results - .0 - .into_iter() - .flat_map(|(object_id, result)| match result { - Success { encode_collab_v1 } => { - match EncodedCollab::decode_from_bytes(&encode_collab_v1) { - Ok(encode) => Some((object_id, DataSource::DocStateV1(encode.doc_state.to_vec()))), - Err(err) => { - error!("Failed to decode collab: {}", err); - None - }, - } - }, - Failed { error } => { - error!("Failed to get {} update: {}", object_id, error); - None - }, - }) - .collect::(), - ) - }) + .flat_map(|(object_id, result)| match result { + Success { encode_collab_v1 } => { + match EncodedCollab::decode_from_bytes(&encode_collab_v1) { + Ok(encode) => Some((object_id, encode)), + Err(err) => { + error!("Failed to decode collab: {}", err); + None + }, + } + }, + Failed { error } => { + error!("Failed to get {} update: {}", object_id, error); + None + }, + }) + .collect::(), + ) } - fn get_database_collab_object_snapshots( + async fn get_database_collab_object_snapshots( &self, _object_id: &str, _limit: usize, - ) -> FutureResult, Error> { - FutureResult::new(async move { Ok(vec![]) }) + ) -> Result, Error> { + Ok(vec![]) } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs index 98732aa521..fc4d1dff75 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs @@ -9,7 +9,7 @@ use tracing::instrument; use flowy_document_pub::cloud::*; use flowy_error::FlowyError; -use lib_infra::future::FutureResult; +use lib_infra::async_trait::async_trait; use crate::af_cloud::define::ServerUser; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; @@ -20,86 +20,83 @@ pub(crate) struct AFCloudDocumentCloudServiceImpl { pub user: Arc, } +#[async_trait] impl DocumentCloudService for AFCloudDocumentCloudServiceImpl where T: AFServer, { #[instrument(level = "debug", skip_all, fields(document_id = %document_id))] - fn get_document_doc_state( + async fn get_document_doc_state( &self, document_id: &str, workspace_id: &str, - ) -> FutureResult, FlowyError> { + ) -> Result, FlowyError> { let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); let document_id = document_id.to_string(); let cloned_user = self.user.clone(); - FutureResult::new(async move { - let params = QueryCollabParams { - workspace_id: workspace_id.clone(), - inner: QueryCollab::new(document_id.to_string(), CollabType::Document), - }; - let doc_state = try_get_client? - .get_collab(params) - .await - .map_err(FlowyError::from)? - .encode_collab - .doc_state - .to_vec(); + let params = QueryCollabParams { + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(document_id.to_string(), CollabType::Document), + }; + let doc_state = try_get_client? + .get_collab(params) + .await + .map_err(FlowyError::from)? + .encode_collab + .doc_state + .to_vec(); - check_request_workspace_id_is_match( - &workspace_id, - &cloned_user, - format!("get document doc state:{}", document_id), - )?; + check_request_workspace_id_is_match( + &workspace_id, + &cloned_user, + format!("get document doc state:{}", document_id), + )?; - Ok(doc_state) - }) + Ok(doc_state) } - fn get_document_snapshots( + async fn get_document_snapshots( &self, _document_id: &str, _limit: usize, _workspace_id: &str, - ) -> FutureResult, Error> { - FutureResult::new(async move { Ok(vec![]) }) + ) -> Result, Error> { + Ok(vec![]) } #[instrument(level = "debug", skip_all)] - fn get_document_data( + async fn get_document_data( &self, document_id: &str, workspace_id: &str, - ) -> FutureResult, Error> { + ) -> Result, Error> { let try_get_client = self.inner.try_get_client(); let document_id = document_id.to_string(); let workspace_id = workspace_id.to_string(); let cloned_user = self.user.clone(); - FutureResult::new(async move { - let params = QueryCollabParams { - workspace_id: workspace_id.clone(), - inner: QueryCollab::new(document_id.clone(), CollabType::Document), - }; - let doc_state = try_get_client? - .get_collab(params) - .await - .map_err(FlowyError::from)? - .encode_collab - .doc_state - .to_vec(); - check_request_workspace_id_is_match( - &workspace_id, - &cloned_user, - format!("Get {} document", document_id), - )?; - let document = Document::from_doc_state( - CollabOrigin::Empty, - DataSource::DocStateV1(doc_state), - &document_id, - vec![], - )?; - Ok(document.get_document_data().ok()) - }) + let params = QueryCollabParams { + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(document_id.clone(), CollabType::Document), + }; + let doc_state = try_get_client? + .get_collab(params) + .await + .map_err(FlowyError::from)? + .encode_collab + .doc_state + .to_vec(); + check_request_workspace_id_is_match( + &workspace_id, + &cloned_user, + format!("Get {} document", document_id), + )?; + let document = Document::open_with_options( + CollabOrigin::Empty, + DataSource::DocStateV1(doc_state), + &document_id, + vec![], + )?; + Ok(document.get_document_data().ok()) } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs index c8296bd01e..ec9bf53d1d 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs @@ -18,7 +18,7 @@ use flowy_folder_pub::cloud::{ WorkspaceRecord, }; use flowy_folder_pub::entities::{PublishInfoResponse, PublishPayload}; -use lib_infra::future::FutureResult; +use lib_infra::async_trait::async_trait; use crate::af_cloud::define::ServerUser; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; @@ -29,167 +29,160 @@ pub(crate) struct AFCloudFolderCloudServiceImpl { pub user: Arc, } +#[async_trait] impl FolderCloudService for AFCloudFolderCloudServiceImpl where T: AFServer, { - fn create_workspace(&self, _uid: i64, name: &str) -> FutureResult { + async fn create_workspace(&self, _uid: i64, name: &str) -> Result { let try_get_client = self.inner.try_get_client(); let cloned_name = name.to_string(); - FutureResult::new(async move { - let client = try_get_client?; - let new_workspace = client - .create_workspace(CreateWorkspaceParam { - workspace_name: Some(cloned_name), - }) - .await?; - Ok(Workspace { - id: new_workspace.workspace_id.to_string(), - name: new_workspace.workspace_name, - created_at: new_workspace.created_at.timestamp(), - child_views: RepeatedViewIdentifier::new(vec![]), - created_by: Some(new_workspace.owner_uid), - last_edited_time: new_workspace.created_at.timestamp(), - last_edited_by: Some(new_workspace.owner_uid), + let client = try_get_client?; + let new_workspace = client + .create_workspace(CreateWorkspaceParam { + workspace_name: Some(cloned_name), }) + .await?; + + Ok(Workspace { + id: new_workspace.workspace_id.to_string(), + name: new_workspace.workspace_name, + created_at: new_workspace.created_at.timestamp(), + child_views: RepeatedViewIdentifier::new(vec![]), + created_by: Some(new_workspace.owner_uid), + last_edited_time: new_workspace.created_at.timestamp(), + last_edited_by: Some(new_workspace.owner_uid), }) } - fn open_workspace(&self, workspace_id: &str) -> FutureResult<(), Error> { + async fn open_workspace(&self, workspace_id: &str) -> Result<(), Error> { let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - let _ = client.open_workspace(&workspace_id).await?; - Ok(()) - }) + + let client = try_get_client?; + let _ = client.open_workspace(&workspace_id).await?; + Ok(()) } - fn get_all_workspace(&self) -> FutureResult, Error> { + async fn get_all_workspace(&self) -> Result, Error> { let try_get_client = self.inner.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - let records = client - .get_user_workspace_info() - .await? - .workspaces - .into_iter() - .map(|af_workspace| WorkspaceRecord { - id: af_workspace.workspace_id.to_string(), - name: af_workspace.workspace_name, - created_at: af_workspace.created_at.timestamp(), - }) - .collect::>(); - Ok(records) - }) + + let client = try_get_client?; + let records = client + .get_user_workspace_info() + .await? + .workspaces + .into_iter() + .map(|af_workspace| WorkspaceRecord { + id: af_workspace.workspace_id.to_string(), + name: af_workspace.workspace_name, + created_at: af_workspace.created_at.timestamp(), + }) + .collect::>(); + Ok(records) } + #[instrument(level = "debug", skip_all)] - fn get_folder_data( + async fn get_folder_data( &self, workspace_id: &str, uid: &i64, - ) -> FutureResult, Error> { + ) -> Result, Error> { let uid = *uid; let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); let cloned_user = self.user.clone(); - FutureResult::new(async move { - let params = QueryCollabParams { - workspace_id: workspace_id.clone(), - inner: QueryCollab::new(workspace_id.clone(), CollabType::Folder), - }; - let doc_state = try_get_client? - .get_collab(params) - .await - .map_err(FlowyError::from)? - .encode_collab - .doc_state - .to_vec(); - check_request_workspace_id_is_match(&workspace_id, &cloned_user, "get folder data")?; - let folder = Folder::from_collab_doc_state( - uid, - CollabOrigin::Empty, - DataSource::DocStateV1(doc_state), - &workspace_id, - vec![], - )?; - Ok(folder.get_folder_data(&workspace_id)) - }) + let params = QueryCollabParams { + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(workspace_id.clone(), CollabType::Folder), + }; + let doc_state = try_get_client? + .get_collab(params) + .await + .map_err(FlowyError::from)? + .encode_collab + .doc_state + .to_vec(); + check_request_workspace_id_is_match(&workspace_id, &cloned_user, "get folder data")?; + let folder = Folder::from_collab_doc_state( + uid, + CollabOrigin::Empty, + DataSource::DocStateV1(doc_state), + &workspace_id, + vec![], + )?; + Ok(folder.get_folder_data(&workspace_id)) } - fn get_folder_snapshots( + async fn get_folder_snapshots( &self, _workspace_id: &str, _limit: usize, - ) -> FutureResult, Error> { - FutureResult::new(async move { Ok(vec![]) }) + ) -> Result, Error> { + Ok(vec![]) } #[instrument(level = "debug", skip_all)] - fn get_folder_doc_state( + async fn get_folder_doc_state( &self, workspace_id: &str, _uid: i64, collab_type: CollabType, object_id: &str, - ) -> FutureResult, Error> { + ) -> Result, Error> { let object_id = object_id.to_string(); let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); let cloned_user = self.user.clone(); - FutureResult::new(async move { - let params = QueryCollabParams { - workspace_id: workspace_id.clone(), - inner: QueryCollab::new(object_id, collab_type), - }; - let doc_state = try_get_client? - .get_collab(params) - .await - .map_err(FlowyError::from)? - .encode_collab - .doc_state - .to_vec(); - check_request_workspace_id_is_match(&workspace_id, &cloned_user, "get folder doc state")?; - Ok(doc_state) - }) + let params = QueryCollabParams { + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(object_id, collab_type), + }; + let doc_state = try_get_client? + .get_collab(params) + .await + .map_err(FlowyError::from)? + .encode_collab + .doc_state + .to_vec(); + check_request_workspace_id_is_match(&workspace_id, &cloned_user, "get folder doc state")?; + Ok(doc_state) } - fn batch_create_folder_collab_objects( + async fn batch_create_folder_collab_objects( &self, workspace_id: &str, objects: Vec, - ) -> FutureResult<(), Error> { + ) -> Result<(), Error> { let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); - FutureResult::new(async move { - let params = objects - .into_iter() - .map(|object| { - CollabParams::new( - object.object_id, - object.collab_type, - object.encoded_collab_v1, - ) - }) - .collect::>(); - try_get_client? - .create_collab_list(&workspace_id, params) - .await - .map_err(FlowyError::from)?; - Ok(()) - }) + let params = objects + .into_iter() + .map(|object| { + CollabParams::new( + object.object_id, + object.collab_type, + object.encoded_collab_v1, + ) + }) + .collect::>(); + try_get_client? + .create_collab_list(&workspace_id, params) + .await + .map_err(FlowyError::from)?; + Ok(()) } fn service_name(&self) -> String { "AppFlowy Cloud".to_string() } - fn publish_view( + async fn publish_view( &self, workspace_id: &str, payload: Vec, - ) -> FutureResult<(), Error> { + ) -> Result<(), Error> { let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); let params = payload @@ -212,76 +205,66 @@ where }) }) .collect::>(); - FutureResult::new(async move { - try_get_client? - .publish_collabs(&workspace_id, params) - .await - .map_err(FlowyError::from)?; - Ok(()) - }) + try_get_client? + .publish_collabs(&workspace_id, params) + .await + .map_err(FlowyError::from)?; + Ok(()) } - fn unpublish_views(&self, workspace_id: &str, view_ids: Vec) -> FutureResult<(), Error> { + async fn unpublish_views(&self, workspace_id: &str, view_ids: Vec) -> Result<(), Error> { let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); let view_uuids = view_ids .iter() .map(|id| Uuid::parse_str(id).unwrap_or(Uuid::nil())) .collect::>(); - FutureResult::new(async move { - try_get_client? - .unpublish_collabs(&workspace_id, &view_uuids) - .await - .map_err(FlowyError::from)?; - Ok(()) - }) + try_get_client? + .unpublish_collabs(&workspace_id, &view_uuids) + .await + .map_err(FlowyError::from)?; + Ok(()) } - fn get_publish_info(&self, view_id: &str) -> FutureResult { + async fn get_publish_info(&self, view_id: &str) -> Result { let try_get_client = self.inner.try_get_client(); let view_id = Uuid::parse_str(view_id) .map_err(|_| FlowyError::new(ErrorCode::InvalidParams, "Invalid view id")); - FutureResult::new(async move { - let view_id = view_id?; - let info = try_get_client? - .get_published_collab_info(&view_id) - .await - .map_err(FlowyError::from)?; - Ok(PublishInfoResponse { - view_id: info.view_id.to_string(), - publish_name: info.publish_name, - namespace: info.namespace, - }) + let view_id = view_id?; + let info = try_get_client? + .get_published_collab_info(&view_id) + .await + .map_err(FlowyError::from)?; + Ok(PublishInfoResponse { + view_id: info.view_id.to_string(), + publish_name: info.publish_name, + namespace: info.namespace, }) } - fn set_publish_namespace( + async fn set_publish_namespace( &self, workspace_id: &str, new_namespace: &str, - ) -> FutureResult<(), Error> { + ) -> Result<(), Error> { let workspace_id = workspace_id.to_string(); let namespace = new_namespace.to_string(); let try_get_client = self.inner.try_get_client(); - FutureResult::new(async move { - try_get_client? - .set_workspace_publish_namespace(&workspace_id, &namespace) - .await - .map_err(FlowyError::from)?; - Ok(()) - }) + try_get_client? + .set_workspace_publish_namespace(&workspace_id, &namespace) + .await + .map_err(FlowyError::from)?; + Ok(()) } - fn get_publish_namespace(&self, workspace_id: &str) -> FutureResult { + async fn get_publish_namespace(&self, workspace_id: &str) -> Result { let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); - FutureResult::new(async move { - let namespace = try_get_client? - .get_workspace_publish_namespace(&workspace_id) - .await - .map_err(FlowyError::from)?; - Ok(namespace) - }) + let namespace = try_get_client? + .get_workspace_publish_namespace(&workspace_id) + .await + .map_err(FlowyError::from)?; + Ok(namespace) } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index e8a14e5ee4..5df9dce1dd 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::sync::Arc; use anyhow::anyhow; +use arc_swap::ArcSwapOption; use client_api::entity::billing_dto::{ RecurringInterval, SetSubscriptionRecurringInterval, SubscriptionCancelRequest, SubscriptionPlan, SubscriptionPlanDetail, WorkspaceSubscriptionStatus, WorkspaceUsageAndLimit, @@ -16,7 +17,6 @@ use client_api::entity::{ use client_api::entity::{QueryCollab, QueryCollabParams}; use client_api::{Client, ClientConfiguration}; use collab_entity::{CollabObject, CollabType}; -use parking_lot::RwLock; use tracing::instrument; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; @@ -25,8 +25,8 @@ use flowy_user_pub::entities::{ AFCloudOAuthParams, AuthResponse, Role, UpdateUserProfileParams, UserCredentials, UserProfile, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; +use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; -use lib_infra::future::FutureResult; use uuid::Uuid; use crate::af_cloud::define::{ServerUser, USER_SIGN_IN_URL}; @@ -41,7 +41,7 @@ use super::dto::{from_af_workspace_invitation_status, to_workspace_invitation_st pub(crate) struct AFCloudUserAuthServiceImpl { server: T, - user_change_recv: RwLock>>, + user_change_recv: ArcSwapOption>, user: Arc, } @@ -53,613 +53,533 @@ impl AFCloudUserAuthServiceImpl { ) -> Self { Self { server, - user_change_recv: RwLock::new(Some(user_change_recv)), + user_change_recv: ArcSwapOption::new(Some(Arc::new(user_change_recv))), user, } } } +#[async_trait] impl UserCloudService for AFCloudUserAuthServiceImpl where T: AFServer, { - fn sign_up(&self, params: BoxAny) -> FutureResult { + async fn sign_up(&self, params: BoxAny) -> Result { let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let params = oauth_params_from_box_any(params)?; - let resp = user_sign_up_request(try_get_client?, params).await?; - Ok(resp) - }) + let params = oauth_params_from_box_any(params)?; + let resp = user_sign_up_request(try_get_client?, params).await?; + Ok(resp) } // Zack: Not sure if this is needed anymore since sign_up handles both cases - fn sign_in(&self, params: BoxAny) -> FutureResult { + async fn sign_in(&self, params: BoxAny) -> Result { let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - let params = oauth_params_from_box_any(params)?; - let resp = user_sign_in_with_url(client, params).await?; - Ok(resp) - }) + let client = try_get_client?; + let params = oauth_params_from_box_any(params)?; + let resp = user_sign_in_with_url(client, params).await?; + Ok(resp) } - fn sign_out(&self, _token: Option) -> FutureResult<(), FlowyError> { + async fn sign_out(&self, _token: Option) -> Result<(), FlowyError> { // Calling the sign_out method that will revoke all connected devices' refresh tokens. // So do nothing here. - FutureResult::new(async move { Ok(()) }) + Ok(()) } - fn generate_sign_in_url_with_email(&self, email: &str) -> FutureResult { + async fn generate_sign_in_url_with_email(&self, email: &str) -> Result { let email = email.to_string(); let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - let admin_client = get_admin_client(&client).await?; - let action_link = admin_client.generate_sign_in_action_link(&email).await?; - let sign_in_url = client.extract_sign_in_url(&action_link).await?; - Ok(sign_in_url) - }) + let client = try_get_client?; + let admin_client = get_admin_client(&client).await?; + let action_link = admin_client.generate_sign_in_action_link(&email).await?; + let sign_in_url = client.extract_sign_in_url(&action_link).await?; + Ok(sign_in_url) } - fn create_user(&self, email: &str, password: &str) -> FutureResult<(), FlowyError> { + async fn create_user(&self, email: &str, password: &str) -> Result<(), FlowyError> { let password = password.to_string(); let email = email.to_string(); let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - let admin_client = get_admin_client(&client).await?; - admin_client - .create_email_verified_user(&email, &password) - .await?; + let client = try_get_client?; + let admin_client = get_admin_client(&client).await?; + admin_client + .create_email_verified_user(&email, &password) + .await?; - Ok(()) - }) + Ok(()) } - fn sign_in_with_password( + async fn sign_in_with_password( &self, email: &str, password: &str, - ) -> FutureResult { + ) -> Result { let password = password.to_string(); let email = email.to_string(); let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - client.sign_in_password(&email, &password).await?; - let profile = client.get_profile().await?; - let token = client.get_token()?; - let profile = user_profile_from_af_profile(token, profile)?; - Ok(profile) - }) + let client = try_get_client?; + client.sign_in_password(&email, &password).await?; + let profile = client.get_profile().await?; + let token = client.get_token()?; + let profile = user_profile_from_af_profile(token, profile)?; + Ok(profile) } - fn sign_in_with_magic_link( + async fn sign_in_with_magic_link( &self, email: &str, redirect_to: &str, - ) -> FutureResult<(), FlowyError> { + ) -> Result<(), FlowyError> { let email = email.to_owned(); let redirect_to = redirect_to.to_owned(); let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - client - .sign_in_with_magic_link(&email, Some(redirect_to)) - .await?; - Ok(()) - }) + let client = try_get_client?; + client + .sign_in_with_magic_link(&email, Some(redirect_to)) + .await?; + Ok(()) } - fn generate_oauth_url_with_provider(&self, provider: &str) -> FutureResult { + async fn generate_oauth_url_with_provider(&self, provider: &str) -> Result { let provider = AuthProvider::from(provider); let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let provider = provider.ok_or(anyhow!("invalid provider"))?; - let url = try_get_client? - .generate_oauth_url_with_provider(&provider) - .await?; - Ok(url) - }) + let provider = provider.ok_or(anyhow!("invalid provider"))?; + let url = try_get_client? + .generate_oauth_url_with_provider(&provider) + .await?; + Ok(url) } - fn update_user( + async fn update_user( &self, _credential: UserCredentials, params: UpdateUserProfileParams, - ) -> FutureResult<(), FlowyError> { + ) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - client - .update_user(af_update_from_update_params(params)) - .await?; - Ok(()) - }) + let client = try_get_client?; + client + .update_user(af_update_from_update_params(params)) + .await?; + Ok(()) } #[instrument(level = "debug", skip_all)] - fn get_user_profile( + async fn get_user_profile( &self, _credential: UserCredentials, - ) -> FutureResult { + ) -> Result { let try_get_client = self.server.try_get_client(); let cloned_user = self.user.clone(); - FutureResult::new(async move { - let expected_workspace_id = cloned_user.workspace_id()?; - let client = try_get_client?; - let profile = client.get_profile().await?; - let token = client.get_token()?; - let profile = user_profile_from_af_profile(token, profile)?; + let expected_workspace_id = cloned_user.workspace_id()?; + let client = try_get_client?; + let profile = client.get_profile().await?; + let token = client.get_token()?; + let profile = user_profile_from_af_profile(token, profile)?; - // Discard the response if the user has switched to a new workspace. This avoids updating the - // user profile with potentially outdated information when the workspace ID no longer matches. - check_request_workspace_id_is_match( - &expected_workspace_id, - &cloned_user, - "get user profile", - )?; - Ok(profile) - }) + // Discard the response if the user has switched to a new workspace. This avoids updating the + // user profile with potentially outdated information when the workspace ID no longer matches. + check_request_workspace_id_is_match(&expected_workspace_id, &cloned_user, "get user profile")?; + Ok(profile) } - fn open_workspace(&self, workspace_id: &str) -> FutureResult { + async fn open_workspace(&self, workspace_id: &str) -> Result { let try_get_client = self.server.try_get_client(); let workspace_id = workspace_id.to_string(); - FutureResult::new(async move { - let client = try_get_client?; - let af_workspace = client.open_workspace(&workspace_id).await?; - Ok(to_user_workspace(af_workspace)) - }) + let client = try_get_client?; + let af_workspace = client.open_workspace(&workspace_id).await?; + Ok(to_user_workspace(af_workspace)) } - fn get_all_workspace(&self, _uid: i64) -> FutureResult, FlowyError> { + async fn get_all_workspace(&self, _uid: i64) -> Result, FlowyError> { let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let workspaces = try_get_client?.get_workspaces().await?; - to_user_workspaces(workspaces) - }) + let workspaces = try_get_client?.get_workspaces().await?; + to_user_workspaces(workspaces) } - fn invite_workspace_member( - &self, - invitee_email: String, - workspace_id: String, - role: Role, - ) -> FutureResult<(), FlowyError> { - let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - try_get_client? - .invite_workspace_members( - &workspace_id, - vec![WorkspaceMemberInvitation { - email: invitee_email, - role: to_af_role(role), - }], - ) - .await?; - Ok(()) - }) - } - - fn list_workspace_invitations( - &self, - filter: Option, - ) -> FutureResult, FlowyError> { - let try_get_client = self.server.try_get_client(); - let filter = filter.map(to_workspace_invitation_status); - - FutureResult::new(async move { - let r = try_get_client? - .list_workspace_invitations(filter) - .await? - .into_iter() - .map(to_workspace_invitation) - .collect(); - Ok(r) - }) - } - - fn accept_workspace_invitations(&self, invite_id: String) -> FutureResult<(), FlowyError> { - let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - try_get_client? - .accept_workspace_invitation(&invite_id) - .await?; - Ok(()) - }) - } - - fn remove_workspace_member( - &self, - user_email: String, - workspace_id: String, - ) -> FutureResult<(), FlowyError> { - let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - try_get_client? - .remove_workspace_members(workspace_id, vec![user_email]) - .await?; - Ok(()) - }) - } - - fn update_workspace_member( - &self, - user_email: String, - workspace_id: String, - role: Role, - ) -> FutureResult<(), FlowyError> { - let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let changeset = WorkspaceMemberChangeset::new(user_email).with_role(to_af_role(role)); - try_get_client? - .update_workspace_member(workspace_id, changeset) - .await?; - Ok(()) - }) - } - - fn get_workspace_members( - &self, - workspace_id: String, - ) -> FutureResult, FlowyError> { - let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let members = try_get_client? - .get_workspace_members(&workspace_id) - .await? - .into_iter() - .map(from_af_workspace_member) - .collect(); - Ok(members) - }) - } - - fn get_workspace_member( - &self, - workspace_id: String, - uid: i64, - ) -> FutureResult { - let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - let query = QueryWorkspaceMember { - workspace_id: workspace_id.clone(), - uid, - }; - let member = client.get_workspace_member(query).await?; - Ok(from_af_workspace_member(member)) - }) - } - - #[instrument(level = "debug", skip_all)] - fn get_user_awareness_doc_state( - &self, - _uid: i64, - workspace_id: &str, - object_id: &str, - ) -> FutureResult, FlowyError> { - let workspace_id = workspace_id.to_string(); - let object_id = object_id.to_string(); - let try_get_client = self.server.try_get_client(); - let cloned_user = self.user.clone(); - FutureResult::new(async move { - let params = QueryCollabParams { - workspace_id: workspace_id.clone(), - inner: QueryCollab::new(object_id, CollabType::UserAwareness), - }; - let resp = try_get_client?.get_collab(params).await?; - check_request_workspace_id_is_match( - &workspace_id, - &cloned_user, - "get user awareness object", - )?; - Ok(resp.encode_collab.doc_state.to_vec()) - }) - } - - fn subscribe_user_update(&self) -> Option { - self.user_change_recv.write().take() - } - - fn reset_workspace(&self, _collab_object: CollabObject) -> FutureResult<(), FlowyError> { - FutureResult::new(async { Ok(()) }) - } - - fn create_collab_object( - &self, - collab_object: &CollabObject, - data: Vec, - ) -> FutureResult<(), FlowyError> { - let try_get_client = self.server.try_get_client(); - let collab_object = collab_object.clone(); - FutureResult::new(async move { - let client = try_get_client?; - let params = CreateCollabParams { - workspace_id: collab_object.workspace_id, - object_id: collab_object.object_id, - collab_type: collab_object.collab_type, - encoded_collab_v1: data, - }; - client.create_collab(params).await?; - Ok(()) - }) - } - - fn batch_create_collab_object( - &self, - workspace_id: &str, - objects: Vec, - ) -> FutureResult<(), FlowyError> { - let workspace_id = workspace_id.to_string(); - let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let params = objects - .into_iter() - .map(|object| { - CollabParams::new( - object.object_id, - u8::from(object.collab_type).into(), - object.encoded_collab, - ) - }) - .collect::>(); - try_get_client? - .create_collab_list(&workspace_id, params) - .await - .map_err(FlowyError::from)?; - Ok(()) - }) - } - - fn create_workspace(&self, workspace_name: &str) -> FutureResult { + async fn create_workspace(&self, workspace_name: &str) -> Result { let try_get_client = self.server.try_get_client(); let workspace_name_owned = workspace_name.to_owned(); - FutureResult::new(async move { - let client = try_get_client?; - let new_workspace = client - .create_workspace(CreateWorkspaceParam { - workspace_name: Some(workspace_name_owned), - }) - .await?; - Ok(to_user_workspace(new_workspace)) - }) + let client = try_get_client?; + let new_workspace = client + .create_workspace(CreateWorkspaceParam { + workspace_name: Some(workspace_name_owned), + }) + .await?; + Ok(to_user_workspace(new_workspace)) } - fn delete_workspace(&self, workspace_id: &str) -> FutureResult<(), FlowyError> { - let try_get_client = self.server.try_get_client(); - let workspace_id_owned = workspace_id.to_owned(); - FutureResult::new(async move { - let client = try_get_client?; - client.delete_workspace(&workspace_id_owned).await?; - Ok(()) - }) - } - - fn patch_workspace( + async fn patch_workspace( &self, workspace_id: &str, new_workspace_name: Option<&str>, new_workspace_icon: Option<&str>, - ) -> FutureResult<(), FlowyError> { + ) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); let owned_workspace_id = workspace_id.to_owned(); let owned_workspace_name = new_workspace_name.map(|s| s.to_owned()); let owned_workspace_icon = new_workspace_icon.map(|s| s.to_owned()); - FutureResult::new(async move { - let workspace_id: Uuid = owned_workspace_id - .parse() - .map_err(|_| ErrorCode::InvalidParams)?; - let client = try_get_client?; - client - .patch_workspace(PatchWorkspaceParam { - workspace_id, - workspace_name: owned_workspace_name, - workspace_icon: owned_workspace_icon, - }) - .await?; - Ok(()) - }) + let workspace_id: Uuid = owned_workspace_id + .parse() + .map_err(|_| ErrorCode::InvalidParams)?; + let client = try_get_client?; + client + .patch_workspace(PatchWorkspaceParam { + workspace_id, + workspace_name: owned_workspace_name, + workspace_icon: owned_workspace_icon, + }) + .await?; + Ok(()) } - fn leave_workspace(&self, workspace_id: &str) -> FutureResult<(), FlowyError> { + async fn delete_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> { + let try_get_client = self.server.try_get_client(); + let workspace_id_owned = workspace_id.to_owned(); + let client = try_get_client?; + client.delete_workspace(&workspace_id_owned).await?; + Ok(()) + } + + async fn invite_workspace_member( + &self, + invitee_email: String, + workspace_id: String, + role: Role, + ) -> Result<(), FlowyError> { + let try_get_client = self.server.try_get_client(); + try_get_client? + .invite_workspace_members( + &workspace_id, + vec![WorkspaceMemberInvitation { + email: invitee_email, + role: to_af_role(role), + }], + ) + .await?; + Ok(()) + } + + async fn list_workspace_invitations( + &self, + filter: Option, + ) -> Result, FlowyError> { + let try_get_client = self.server.try_get_client(); + let filter = filter.map(to_workspace_invitation_status); + + let r = try_get_client? + .list_workspace_invitations(filter) + .await? + .into_iter() + .map(to_workspace_invitation) + .collect(); + Ok(r) + } + + async fn accept_workspace_invitations(&self, invite_id: String) -> Result<(), FlowyError> { + let try_get_client = self.server.try_get_client(); + try_get_client? + .accept_workspace_invitation(&invite_id) + .await?; + Ok(()) + } + + async fn remove_workspace_member( + &self, + user_email: String, + workspace_id: String, + ) -> Result<(), FlowyError> { + let try_get_client = self.server.try_get_client(); + try_get_client? + .remove_workspace_members(workspace_id, vec![user_email]) + .await?; + Ok(()) + } + + async fn update_workspace_member( + &self, + user_email: String, + workspace_id: String, + role: Role, + ) -> Result<(), FlowyError> { + let try_get_client = self.server.try_get_client(); + let changeset = WorkspaceMemberChangeset::new(user_email).with_role(to_af_role(role)); + try_get_client? + .update_workspace_member(workspace_id, changeset) + .await?; + Ok(()) + } + + async fn get_workspace_members( + &self, + workspace_id: String, + ) -> Result, FlowyError> { + let try_get_client = self.server.try_get_client(); + let members = try_get_client? + .get_workspace_members(&workspace_id) + .await? + .into_iter() + .map(from_af_workspace_member) + .collect(); + Ok(members) + } + + async fn get_workspace_member( + &self, + workspace_id: String, + uid: i64, + ) -> Result { + let try_get_client = self.server.try_get_client(); + let client = try_get_client?; + let query = QueryWorkspaceMember { + workspace_id: workspace_id.clone(), + uid, + }; + let member = client.get_workspace_member(query).await?; + Ok(from_af_workspace_member(member)) + } + + #[instrument(level = "debug", skip_all)] + async fn get_user_awareness_doc_state( + &self, + _uid: i64, + workspace_id: &str, + object_id: &str, + ) -> Result, FlowyError> { + let workspace_id = workspace_id.to_string(); + let object_id = object_id.to_string(); + let try_get_client = self.server.try_get_client(); + let cloned_user = self.user.clone(); + let params = QueryCollabParams { + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(object_id, CollabType::UserAwareness), + }; + let resp = try_get_client?.get_collab(params).await?; + check_request_workspace_id_is_match(&workspace_id, &cloned_user, "get user awareness object")?; + Ok(resp.encode_collab.doc_state.to_vec()) + } + + fn subscribe_user_update(&self) -> Option { + let rx = self.user_change_recv.swap(None)?; + Arc::into_inner(rx) + } + + async fn reset_workspace(&self, _collab_object: CollabObject) -> Result<(), FlowyError> { + Ok(()) + } + + async fn create_collab_object( + &self, + collab_object: &CollabObject, + data: Vec, + ) -> Result<(), FlowyError> { + let try_get_client = self.server.try_get_client(); + let collab_object = collab_object.clone(); + let client = try_get_client?; + let params = CreateCollabParams { + workspace_id: collab_object.workspace_id, + object_id: collab_object.object_id, + collab_type: collab_object.collab_type, + encoded_collab_v1: data, + }; + client.create_collab(params).await?; + Ok(()) + } + + async fn batch_create_collab_object( + &self, + workspace_id: &str, + objects: Vec, + ) -> Result<(), FlowyError> { + let workspace_id = workspace_id.to_string(); + let try_get_client = self.server.try_get_client(); + let params = objects + .into_iter() + .map(|object| { + CollabParams::new( + object.object_id, + u8::from(object.collab_type).into(), + object.encoded_collab, + ) + }) + .collect::>(); + try_get_client? + .create_collab_list(&workspace_id, params) + .await + .map_err(FlowyError::from)?; + Ok(()) + } + + async fn leave_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); let workspace_id = workspace_id.to_string(); - FutureResult::new(async move { - let client = try_get_client?; - client.leave_workspace(&workspace_id).await?; - Ok(()) - }) + let client = try_get_client?; + client.leave_workspace(&workspace_id).await?; + Ok(()) } - fn subscribe_workspace( + async fn subscribe_workspace( &self, workspace_id: String, recurring_interval: RecurringInterval, subscription_plan: SubscriptionPlan, success_url: String, - ) -> FutureResult { + ) -> Result { let try_get_client = self.server.try_get_client(); let workspace_id = workspace_id.to_string(); - FutureResult::new(async move { - let client = try_get_client?; - let payment_link = client - .create_subscription( - &workspace_id, - recurring_interval, - subscription_plan, - &success_url, - ) - .await?; - Ok(payment_link) - }) + let client = try_get_client?; + let payment_link = client + .create_subscription( + &workspace_id, + recurring_interval, + subscription_plan, + &success_url, + ) + .await?; + Ok(payment_link) } - fn get_workspace_member_info( + async fn get_workspace_member_info( &self, workspace_id: &str, uid: i64, - ) -> FutureResult { + ) -> Result { let try_get_client = self.server.try_get_client(); let workspace_id = workspace_id.to_string(); - FutureResult::new(async move { - let client = try_get_client?; - let params = QueryWorkspaceMember { - workspace_id: workspace_id.to_string(), - uid, - }; - let member = client.get_workspace_member(params).await?; - let role = match member.role { - AFRole::Owner => Role::Owner, - AFRole::Member => Role::Member, - AFRole::Guest => Role::Guest, - }; - Ok(WorkspaceMember { - email: member.email, - role, - name: member.name, - avatar_url: member.avatar_url, - }) + let client = try_get_client?; + let params = QueryWorkspaceMember { + workspace_id: workspace_id.to_string(), + uid, + }; + let member = client.get_workspace_member(params).await?; + let role = match member.role { + AFRole::Owner => Role::Owner, + AFRole::Member => Role::Member, + AFRole::Guest => Role::Guest, + }; + Ok(WorkspaceMember { + email: member.email, + role, + name: member.name, + avatar_url: member.avatar_url, }) } - fn get_workspace_subscriptions( + async fn get_workspace_subscriptions( &self, - ) -> FutureResult, FlowyError> { + ) -> Result, FlowyError> { let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - let workspace_subscriptions = client.list_subscription().await?; - Ok(workspace_subscriptions) - }) + let client = try_get_client?; + let workspace_subscriptions = client.list_subscription().await?; + Ok(workspace_subscriptions) } - fn get_workspace_subscription_one( + async fn get_workspace_subscription_one( &self, workspace_id: String, - ) -> FutureResult, FlowyError> { + ) -> Result, FlowyError> { let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - let workspace_subscriptions = client.get_workspace_subscriptions(&workspace_id).await?; - Ok(workspace_subscriptions) - }) + let client = try_get_client?; + let workspace_subscriptions = client.get_workspace_subscriptions(&workspace_id).await?; + Ok(workspace_subscriptions) } - fn cancel_workspace_subscription( + async fn cancel_workspace_subscription( &self, workspace_id: String, plan: SubscriptionPlan, reason: Option, - ) -> FutureResult<(), FlowyError> { + ) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - client - .cancel_subscription(&SubscriptionCancelRequest { - workspace_id, - plan, - sync: true, - reason, - }) - .await?; - Ok(()) - }) + let client = try_get_client?; + client + .cancel_subscription(&SubscriptionCancelRequest { + workspace_id, + plan, + sync: true, + reason, + }) + .await?; + Ok(()) } - fn get_workspace_plan( + async fn get_workspace_plan( &self, workspace_id: String, - ) -> FutureResult, FlowyError> { + ) -> Result, FlowyError> { let workspace_id = workspace_id.to_string(); let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - let plans = client - .get_active_workspace_subscriptions(&workspace_id) - .await?; - Ok(plans) - }) + let client = try_get_client?; + let plans = client + .get_active_workspace_subscriptions(&workspace_id) + .await?; + Ok(plans) } - fn get_workspace_usage( + async fn get_workspace_usage( &self, workspace_id: String, - ) -> FutureResult { + ) -> Result { let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - let usage = client.get_workspace_usage_and_limit(&workspace_id).await?; - Ok(usage) - }) + let client = try_get_client?; + let usage = client.get_workspace_usage_and_limit(&workspace_id).await?; + Ok(usage) } - fn get_billing_portal_url(&self) -> FutureResult { + async fn get_billing_portal_url(&self) -> Result { let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - let url = client.get_portal_session_link().await?; - Ok(url) - }) + let client = try_get_client?; + let url = client.get_portal_session_link().await?; + Ok(url) } - fn update_workspace_subscription_payment_period( + async fn update_workspace_subscription_payment_period( &self, workspace_id: String, plan: SubscriptionPlan, recurring_interval: RecurringInterval, - ) -> FutureResult<(), FlowyError> { + ) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - client - .set_subscription_recurring_interval(&SetSubscriptionRecurringInterval { - workspace_id, - plan, - recurring_interval, - }) - .await?; - Ok(()) - }) + let client = try_get_client?; + client + .set_subscription_recurring_interval(&SetSubscriptionRecurringInterval { + workspace_id, + plan, + recurring_interval, + }) + .await?; + Ok(()) } - fn get_subscription_plan_details(&self) -> FutureResult, FlowyError> { + async fn get_subscription_plan_details(&self) -> Result, FlowyError> { let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - let plan_details = client.get_subscription_plan_details().await?; - Ok(plan_details) - }) + let client = try_get_client?; + let plan_details = client.get_subscription_plan_details().await?; + Ok(plan_details) } - fn get_workspace_setting( + async fn get_workspace_setting( &self, workspace_id: &str, - ) -> FutureResult { + ) -> Result { let workspace_id = workspace_id.to_string(); let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - let settings = client.get_workspace_settings(&workspace_id).await?; - Ok(settings) - }) + let client = try_get_client?; + let settings = client.get_workspace_settings(&workspace_id).await?; + Ok(settings) } - fn update_workspace_setting( + async fn update_workspace_setting( &self, workspace_id: &str, workspace_settings: AFWorkspaceSettingsChange, - ) -> FutureResult { + ) -> Result { let workspace_id = workspace_id.to_string(); let try_get_client = self.server.try_get_client(); - FutureResult::new(async move { - let client = try_get_client?; - let settings = client - .update_workspace_settings(&workspace_id, &workspace_settings) - .await?; - Ok(settings) - }) + let client = try_get_client?; + let settings = client + .update_workspace_settings(&workspace_id, &workspace_settings) + .await?; + Ok(settings) } } diff --git a/frontend/rust-lib/flowy-server/src/lib.rs b/frontend/rust-lib/flowy-server/src/lib.rs index 704e9e0e49..33f4b0c0d8 100644 --- a/frontend/rust-lib/flowy-server/src/lib.rs +++ b/frontend/rust-lib/flowy-server/src/lib.rs @@ -5,8 +5,5 @@ pub mod local_server; mod response; mod server; -#[cfg(feature = "enable_supabase")] -pub mod supabase; - mod default_impl; pub mod util; diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs index 7195430a8f..6d2ad4deab 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs @@ -1,79 +1,64 @@ use anyhow::Error; +use collab::core::transaction::DocTransactionExtension; +use collab::entity::EncodedCollab; use collab::preclude::Collab; use collab_entity::define::{DATABASE, DATABASE_ROW_DATA, WORKSPACE_DATABASES}; use collab_entity::CollabType; -use yrs::MapPrelim; +use yrs::{ArrayPrelim, Map, MapPrelim}; -use flowy_database_pub::cloud::{CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot}; +use flowy_database_pub::cloud::{DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid}; use lib_infra::async_trait::async_trait; -use lib_infra::future::FutureResult; pub(crate) struct LocalServerDatabaseCloudServiceImpl(); #[async_trait] impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { - fn get_database_object_doc_state( + async fn get_database_encode_collab( &self, object_id: &str, collab_type: CollabType, _workspace_id: &str, - ) -> FutureResult>, Error> { + ) -> Result, Error> { let object_id = object_id.to_string(); // create the minimal required data for the given collab type - FutureResult::new(async move { - let data = match collab_type { - CollabType::Database => { - let collab = Collab::new(1, object_id, collab_type, vec![], false); - collab.with_origin_transact_mut(|txn| { - collab.insert_map_with_txn(txn, DATABASE); - }); - collab - .encode_collab_v1(|_| Ok::<(), Error>(()))? - .doc_state - .to_vec() - }, - CollabType::WorkspaceDatabase => { - let collab = Collab::new(1, object_id, collab_type, vec![], false); - collab.with_origin_transact_mut(|txn| { - collab.create_array_with_txn::(txn, WORKSPACE_DATABASES, vec![]); - }); - collab - .encode_collab_v1(|_| Ok::<(), Error>(()))? - .doc_state - .to_vec() - }, - CollabType::DatabaseRow => { - let collab = Collab::new(1, object_id, collab_type, vec![], false); - collab.with_origin_transact_mut(|txn| { - collab.insert_map_with_txn(txn, DATABASE_ROW_DATA); - }); - collab - .encode_collab_v1(|_| Ok::<(), Error>(()))? - .doc_state - .to_vec() - }, - _ => vec![], - }; - Ok(Some(data)) - }) + let mut collab = Collab::new(1, object_id, collab_type.clone(), vec![], false); + let mut txn = collab.context.transact_mut(); + match collab_type { + CollabType::Database => { + collab.data.insert(&mut txn, DATABASE, MapPrelim::default()); + }, + CollabType::WorkspaceDatabase => { + collab + .data + .insert(&mut txn, WORKSPACE_DATABASES, ArrayPrelim::default()); + }, + CollabType::DatabaseRow => { + collab + .data + .insert(&mut txn, DATABASE_ROW_DATA, MapPrelim::default()); + }, + _ => { /* do nothing */ }, + }; + + Ok(Some(txn.get_encoded_collab_v1())) } - fn batch_get_database_object_doc_state( + async fn batch_get_database_encode_collab( &self, _object_ids: Vec, _object_ty: CollabType, _workspace_id: &str, - ) -> FutureResult { - FutureResult::new(async move { Ok(CollabDocStateByOid::default()) }) + ) -> Result { + Ok(EncodeCollabByOid::default()) } - fn get_database_collab_object_snapshots( + async fn get_database_collab_object_snapshots( &self, _object_id: &str, _limit: usize, - ) -> FutureResult, Error> { - FutureResult::new(async move { Ok(vec![]) }) + ) -> Result, Error> { + Ok(vec![]) } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs index bc712d03d0..2a69a361f8 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs @@ -2,39 +2,39 @@ use anyhow::Error; use flowy_document_pub::cloud::*; use flowy_error::{ErrorCode, FlowyError}; -use lib_infra::future::FutureResult; +use lib_infra::async_trait::async_trait; pub(crate) struct LocalServerDocumentCloudServiceImpl(); +#[async_trait] impl DocumentCloudService for LocalServerDocumentCloudServiceImpl { - fn get_document_doc_state( + async fn get_document_doc_state( &self, document_id: &str, _workspace_id: &str, - ) -> FutureResult, FlowyError> { + ) -> Result, FlowyError> { let document_id = document_id.to_string(); - FutureResult::new(async move { - Err(FlowyError::new( - ErrorCode::RecordNotFound, - format!("Document {} not found", document_id), - )) - }) + + Err(FlowyError::new( + ErrorCode::RecordNotFound, + format!("Document {} not found", document_id), + )) } - fn get_document_snapshots( + async fn get_document_snapshots( &self, _document_id: &str, _limit: usize, _workspace_id: &str, - ) -> FutureResult, Error> { - FutureResult::new(async move { Ok(vec![]) }) + ) -> Result, Error> { + Ok(vec![]) } - fn get_document_data( + async fn get_document_data( &self, _document_id: &str, _workspace_id: &str, - ) -> FutureResult, Error> { - FutureResult::new(async move { Ok(None) }) + ) -> Result, Error> { + Ok(None) } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index 3451212f6f..9c2802b46a 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -3,123 +3,113 @@ use std::sync::Arc; use anyhow::{anyhow, Error}; use collab_entity::CollabType; +use crate::local_server::LocalServerDB; use flowy_folder_pub::cloud::{ gen_workspace_id, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, }; use flowy_folder_pub::entities::{PublishInfoResponse, PublishPayload}; -use lib_infra::future::FutureResult; - -use crate::local_server::LocalServerDB; +use lib_infra::async_trait::async_trait; pub(crate) struct LocalServerFolderCloudServiceImpl { #[allow(dead_code)] pub db: Arc, } +#[async_trait] impl FolderCloudService for LocalServerFolderCloudServiceImpl { - fn create_workspace(&self, uid: i64, name: &str) -> FutureResult { + async fn create_workspace(&self, uid: i64, name: &str) -> Result { let name = name.to_string(); - FutureResult::new(async move { - Ok(Workspace::new( - gen_workspace_id().to_string(), - name.to_string(), - uid, - )) - }) + Ok(Workspace::new( + gen_workspace_id().to_string(), + name.to_string(), + uid, + )) } - fn open_workspace(&self, _workspace_id: &str) -> FutureResult<(), Error> { - FutureResult::new(async { Ok(()) }) + async fn open_workspace(&self, _workspace_id: &str) -> Result<(), Error> { + Ok(()) } - fn get_all_workspace(&self) -> FutureResult, Error> { - FutureResult::new(async { Ok(vec![]) }) + async fn get_all_workspace(&self) -> Result, Error> { + Ok(vec![]) } - fn get_folder_data( + async fn get_folder_data( &self, _workspace_id: &str, _uid: &i64, - ) -> FutureResult, Error> { - FutureResult::new(async move { Ok(None) }) + ) -> Result, Error> { + Ok(None) } - fn get_folder_snapshots( + async fn get_folder_snapshots( &self, _workspace_id: &str, _limit: usize, - ) -> FutureResult, Error> { - FutureResult::new(async move { Ok(vec![]) }) + ) -> Result, Error> { + Ok(vec![]) } - fn get_folder_doc_state( + async fn get_folder_doc_state( &self, _workspace_id: &str, _uid: i64, _collab_type: CollabType, _object_id: &str, - ) -> FutureResult, Error> { - FutureResult::new(async { - Err(anyhow!( - "Local server doesn't support get collab doc state from remote" - )) - }) + ) -> Result, Error> { + Err(anyhow!( + "Local server doesn't support get collab doc state from remote" + )) } - fn batch_create_folder_collab_objects( + async fn batch_create_folder_collab_objects( &self, _workspace_id: &str, _objects: Vec, - ) -> FutureResult<(), Error> { - FutureResult::new(async { Err(anyhow!("Local server doesn't support create collab")) }) + ) -> Result<(), Error> { + Err(anyhow!("Local server doesn't support create collab")) } fn service_name(&self) -> String { "Local".to_string() } - fn publish_view( + async fn publish_view( &self, _workspace_id: &str, _payload: Vec, - ) -> FutureResult<(), Error> { - FutureResult::new(async { Err(anyhow!("Local server doesn't support publish view")) }) + ) -> Result<(), Error> { + Err(anyhow!("Local server doesn't support publish view")) } - fn unpublish_views( + async fn unpublish_views( &self, _workspace_id: &str, _view_ids: Vec, - ) -> FutureResult<(), Error> { - FutureResult::new(async { Err(anyhow!("Local server doesn't support unpublish views")) }) + ) -> Result<(), Error> { + Err(anyhow!("Local server doesn't support unpublish views")) } - fn get_publish_info(&self, _view_id: &str) -> FutureResult { - FutureResult::new(async move { - Err(anyhow!( - "Local server doesn't support get publish info from remote" - )) - }) + async fn get_publish_info(&self, _view_id: &str) -> Result { + Err(anyhow!( + "Local server doesn't support get publish info from remote" + )) } - fn set_publish_namespace( + async fn set_publish_namespace( &self, _workspace_id: &str, _new_namespace: &str, - ) -> FutureResult<(), Error> { - FutureResult::new(async { - Err(anyhow!( - "Local server doesn't support set publish namespace" - )) - }) + ) -> Result<(), Error> { + Err(anyhow!( + "Local server doesn't support set publish namespace" + )) } - fn get_publish_namespace(&self, _workspace_id: &str) -> FutureResult { - FutureResult::new(async { - Err(anyhow!( - "Local server doesn't support get publish namespace" - )) - }) + async fn get_publish_namespace(&self, _workspace_id: &str) -> Result { + Err(anyhow!( + "Local server doesn't support get publish namespace" + )) } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index d5fa1524b6..092eb946ef 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -2,21 +2,22 @@ use std::sync::Arc; use collab_entity::CollabObject; use lazy_static::lazy_static; -use parking_lot::Mutex; +use tokio::sync::Mutex; use uuid::Uuid; use flowy_error::FlowyError; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; use flowy_user_pub::entities::*; use flowy_user_pub::DEFAULT_USER_NAME; +use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; -use lib_infra::future::FutureResult; use lib_infra::util::timestamp; use crate::local_server::uid::UserIDGenerator; use crate::local_server::LocalServerDB; lazy_static! { + //FIXME: seriously, userID generation should work using lock-free algorithm static ref ID_GEN: Mutex = Mutex::new(UserIDGenerator::new(1)); } @@ -25,114 +26,101 @@ pub(crate) struct LocalServerUserAuthServiceImpl { pub db: Arc, } +#[async_trait] impl UserCloudService for LocalServerUserAuthServiceImpl { - fn sign_up(&self, params: BoxAny) -> FutureResult { - FutureResult::new(async move { - let params = params.unbox_or_error::()?; - let uid = ID_GEN.lock().next_id(); - let workspace_id = uuid::Uuid::new_v4().to_string(); - let user_workspace = UserWorkspace::new(&workspace_id, uid); - let user_name = if params.name.is_empty() { - DEFAULT_USER_NAME() - } else { - params.name.clone() - }; - Ok(AuthResponse { - user_id: uid, - user_uuid: Uuid::new_v4(), - name: user_name, - latest_workspace: user_workspace.clone(), - user_workspaces: vec![user_workspace], - is_new_user: true, - email: Some(params.email), - token: None, - encryption_type: EncryptionType::NoEncryption, - updated_at: timestamp(), - metadata: None, - }) + async fn sign_up(&self, params: BoxAny) -> Result { + let params = params.unbox_or_error::()?; + let uid = ID_GEN.lock().await.next_id(); + let workspace_id = uuid::Uuid::new_v4().to_string(); + let user_workspace = UserWorkspace::new(&workspace_id, uid); + let user_name = if params.name.is_empty() { + DEFAULT_USER_NAME() + } else { + params.name.clone() + }; + Ok(AuthResponse { + user_id: uid, + user_uuid: Uuid::new_v4(), + name: user_name, + latest_workspace: user_workspace.clone(), + user_workspaces: vec![user_workspace], + is_new_user: true, + email: Some(params.email), + token: None, + encryption_type: EncryptionType::NoEncryption, + updated_at: timestamp(), + metadata: None, }) } - fn sign_in(&self, params: BoxAny) -> FutureResult { + async fn sign_in(&self, params: BoxAny) -> Result { let db = self.db.clone(); - FutureResult::new(async move { - let params: SignInParams = params.unbox_or_error::()?; - let uid = ID_GEN.lock().next_id(); + let params: SignInParams = params.unbox_or_error::()?; + let uid = ID_GEN.lock().await.next_id(); - let user_workspace = db - .get_user_workspace(uid)? - .unwrap_or_else(make_user_workspace); - Ok(AuthResponse { - user_id: uid, - user_uuid: Uuid::new_v4(), - name: params.name, - latest_workspace: user_workspace.clone(), - user_workspaces: vec![user_workspace], - is_new_user: false, - email: Some(params.email), - token: None, - encryption_type: EncryptionType::NoEncryption, - updated_at: timestamp(), - metadata: None, - }) + let user_workspace = db + .get_user_workspace(uid)? + .unwrap_or_else(make_user_workspace); + Ok(AuthResponse { + user_id: uid, + user_uuid: Uuid::new_v4(), + name: params.name, + latest_workspace: user_workspace.clone(), + user_workspaces: vec![user_workspace], + is_new_user: false, + email: Some(params.email), + token: None, + encryption_type: EncryptionType::NoEncryption, + updated_at: timestamp(), + metadata: None, }) } - fn sign_out(&self, _token: Option) -> FutureResult<(), FlowyError> { - FutureResult::new(async { Ok(()) }) + async fn sign_out(&self, _token: Option) -> Result<(), FlowyError> { + Ok(()) } - fn generate_sign_in_url_with_email(&self, _email: &str) -> FutureResult { - FutureResult::new(async { - Err( - FlowyError::local_version_not_support() - .with_context("Not support generate sign in url with email"), - ) - }) + async fn generate_sign_in_url_with_email(&self, _email: &str) -> Result { + Err( + FlowyError::local_version_not_support() + .with_context("Not support generate sign in url with email"), + ) } - fn create_user(&self, _email: &str, _password: &str) -> FutureResult<(), FlowyError> { - FutureResult::new(async { - Err(FlowyError::local_version_not_support().with_context("Not support create user")) - }) + async fn create_user(&self, _email: &str, _password: &str) -> Result<(), FlowyError> { + Err(FlowyError::local_version_not_support().with_context("Not support create user")) } - fn sign_in_with_password( + async fn sign_in_with_password( &self, _email: &str, _password: &str, - ) -> FutureResult { - FutureResult::new(async { - Err(FlowyError::local_version_not_support().with_context("Not support")) - }) + ) -> Result { + Err(FlowyError::local_version_not_support().with_context("Not support")) } - fn sign_in_with_magic_link( + async fn sign_in_with_magic_link( &self, _email: &str, _redirect_to: &str, - ) -> FutureResult<(), FlowyError> { - FutureResult::new(async { - Err(FlowyError::local_version_not_support().with_context("Not support")) - }) + ) -> Result<(), FlowyError> { + Err(FlowyError::local_version_not_support().with_context("Not support")) } - fn generate_oauth_url_with_provider(&self, _provider: &str) -> FutureResult { - FutureResult::new(async { - Err(FlowyError::internal().with_context("Can't oauth url when using offline mode")) - }) + async fn generate_oauth_url_with_provider(&self, _provider: &str) -> Result { + Err(FlowyError::internal().with_context("Can't oauth url when using offline mode")) } - fn update_user( + async fn update_user( &self, _credential: UserCredentials, _params: UpdateUserProfileParams, - ) -> FutureResult<(), FlowyError> { - FutureResult::new(async { Ok(()) }) + ) -> Result<(), FlowyError> { + Ok(()) } - fn get_user_profile(&self, credential: UserCredentials) -> FutureResult { - let result = match credential.uid { + async fn get_user_profile(&self, credential: UserCredentials) -> Result { + match credential.uid { None => Err(FlowyError::record_not_found()), Some(uid) => { self.db.get_user_profile(uid).map(|mut profile| { @@ -141,88 +129,77 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { profile }) }, - }; - FutureResult::new(async { result }) + } } - fn open_workspace(&self, _workspace_id: &str) -> FutureResult { - FutureResult::new(async { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support open workspace"), - ) - }) + async fn open_workspace(&self, _workspace_id: &str) -> Result { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support open workspace"), + ) } - fn get_all_workspace(&self, _uid: i64) -> FutureResult, FlowyError> { - FutureResult::new(async { Ok(vec![]) }) + async fn get_all_workspace(&self, _uid: i64) -> Result, FlowyError> { + Ok(vec![]) } - fn get_user_awareness_doc_state( + async fn get_user_awareness_doc_state( &self, _uid: i64, _workspace_id: &str, _object_id: &str, - ) -> FutureResult, FlowyError> { + ) -> Result, FlowyError> { // must return record not found error - FutureResult::new(async { Err(FlowyError::record_not_found()) }) + Err(FlowyError::record_not_found()) } - fn reset_workspace(&self, _collab_object: CollabObject) -> FutureResult<(), FlowyError> { - FutureResult::new(async { Ok(()) }) + async fn reset_workspace(&self, _collab_object: CollabObject) -> Result<(), FlowyError> { + Ok(()) } - fn create_collab_object( + async fn create_collab_object( &self, _collab_object: &CollabObject, _data: Vec, - ) -> FutureResult<(), FlowyError> { - FutureResult::new(async { Ok(()) }) + ) -> Result<(), FlowyError> { + Ok(()) } - fn batch_create_collab_object( + async fn batch_create_collab_object( &self, _workspace_id: &str, _objects: Vec, - ) -> FutureResult<(), FlowyError> { - FutureResult::new(async { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support batch create collab object"), - ) - }) + ) -> Result<(), FlowyError> { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support batch create collab object"), + ) } - fn create_workspace(&self, _workspace_name: &str) -> FutureResult { - FutureResult::new(async { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support multiple workspaces"), - ) - }) + async fn create_workspace(&self, _workspace_name: &str) -> Result { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support multiple workspaces"), + ) } - fn delete_workspace(&self, _workspace_id: &str) -> FutureResult<(), FlowyError> { - FutureResult::new(async { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support multiple workspaces"), - ) - }) + async fn delete_workspace(&self, _workspace_id: &str) -> Result<(), FlowyError> { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support multiple workspaces"), + ) } - fn patch_workspace( + async fn patch_workspace( &self, _workspace_id: &str, _new_workspace_name: Option<&str>, _new_workspace_icon: Option<&str>, - ) -> FutureResult<(), FlowyError> { - FutureResult::new(async { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support multiple workspaces"), - ) - }) + ) -> Result<(), FlowyError> { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support multiple workspaces"), + ) } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/server.rs b/frontend/rust-lib/flowy-server/src/local_server/server.rs index e0ab174f75..cb8b545c53 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -1,7 +1,6 @@ use flowy_search_pub::cloud::SearchCloudService; use std::sync::Arc; -use parking_lot::RwLock; use tokio::sync::mpsc; use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; @@ -28,7 +27,7 @@ pub trait LocalServerDB: Send + Sync + 'static { pub struct LocalServer { local_db: Arc, - stop_tx: RwLock>>, + stop_tx: Option>, } impl LocalServer { @@ -40,7 +39,7 @@ impl LocalServer { } pub async fn stop(&self) { - let sender = self.stop_tx.read().clone(); + let sender = self.stop_tx.clone(); if let Some(stop_tx) = sender { let _ = stop_tx.send(()).await; } diff --git a/frontend/rust-lib/flowy-server/src/server.rs b/frontend/rust-lib/flowy-server/src/server.rs index 2c4ee66b03..ee07eefa5a 100644 --- a/frontend/rust-lib/flowy-server/src/server.rs +++ b/frontend/rust-lib/flowy-server/src/server.rs @@ -5,9 +5,9 @@ use flowy_search_pub::cloud::SearchCloudService; use std::sync::Arc; use anyhow::Error; +use arc_swap::ArcSwapOption; use client_api::collab_sync::ServerCollabMessage; use flowy_ai_pub::cloud::ChatCloudService; -use parking_lot::RwLock; use tokio_stream::wrappers::WatchStream; #[cfg(feature = "enable_supabase")] use {collab_entity::CollabObject, collab_plugins::cloud_storage::RemoteCollabStorage}; @@ -154,23 +154,23 @@ pub trait AppFlowyServer: Send + Sync + 'static { } pub struct EncryptionImpl { - secret: RwLock>, + secret: ArcSwapOption, } impl EncryptionImpl { pub fn new(secret: Option) -> Self { Self { - secret: RwLock::new(secret), + secret: ArcSwapOption::from(secret.map(Arc::new)), } } } impl AppFlowyEncryption for EncryptionImpl { fn get_secret(&self) -> Option { - self.secret.read().clone() + self.secret.load().as_ref().map(|s| s.to_string()) } fn set_secret(&self, secret: String) { - *self.secret.write() = Some(secret); + self.secret.store(Some(secret.into())); } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs b/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs index c442e686ea..bb5705cbc8 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use std::sync::{Arc, Weak}; use anyhow::Error; +use arc_swap::ArcSwapOption; use chrono::{DateTime, Utc}; use client_api::collab_sync::MsgId; use collab::core::collab::DataSource; @@ -10,7 +11,6 @@ use collab_entity::CollabObject; use collab_plugins::cloud_storage::{ RemoteCollabSnapshot, RemoteCollabState, RemoteCollabStorage, RemoteUpdateReceiver, }; -use parking_lot::Mutex; use tokio::task::spawn_blocking; use lib_infra::async_trait::async_trait; @@ -28,7 +28,7 @@ use crate::AppFlowyEncryption; pub struct SupabaseCollabStorageImpl { server: T, - rx: Mutex>, + rx: ArcSwapOption, encryption: Weak, } @@ -40,7 +40,7 @@ impl SupabaseCollabStorageImpl { ) -> Self { Self { server, - rx: Mutex::new(rx), + rx: ArcSwapOption::new(rx.map(Arc::new)), encryption, } } @@ -186,11 +186,14 @@ where } fn subscribe_remote_updates(&self, _object: &CollabObject) -> Option { - let rx = self.rx.lock().take(); - if rx.is_none() { - tracing::warn!("The receiver is already taken"); + let rx = self.rx.swap(None); + match rx { + Some(rx) => Arc::into_inner(rx), + None => { + tracing::warn!("The receiver is already taken"); + None + }, } - rx } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/document.rs b/frontend/rust-lib/flowy-server/src/supabase/api/document.rs index a0e5087938..66edb7ed7c 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/document.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/document.rs @@ -94,7 +94,7 @@ where let action = FetchObjectUpdateAction::new(document_id.clone(), CollabType::Document, postgrest); let doc_state = action.run_with_fix_interval(5, 10).await?; - let document = Document::from_doc_state( + let document = Document::open_with_options( CollabOrigin::Empty, DataSource::DocStateV1(doc_state), &document_id, diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/postgres_server.rs b/frontend/rust-lib/flowy-server/src/supabase/api/postgres_server.rs index 8db0910896..9ab3379486 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/postgres_server.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/postgres_server.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use std::sync::{Arc, Weak}; use anyhow::Error; -use parking_lot::RwLock; +use arc_swap::ArcSwapOption; use postgrest::Postgrest; use flowy_error::{ErrorCode, FlowyError}; @@ -77,11 +77,11 @@ where } #[derive(Clone)] -pub struct SupabaseServerServiceImpl(pub Arc>>>); +pub struct SupabaseServerServiceImpl(pub Arc>); impl SupabaseServerServiceImpl { pub fn new(postgrest: Arc) -> Self { - Self(Arc::new(RwLock::new(Some(postgrest)))) + Self(Arc::new(ArcSwapOption::from(Some(postgrest)))) } } @@ -89,7 +89,7 @@ impl SupabaseServerService for SupabaseServerServiceImpl { fn get_postgrest(&self) -> Option> { self .0 - .read() + .load() .as_ref() .map(|server| server.postgrest.clone()) } @@ -97,7 +97,7 @@ impl SupabaseServerService for SupabaseServerServiceImpl { fn try_get_postgrest(&self) -> Result, Error> { self .0 - .read() + .load() .as_ref() .map(|server| server.postgrest.clone()) .ok_or_else(|| { diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index b537a5689a..b8c55cf535 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -6,11 +6,10 @@ use std::sync::{Arc, Weak}; use std::time::Duration; use anyhow::Error; -use collab::core::collab::MutexCollab; +use arc_swap::ArcSwapOption; use collab::core::origin::CollabOrigin; use collab::preclude::Collab; use collab_entity::{CollabObject, CollabType}; -use parking_lot::RwLock; use serde_json::Value; use tokio::sync::oneshot::channel; use tokio_retry::strategy::FixedInterval; @@ -44,7 +43,7 @@ use crate::AppFlowyEncryption; pub struct SupabaseUserServiceImpl { server: T, realtime_event_handlers: Vec>, - user_update_rx: RwLock>, + user_update_rx: ArcSwapOption, } impl SupabaseUserServiceImpl { @@ -56,7 +55,7 @@ impl SupabaseUserServiceImpl { Self { server, realtime_event_handlers, - user_update_rx: RwLock::new(user_update_rx), + user_update_rx: ArcSwapOption::from(user_update_rx.map(Arc::new)), } } } @@ -306,7 +305,8 @@ where } fn subscribe_user_update(&self) -> Option { - self.user_update_rx.write().take() + let rx = self.user_update_rx.swap(None)?; + Arc::into_inner(rx) } fn reset_workspace(&self, collab_object: CollabObject) -> FutureResult<(), FlowyError> { @@ -647,7 +647,7 @@ impl RealtimeEventHandler for RealtimeCollabUpdateHandler { serde_json::from_value::(event.new.clone()) { if let Some(sender_by_oid) = self.sender_by_oid.upgrade() { - if let Some(sender) = sender_by_oid.read().get(collab_update.oid.as_str()) { + if let Some(sender) = sender_by_oid.get(collab_update.oid.as_str()) { tracing::trace!( "current device: {}, event device: {}", self.device_id, @@ -688,15 +688,16 @@ impl RealtimeEventHandler for RealtimeCollabUpdateHandler { fn default_workspace_doc_state(collab_object: &CollabObject) -> Vec { let workspace_id = collab_object.object_id.clone(); - let collab = Arc::new(MutexCollab::new(Collab::new_with_origin( - CollabOrigin::Empty, - &collab_object.object_id, - vec![], - false, - ))); + let collab = + Collab::new_with_origin(CollabOrigin::Empty, &collab_object.object_id, vec![], false); let workspace = Workspace::new(workspace_id, "My workspace".to_string(), collab_object.uid); - let folder = Folder::create(collab_object.uid, collab, None, FolderData::new(workspace)); - folder.encode_collab_v1().unwrap().doc_state.to_vec() + let folder = Folder::open_with( + collab_object.uid, + collab, + None, + Some(FolderData::new(workspace)), + ); + folder.encode_collab().unwrap().doc_state.to_vec() } fn oauth_params_from_box_any(any: BoxAny) -> Result { diff --git a/frontend/rust-lib/flowy-server/src/supabase/file_storage/plan.rs b/frontend/rust-lib/flowy-server/src/supabase/file_storage/plan.rs new file mode 100644 index 0000000000..39a33c8853 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/file_storage/plan.rs @@ -0,0 +1,37 @@ +use std::sync::Weak; + +use flowy_error::FlowyError; +use flowy_storage_pub::cloud::{FileStoragePlan, StorageObject}; +use lib_infra::future::FutureResult; + +use crate::supabase::api::RESTfulPostgresServer; + +#[derive(Default)] +pub struct FileStoragePlanImpl { + #[allow(dead_code)] + uid: Weak>, + #[allow(dead_code)] + postgrest: Option>, +} + +impl FileStoragePlanImpl { + pub fn new(uid: Weak>, postgrest: Option>) -> Self { + Self { uid, postgrest } + } +} + +impl FileStoragePlan for FileStoragePlanImpl { + fn storage_size(&self) -> FutureResult { + // 1 GB + FutureResult::new(async { Ok(1024 * 1024 * 1024) }) + } + + fn maximum_file_size(&self) -> FutureResult { + // 5 MB + FutureResult::new(async { Ok(5 * 1024 * 1024) }) + } + + fn check_upload_object(&self, _object: &StorageObject) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) + } +} diff --git a/frontend/rust-lib/flowy-server/src/supabase/server.rs b/frontend/rust-lib/flowy-server/src/supabase/server.rs index b02d7a9030..00dd46e8ba 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/server.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/server.rs @@ -1,10 +1,10 @@ +use arc_swap::ArcSwapOption; use flowy_search_pub::cloud::SearchCloudService; -use std::collections::HashMap; use std::sync::{Arc, Weak}; use collab_entity::CollabObject; use collab_plugins::cloud_storage::{RemoteCollabStorage, RemoteUpdateSender}; -use parking_lot::RwLock; +use dashmap::DashMap; use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; use flowy_document_pub::cloud::DocumentCloudService; @@ -55,7 +55,7 @@ impl PgPoolMode { } } -pub type CollabUpdateSenderByOid = RwLock>; +pub type CollabUpdateSenderByOid = DashMap; /// Supabase server is used to provide the implementation of the [AppFlowyServer] trait. /// It contains the configuration of the supabase server and the postgres server. pub struct SupabaseServer { @@ -63,15 +63,15 @@ pub struct SupabaseServer { config: SupabaseConfiguration, device_id: String, #[allow(dead_code)] - uid: Arc>>, + uid: Arc>, collab_update_sender: Arc, - restful_postgres: Arc>>>, + restful_postgres: Arc>, encryption: Weak, } impl SupabaseServer { pub fn new( - uid: Arc>>, + uid: Arc>, config: SupabaseConfiguration, enable_sync: bool, device_id: String, @@ -90,7 +90,7 @@ impl SupabaseServer { config, device_id, collab_update_sender, - restful_postgres: Arc::new(RwLock::new(restful_postgres)), + restful_postgres: Arc::new(ArcSwapOption::from(restful_postgres)), encryption, uid, } @@ -102,12 +102,18 @@ impl AppFlowyServer for SupabaseServer { tracing::info!("{} supabase sync: {}", uid, enable); if enable { - if self.restful_postgres.read().is_none() { - let postgres = RESTfulPostgresServer::new(self.config.clone(), self.encryption.clone()); - *self.restful_postgres.write() = Some(Arc::new(postgres)); - } + self.restful_postgres.rcu(|old| match old { + Some(existing) => Some(existing.clone()), + None => { + let postgres = Arc::new(RESTfulPostgresServer::new( + self.config.clone(), + self.encryption.clone(), + )); + Some(postgres) + }, + }); } else { - *self.restful_postgres.write() = None; + self.restful_postgres.store(None); } } @@ -158,7 +164,6 @@ impl AppFlowyServer for SupabaseServer { let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); self .collab_update_sender - .write() .insert(collab_object.object_id.clone(), tx); Some(Arc::new(SupabaseCollabStorageImpl::new( diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs index 71dacfab04..ecf34ec31d 100644 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs @@ -8,7 +8,6 @@ use uuid::Uuid; use flowy_server::af_cloud::define::ServerUser; use flowy_server::af_cloud::AppFlowyCloudServer; -use flowy_server::supabase::define::{USER_DEVICE_ID, USER_SIGN_IN_URL}; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use crate::setup_log; @@ -82,10 +81,10 @@ pub async fn af_cloud_sign_up_param( ) -> HashMap { let mut params = HashMap::new(); params.insert( - USER_SIGN_IN_URL.to_string(), + "sign_in_url".to_string(), generate_sign_in_url(email, config).await, ); - params.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string()); + params.insert("device_id".to_string(), Uuid::new_v4().to_string()); params } diff --git a/frontend/rust-lib/flowy-sqlite/Cargo.toml b/frontend/rust-lib/flowy-sqlite/Cargo.toml index e49452df75..0e85aebee5 100644 --- a/frontend/rust-lib/flowy-sqlite/Cargo.toml +++ b/frontend/rust-lib/flowy-sqlite/Cargo.toml @@ -13,7 +13,6 @@ tracing.workspace = true serde.workspace = true serde_json.workspace = true anyhow.workspace = true -parking_lot.workspace = true r2d2 = "0.8.10" libsqlite3-sys = { version = "0.27.0", features = ["bundled"] } diff --git a/frontend/rust-lib/flowy-sqlite/src/kv/kv.rs b/frontend/rust-lib/flowy-sqlite/src/kv/kv.rs index da35facaf2..799f5b0666 100644 --- a/frontend/rust-lib/flowy-sqlite/src/kv/kv.rs +++ b/frontend/rust-lib/flowy-sqlite/src/kv/kv.rs @@ -46,8 +46,8 @@ impl KVStorePreferences { } /// Set a object that implements [Serialize] trait of a key - pub fn set_object(&self, key: &str, value: T) -> Result<(), anyhow::Error> { - let value = serde_json::to_string(&value)?; + pub fn set_object(&self, key: &str, value: &T) -> Result<(), anyhow::Error> { + let value = serde_json::to_string(value)?; self.set_key_value(key, Some(value))?; Ok(()) } @@ -175,7 +175,7 @@ mod tests { name: "nathan".to_string(), age: 30, }; - store.set_object("1", person.clone()).unwrap(); + store.set_object("1", &person.clone()).unwrap(); assert_eq!(store.get_object::("1").unwrap(), person); } } diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index 117efac414..f20d66edbe 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -7,8 +7,8 @@ use client_api::entity::billing_dto::WorkspaceUsageAndLimit; pub use client_api::entity::{AFWorkspaceSettings, AFWorkspaceSettingsChange}; use collab_entity::{CollabObject, CollabType}; use flowy_error::{internal_error, ErrorCode, FlowyError}; +use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; -use lib_infra::future::FutureResult; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; @@ -119,130 +119,131 @@ pub trait UserCloudServiceProvider: Send + Sync { /// Provide the generic interface for the user cloud service /// The user cloud service is responsible for the user authentication and user profile management #[allow(unused_variables)] +#[async_trait] pub trait UserCloudService: Send + Sync + 'static { /// Sign up a new account. /// The type of the params is defined the this trait's implementation. /// Use the `unbox_or_error` of the [BoxAny] to get the params. - fn sign_up(&self, params: BoxAny) -> FutureResult; + async fn sign_up(&self, params: BoxAny) -> Result; /// Sign in an account /// The type of the params is defined the this trait's implementation. - fn sign_in(&self, params: BoxAny) -> FutureResult; + async fn sign_in(&self, params: BoxAny) -> Result; /// Sign out an account - fn sign_out(&self, token: Option) -> FutureResult<(), FlowyError>; + async fn sign_out(&self, token: Option) -> Result<(), FlowyError>; /// Generate a sign in url for the user with the given email /// Currently, only use the admin client for testing - fn generate_sign_in_url_with_email(&self, email: &str) -> FutureResult; + async fn generate_sign_in_url_with_email(&self, email: &str) -> Result; - fn create_user(&self, email: &str, password: &str) -> FutureResult<(), FlowyError>; + async fn create_user(&self, email: &str, password: &str) -> Result<(), FlowyError>; - fn sign_in_with_password( + async fn sign_in_with_password( &self, email: &str, password: &str, - ) -> FutureResult; + ) -> Result; - fn sign_in_with_magic_link(&self, email: &str, redirect_to: &str) - -> FutureResult<(), FlowyError>; + async fn sign_in_with_magic_link(&self, email: &str, redirect_to: &str) + -> Result<(), FlowyError>; /// When the user opens the OAuth URL, it redirects to the corresponding provider's OAuth web page. /// After the user is authenticated, the browser will open a deep link to the AppFlowy app (iOS, macOS, etc.), /// which will call [Client::sign_in_with_url]generate_sign_in_url_with_email to sign in. /// /// For example, the OAuth URL on Google looks like `https://appflowy.io/authorize?provider=google`. - fn generate_oauth_url_with_provider(&self, provider: &str) -> FutureResult; + async fn generate_oauth_url_with_provider(&self, provider: &str) -> Result; /// Using the user's token to update the user information - fn update_user( + async fn update_user( &self, credential: UserCredentials, params: UpdateUserProfileParams, - ) -> FutureResult<(), FlowyError>; + ) -> Result<(), FlowyError>; /// Get the user information using the user's token or uid /// return None if the user is not found - fn get_user_profile(&self, credential: UserCredentials) -> FutureResult; + async fn get_user_profile(&self, credential: UserCredentials) -> Result; - fn open_workspace(&self, workspace_id: &str) -> FutureResult; + async fn open_workspace(&self, workspace_id: &str) -> Result; /// Return the all the workspaces of the user - fn get_all_workspace(&self, uid: i64) -> FutureResult, FlowyError>; + async fn get_all_workspace(&self, uid: i64) -> Result, FlowyError>; /// Creates a new workspace for the user. /// Returns the new workspace if successful - fn create_workspace(&self, workspace_name: &str) -> FutureResult; + async fn create_workspace(&self, workspace_name: &str) -> Result; // Updates the workspace name and icon - fn patch_workspace( + async fn patch_workspace( &self, workspace_id: &str, new_workspace_name: Option<&str>, new_workspace_icon: Option<&str>, - ) -> FutureResult<(), FlowyError>; + ) -> Result<(), FlowyError>; /// Deletes a workspace owned by the user. - fn delete_workspace(&self, workspace_id: &str) -> FutureResult<(), FlowyError>; + async fn delete_workspace(&self, workspace_id: &str) -> Result<(), FlowyError>; - fn invite_workspace_member( + async fn invite_workspace_member( &self, invitee_email: String, workspace_id: String, role: Role, - ) -> FutureResult<(), FlowyError> { - FutureResult::new(async { Ok(()) }) + ) -> Result<(), FlowyError> { + Ok(()) } - fn list_workspace_invitations( + async fn list_workspace_invitations( &self, filter: Option, - ) -> FutureResult, FlowyError> { - FutureResult::new(async { Ok(vec![]) }) + ) -> Result, FlowyError> { + Ok(vec![]) } - fn accept_workspace_invitations(&self, invite_id: String) -> FutureResult<(), FlowyError> { - FutureResult::new(async { Ok(()) }) + async fn accept_workspace_invitations(&self, invite_id: String) -> Result<(), FlowyError> { + Ok(()) } - fn remove_workspace_member( + async fn remove_workspace_member( &self, user_email: String, workspace_id: String, - ) -> FutureResult<(), FlowyError> { - FutureResult::new(async { Ok(()) }) + ) -> Result<(), FlowyError> { + Ok(()) } - fn update_workspace_member( + async fn update_workspace_member( &self, user_email: String, workspace_id: String, role: Role, - ) -> FutureResult<(), FlowyError> { - FutureResult::new(async { Ok(()) }) + ) -> Result<(), FlowyError> { + Ok(()) } - fn get_workspace_members( + async fn get_workspace_members( &self, workspace_id: String, - ) -> FutureResult, FlowyError> { - FutureResult::new(async { Ok(vec![]) }) + ) -> Result, FlowyError> { + Ok(vec![]) } - fn get_workspace_member( + async fn get_workspace_member( &self, workspace_id: String, uid: i64, - ) -> FutureResult { - FutureResult::new(async { Err(FlowyError::not_support()) }) + ) -> Result { + Err(FlowyError::not_support()) } - fn get_user_awareness_doc_state( + async fn get_user_awareness_doc_state( &self, uid: i64, workspace_id: &str, object_id: &str, - ) -> FutureResult, FlowyError>; + ) -> Result, FlowyError>; fn receive_realtime_event(&self, _json: Value) {} @@ -250,110 +251,110 @@ pub trait UserCloudService: Send + Sync + 'static { None } - fn reset_workspace(&self, collab_object: CollabObject) -> FutureResult<(), FlowyError>; + async fn reset_workspace(&self, collab_object: CollabObject) -> Result<(), FlowyError>; - fn create_collab_object( + async fn create_collab_object( &self, collab_object: &CollabObject, data: Vec, - ) -> FutureResult<(), FlowyError>; + ) -> Result<(), FlowyError>; - fn batch_create_collab_object( + async fn batch_create_collab_object( &self, workspace_id: &str, objects: Vec, - ) -> FutureResult<(), FlowyError>; + ) -> Result<(), FlowyError>; - fn leave_workspace(&self, workspace_id: &str) -> FutureResult<(), FlowyError> { - FutureResult::new(async { Ok(()) }) + async fn leave_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> { + Ok(()) } - fn subscribe_workspace( + async fn subscribe_workspace( &self, workspace_id: String, recurring_interval: RecurringInterval, workspace_subscription_plan: SubscriptionPlan, success_url: String, - ) -> FutureResult { - FutureResult::new(async { Err(FlowyError::not_support()) }) + ) -> Result { + Err(FlowyError::not_support()) } - fn get_workspace_member_info( + async fn get_workspace_member_info( &self, workspace_id: &str, uid: i64, - ) -> FutureResult { - FutureResult::new(async { Err(FlowyError::not_support()) }) + ) -> Result { + Err(FlowyError::not_support()) } /// Get all subscriptions for all workspaces for a user (email) - fn get_workspace_subscriptions( + async fn get_workspace_subscriptions( &self, - ) -> FutureResult, FlowyError> { - FutureResult::new(async { Err(FlowyError::not_support()) }) + ) -> Result, FlowyError> { + Err(FlowyError::not_support()) } /// Get the workspace subscriptions for a workspace - fn get_workspace_subscription_one( + async fn get_workspace_subscription_one( &self, workspace_id: String, - ) -> FutureResult, FlowyError> { - FutureResult::new(async { Err(FlowyError::not_support()) }) + ) -> Result, FlowyError> { + Err(FlowyError::not_support()) } - fn cancel_workspace_subscription( + async fn cancel_workspace_subscription( &self, workspace_id: String, plan: SubscriptionPlan, reason: Option, - ) -> FutureResult<(), FlowyError> { - FutureResult::new(async { Err(FlowyError::not_support()) }) + ) -> Result<(), FlowyError> { + Err(FlowyError::not_support()) } - fn get_workspace_plan( + async fn get_workspace_plan( &self, workspace_id: String, - ) -> FutureResult, FlowyError> { - FutureResult::new(async { Err(FlowyError::not_support()) }) + ) -> Result, FlowyError> { + Err(FlowyError::not_support()) } - fn get_workspace_usage( + async fn get_workspace_usage( &self, workspace_id: String, - ) -> FutureResult { - FutureResult::new(async { Err(FlowyError::not_support()) }) + ) -> Result { + Err(FlowyError::not_support()) } - fn get_billing_portal_url(&self) -> FutureResult { - FutureResult::new(async { Err(FlowyError::not_support()) }) + async fn get_billing_portal_url(&self) -> Result { + Err(FlowyError::not_support()) } - fn update_workspace_subscription_payment_period( + async fn update_workspace_subscription_payment_period( &self, workspace_id: String, plan: SubscriptionPlan, recurring_interval: RecurringInterval, - ) -> FutureResult<(), FlowyError> { - FutureResult::new(async { Err(FlowyError::not_support()) }) + ) -> Result<(), FlowyError> { + Err(FlowyError::not_support()) } - fn get_subscription_plan_details(&self) -> FutureResult, FlowyError> { - FutureResult::new(async { Err(FlowyError::not_support()) }) + async fn get_subscription_plan_details(&self) -> Result, FlowyError> { + Err(FlowyError::not_support()) } - fn get_workspace_setting( + async fn get_workspace_setting( &self, workspace_id: &str, - ) -> FutureResult { - FutureResult::new(async { Err(FlowyError::not_support()) }) + ) -> Result { + Err(FlowyError::not_support()) } - fn update_workspace_setting( + async fn update_workspace_setting( &self, workspace_id: &str, workspace_settings: AFWorkspaceSettingsChange, - ) -> FutureResult { - FutureResult::new(async { Err(FlowyError::not_support()) }) + ) -> Result { + Err(FlowyError::not_support()) } } diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index 95a38ab3c0..4b2ab3fdd0 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -346,8 +346,6 @@ pub enum Authenticator { /// Currently not supported. It will be supported in the future when the /// [AppFlowy-Server](https://github.com/AppFlowy-IO/AppFlowy-Server) ready. AppFlowyCloud = 1, - /// It uses Supabase as the backend. - Supabase = 2, } impl Default for Authenticator { @@ -371,7 +369,6 @@ impl From for Authenticator { match value { 0 => Authenticator::Local, 1 => Authenticator::AppFlowyCloud, - 2 => Authenticator::Supabase, _ => Authenticator::Local, } } diff --git a/frontend/rust-lib/flowy-user/Cargo.toml b/frontend/rust-lib/flowy-user/Cargo.toml index f2eb89dc4c..3894ce0ee6 100644 --- a/frontend/rust-lib/flowy-user/Cargo.toml +++ b/frontend/rust-lib/flowy-user/Cargo.toml @@ -26,9 +26,11 @@ collab-plugins = { workspace = true } flowy-user-pub = { workspace = true } client-api = { workspace = true } anyhow.workspace = true +arc-swap.workspace = true +dashmap.workspace = true tracing.workspace = true bytes.workspace = true -serde.workspace = true +serde = { workspace = true, features = ["rc"] } serde_json.workspace = true serde_repr.workspace = true protobuf.workspace = true @@ -36,7 +38,6 @@ lazy_static = "1.4.0" diesel.workspace = true diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] } once_cell = "1.17.1" -parking_lot.workspace = true strum = "0.25" strum_macros = "0.25.2" tokio = { workspace = true, features = ["rt"] } diff --git a/frontend/rust-lib/flowy-user/src/anon_user/migrate_anon_user_collab.rs b/frontend/rust-lib/flowy-user/src/anon_user/migrate_anon_user_collab.rs index a7adcbe803..ae7d5329bf 100644 --- a/frontend/rust-lib/flowy-user/src/anon_user/migrate_anon_user_collab.rs +++ b/frontend/rust-lib/flowy-user/src/anon_user/migrate_anon_user_collab.rs @@ -3,7 +3,7 @@ use std::ops::{Deref, DerefMut}; use std::sync::Arc; use anyhow::anyhow; -use collab::core::collab::{DataSource, MutexCollab}; +use collab::core::collab::DataSource; use collab::core::origin::{CollabClient, CollabOrigin}; use collab::preclude::Collab; use collab_database::database::{ @@ -13,7 +13,6 @@ use collab_database::rows::{database_row_document_id_from_row_id, mut_row_with_c use collab_database::workspace_database::DatabaseMetaList; use collab_folder::{Folder, UserId}; use collab_plugins::local_storage::kv::KVTransactionDB; -use parking_lot::{Mutex, RwLock}; use tracing::info; use collab_integrate::{CollabKVAction, CollabKVDB, PersistenceError}; @@ -34,16 +33,12 @@ pub fn migration_anon_user_on_sign_up( new_collab_db .with_write_txn(|new_collab_w_txn| { let old_collab_r_txn = old_collab_db.read_txn(); - let old_to_new_id_map = Arc::new(Mutex::new(OldToNewIdMap::new())); + let mut old_to_new_id_map = OldToNewIdMap::new(); - migrate_user_awareness( - old_to_new_id_map.lock().deref_mut(), - old_user, - new_user_session, - )?; + migrate_user_awareness(&mut old_to_new_id_map, old_user, new_user_session)?; migrate_database_with_views_object( - &mut old_to_new_id_map.lock(), + &mut old_to_new_id_map, old_user, &old_collab_r_txn, new_user_session, @@ -62,20 +57,20 @@ pub fn migration_anon_user_on_sign_up( }); info!("migrate collab objects: {:?}", object_ids.len()); - let collab_by_oid = make_collab_by_oid(old_user, &old_collab_r_txn, &object_ids); + let mut collab_by_oid = make_collab_by_oid(old_user, &old_collab_r_txn, &object_ids); migrate_databases( - &old_to_new_id_map, + &mut old_to_new_id_map, new_user_session, new_collab_w_txn, &mut object_ids, - &collab_by_oid, + &mut collab_by_oid, )?; // Migrates the folder, replacing all existing view IDs with new ones. // This function handles the process of migrating folder data between two users. As a part of this migration, // all existing view IDs associated with the old user will be replaced by new IDs relevant to the new user. migrate_workspace_folder( - &mut old_to_new_id_map.lock(), + &mut old_to_new_id_map, old_user, &old_collab_r_txn, new_user_session, @@ -85,7 +80,7 @@ pub fn migration_anon_user_on_sign_up( // Migrate other collab objects for object_id in &object_ids { if let Some(collab) = collab_by_oid.get(object_id) { - let new_object_id = old_to_new_id_map.lock().exchange_new_id(object_id); + let new_object_id = old_to_new_id_map.exchange_new_id(object_id); tracing::debug!("migrate from: {}, to: {}", object_id, new_object_id,); migrate_collab_object( collab, @@ -147,27 +142,26 @@ where PersistenceError: From, PersistenceError: From, { - let database_with_views_collab = Collab::new( + let mut database_with_views_collab = Collab::new( old_user.session.user_id, &old_user.session.user_workspace.database_indexer_id, "phantom", vec![], false, ); - database_with_views_collab.with_origin_transact_mut(|txn| { - old_collab_r_txn.load_doc_with_txn( - old_user.session.user_id, - &old_user.session.user_workspace.database_indexer_id, - txn, - ) - })?; + old_collab_r_txn.load_doc_with_txn( + old_user.session.user_id, + &old_user.session.user_workspace.database_indexer_id, + &mut database_with_views_collab.transact_mut(), + )?; let new_uid = new_user_session.user_id; let new_object_id = &new_user_session.user_workspace.database_indexer_id; - let array = DatabaseMetaList::from_collab(&database_with_views_collab); - for database_meta in array.get_all_database_meta() { - array.update_database(&database_meta.database_id, |update| { + let array = DatabaseMetaList::new(&mut database_with_views_collab); + let mut txn = database_with_views_collab.transact_mut(); + for database_meta in array.get_all_database_meta(&txn) { + array.update_database(&mut txn, &database_meta.database_id, |update| { let new_linked_views = update .linked_views .iter() @@ -178,7 +172,6 @@ where }) } - let txn = database_with_views_collab.transact(); if let Err(err) = new_collab_w_txn.create_new_doc(new_uid, new_object_id, &txn) { tracing::error!("🔴migrate database storage failed: {:?}", err); } @@ -216,17 +209,15 @@ where let new_uid = new_user_session.user_id; let new_workspace_id = &new_user_session.user_workspace.id; - let old_folder_collab = Collab::new(old_uid, old_workspace_id, "phantom", vec![], false); - old_folder_collab.with_origin_transact_mut(|txn| { - old_collab_r_txn.load_doc_with_txn(old_uid, old_workspace_id, txn) - })?; + let mut old_folder_collab = Collab::new(old_uid, old_workspace_id, "phantom", vec![], false); + old_collab_r_txn.load_doc_with_txn( + old_uid, + old_workspace_id, + &mut old_folder_collab.transact_mut(), + )?; let old_user_id = UserId::from(old_uid); - let old_folder = Folder::open( - old_user_id.clone(), - Arc::new(MutexCollab::new(old_folder_collab)), - None, - ) - .map_err(|err| PersistenceError::InvalidData(err.to_string()))?; + let old_folder = Folder::open(old_user_id.clone(), old_folder_collab, None) + .map_err(|err| PersistenceError::InvalidData(err.to_string()))?; let mut folder_data = old_folder .get_folder_data(old_workspace_id) @@ -310,14 +301,12 @@ where let new_folder_collab = Collab::new_with_source(origin, new_workspace_id, DataSource::Disk, vec![], false) .map_err(|err| PersistenceError::Internal(err.into()))?; - let mutex_collab = Arc::new(MutexCollab::new(new_folder_collab)); let new_user_id = UserId::from(new_uid); info!("migrated folder: {:?}", folder_data); - let _ = Folder::create(new_user_id, mutex_collab.clone(), None, folder_data); + let folder = Folder::open_with(new_user_id, new_folder_collab, None, Some(folder_data)); { - let mutex_collab = mutex_collab.lock(); - let txn = mutex_collab.transact(); + let txn = folder.transact(); if let Err(err) = new_collab_w_txn.create_new_doc(new_uid, new_workspace_id, &txn) { tracing::error!("🔴migrate folder failed: {:?}", err); } @@ -338,11 +327,11 @@ fn migrate_user_awareness( } fn migrate_databases<'a, W>( - old_to_new_id_map: &Arc>, + old_to_new_id_map: &mut OldToNewIdMap, new_user_session: &Session, new_collab_w_txn: &'a W, object_ids: &mut Vec, - collab_by_oid: &HashMap, + collab_by_oid: &mut HashMap, ) -> Result<(), PersistenceError> where W: CollabKVAction<'a>, @@ -350,28 +339,23 @@ where { // Migrate databases let mut database_object_ids = vec![]; - let imported_database_row_object_ids: RwLock>> = - RwLock::new(HashMap::new()); + let mut imported_database_row_object_ids: HashMap> = HashMap::new(); - for object_id in &mut *object_ids { - if let Some(collab) = collab_by_oid.get(object_id) { + for object_id in object_ids.iter() { + if let Some(collab) = collab_by_oid.get_mut(object_id) { if !is_database_collab(collab) { continue; } database_object_ids.push(object_id.clone()); reset_inline_view_id(collab, |old_inline_view_id| { - old_to_new_id_map - .lock() - .exchange_new_id(&old_inline_view_id) + old_to_new_id_map.exchange_new_id(&old_inline_view_id) }); mut_database_views_with_collab(collab, |database_view| { let old_database_id = database_view.database_id.clone(); - let new_view_id = old_to_new_id_map.lock().exchange_new_id(&database_view.id); - let new_database_id = old_to_new_id_map - .lock() - .exchange_new_id(&database_view.database_id); + let new_view_id = old_to_new_id_map.exchange_new_id(&database_view.id); + let new_database_id = old_to_new_id_map.exchange_new_id(&database_view.database_id); tracing::trace!( "migrate database view id from: {}, to: {}", @@ -389,7 +373,7 @@ where database_view.row_orders.iter_mut().for_each(|row_order| { let old_row_id = String::from(row_order.id.clone()); let old_row_document_id = database_row_document_id_from_row_id(&old_row_id); - let new_row_id = old_to_new_id_map.lock().exchange_new_id(&old_row_id); + let new_row_id = old_to_new_id_map.exchange_new_id(&old_row_id); let new_row_document_id = database_row_document_id_from_row_id(&new_row_id); tracing::debug!("migrate row id: {} to {}", row_order.id, new_row_id); tracing::debug!( @@ -397,20 +381,17 @@ where old_row_document_id, new_row_document_id ); - old_to_new_id_map - .lock() - .insert(old_row_document_id, new_row_document_id); + old_to_new_id_map.insert(old_row_document_id, new_row_document_id); row_order.id = RowId::from(new_row_id); imported_database_row_object_ids - .write() .entry(old_database_id.clone()) .or_default() .insert(old_row_id); }); }); - let new_object_id = old_to_new_id_map.lock().exchange_new_id(object_id); + let new_object_id = old_to_new_id_map.exchange_new_id(object_id); tracing::debug!( "migrate database from: {}, to: {}", object_id, @@ -425,7 +406,6 @@ where } } - let imported_database_row_object_ids = imported_database_row_object_ids.read(); // remove the database object ids from the object ids object_ids.retain(|id| !database_object_ids.contains(id)); @@ -436,11 +416,11 @@ where .flatten() .any(|row_id| row_id == id) }); - for (database_id, imported_row_ids) in &*imported_database_row_object_ids { + for (database_id, imported_row_ids) in imported_database_row_object_ids { for imported_row_id in imported_row_ids { - if let Some(imported_collab) = collab_by_oid.get(imported_row_id) { - let new_database_id = old_to_new_id_map.lock().exchange_new_id(database_id); - let new_row_id = old_to_new_id_map.lock().exchange_new_id(imported_row_id); + if let Some(imported_collab) = collab_by_oid.get_mut(&imported_row_id) { + let new_database_id = old_to_new_id_map.exchange_new_id(&database_id); + let new_row_id = old_to_new_id_map.exchange_new_id(&imported_row_id); info!( "import database row from: {}, to: {}", imported_row_id, new_row_id, @@ -458,11 +438,9 @@ where // imported_collab_by_oid contains all the collab object ids, including the row document collab object ids. // So, if the id exist in the imported_collab_by_oid, it means the row document collab object is exist. - let imported_row_document_id = database_row_document_id_from_row_id(imported_row_id); + let imported_row_document_id = database_row_document_id_from_row_id(&imported_row_id); if collab_by_oid.get(&imported_row_document_id).is_some() { - let _ = old_to_new_id_map - .lock() - .exchange_new_id(&imported_row_document_id); + let _ = old_to_new_id_map.exchange_new_id(&imported_row_document_id); } } } @@ -481,21 +459,21 @@ where { let mut collab_by_oid = HashMap::new(); for object_id in object_ids { - let collab = Collab::new( + let mut collab = Collab::new( old_user.session.user_id, object_id, "migrate_device", vec![], false, ); - match collab.with_origin_transact_mut(|txn| { - old_collab_r_txn.load_doc_with_txn(old_user.session.user_id, &object_id, txn) - }) { + let mut txn = collab.transact_mut(); + match old_collab_r_txn.load_doc_with_txn(old_user.session.user_id, &object_id, &mut txn) { Ok(_) => { + drop(txn); collab_by_oid.insert(object_id.clone(), collab); }, Err(err) => tracing::error!("🔴Initialize migration collab failed: {:?} ", err), - } + }; } collab_by_oid diff --git a/frontend/rust-lib/flowy-user/src/anon_user/mod.rs b/frontend/rust-lib/flowy-user/src/anon_user/mod.rs index 974850755f..8a65b6fa94 100644 --- a/frontend/rust-lib/flowy-user/src/anon_user/mod.rs +++ b/frontend/rust-lib/flowy-user/src/anon_user/mod.rs @@ -1,5 +1,3 @@ -pub use migrate_anon_user_collab::*; -pub use sync_supabase_user_collab::*; +//pub use migrate_anon_user_collab::*; -mod migrate_anon_user_collab; -mod sync_supabase_user_collab; +//mod migrate_anon_user_collab; diff --git a/frontend/rust-lib/flowy-user/src/anon_user/sync_supabase_user_collab.rs b/frontend/rust-lib/flowy-user/src/anon_user/sync_supabase_user_collab.rs index cee388e77b..7e95d9887c 100644 --- a/frontend/rust-lib/flowy-user/src/anon_user/sync_supabase_user_collab.rs +++ b/frontend/rust-lib/flowy-user/src/anon_user/sync_supabase_user_collab.rs @@ -1,18 +1,13 @@ -use std::future::Future; -use std::ops::Deref; -use std::pin::Pin; use std::sync::Arc; use anyhow::{anyhow, Error}; -use collab::core::collab::MutexCollab; -use collab::preclude::Collab; +use collab::preclude::{Collab, ReadTxn, StateVector}; use collab_database::database::get_database_row_ids; use collab_database::rows::database_row_document_id_from_row_id; -use collab_database::workspace_database::{get_all_database_meta, DatabaseMeta}; +use collab_database::workspace_database::{DatabaseMeta, DatabaseMetaList}; use collab_entity::{CollabObject, CollabType}; use collab_folder::{Folder, View, ViewLayout}; use collab_plugins::local_storage::kv::KVTransactionDB; -use parking_lot::Mutex; use collab_integrate::{CollabKVAction, CollabKVDB, PersistenceError}; use flowy_error::FlowyResult; @@ -28,16 +23,14 @@ pub async fn sync_supabase_user_data_to_cloud( ) -> FlowyResult<()> { let workspace_id = new_user_session.user_workspace.id.clone(); let uid = new_user_session.user_id; - let folder = Arc::new( - sync_folder( - uid, - &workspace_id, - device_id, - collab_db, - user_service.clone(), - ) - .await?, - ); + let folder = sync_folder( + uid, + &workspace_id, + device_id, + collab_db, + user_service.clone(), + ) + .await?; let database_records = sync_database_views( uid, @@ -49,12 +42,12 @@ pub async fn sync_supabase_user_data_to_cloud( ) .await; - let views = folder.lock().get_views_belong_to(&workspace_id); + let views = folder.get_views_belong_to(&workspace_id); for view in views { let view_id = view.id.clone(); if let Err(err) = sync_view( uid, - folder.clone(), + &folder, database_records.clone(), workspace_id.to_string(), device_id.to_string(), @@ -72,135 +65,132 @@ pub async fn sync_supabase_user_data_to_cloud( } #[allow(clippy::too_many_arguments)] -fn sync_view( +async fn sync_view( uid: i64, - folder: Arc, + folder: &Folder, database_metas: Vec>, workspace_id: String, device_id: String, view: Arc, collab_db: Arc, user_service: Arc, -) -> Pin> + Send + Sync>> { - Box::pin(async move { - let collab_type = collab_type_from_view_layout(&view.layout); - let object_id = object_id_from_view(&view, &database_metas)?; - tracing::debug!( - "sync view: {:?}:{} with object_id: {}", - view.layout, - view.id, - object_id - ); +) -> Result<(), Error> { + let collab_type = collab_type_from_view_layout(&view.layout); + let object_id = object_id_from_view(&view, &database_metas)?; + tracing::debug!( + "sync view: {:?}:{} with object_id: {}", + view.layout, + view.id, + object_id + ); - let collab_object = CollabObject::new( - uid, - object_id, - collab_type, - workspace_id.to_string(), - device_id.clone(), - ); + let collab_object = CollabObject::new( + uid, + object_id, + collab_type, + workspace_id.to_string(), + device_id.clone(), + ); - match view.layout { - ViewLayout::Document => { - let doc_state = get_collab_doc_state(uid, &collab_object, &collab_db)?; + match view.layout { + ViewLayout::Document => { + let doc_state = get_collab_doc_state(uid, &collab_object, &collab_db)?; + tracing::info!( + "sync object: {} with update: {}", + collab_object, + doc_state.len() + ); + user_service + .create_collab_object(&collab_object, doc_state) + .await?; + }, + ViewLayout::Grid | ViewLayout::Board | ViewLayout::Calendar => { + let (database_doc_state, row_ids) = get_database_doc_state(uid, &collab_object, &collab_db)?; + tracing::info!( + "sync object: {} with update: {}", + collab_object, + database_doc_state.len() + ); + user_service + .create_collab_object(&collab_object, database_doc_state) + .await?; + + // sync database's row + for row_id in row_ids { + tracing::debug!("sync row: {}", row_id); + let document_id = database_row_document_id_from_row_id(&row_id); + + let database_row_collab_object = CollabObject::new( + uid, + row_id, + CollabType::DatabaseRow, + workspace_id.to_string(), + device_id.clone(), + ); + let database_row_doc_state = + get_collab_doc_state(uid, &database_row_collab_object, &collab_db)?; tracing::info!( "sync object: {} with update: {}", - collab_object, - doc_state.len() + database_row_collab_object, + database_row_doc_state.len() ); - user_service - .create_collab_object(&collab_object, doc_state) - .await?; - }, - ViewLayout::Grid | ViewLayout::Board | ViewLayout::Calendar => { - let (database_doc_state, row_ids) = - get_database_doc_state(uid, &collab_object, &collab_db)?; - tracing::info!( - "sync object: {} with update: {}", - collab_object, - database_doc_state.len() + + let _ = user_service + .create_collab_object(&database_row_collab_object, database_row_doc_state) + .await; + + let database_row_document = CollabObject::new( + uid, + document_id, + CollabType::Document, + workspace_id.to_string(), + device_id.to_string(), ); - user_service - .create_collab_object(&collab_object, database_doc_state) - .await?; - - // sync database's row - for row_id in row_ids { - tracing::debug!("sync row: {}", row_id); - let document_id = database_row_document_id_from_row_id(&row_id); - - let database_row_collab_object = CollabObject::new( - uid, - row_id, - CollabType::DatabaseRow, - workspace_id.to_string(), - device_id.clone(), - ); - let database_row_doc_state = - get_collab_doc_state(uid, &database_row_collab_object, &collab_db)?; + // sync document in the row if exist + if let Ok(document_doc_state) = + get_collab_doc_state(uid, &database_row_document, &collab_db) + { tracing::info!( - "sync object: {} with update: {}", - database_row_collab_object, - database_row_doc_state.len() + "sync database row document: {} with update: {}", + database_row_document, + document_doc_state.len() ); - let _ = user_service - .create_collab_object(&database_row_collab_object, database_row_doc_state) + .create_collab_object(&database_row_document, document_doc_state) .await; - - let database_row_document = CollabObject::new( - uid, - document_id, - CollabType::Document, - workspace_id.to_string(), - device_id.to_string(), - ); - // sync document in the row if exist - if let Ok(document_doc_state) = - get_collab_doc_state(uid, &database_row_document, &collab_db) - { - tracing::info!( - "sync database row document: {} with update: {}", - database_row_document, - document_doc_state.len() - ); - let _ = user_service - .create_collab_object(&database_row_document, document_doc_state) - .await; - } } - }, - ViewLayout::Chat => {}, - } - - tokio::task::yield_now().await; - - let child_views = folder.lock().views.get_views_belong_to(&view.id); - for child_view in child_views { - let cloned_child_view = child_view.clone(); - if let Err(err) = Box::pin(sync_view( - uid, - folder.clone(), - database_metas.clone(), - workspace_id.clone(), - device_id.to_string(), - child_view, - collab_db.clone(), - user_service.clone(), - )) - .await - { - tracing::error!( - "🔴sync {:?}:{} failed: {:?}", - cloned_child_view.layout, - cloned_child_view.id, - err - ) } - tokio::task::yield_now().await; + }, + ViewLayout::Chat => {}, + } + + tokio::task::yield_now().await; + + let child_views = folder.get_views_belong_to(&view.id); + for child_view in child_views { + let cloned_child_view = child_view.clone(); + if let Err(err) = Box::pin(sync_view( + uid, + folder, + database_metas.clone(), + workspace_id.clone(), + device_id.to_string(), + child_view, + collab_db.clone(), + user_service.clone(), + )) + .await + { + tracing::error!( + "🔴sync {:?}:{} failed: {:?}", + cloned_child_view.layout, + cloned_child_view.id, + err + ) } - Ok(()) - }) + tokio::task::yield_now().await; + } + Ok(()) } fn get_collab_doc_state( @@ -208,12 +198,12 @@ fn get_collab_doc_state( collab_object: &CollabObject, collab_db: &Arc, ) -> Result, PersistenceError> { - let collab = Collab::new(uid, &collab_object.object_id, "phantom", vec![], false); - let _ = collab.with_origin_transact_mut(|txn| { - collab_db - .read_txn() - .load_doc_with_txn(uid, &collab_object.object_id, txn) - })?; + let mut collab = Collab::new(uid, &collab_object.object_id, "phantom", vec![], false); + collab_db.read_txn().load_doc_with_txn( + uid, + &collab_object.object_id, + &mut collab.transact_mut(), + )?; let doc_state = collab .encode_collab_v1(|_| Ok::<(), PersistenceError>(()))? .doc_state; @@ -229,12 +219,12 @@ fn get_database_doc_state( collab_object: &CollabObject, collab_db: &Arc, ) -> Result<(Vec, Vec), PersistenceError> { - let collab = Collab::new(uid, &collab_object.object_id, "phantom", vec![], false); - let _ = collab.with_origin_transact_mut(|txn| { - collab_db - .read_txn() - .load_doc_with_txn(uid, &collab_object.object_id, txn) - })?; + let mut collab = Collab::new(uid, &collab_object.object_id, "phantom", vec![], false); + collab_db.read_txn().load_doc_with_txn( + uid, + &collab_object.object_id, + &mut collab.transact_mut(), + )?; let row_ids = get_database_row_ids(&collab).unwrap_or_default(); let doc_state = collab @@ -253,22 +243,17 @@ async fn sync_folder( device_id: &str, collab_db: &Arc, user_service: Arc, -) -> Result { +) -> Result { let (folder, update) = { - let collab = Collab::new(uid, workspace_id, "phantom", vec![], false); + let mut collab = Collab::new(uid, workspace_id, "phantom", vec![], false); // Use the temporary result to short the lifetime of the TransactionMut - collab.with_origin_transact_mut(|txn| { - collab_db - .read_txn() - .load_doc_with_txn(uid, workspace_id, txn) - })?; + collab_db + .read_txn() + .load_doc_with_txn(uid, workspace_id, &mut collab.transact_mut())?; let doc_state = collab .encode_collab_v1(|_| Ok::<(), PersistenceError>(()))? .doc_state; - ( - MutexFolder::new(Folder::open(uid, Arc::new(MutexCollab::new(collab)), None)?), - doc_state, - ) + (Folder::open(uid, collab, None)?, doc_state) }; let collab_object = CollabObject::new( @@ -311,49 +296,38 @@ async fn sync_database_views( // Use the temporary result to short the lifetime of the TransactionMut let result = { - let collab = Collab::new(uid, database_views_aggregate_id, "phantom", vec![], false); - collab - .with_origin_transact_mut(|txn| { - collab_db - .read_txn() - .load_doc_with_txn(uid, database_views_aggregate_id, txn) - }) + let mut collab = Collab::new(uid, database_views_aggregate_id, "phantom", vec![], false); + let meta_list = DatabaseMetaList::new(&mut collab); + let mut txn = collab.transact_mut(); + collab_db + .read_txn() + .load_doc_with_txn(uid, database_views_aggregate_id, &mut txn) .map(|_| { - ( - get_all_database_meta(&collab), - collab - .encode_collab_v1(|_| Ok::<(), PersistenceError>(())) - .unwrap() - .doc_state, - ) + let records = meta_list.get_all_database_meta(&txn); + let doc_state = txn.encode_state_as_update_v2(&StateVector::default()); + (records, doc_state) }) }; - - if let Ok((records, doc_state)) = result { - let _ = user_service - .create_collab_object(&collab_object, doc_state.to_vec()) - .await; - records.into_iter().map(Arc::new).collect() - } else { - vec![] + match result { + Ok((records, doc_state)) => { + if let Err(e) = user_service + .create_collab_object(&collab_object, doc_state) + .await + { + tracing::error!( + "sync database views failed to create collab object: {:?}", + e + ); + } + records.into_iter().map(Arc::new).collect() + }, + Err(e) => { + tracing::error!("load doc {} failed: {:?}", database_views_aggregate_id, e); + vec![] + }, } } -struct MutexFolder(Mutex); -impl MutexFolder { - pub fn new(folder: Folder) -> Self { - Self(Mutex::new(folder)) - } -} -impl Deref for MutexFolder { - type Target = Mutex; - fn deref(&self) -> &Self::Target { - &self.0 - } -} -unsafe impl Sync for MutexFolder {} -unsafe impl Send for MutexFolder {} - fn collab_type_from_view_layout(view_layout: &ViewLayout) -> CollabType { match view_layout { ViewLayout::Document => CollabType::Document, diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index edad70387b..dbfd9b811a 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -181,17 +181,16 @@ pub struct OauthProviderDataPB { pub oauth_url: String, } +#[repr(u8)] #[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] pub enum AuthenticatorPB { Local = 0, - Supabase = 1, AppFlowyCloud = 2, } impl From for AuthenticatorPB { fn from(auth_type: Authenticator) -> Self { match auth_type { - Authenticator::Supabase => AuthenticatorPB::Supabase, Authenticator::Local => AuthenticatorPB::Local, Authenticator::AppFlowyCloud => AuthenticatorPB::AppFlowyCloud, } @@ -201,7 +200,6 @@ impl From for AuthenticatorPB { impl From for Authenticator { fn from(pb: AuthenticatorPB) -> Self { match pb { - AuthenticatorPB::Supabase => Authenticator::Supabase, AuthenticatorPB::Local => Authenticator::Local, AuthenticatorPB::AppFlowyCloud => Authenticator::AppFlowyCloud, } diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 2ecd11608c..c2e0c0d917 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -161,7 +161,7 @@ pub async fn set_appearance_setting( if setting.theme.is_empty() { setting.theme = APPEARANCE_DEFAULT_THEME.to_string(); } - store_preferences.set_object(APPEARANCE_SETTING_CACHE_KEY, setting)?; + store_preferences.set_object(APPEARANCE_SETTING_CACHE_KEY, &setting)?; Ok(()) } @@ -198,7 +198,7 @@ pub async fn set_date_time_settings( setting.timezone_id = "".to_string(); } - store_preferences.set_object(DATE_TIME_SETTINGS_CACHE_KEY, setting)?; + store_preferences.set_object(DATE_TIME_SETTINGS_CACHE_KEY, &setting)?; Ok(()) } @@ -234,7 +234,7 @@ pub async fn set_notification_settings( ) -> Result<(), FlowyError> { let store_preferences = upgrade_store_preferences(store_preferences)?; let setting = data.into_inner(); - store_preferences.set_object(NOTIFICATION_SETTINGS_CACHE_KEY, setting)?; + store_preferences.set_object(NOTIFICATION_SETTINGS_CACHE_KEY, &setting)?; Ok(()) } @@ -374,7 +374,7 @@ pub async fn set_encrypt_secret_handler( EncryptionType::SelfEncryption(data.encryption_sign), ) .await?; - save_cloud_config(data.user_id, &store_preferences, config)?; + save_cloud_config(data.user_id, &store_preferences, &config)?; }, } @@ -448,7 +448,7 @@ pub async fn set_cloud_config_handler( } } - save_cloud_config(session.user_id, &store_preferences, config.clone())?; + save_cloud_config(session.user_id, &store_preferences, &config)?; let payload = CloudSettingPB { enable_sync: config.enable_sync, diff --git a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs index cf59bac68c..8b0b0694b5 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use collab::core::collab::MutexCollab; use collab::core::origin::{CollabClient, CollabOrigin}; use collab::preclude::Collab; use collab_document::document::Document; @@ -53,8 +52,8 @@ impl UserDataMigration for HistoricalEmptyDocumentMigration { let folder = Folder::open(session.user_id, folder_collab, None) .map_err(|err| PersistenceError::Internal(err.into()))?; - if let Ok(workspace_id) = folder.try_get_workspace_id() { - let migration_views = folder.views.get_views_belong_to(&workspace_id); + if let Some(workspace_id) = folder.get_workspace_id() { + let migration_views = folder.get_views_belong_to(&workspace_id); // For historical reasons, the first level documents are empty. So migrate them by inserting // the default document data. for view in migration_views { @@ -87,17 +86,9 @@ where { // If the document is not exist, we don't need to migrate it. if load_collab(user_id, write_txn, &view.id).is_err() { - let collab = Arc::new(MutexCollab::new(Collab::new_with_origin( - origin.clone(), - &view.id, - vec![], - false, - ))); - let document = Document::create_with_data(collab, default_document_data(&view.id))?; - let encode = document - .get_collab() - .lock() - .encode_collab_v1(|_| Ok::<(), PersistenceError>(()))?; + let collab = Collab::new_with_origin(origin.clone(), &view.id, vec![], false); + let document = Document::open_with(collab, Some(default_document_data(&view.id)))?; + let encode = document.encode_collab_v1(|_| Ok::<(), PersistenceError>(()))?; write_txn.flush_doc_with(user_id, &view.id, &encode.doc_state, &encode.state_vector)?; event!( tracing::Level::INFO, diff --git a/frontend/rust-lib/flowy-user/src/migrations/mod.rs b/frontend/rust-lib/flowy-user/src/migrations/mod.rs index d5f83d47c9..bb43426e8a 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/mod.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/mod.rs @@ -1,4 +1,5 @@ use flowy_user_pub::session::Session; +use std::sync::Arc; pub mod document_empty_content; pub mod migration; @@ -9,5 +10,5 @@ pub mod workspace_trash_v1; #[derive(Clone, Debug)] pub struct AnonUser { - pub session: Session, + pub session: Arc, } diff --git a/frontend/rust-lib/flowy-user/src/migrations/util.rs b/frontend/rust-lib/flowy-user/src/migrations/util.rs index f0c4c3f7f7..f432ce05a6 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/util.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/util.rs @@ -1,21 +1,14 @@ -use std::sync::Arc; - -use collab::core::collab::MutexCollab; use collab::preclude::Collab; use collab_integrate::{CollabKVAction, PersistenceError}; use flowy_error::FlowyResult; -pub(crate) fn load_collab<'a, R>( - uid: i64, - collab_r_txn: &R, - object_id: &str, -) -> FlowyResult> +pub(crate) fn load_collab<'a, R>(uid: i64, collab_r_txn: &R, object_id: &str) -> FlowyResult where R: CollabKVAction<'a>, PersistenceError: From, { - let collab = Collab::new(uid, object_id, "phantom", vec![], false); - collab.with_origin_transact_mut(|txn| collab_r_txn.load_doc_with_txn(uid, &object_id, txn))?; - Ok(Arc::new(MutexCollab::new(collab))) + let mut collab = Collab::new(uid, object_id, "phantom", vec![], false); + collab_r_txn.load_doc_with_txn(uid, &object_id, &mut collab.transact_mut())?; + Ok(collab) } diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs index b6d5e3e8ff..e15bc5109c 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs @@ -36,9 +36,11 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { ) -> FlowyResult<()> { collab_db.with_write_txn(|write_txn| { if let Ok(collab) = load_collab(session.user_id, write_txn, &session.user_workspace.id) { - let folder = Folder::open(session.user_id, collab, None) + let mut folder = Folder::open(session.user_id, collab, None) .map_err(|err| PersistenceError::Internal(err.into()))?; - folder.migrate_workspace_to_view(); + folder + .body + .migrate_workspace_to_view(&mut folder.collab.transact_mut()); let favorite_view_ids = folder .get_favorite_v1() @@ -51,7 +53,7 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { } let encode = folder - .encode_collab_v1() + .encode_collab() .map_err(|err| PersistenceError::Internal(err.into()))?; write_txn.flush_doc_with( session.user_id, diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs index e15f2597b4..168c7e2510 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs @@ -34,7 +34,7 @@ impl UserDataMigration for WorkspaceTrashMapToSectionMigration { ) -> FlowyResult<()> { collab_db.with_write_txn(|write_txn| { if let Ok(collab) = load_collab(session.user_id, write_txn, &session.user_workspace.id) { - let folder = Folder::open(session.user_id, collab, None) + let mut folder = Folder::open(session.user_id, collab, None) .map_err(|err| PersistenceError::Internal(err.into()))?; let trash_ids = folder .get_trash_v1() @@ -47,7 +47,7 @@ impl UserDataMigration for WorkspaceTrashMapToSectionMigration { } let encode = folder - .encode_collab_v1() + .encode_collab() .map_err(|err| PersistenceError::Internal(err.into()))?; write_txn.flush_doc_with( session.user_id, diff --git a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs index 1df4fda3e2..a0d507a347 100644 --- a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs +++ b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs @@ -4,6 +4,9 @@ use crate::services::entities::{UserConfig, UserPaths}; use crate::services::sqlite_sql::user_sql::vacuum_database; use collab_integrate::CollabKVDB; +use arc_swap::ArcSwapOption; +use collab_plugins::local_storage::kv::doc::CollabKVAction; +use collab_plugins::local_storage::kv::KVTransactionDB; use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::DBConnection; @@ -20,22 +23,22 @@ pub struct AuthenticateUser { pub(crate) database: Arc, pub(crate) user_paths: UserPaths, store_preferences: Arc, - session: Arc>>, + session: ArcSwapOption, } impl AuthenticateUser { pub fn new(user_config: UserConfig, store_preferences: Arc) -> Self { let user_paths = UserPaths::new(user_config.storage_path.clone()); let database = Arc::new(UserDB::new(user_paths.clone())); - let session = Arc::new(parking_lot::RwLock::new(None)); - *session.write() = - migrate_session_with_user_uuid(&user_config.session_cache_key, &store_preferences); + let session = + migrate_session_with_user_uuid(&user_config.session_cache_key, &store_preferences) + .map(Arc::new); Self { user_config, database, user_paths, store_preferences, - session, + session: ArcSwapOption::from(session), } } @@ -67,7 +70,7 @@ impl AuthenticateUser { pub fn workspace_id(&self) -> FlowyResult { let session = self.get_session()?; - Ok(session.user_workspace.id) + Ok(session.user_workspace.id.clone()) } pub fn workspace_database_object_id(&self) -> FlowyResult { @@ -107,49 +110,57 @@ impl AuthenticateUser { Ok(()) } - pub fn set_session(&self, session: Option) -> Result<(), FlowyError> { - match &session { + pub fn is_collab_on_disk(&self, uid: i64, object_id: &str) -> FlowyResult { + let collab_db = self.database.get_collab_db(uid)?; + let read_txn = collab_db.read_txn(); + Ok(read_txn.is_exist(uid, &object_id)) + } + + pub fn set_session(&self, session: Option>) -> Result<(), FlowyError> { + match session { None => { - let removed_session = self.session.write().take(); - info!("remove session: {:?}", removed_session); + let previous = self.session.swap(session); + info!("remove session: {:?}", previous); self .store_preferences .remove(self.user_config.session_cache_key.as_ref()); - Ok(()) }, Some(session) => { + self.session.swap(Some(session.clone())); info!("Set current session: {:?}", session); - self.session.write().replace(session.clone()); self .store_preferences - .set_object(&self.user_config.session_cache_key, session.clone()) + .set_object(&self.user_config.session_cache_key, &session) .map_err(internal_error)?; - Ok(()) }, } + Ok(()) } pub fn set_user_workspace(&self, user_workspace: UserWorkspace) -> FlowyResult<()> { - let mut session = self.get_session()?; - session.user_workspace = user_workspace; - self.set_session(Some(session)) + let session = self.get_session()?; + self.set_session(Some(Arc::new(Session { + user_id: session.user_id, + user_uuid: session.user_uuid, + user_workspace, + }))) } - pub fn get_session(&self) -> FlowyResult { - if let Some(session) = (self.session.read()).clone() { + pub fn get_session(&self) -> FlowyResult> { + if let Some(session) = self.session.load_full() { return Ok(session); } match self .store_preferences - .get_object::(&self.user_config.session_cache_key) + .get_object::>(&self.user_config.session_cache_key) { None => Err(FlowyError::new( ErrorCode::RecordNotFound, "User is not logged in", )), Some(session) => { - self.session.write().replace(session.clone()); + self.session.store(Some(session.clone())); Ok(session) }, } diff --git a/frontend/rust-lib/flowy-user/src/services/cloud_config.rs b/frontend/rust-lib/flowy-user/src/services/cloud_config.rs index 62ab5a5e72..d4b4afc7a8 100644 --- a/frontend/rust-lib/flowy-user/src/services/cloud_config.rs +++ b/frontend/rust-lib/flowy-user/src/services/cloud_config.rs @@ -10,14 +10,14 @@ const CLOUD_CONFIG_KEY: &str = "af_user_cloud_config"; fn generate_cloud_config(uid: i64, store_preference: &Arc) -> UserCloudConfig { let config = UserCloudConfig::new(generate_encryption_secret()); let key = cache_key_for_cloud_config(uid); - store_preference.set_object(&key, config.clone()).unwrap(); + store_preference.set_object(&key, &config).unwrap(); config } pub fn save_cloud_config( uid: i64, store_preference: &Arc, - config: UserCloudConfig, + config: &UserCloudConfig, ) -> FlowyResult<()> { tracing::info!("save user:{} cloud config: {}", uid, config); let key = cache_key_for_cloud_config(uid); diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs index 6f7271658e..f7579d73e6 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs @@ -6,11 +6,10 @@ use crate::services::entities::UserPaths; use crate::services::sqlite_sql::user_sql::select_user_profile; use crate::user_manager::run_collab_data_migration; use anyhow::anyhow; -use collab::core::collab::{DataSource, MutexCollab}; +use collab::core::collab::DataSource; use collab::core::origin::CollabOrigin; -use collab::core::transaction::DocTransactionExtension; use collab::preclude::updates::decoder::Decode; -use collab::preclude::{Collab, Doc, Transact, Update}; +use collab::preclude::{Collab, Doc, ReadTxn, StateVector, Transact, Update}; use collab_database::database::{ is_database_collab, mut_database_views_with_collab, reset_inline_view_id, }; @@ -22,6 +21,7 @@ use collab_folder::{Folder, UserId, View, ViewIdentifier, ViewLayout}; use collab_integrate::{CollabKVAction, CollabKVDB, PersistenceError}; use collab_plugins::local_storage::kv::KVTransactionDB; +use collab::preclude::updates::encoder::Encode; use flowy_error::FlowyError; use flowy_folder_pub::cloud::gen_view_id; use flowy_folder_pub::entities::{AppFlowyData, ImportData}; @@ -30,7 +30,6 @@ use flowy_sqlite::kv::KVStorePreferences; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; use flowy_user_pub::entities::{user_awareness_object_id, Authenticator}; use flowy_user_pub::session::Session; -use parking_lot::{Mutex, RwLock}; use std::collections::{HashMap, HashSet}; use std::ops::{Deref, DerefMut}; use std::path::Path; @@ -129,9 +128,9 @@ pub(crate) fn generate_import_data( let imported_container_view_name = imported_folder.container_name.clone(); let mut database_view_ids_by_database_id: HashMap> = HashMap::new(); - let row_object_ids = Mutex::new(HashSet::new()); - let document_object_ids = Mutex::new(HashSet::new()); - let database_object_ids = Mutex::new(HashSet::new()); + let mut row_object_ids = HashSet::new(); + let mut document_object_ids = HashSet::new(); + let mut database_object_ids = HashSet::new(); // All the imported views will be attached to the container view. If the container view name is not provided, // the container view will be the workspace, which mean the root of the workspace. @@ -146,7 +145,7 @@ pub(crate) fn generate_import_data( let views = collab_db.with_write_txn(|collab_write_txn| { let imported_collab_read_txn = imported_collab_db.read_txn(); // use the old_to_new_id_map to keep track of the other collab object id and the new collab object id - let old_to_new_id_map = Arc::new(Mutex::new(OldToNewIdMap::new())); + let mut old_to_new_id_map = OldToNewIdMap::new(); // 1. Get all the imported collab object ids let mut all_imported_object_ids = imported_collab_read_txn @@ -171,17 +170,17 @@ pub(crate) fn generate_import_data( ImportedSource::ExternalFolder => { // 2. mapping the database indexer ids mapping_database_indexer_ids( - &mut old_to_new_id_map.lock(), + &mut old_to_new_id_map, &imported_session, &imported_collab_read_txn, &mut database_view_ids_by_database_id, - &database_object_ids, + &mut database_object_ids, )?; }, ImportedSource::AnonUser => { // 2. migrate the database with views object migrate_database_with_views_object( - &mut old_to_new_id_map.lock(), + &mut old_to_new_id_map, &imported_session, &imported_collab_read_txn, current_session, @@ -200,7 +199,7 @@ pub(crate) fn generate_import_data( all_imported_object_ids.retain(|id| !database_view_ids.contains(id)); // 3. load imported collab objects data. - let imported_collab_by_oid = load_collab_by_object_ids( + let mut imported_collab_by_oid = load_collab_by_object_ids( imported_session.user_id, &imported_collab_read_txn, &all_imported_object_ids, @@ -208,19 +207,19 @@ pub(crate) fn generate_import_data( // import the database migrate_databases( - &old_to_new_id_map, + &mut old_to_new_id_map, current_session, collab_write_txn, &mut all_imported_object_ids, - &imported_collab_by_oid, - &row_object_ids, + &mut imported_collab_by_oid, + &mut row_object_ids, )?; // the object ids now only contains the document collab object ids for object_id in &all_imported_object_ids { if let Some(imported_collab) = imported_collab_by_oid.get(object_id) { - let new_object_id = old_to_new_id_map.lock().exchange_new_id(object_id); - document_object_ids.lock().insert(new_object_id.clone()); + let new_object_id = old_to_new_id_map.exchange_new_id(object_id); + document_object_ids.insert(new_object_id.clone()); debug!("import from: {}, to: {}", object_id, new_object_id,); write_collab_object( imported_collab, @@ -236,7 +235,7 @@ pub(crate) fn generate_import_data( // structure is correctly maintained. let (mut child_views, orphan_views) = mapping_folder_views( &import_container_view_id, - &mut old_to_new_id_map.lock(), + &mut old_to_new_id_map, &imported_session, &imported_collab_read_txn, )?; @@ -251,7 +250,7 @@ pub(crate) fn generate_import_data( // create a new view with given name and then attach views to it attach_to_new_view( current_session, - &document_object_ids, + &mut document_object_ids, &import_container_view_id, collab_write_txn, child_views, @@ -274,16 +273,16 @@ pub(crate) fn generate_import_data( database_view_ids_by_database_id, }, AppFlowyData::CollabObject { - row_object_ids: row_object_ids.into_inner().into_iter().collect(), - database_object_ids: database_object_ids.into_inner().into_iter().collect(), - document_object_ids: document_object_ids.into_inner().into_iter().collect(), + row_object_ids: row_object_ids.into_iter().collect(), + database_object_ids: database_object_ids.into_iter().collect(), + document_object_ids: document_object_ids.into_iter().collect(), }, ], }) } fn attach_to_new_view<'a, W>( current_session: &Session, - document_object_ids: &Mutex>, + document_object_ids: &mut HashSet, import_container_view_id: &str, collab_write_txn: &'a W, child_views: Vec, @@ -315,9 +314,7 @@ where collab_write_txn, )?; - document_object_ids - .lock() - .insert(import_container_view_id.to_string()); + document_object_ids.insert(import_container_view_id.to_string()); let mut import_container_views = vec![ViewBuilder::new( current_session.user_id, current_session.user_workspace.id.clone(), @@ -337,29 +334,27 @@ fn mapping_database_indexer_ids<'a, W>( imported_session: &Session, imported_collab_read_txn: &W, database_view_ids_by_database_id: &mut HashMap>, - database_object_ids: &Mutex>, + database_object_ids: &mut HashSet, ) -> Result<(), PersistenceError> where W: CollabKVAction<'a>, PersistenceError: From, { - let imported_database_indexer = Collab::new( + let mut imported_database_indexer = Collab::new( imported_session.user_id, &imported_session.user_workspace.database_indexer_id, "import_device", vec![], false, ); - imported_database_indexer.with_origin_transact_mut(|txn| { - imported_collab_read_txn.load_doc_with_txn( - imported_session.user_id, - &imported_session.user_workspace.database_indexer_id, - txn, - ) - })?; + imported_collab_read_txn.load_doc_with_txn( + imported_session.user_id, + &imported_session.user_workspace.database_indexer_id, + &mut imported_database_indexer.transact_mut(), + )?; - let array = DatabaseMetaList::from_collab(&imported_database_indexer); - for database_meta_list in array.get_all_database_meta() { + let array = DatabaseMetaList::new(&mut imported_database_indexer); + for database_meta_list in array.get_all_database_meta(&imported_database_indexer.transact()) { database_view_ids_by_database_id.insert( old_to_new_id_map.exchange_new_id(&database_meta_list.database_id), database_meta_list @@ -369,7 +364,7 @@ where .collect(), ); } - database_object_ids.lock().extend( + database_object_ids.extend( database_view_ids_by_database_id .keys() .cloned() @@ -392,27 +387,26 @@ where PersistenceError: From, PersistenceError: From, { - let database_with_views_collab = Collab::new( + let mut database_with_views_collab = Collab::new( old_user_session.user_id, &old_user_session.user_workspace.database_indexer_id, "migrate_device", vec![], false, ); - database_with_views_collab.with_origin_transact_mut(|txn| { - old_collab_r_txn.load_doc_with_txn( - old_user_session.user_id, - &old_user_session.user_workspace.database_indexer_id, - txn, - ) - })?; + old_collab_r_txn.load_doc_with_txn( + old_user_session.user_id, + &old_user_session.user_workspace.database_indexer_id, + &mut database_with_views_collab.transact_mut(), + )?; let new_uid = new_user_session.user_id; let new_object_id = &new_user_session.user_workspace.database_indexer_id; - let array = DatabaseMetaList::from_collab(&database_with_views_collab); - for database_meta in array.get_all_database_meta() { - array.update_database(&database_meta.database_id, |update| { + let array = DatabaseMetaList::new(&mut database_with_views_collab); + let mut txn = database_with_views_collab.transact_mut(); + for database_meta in array.get_all_database_meta(&txn) { + array.update_database(&mut txn, &database_meta.database_id, |update| { let new_linked_views = update .linked_views .iter() @@ -423,7 +417,6 @@ where }) } - let txn = database_with_views_collab.transact(); if let Err(err) = new_collab_w_txn.create_new_doc(new_uid, new_object_id, &txn) { error!("🔴migrate database storage failed: {:?}", err); } @@ -432,61 +425,53 @@ where } fn migrate_databases<'a, W>( - old_to_new_id_map: &Arc>, + old_to_new_id_map: &mut OldToNewIdMap, session: &Session, collab_write_txn: &'a W, imported_object_ids: &mut Vec, - imported_collab_by_oid: &HashMap, - row_object_ids: &Mutex>, + imported_collab_by_oid: &mut HashMap, + row_object_ids: &mut HashSet, ) -> Result<(), PersistenceError> where W: CollabKVAction<'a>, PersistenceError: From, { // Migrate databases - let row_document_object_ids = Mutex::new(HashSet::new()); + let mut row_document_object_ids = HashSet::new(); let mut database_object_ids = vec![]; - let imported_database_row_object_ids: RwLock>> = - RwLock::new(HashMap::new()); + let mut imported_database_row_object_ids: HashMap> = HashMap::new(); - for object_id in &mut *imported_object_ids { - if let Some(database_collab) = imported_collab_by_oid.get(object_id) { + for object_id in imported_object_ids.iter() { + if let Some(database_collab) = imported_collab_by_oid.get_mut(object_id) { if !is_database_collab(database_collab) { continue; } database_object_ids.push(object_id.clone()); reset_inline_view_id(database_collab, |old_inline_view_id| { - old_to_new_id_map - .lock() - .exchange_new_id(&old_inline_view_id) + old_to_new_id_map.exchange_new_id(&old_inline_view_id) }); mut_database_views_with_collab(database_collab, |database_view| { - let new_view_id = old_to_new_id_map.lock().exchange_new_id(&database_view.id); + let new_view_id = old_to_new_id_map.exchange_new_id(&database_view.id); let old_database_id = database_view.database_id.clone(); - let new_database_id = old_to_new_id_map - .lock() - .exchange_new_id(&database_view.database_id); + let new_database_id = old_to_new_id_map.exchange_new_id(&database_view.database_id); database_view.id = new_view_id; database_view.database_id = new_database_id; database_view.row_orders.iter_mut().for_each(|row_order| { let old_row_id = String::from(row_order.id.clone()); let old_row_document_id = database_row_document_id_from_row_id(&old_row_id); - let new_row_id = old_to_new_id_map.lock().exchange_new_id(&old_row_id); + let new_row_id = old_to_new_id_map.exchange_new_id(&old_row_id); // The row document might not exist in the database row. But by querying the old_row_document_id, // we can know the document of the row is exist or not. let new_row_document_id = database_row_document_id_from_row_id(&new_row_id); - old_to_new_id_map - .lock() - .insert(old_row_document_id.clone(), new_row_document_id); + old_to_new_id_map.insert(old_row_document_id.clone(), new_row_document_id); row_order.id = RowId::from(new_row_id); imported_database_row_object_ids - .write() .entry(old_database_id.clone()) .or_default() .insert(old_row_id); @@ -498,10 +483,10 @@ where .iter() .map(|order| order.id.clone().into_inner()) .collect::>(); - row_object_ids.lock().extend(new_row_ids); + row_object_ids.extend(new_row_ids); }); - let new_object_id = old_to_new_id_map.lock().exchange_new_id(object_id); + let new_object_id = old_to_new_id_map.exchange_new_id(object_id); debug!( "migrate database from: {}, to: {}", object_id, new_object_id, @@ -514,7 +499,6 @@ where ); } } - let imported_database_row_object_ids = imported_database_row_object_ids.read(); // remove the database object ids from the object ids imported_object_ids.retain(|id| !database_object_ids.contains(id)); @@ -527,11 +511,11 @@ where .any(|row_id| row_id == id) }); - for (database_id, imported_row_ids) in &*imported_database_row_object_ids { + for (database_id, imported_row_ids) in imported_database_row_object_ids { for imported_row_id in imported_row_ids { - if let Some(imported_collab) = imported_collab_by_oid.get(imported_row_id) { - let new_database_id = old_to_new_id_map.lock().exchange_new_id(database_id); - let new_row_id = old_to_new_id_map.lock().exchange_new_id(imported_row_id); + if let Some(imported_collab) = imported_collab_by_oid.get_mut(&imported_row_id) { + let new_database_id = old_to_new_id_map.exchange_new_id(&database_id); + let new_row_id = old_to_new_id_map.exchange_new_id(&imported_row_id); info!( "import database row from: {}, to: {}", imported_row_id, new_row_id, @@ -550,25 +534,20 @@ where // imported_collab_by_oid contains all the collab object ids, including the row document collab object ids. // So, if the id exist in the imported_collab_by_oid, it means the row document collab object is exist. - let imported_row_document_id = database_row_document_id_from_row_id(imported_row_id); + let imported_row_document_id = database_row_document_id_from_row_id(&imported_row_id); if imported_collab_by_oid .get(&imported_row_document_id) .is_some() { - let new_row_document_id = old_to_new_id_map - .lock() - .exchange_new_id(&imported_row_document_id); - row_document_object_ids.lock().insert(new_row_document_id); + let new_row_document_id = old_to_new_id_map.exchange_new_id(&imported_row_document_id); + row_document_object_ids.insert(new_row_document_id); } } } debug!( "import row document ids: {:?}", - row_document_object_ids - .lock() - .iter() - .collect::>() + row_document_object_ids.iter().collect::>() ); Ok(()) @@ -588,18 +567,17 @@ where drop(txn); } - let encoded_collab = doc.get_encoded_collab_v1(); + let txn = doc.transact(); + let state_vector = txn.state_vector(); + let doc_state = txn.encode_state_as_update_v1(&StateVector::default()); info!( "import collab:{} with len: {}", new_object_id, - encoded_collab.doc_state.len() + doc_state.len() ); - if let Err(err) = w_txn.flush_doc( - new_uid, - &new_object_id, - encoded_collab.state_vector.to_vec(), - encoded_collab.doc_state.to_vec(), - ) { + if let Err(err) = + w_txn.flush_doc(new_uid, &new_object_id, state_vector.encode_v1(), doc_state) + { error!("import collab:{} failed: {:?}", new_object_id, err); } } @@ -639,27 +617,21 @@ where W: CollabKVAction<'a>, PersistenceError: From, { - let imported_folder_collab = Collab::new( + let mut imported_folder_collab = Collab::new( imported_session.user_id, &imported_session.user_workspace.id, "migrate_device", vec![], false, ); - imported_folder_collab.with_origin_transact_mut(|txn| { - imported_collab_read_txn.load_doc_with_txn( - imported_session.user_id, - &imported_session.user_workspace.id, - txn, - ) - })?; + imported_collab_read_txn.load_doc_with_txn( + imported_session.user_id, + &imported_session.user_workspace.id, + &mut imported_folder_collab.transact_mut(), + )?; let other_user_id = UserId::from(imported_session.user_id); - let imported_folder = Folder::open( - other_user_id, - Arc::new(MutexCollab::new(imported_folder_collab)), - None, - ) - .map_err(|err| PersistenceError::InvalidData(err.to_string()))?; + let imported_folder = Folder::open(other_user_id, imported_folder_collab, None) + .map_err(|err| PersistenceError::InvalidData(err.to_string()))?; let imported_folder_data = imported_folder .get_folder_data(&imported_session.user_workspace.id) diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/importer.rs b/frontend/rust-lib/flowy-user/src/services/data_import/importer.rs index 47d7167fb4..be2712827f 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/importer.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/importer.rs @@ -39,8 +39,7 @@ where R: CollabKVAction<'a>, PersistenceError: From, { - let collab = Collab::new(uid, object_id, "phantom", vec![], false); - collab - .with_origin_transact_mut(|txn| collab_read_txn.load_doc_with_txn(uid, object_id, txn)) - .map(|_| collab) + let mut collab = Collab::new(uid, object_id, "phantom", vec![], false); + collab_read_txn.load_doc_with_txn(uid, object_id, &mut collab.transact_mut())?; + Ok(collab) } diff --git a/frontend/rust-lib/flowy-user/src/services/db.rs b/frontend/rust-lib/flowy-user/src/services/db.rs index 3305fca41a..f16d242a96 100644 --- a/frontend/rust-lib/flowy-user/src/services/db.rs +++ b/frontend/rust-lib/flowy-user/src/services/db.rs @@ -1,9 +1,11 @@ use std::path::{Path, PathBuf}; -use std::{collections::HashMap, fs, io, sync::Arc, time::Duration}; +use std::{fs, io, sync::Arc}; use chrono::{Days, Local}; use collab_integrate::{CollabKVAction, CollabKVDB, PersistenceError}; use collab_plugins::local_storage::kv::KVTransactionDB; +use dashmap::mapref::entry::Entry; +use dashmap::DashMap; use flowy_error::FlowyError; use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::ConnectionPool; @@ -15,7 +17,6 @@ use flowy_sqlite::{ use flowy_user_pub::entities::{UserProfile, UserWorkspace}; use lib_dispatch::prelude::af_spawn; use lib_infra::file_util::{unzip_and_replace, zip_folder}; -use parking_lot::RwLock; use tracing::{error, event, info, instrument}; use crate::services::sqlite_sql::user_sql::UserTable; @@ -29,8 +30,8 @@ pub trait UserDBPath: Send + Sync + 'static { pub struct UserDB { paths: Box, - sqlite_map: RwLock>, - collab_db_map: RwLock>>, + sqlite_map: DashMap, + collab_db_map: DashMap>, } impl UserDB { @@ -112,18 +113,14 @@ impl UserDB { /// Close the database connection for the user. pub(crate) fn close(&self, user_id: i64) -> Result<(), FlowyError> { - if let Some(mut sqlite_dbs) = self.sqlite_map.try_write_for(Duration::from_millis(300)) { - if sqlite_dbs.remove(&user_id).is_some() { - tracing::trace!("close sqlite db for user {}", user_id); - } + if self.sqlite_map.remove(&user_id).is_some() { + tracing::trace!("close sqlite db for user {}", user_id); } - if let Some(mut collab_dbs) = self.collab_db_map.try_write_for(Duration::from_millis(300)) { - if let Some(db) = collab_dbs.remove(&user_id) { - tracing::trace!("close collab db for user {}", user_id); - let _ = db.flush(); - drop(db); - } + if let Some((_, db)) = self.collab_db_map.remove(&user_id) { + tracing::trace!("close collab db for user {}", user_id); + let _ = db.flush(); + drop(db); } Ok(()) } @@ -148,18 +145,18 @@ impl UserDB { db_path: impl AsRef, user_id: i64, ) -> Result, FlowyError> { - if let Some(database) = self.sqlite_map.read().get(&user_id) { - return Ok(database.get_pool()); + match self.sqlite_map.entry(user_id) { + Entry::Occupied(e) => Ok(e.get().get_pool()), + Entry::Vacant(e) => { + tracing::debug!("open sqlite db {} at path: {:?}", user_id, db_path.as_ref()); + let db = flowy_sqlite::init(&db_path).map_err(|e| { + FlowyError::internal().with_context(format!("open user db failed, {:?}", e)) + })?; + let pool = db.get_pool(); + e.insert(db); + Ok(pool) + }, } - - let mut write_guard = self.sqlite_map.write(); - tracing::debug!("open sqlite db {} at path: {:?}", user_id, db_path.as_ref()); - let db = flowy_sqlite::init(&db_path) - .map_err(|e| FlowyError::internal().with_context(format!("open user db failed, {:?}", e)))?; - let pool = db.get_pool(); - write_guard.insert(user_id.to_owned(), db); - drop(write_guard); - Ok(pool) } pub fn get_user_profile( @@ -195,28 +192,27 @@ impl UserDB { collab_db_path: impl AsRef, uid: i64, ) -> Result, PersistenceError> { - if let Some(collab_db) = self.collab_db_map.read().get(&uid) { - return Ok(collab_db.clone()); - } + match self.collab_db_map.entry(uid) { + Entry::Occupied(e) => Ok(e.get().clone()), + Entry::Vacant(e) => { + info!( + "open collab db for user {} at path: {:?}", + uid, + collab_db_path.as_ref() + ); + let db = match CollabKVDB::open(&collab_db_path) { + Ok(db) => Ok(db), + Err(err) => { + error!("open collab db error, {:?}", err); + Err(err) + }, + }?; - let mut write_guard = self.collab_db_map.write(); - info!( - "open collab db for user {} at path: {:?}", - uid, - collab_db_path.as_ref() - ); - let db = match CollabKVDB::open(&collab_db_path) { - Ok(db) => Ok(db), - Err(err) => { - error!("open collab db error, {:?}", err); - Err(err) + let db = Arc::new(db); + e.insert(db.clone()); + Ok(db) }, - }?; - - let db = Arc::new(db); - write_guard.insert(uid.to_owned(), db.clone()); - drop(write_guard); - Ok(db) + } } } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index 8d8cb151fd..78443360aa 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -1,8 +1,10 @@ use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; -use collab_user::core::MutexUserAwareness; use flowy_error::{internal_error, ErrorCode, FlowyResult}; +use arc_swap::ArcSwapOption; +use collab_user::core::UserAwareness; +use dashmap::DashMap; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::schema::user_table; @@ -14,7 +16,7 @@ use flowy_user_pub::workspace_service::UserWorkspaceService; use semver::Version; use serde_json::Value; use std::string::ToString; -use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; +use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::{Arc, Weak}; use tokio::sync::{Mutex, RwLock}; use tokio_stream::StreamExt; @@ -23,7 +25,6 @@ use tracing::{debug, error, event, info, instrument, trace, warn}; use lib_dispatch::prelude::af_spawn; use lib_infra::box_any::BoxAny; -use crate::anon_user::{migration_anon_user_on_sign_up, sync_supabase_user_data_to_cloud}; use crate::entities::{AuthStateChangedPB, AuthStatePB, UserProfilePB, UserSettingPB}; use crate::event_map::{DefaultUserStatusCallback, UserStatusCallback}; use crate::migrations::document_empty_content::HistoricalEmptyDocumentMigration; @@ -49,7 +50,7 @@ use super::manager_user_workspace::save_user_workspace; pub struct UserManager { pub(crate) cloud_services: Arc, pub(crate) store_preferences: Arc, - pub(crate) user_awareness: Arc>>, + pub(crate) user_awareness: Arc>>, pub(crate) user_status_callback: RwLock>, pub(crate) collab_builder: Weak, pub(crate) collab_interact: RwLock>, @@ -57,7 +58,7 @@ pub struct UserManager { auth_process: Mutex>, pub(crate) authenticate_user: Arc, refresh_user_profile_since: AtomicI64, - pub(crate) is_loading_awareness: Arc, + pub(crate) is_loading_awareness: Arc>, } impl UserManager { @@ -75,7 +76,7 @@ impl UserManager { let user_manager = Arc::new(Self { cloud_services, store_preferences, - user_awareness: Arc::new(Default::default()), + user_awareness: Default::default(), user_status_callback, collab_builder, collab_interact: RwLock::new(Arc::new(DefaultCollabInteract)), @@ -83,7 +84,7 @@ impl UserManager { authenticate_user, refresh_user_profile_since, user_workspace_service, - is_loading_awareness: Arc::new(AtomicBool::new(false)), + is_loading_awareness: Arc::new(Default::default()), }); let weak_user_manager = Arc::downgrade(&user_manager); @@ -267,8 +268,10 @@ impl UserManager { } self.authenticate_user.vacuum_database_if_need(); let cloud_config = get_cloud_config(session.user_id, &self.store_preferences); - // Init the user awareness - self.initialize_user_awareness(&session).await; + // Init the user awareness. here we ignore the error + let _ = self + .initial_user_awareness(&session, &user.authenticator) + .await; user_status_callback .did_init( @@ -283,7 +286,7 @@ impl UserManager { Ok(()) } - pub fn get_session(&self) -> FlowyResult { + pub fn get_session(&self) -> FlowyResult> { self.authenticate_user.get_session() } @@ -338,7 +341,9 @@ impl UserManager { .save_auth_data(&response, &authenticator, &session) .await?; - let _ = self.initialize_user_awareness(&session).await; + let _ = self + .initial_user_awareness(&session, &user_profile.authenticator) + .await; self .user_status_callback .read() @@ -426,7 +431,9 @@ impl UserManager { self .save_auth_data(&response, authenticator, &new_session) .await?; - let _ = self.try_initial_user_awareness(&new_session).await; + let _ = self + .initial_user_awareness(&new_session, &new_user_profile.authenticator) + .await; self .user_status_callback .read() @@ -617,7 +624,8 @@ impl UserManager { } pub fn workspace_id(&self) -> Result { - Ok(self.get_session()?.user_workspace.id) + let session = self.get_session()?; + Ok(session.user_workspace.id.clone()) } pub fn token(&self) -> Result, FlowyError> { @@ -714,7 +722,7 @@ impl UserManager { let uid = user_profile.uid; if authenticator.is_local() { event!(tracing::Level::DEBUG, "Save new anon user: {:?}", uid); - self.set_anon_user(session.clone()); + self.set_anon_user(session); } save_all_user_workspaces(uid, self.db_connection(uid)?, response.user_workspaces())?; @@ -723,7 +731,9 @@ impl UserManager { authenticator ); - self.authenticate_user.set_session(Some(session.clone()))?; + self + .authenticate_user + .set_session(Some(session.clone().into()))?; self .save_user(uid, (user_profile, authenticator.clone()).into()) .await?; @@ -753,38 +763,18 @@ impl UserManager { async fn migrate_anon_user_data_to_cloud( &self, old_user: &AnonUser, - new_user_session: &Session, + _new_user_session: &Session, authenticator: &Authenticator, ) -> Result<(), FlowyError> { let old_collab_db = self .authenticate_user .database .get_collab_db(old_user.session.user_id)?; - let new_collab_db = self - .authenticate_user - .database - .get_collab_db(new_user_session.user_id)?; - match authenticator { - Authenticator::Supabase => { - migration_anon_user_on_sign_up(old_user, &old_collab_db, new_user_session, &new_collab_db)?; - if let Err(err) = sync_supabase_user_data_to_cloud( - self.cloud_services.get_user_service()?, - &self.authenticate_user.user_config.device_id, - new_user_session, - &new_collab_db, - ) - .await - { - error!("Sync user data to cloud failed: {:?}", err); - } - }, - Authenticator::AppFlowyCloud => { - self - .migration_anon_user_on_appflowy_cloud_sign_up(old_user, &old_collab_db) - .await?; - }, - _ => {}, + if authenticator == &Authenticator::AppFlowyCloud { + self + .migration_anon_user_on_appflowy_cloud_sign_up(old_user, &old_collab_db) + .await?; } // Save the old user workspace setting. @@ -803,7 +793,6 @@ impl UserManager { fn current_authenticator() -> Authenticator { match AuthenticatorType::from_env() { AuthenticatorType::Local => Authenticator::Local, - AuthenticatorType::Supabase => Authenticator::Supabase, AuthenticatorType::AppFlowyCloud => Authenticator::AppFlowyCloud, } } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs index 251a77bd98..8d20bae427 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs @@ -1,3 +1,4 @@ +use std::sync::Arc; use tracing::instrument; use crate::entities::UserProfilePB; @@ -33,7 +34,7 @@ impl UserManager { } } - pub fn set_anon_user(&self, session: Session) { + pub fn set_anon_user(&self, session: &Session) { let _ = self.store_preferences.set_object(ANON_USER, session); } @@ -63,7 +64,7 @@ impl UserManager { pub async fn open_anon_user(&self) -> FlowyResult<()> { let anon_session = self .store_preferences - .get_object::(ANON_USER) + .get_object::>(ANON_USER) .ok_or(FlowyError::new( ErrorCode::RecordNotFound, "Anon user not found", diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs index ec6dab5499..224c91467a 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs @@ -1,17 +1,20 @@ -use std::sync::atomic::Ordering; use std::sync::{Arc, Weak}; use anyhow::Context; -use collab::core::collab::{DataSource, MutexCollab}; +use collab::core::collab::DataSource; use collab_entity::reminder::Reminder; use collab_entity::CollabType; -use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; -use collab_user::core::{MutexUserAwareness, UserAwareness}; -use tracing::{debug, error, info, instrument, trace}; +use collab_integrate::collab_builder::{ + AppFlowyCollabBuilder, CollabBuilderConfig, KVDBCollabPersistenceImpl, +}; +use collab_user::core::{UserAwareness, UserAwarenessNotifier}; +use dashmap::try_result::TryResult; +use tokio::sync::RwLock; +use tracing::{error, info, instrument, trace}; use collab_integrate::CollabKVDB; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_user_pub::entities::user_awareness_object_id; +use flowy_user_pub::entities::{user_awareness_object_id, Authenticator}; use crate::entities::ReminderPB; use crate::user_manager::UserManager; @@ -34,10 +37,10 @@ impl UserManager { pub async fn add_reminder(&self, reminder_pb: ReminderPB) -> FlowyResult<()> { let reminder = Reminder::from(reminder_pb); self - .with_awareness((), |user_awareness| { + .mut_awareness(|user_awareness| { user_awareness.add_reminder(reminder.clone()); }) - .await; + .await?; self .collab_interact .read() @@ -51,10 +54,10 @@ impl UserManager { /// pub async fn remove_reminder(&self, reminder_id: &str) -> FlowyResult<()> { self - .with_awareness((), |user_awareness| { + .mut_awareness(|user_awareness| { user_awareness.remove_reminder(reminder_id); }) - .await; + .await?; self .collab_interact .read() @@ -69,12 +72,12 @@ impl UserManager { pub async fn update_reminder(&self, reminder_pb: ReminderPB) -> FlowyResult<()> { let reminder = Reminder::from(reminder_pb); self - .with_awareness((), |user_awareness| { + .mut_awareness(|user_awareness| { user_awareness.update_reminder(&reminder.id, |new_reminder| { new_reminder.clone_from(&reminder) }); }) - .await; + .await?; self .collab_interact .read() @@ -95,117 +98,203 @@ impl UserManager { /// - Returns a vector of `Reminder` objects containing all reminders for the user. /// pub async fn get_all_reminders(&self) -> Vec { - self - .with_awareness(vec![], |user_awareness| user_awareness.get_all_reminders()) - .await + let reminders = self + .mut_awareness(|user_awareness| user_awareness.get_all_reminders()) + .await; + reminders.unwrap_or_default() } - pub async fn initialize_user_awareness(&self, session: &Session) { - match self.try_initial_user_awareness(session).await { - Ok(_) => {}, - Err(e) => error!("Failed to initialize user awareness: {:?}", e), - } - } - - /// Initializes the user's awareness based on the specified data source. - /// - /// This asynchronous function attempts to initialize the user's awareness from either a local or remote data source. - /// Depending on the chosen source, it will either construct the user awareness from an empty dataset or fetch it - /// from a remote service. Once obtained, the user's awareness is stored in a shared mutex-protected structure. - /// - /// # Parameters - /// - `session`: The current user's session data. - /// - `source`: The source from which the user's awareness data should be obtained, either local or remote. - /// - /// # Returns - /// - Returns `Ok(())` if the user's awareness is successfully initialized. - /// - May return errors of type `FlowyError` if any issues arise during the initialization. + /// Init UserAwareness for user + /// 1. check if user awareness exists on disk. If yes init awareness from disk + /// 2. If not, init awareness from server. #[instrument(level = "info", skip(self, session), err)] - pub(crate) async fn try_initial_user_awareness(&self, session: &Session) -> FlowyResult<()> { - if self.is_loading_awareness.load(Ordering::SeqCst) { - return Ok(()); - } - self.is_loading_awareness.store(true, Ordering::SeqCst); - - if let Some(old_user_awareness) = self.user_awareness.lock().await.take() { - debug!("Closing old user awareness"); - old_user_awareness.lock().close(); - drop(old_user_awareness); - } - + pub(crate) async fn initial_user_awareness( + &self, + session: &Session, + authenticator: &Authenticator, + ) -> FlowyResult<()> { + let authenticator = authenticator.clone(); let object_id = user_awareness_object_id(&session.user_uuid, &session.user_workspace.id).to_string(); - trace!("Initializing user awareness {}", object_id); - let collab_db = self.get_collab_db(session.user_id)?; - let weak_cloud_services = Arc::downgrade(&self.cloud_services); - let weak_user_awareness = Arc::downgrade(&self.user_awareness); - let weak_builder = self.collab_builder.clone(); - let cloned_is_loading = self.is_loading_awareness.clone(); - let session = session.clone(); - let workspace_id = session.user_workspace.id.clone(); - tokio::spawn(async move { - if cloned_is_loading.load(Ordering::SeqCst) { - return Ok(()); + + // Try to acquire mutable access to `is_loading_awareness`. + // Thread-safety is ensured by DashMap + let should_init = match self.is_loading_awareness.try_get_mut(&object_id) { + TryResult::Present(mut is_loading) => { + if *is_loading { + false + } else { + *is_loading = true; + true + } + }, + TryResult::Absent => true, + TryResult::Locked => { + return Err(FlowyError::new( + ErrorCode::Internal, + format!( + "Failed to lock is_loading_awareness for object: {}", + object_id + ), + )); + }, + }; + + if should_init { + if let Some(old_user_awareness) = self.user_awareness.swap(None) { + info!("Closing previous user awareness"); + old_user_awareness.read().await.close(); // Ensure that old awareness is closed } - if let (Some(cloud_services), Some(user_awareness)) = - (weak_cloud_services.upgrade(), weak_user_awareness.upgrade()) - { + let is_exist_on_disk = self + .authenticate_user + .is_collab_on_disk(session.user_id, &object_id)?; + if authenticator.is_local() || is_exist_on_disk { + trace!( + "Initializing new user awareness from disk:{}, {:?}", + object_id, + authenticator + ); + let collab_db = self.get_collab_db(session.user_id)?; + let doc_state = + KVDBCollabPersistenceImpl::new(collab_db.clone(), session.user_id).into_data_source(); + let awareness = Self::collab_for_user_awareness( + &self.collab_builder.clone(), + &session.user_workspace.id, + session.user_id, + &object_id, + collab_db, + doc_state, + None, + )?; + info!("User awareness initialized successfully"); + self.user_awareness.store(Some(awareness)); + if let Some(mut is_loading) = self.is_loading_awareness.get_mut(&object_id) { + *is_loading = false; + } + } else { + info!( + "Initializing new user awareness from server:{}, {:?}", + object_id, authenticator + ); + self.load_awareness_from_server(session, object_id, authenticator.clone())?; + } + } else { + return Err(FlowyError::new( + ErrorCode::Internal, + format!( + "User awareness is already being loaded for object: {}", + object_id + ), + )); + } + + Ok(()) + } + + /// Initialize UserAwareness from server. + /// It will spawn a task in the background in order to no block the caller. This functions is + /// designed to be thread safe. + fn load_awareness_from_server( + &self, + session: &Session, + object_id: String, + authenticator: Authenticator, + ) -> FlowyResult<()> { + // Clone necessary data + let session = session.clone(); + let collab_db = self.get_collab_db(session.user_id)?; + let weak_builder = self.collab_builder.clone(); + let user_awareness = Arc::downgrade(&self.user_awareness); + let cloud_services = self.cloud_services.clone(); + let authenticate_user = self.authenticate_user.clone(); + let is_loading_awareness = self.is_loading_awareness.clone(); + + // Spawn an async task to fetch or create user awareness + tokio::spawn(async move { + let set_is_loading_false = || { + if let Some(mut is_loading) = is_loading_awareness.get_mut(&object_id) { + *is_loading = false; + } + }; + + let create_awareness = if authenticator.is_local() { + let doc_state = + KVDBCollabPersistenceImpl::new(collab_db.clone(), session.user_id).into_data_source(); + Self::collab_for_user_awareness( + &weak_builder, + &session.user_workspace.id, + session.user_id, + &object_id, + collab_db, + doc_state, + None, + ) + } else { let result = cloud_services .get_user_service()? .get_user_awareness_doc_state(session.user_id, &session.user_workspace.id, &object_id) .await; - let mut lock_awareness = user_awareness - .try_lock() - .map_err(|err| FlowyError::internal().with_context(err))?; - if lock_awareness.is_some() { - return Ok(()); - } - - let awareness = match result { + match result { Ok(data) => { - trace!("Get user awareness collab from remote: {}", data.len()); - let collab = Self::collab_for_user_awareness( - &workspace_id, + trace!("Fetched user awareness collab from remote: {}", data.len()); + Self::collab_for_user_awareness( &weak_builder, + &session.user_workspace.id, session.user_id, &object_id, collab_db, DataSource::DocStateV1(data), + None, ) - .await?; - MutexUserAwareness::new(UserAwareness::create(collab, None)) }, Err(err) => { if err.is_record_not_found() { info!("User awareness not found, creating new"); - let collab = Self::collab_for_user_awareness( - &workspace_id, + let doc_state = KVDBCollabPersistenceImpl::new(collab_db.clone(), session.user_id) + .into_data_source(); + Self::collab_for_user_awareness( &weak_builder, + &session.user_workspace.id, session.user_id, &object_id, collab_db, - DataSource::Disk, + doc_state, + None, ) - .await?; - MutexUserAwareness::new(UserAwareness::create(collab, None)) } else { - error!("Failed to fetch user awareness: {:?}", err); - return Err(err); + Err(err) } }, - }; + } + }; - trace!("User awareness initialized"); - lock_awareness.replace(awareness); + match create_awareness { + Ok(new_user_awareness) => { + // Validate session before storing the awareness + if let Ok(current_session) = authenticate_user.get_session() { + if current_session.user_workspace.id == session.user_workspace.id { + if let Some(user_awareness) = user_awareness.upgrade() { + info!("User awareness initialized successfully"); + user_awareness.store(Some(new_user_awareness)); + } else { + error!("Failed to upgrade user awareness"); + } + } else { + info!("User awareness is outdated, ignoring"); + } + } + set_is_loading_false(); + Ok(()) + }, + Err(err) => { + error!("Error while creating user awareness: {:?}", err); + set_is_loading_false(); + Err(err) + }, } - Ok(()) }); - - // mark the user awareness as not loading - self.is_loading_awareness.store(false, Ordering::SeqCst); - Ok(()) } @@ -214,29 +303,29 @@ impl UserManager { /// This function constructs a collaboration instance based on the given session and raw data, /// using a collaboration builder. This instance is specifically geared towards handling /// user awareness. - async fn collab_for_user_awareness( - workspace_id: &str, + fn collab_for_user_awareness( collab_builder: &Weak, + workspace_id: &str, uid: i64, object_id: &str, collab_db: Weak, doc_state: DataSource, - ) -> Result, FlowyError> { + notifier: Option, + ) -> Result>, FlowyError> { let collab_builder = collab_builder.upgrade().ok_or(FlowyError::new( ErrorCode::Internal, "Unexpected error: collab builder is not available", ))?; + let collab_object = + collab_builder.collab_object(workspace_id, uid, object_id, CollabType::UserAwareness)?; let collab = collab_builder - .build( - workspace_id, - uid, - object_id, - CollabType::UserAwareness, + .create_user_awareness( + collab_object, doc_state, collab_db, CollabBuilderConfig::default().sync_enable(true), + notifier, ) - .await .context("Build collab for user awareness failed")?; Ok(collab) } @@ -252,26 +341,39 @@ impl UserManager { /// # Parameters /// - `default_value`: A default value to return if the user awareness is `None` and cannot be initialized. /// - `f`: The asynchronous closure to execute with the user awareness. - async fn with_awareness(&self, default_value: Output, f: F) -> Output + async fn mut_awareness(&self, f: F) -> FlowyResult where - F: FnOnce(&UserAwareness) -> Output, + F: FnOnce(&mut UserAwareness) -> Output, { - // Check if initialization is needed and perform it if necessary - if self.user_awareness.lock().await.is_none() { - if let Ok(session) = self.get_session() { - self.initialize_user_awareness(&session).await; - } - } + match self.user_awareness.load_full() { + None => { + info!("User awareness is not loaded when trying to access it"); - let user_awareness = self.user_awareness.lock().await; - match &*user_awareness { - Some(inner_awareness) => { - let inner_awareness_clone = inner_awareness.clone(); - drop(user_awareness); - let result = f(&inner_awareness_clone.lock()); - result + let session = self.get_session()?; + let object_id = + user_awareness_object_id(&session.user_uuid, &session.user_workspace.id).to_string(); + let is_loading = self + .is_loading_awareness + .get(&object_id) + .map(|r| *r.value()) + .unwrap_or(false); + + if !is_loading { + let user_profile = self.get_user_profile_from_disk(session.user_id).await?; + self + .initial_user_awareness(&session, &user_profile.authenticator) + .await?; + } + + Err(FlowyError::new( + ErrorCode::InProgress, + "User awareness is loading", + )) + }, + Some(lock) => { + let mut user_awareness = lock.write().await; + Ok(f(&mut user_awareness)) }, - None => default_value, } } } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index 97cc6747f2..4b56b51df0 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -157,7 +157,7 @@ impl UserManager { old_collab_db: &Arc, ) -> FlowyResult<()> { let import_context = ImportedFolder { - imported_session: old_user.session.clone(), + imported_session: old_user.session.as_ref().clone(), imported_collab_db: old_collab_db.clone(), container_name: None, source: ImportedSource::AnonUser, @@ -179,14 +179,19 @@ impl UserManager { .authenticate_user .set_user_workspace(user_workspace.clone())?; - if let Err(err) = self.try_initial_user_awareness(&self.get_session()?).await { + let uid = self.user_id()?; + let user_profile = self.get_user_profile_from_disk(uid).await?; + + if let Err(err) = self + .initial_user_awareness(self.get_session()?.as_ref(), &user_profile.authenticator) + .await + { error!( "Failed to initialize user awareness when opening workspace: {:?}", err ); } - let uid = self.user_id()?; if let Err(err) = self .user_status_callback .read() diff --git a/frontend/rust-lib/lib-dispatch/Cargo.toml b/frontend/rust-lib/lib-dispatch/Cargo.toml index 0d835915c7..a9636c3c8d 100644 --- a/frontend/rust-lib/lib-dispatch/Cargo.toml +++ b/frontend/rust-lib/lib-dispatch/Cargo.toml @@ -23,7 +23,6 @@ serde = { version = "1.0", features = ["derive"], optional = true } serde_repr = { workspace = true, optional = true } validator = "0.16.1" tracing.workspace = true -parking_lot = "0.12" bincode = { version = "1.3", optional = true } protobuf = { workspace = true, optional = true } diff --git a/frontend/rust-lib/lib-dispatch/src/module/module.rs b/frontend/rust-lib/lib-dispatch/src/module/module.rs index ae92cf9a0c..883225c1b0 100644 --- a/frontend/rust-lib/lib-dispatch/src/module/module.rs +++ b/frontend/rust-lib/lib-dispatch/src/module/module.rs @@ -1,3 +1,15 @@ +use crate::dispatcher::AFConcurrent; +use crate::prelude::{AFBoxFuture, AFStateMap}; +use crate::service::AFPluginHandler; +use crate::{ + errors::{DispatchError, InternalError}, + request::{payload::Payload, AFPluginEventRequest, FromAFPluginRequest}, + response::{AFPluginEventResponse, AFPluginResponder}, + service::{ + factory, AFPluginHandlerService, AFPluginServiceFactory, BoxService, BoxServiceFactory, + Service, ServiceRequest, ServiceResponse, + }, +}; use futures_core::ready; use nanoid::nanoid; use pin_project::pin_project; @@ -13,19 +25,6 @@ use std::{ task::{Context, Poll}, }; -use crate::dispatcher::AFConcurrent; -use crate::prelude::{AFBoxFuture, AFStateMap}; -use crate::service::AFPluginHandler; -use crate::{ - errors::{DispatchError, InternalError}, - request::{payload::Payload, AFPluginEventRequest, FromAFPluginRequest}, - response::{AFPluginEventResponse, AFPluginResponder}, - service::{ - factory, AFPluginHandlerService, AFPluginServiceFactory, BoxService, BoxServiceFactory, - Service, ServiceRequest, ServiceResponse, - }, -}; - pub type AFPluginMap = Rc>>; pub(crate) fn plugin_map_or_crash(plugins: Vec) -> AFPluginMap { let mut plugin_map: HashMap> = HashMap::new(); diff --git a/frontend/rust-lib/lib-infra/src/native/future.rs b/frontend/rust-lib/lib-infra/src/native/future.rs index 4d918d7e7c..0f1c174c55 100644 --- a/frontend/rust-lib/lib-infra/src/native/future.rs +++ b/frontend/rust-lib/lib-infra/src/native/future.rs @@ -2,7 +2,6 @@ use futures_core::future::BoxFuture; use futures_core::ready; use pin_project::pin_project; use std::{ - fmt::Debug, future::Future, pin::Pin, task::{Context, Poll}, @@ -33,33 +32,4 @@ where } } -#[pin_project] -pub struct FutureResult { - #[pin] - pub fut: Pin> + Sync + Send>>, -} - -impl FutureResult { - pub fn new(f: F) -> Self - where - F: Future> + Send + Sync + 'static, - { - Self { fut: Box::pin(f) } - } -} - -impl Future for FutureResult -where - T: Send + Sync, - E: Debug, -{ - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.as_mut().project(); - let result = ready!(this.fut.poll(cx)); - Poll::Ready(result) - } -} - pub type BoxResultFuture<'a, T, E> = BoxFuture<'a, Result>; From 5878379b2eec742320f4a1a634a54e29fd409800 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sun, 18 Aug 2024 18:44:32 +0800 Subject: [PATCH 14/26] chore: lazy load database row (#6001) * chore: fix potential load database rows fail * chore: fix layout padding --- .../row/related_row_detail_bloc.dart | 10 +-- .../database/application/row/row_cache.dart | 11 ++- .../application/row/row_controller.dart | 39 +++++++++-- .../calendar_event_editor_bloc.dart | 4 +- .../grid/application/row/row_bloc.dart | 6 +- .../grid/application/row/row_detail_bloc.dart | 4 +- .../database/grid/presentation/grid_page.dart | 66 ++++++++++-------- .../presentation/widgets/row/mobile_row.dart | 2 +- .../tab_bar/desktop/setting_menu.dart | 2 +- frontend/appflowy_tauri/src-tauri/Cargo.lock | 14 ++-- frontend/appflowy_tauri/src-tauri/Cargo.toml | 14 ++-- .../appflowy_web_app/src-tauri/Cargo.lock | 14 ++-- .../appflowy_web_app/src-tauri/Cargo.toml | 14 ++-- frontend/rust-lib/Cargo.lock | 14 ++-- frontend/rust-lib/Cargo.toml | 14 ++-- .../rust-lib/flowy-database2/src/manager.rs | 1 + .../src/services/database/database_editor.rs | 68 +++++++++++++------ .../src/services/database/database_observe.rs | 1 - .../src/services/database_view/view_editor.rs | 8 +-- .../src/services/database_view/view_filter.rs | 4 +- .../src/services/database_view/view_group.rs | 2 +- .../services/database_view/view_operation.rs | 4 +- .../src/services/database_view/view_sort.rs | 2 +- .../tests/database/database_editor.rs | 4 +- .../tests/database/field_test/script.rs | 2 +- .../database/pre_fill_cell_test/script.rs | 8 +-- .../tests/database/share_test/export_test.rs | 4 +- .../tests/database/sort_test/script.rs | 2 +- 28 files changed, 204 insertions(+), 134 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/row/related_row_detail_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/row/related_row_detail_bloc.dart index 06e1e2b70f..1390d9ff97 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/related_row_detail_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/row/related_row_detail_bloc.dart @@ -23,9 +23,9 @@ class RelatedRowDetailPageBloc @override Future close() { state.whenOrNull( - ready: (databaseController, rowController) { - rowController.dispose(); - databaseController.dispose(); + ready: (databaseController, rowController) async { + await rowController.dispose(); + await databaseController.dispose(); }, ); return super.close(); @@ -36,8 +36,8 @@ class RelatedRowDetailPageBloc event.when( didInitialize: (databaseController, rowController) { state.maybeWhen( - ready: (_, oldRowController) { - oldRowController.dispose(); + ready: (_, oldRowController) async { + await oldRowController.dispose(); emit( RelatedRowDetailPageState.ready( databaseController: databaseController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_cache.dart b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_cache.dart index 90f20b2fe7..6f4d886f80 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_cache.dart @@ -81,6 +81,12 @@ class RowCache { _changedNotifier.receive(const ChangedReason.setInitialRows()); } + void setRowMeta(RowMetaPB rowMeta) { + final rowInfo = buildGridRow(rowMeta); + _rowList.add(rowInfo); + _changedNotifier.receive(const ChangedReason.didFetchRow()); + } + void dispose() { _rowLifeCycle.onRowDisposed(); _changedNotifier.dispose(); @@ -215,7 +221,8 @@ class RowCache { if (rowInfo == null) { _loadRow(rowMeta.id); } - return _makeCells(rowMeta); + final cells = _makeCells(rowMeta); + return cells; } Future _loadRow(RowId rowId) async { @@ -277,6 +284,7 @@ class RowChangesetNotifier extends ChangeNotifier { reorderRows: (_) => notifyListeners(), reorderSingleRow: (_) => notifyListeners(), setInitialRows: (_) => notifyListeners(), + didFetchRow: (_) => notifyListeners(), ); } } @@ -305,6 +313,7 @@ class ChangedReason with _$ChangedReason { const factory ChangedReason.update(UpdatedIndexMap indexs) = _Update; const factory ChangedReason.fieldDidChange() = _FieldDidChange; const factory ChangedReason.initial() = InitialListState; + const factory ChangedReason.didFetchRow() = _DidFetchRow; const factory ChangedReason.reorderRows() = _ReorderRows; const factory ChangedReason.reorderSingleRow( ReorderSingleRowPB reorderRow, diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_controller.dart index b34beba275..a52bd66199 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_controller.dart @@ -1,3 +1,5 @@ +import 'package:appflowy/plugins/database/application/row/row_service.dart'; +import 'package:appflowy/plugins/database/domain/row_listener.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:flutter/material.dart'; @@ -9,35 +11,60 @@ typedef OnRowChanged = void Function(List, ChangedReason); class RowController { RowController({ - required this.rowMeta, + required RowMetaPB rowMeta, required this.viewId, required RowCache rowCache, this.groupId, - }) : _rowCache = rowCache; + }) : _rowMeta = rowMeta, + _rowCache = rowCache, + _rowBackendSvc = RowBackendService(viewId: viewId), + _rowListener = RowListener(rowMeta.id) { + _rowBackendSvc.initRow(rowMeta.id); + _rowListener.start( + onMetaChanged: (newRowMeta) { + if (_isDisposed) { + return; + } + _rowMeta = newRowMeta; + _rowCache.setRowMeta(newRowMeta); + }, + ); + } - final RowMetaPB rowMeta; + RowMetaPB _rowMeta; final String? groupId; final String viewId; final List _onRowChangedListeners = []; final RowCache _rowCache; + final RowListener _rowListener; + final RowBackendService _rowBackendSvc; + bool _isDisposed = false; CellMemCache get cellCache => _rowCache.cellCache; String get rowId => rowMeta.id; + RowMetaPB get rowMeta => _rowMeta; - List loadData() => _rowCache.loadCells(rowMeta); + List loadCells() => _rowCache.loadCells(rowMeta); void addListener({OnRowChanged? onRowChanged}) { final fn = _rowCache.addListener( rowId: rowMeta.id, - onRowChanged: onRowChanged, + onRowChanged: (context, reasons) { + if (_isDisposed) { + return; + } + onRowChanged?.call(context, reasons); + }, ); // Add the listener to the list so that we can remove it later. _onRowChangedListeners.add(fn); } - void dispose() { + Future dispose() async { + _isDisposed = true; + await _rowListener.stop(); for (final fn in _onRowChangedListeners) { _rowCache.removeRowListener(fn); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_event_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_event_editor_bloc.dart index 303daff87e..b122d951be 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_event_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_event_editor_bloc.dart @@ -34,7 +34,7 @@ class CalendarEventEditorBloc .firstWhere((fieldInfo) => fieldInfo.isPrimary) .id; final cells = rowController - .loadData() + .loadCells() .where( (cellContext) => _filterCellContext(cellContext, primaryFieldId), @@ -88,7 +88,7 @@ class CalendarEventEditorBloc @override Future close() async { - rowController.dispose(); + await rowController.dispose(); return super.close(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_bloc.dart index 322d7d59a4..69feda410c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_bloc.dart @@ -23,8 +23,6 @@ class RowBloc extends Bloc { }) : _rowBackendSvc = RowBackendService(viewId: viewId), _rowController = rowController, super(RowState.initial()) { - _rowBackendSvc.initRow(rowId); - _dispatch(); _startListening(); _init(); @@ -38,7 +36,7 @@ class RowBloc extends Bloc { @override Future close() async { - _rowController.dispose(); + await _rowController.dispose(); return super.close(); } @@ -84,7 +82,7 @@ class RowBloc extends Bloc { void _init() { add( RowEvent.didReceiveCells( - _rowController.loadData(), + _rowController.loadCells(), const ChangedReason.setInitialRows(), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_detail_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_detail_bloc.dart index 0d655a840b..5c25fc851f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_detail_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_detail_bloc.dart @@ -29,7 +29,7 @@ class RowDetailBloc extends Bloc { @override Future close() async { - rowController.dispose(); + await rowController.dispose(); return super.close(); } @@ -125,7 +125,7 @@ class RowDetailBloc extends Bloc { } void _init() { - allCells.addAll(rowController.loadData()); + allCells.addAll(rowController.loadCells()); int numHiddenFields = 0; final visibleCells = []; for (final cell in allCells) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart index 50b67e7a8f..b2e873e67e 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart @@ -9,7 +9,6 @@ import 'package:appflowy/workspace/application/action_navigation/navigation_acti import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart'; @@ -154,6 +153,7 @@ class _GridPageState extends State { finish: (result) => result.successOrFail.fold( (_) => GridShortcuts( child: GridPageContent( + key: ValueKey(widget.view.id), view: widget.view, ), ), @@ -331,33 +331,10 @@ class _GridRowsState extends State<_GridRows> { BuildContext context, GridState state, ) { - final children = state.rowInfos.mapIndexed((index, rowInfo) { - return _renderRow( - context, - rowInfo.rowId, - isDraggable: state.reorderable, - index: index, - ); - }).toList() - ..add(const GridRowBottomBar(key: Key('grid_footer'))); - - if (showFloatingCalculations) { - children.add( - const SizedBox( - key: Key('calculations_bottom_padding'), - height: 36, - ), - ); - } else { - children.add( - GridCalculationsRow( - key: const Key('grid_calculations'), - viewId: widget.viewId, - ), - ); - } - - children.add(const SizedBox(key: Key('footer_padding'), height: 10)); + // 1. GridRowBottomBar + // 2. GridCalculationsRow + // 3. Footer Padding + final itemCount = state.rowInfos.length + 3; return Stack( children: [ @@ -381,8 +358,37 @@ class _GridRowsState extends State<_GridRows> { .add(GridEvent.moveRow(fromIndex, toIndex)); } }, - itemCount: children.length, - itemBuilder: (context, index) => children[index], + itemCount: itemCount, + itemBuilder: (context, index) { + if (index < state.rowInfos.length) { + return _renderRow( + context, + state.rowInfos[index].rowId, + isDraggable: state.reorderable, + index: index, + ); + } + + if (index == state.rowInfos.length) { + return const GridRowBottomBar(key: Key('grid_footer')); + } + + if (index == state.rowInfos.length + 1) { + if (showFloatingCalculations) { + return const SizedBox( + key: Key('calculations_bottom_padding'), + height: 36, + ); + } else { + return GridCalculationsRow( + key: const Key('grid_calculations'), + viewId: widget.viewId, + ); + } + } + + return const SizedBox(key: Key('footer_padding'), height: 10); + }, ), ), if (showFloatingCalculations) ...[ diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart index b6817fc848..f4e9d0c751 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart @@ -81,7 +81,7 @@ class _MobileGridRowState extends State { @override Future dispose() async { - _rowController.dispose(); + await _rowController.dispose(); super.dispose(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/setting_menu.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/setting_menu.dart index 5b66c3a149..ad08d6b8e2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/setting_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/setting_menu.dart @@ -57,7 +57,7 @@ class _DatabaseViewSettingContent extends StatelessWidget { builder: (context, state) { return Padding( padding: EdgeInsets.symmetric( - horizontal: GridSize.horizontalHeaderPadding, + horizontal: GridSize.horizontalHeaderPadding + 40, ), child: DecoratedBox( decoration: BoxDecoration( diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 816bc62b34..0beeb29db6 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -963,7 +963,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "arc-swap", @@ -988,7 +988,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "async-trait", @@ -1018,7 +1018,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "arc-swap", @@ -1038,7 +1038,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "bytes", @@ -1057,7 +1057,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "arc-swap", @@ -1100,7 +1100,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "async-stream", @@ -1180,7 +1180,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "collab", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index eba3aa40de..5f8c9e4117 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -116,13 +116,13 @@ custom-protocol = ["tauri/custom-protocol"] # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index 6a6d842bf7..ddb1a8fd54 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -946,7 +946,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "arc-swap", @@ -971,7 +971,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "async-trait", @@ -1001,7 +1001,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "arc-swap", @@ -1021,7 +1021,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "bytes", @@ -1040,7 +1040,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "arc-swap", @@ -1083,7 +1083,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "async-stream", @@ -1163,7 +1163,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "collab", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index b3d45657bc..66de0bbd84 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -116,13 +116,13 @@ custom-protocol = ["tauri/custom-protocol"] # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 4f8ce47d41..95748a558c 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -824,7 +824,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "arc-swap", @@ -849,7 +849,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "async-trait", @@ -879,7 +879,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "arc-swap", @@ -899,7 +899,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "bytes", @@ -918,7 +918,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "arc-swap", @@ -961,7 +961,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "async-stream", @@ -1041,7 +1041,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d03bd474e551ab5583780abe051a85b8063e6aa9#d03bd474e551ab5583780abe051a85b8063e6aa9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" dependencies = [ "anyhow", "collab", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index da234b004e..163aafd05e 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -136,13 +136,13 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d03bd474e551ab5583780abe051a85b8063e6aa9" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index c37321fc92..7bce35e053 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -279,6 +279,7 @@ impl DatabaseManager { self.open_database(database_id).await } + #[instrument(level = "trace", skip_all, err)] pub async fn open_database(&self, database_id: &str) -> FlowyResult> { trace!("open database editor:{}", database_id); let lock = self.workspace_database()?; diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index bf913db50c..35a82e3578 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -37,7 +37,7 @@ use lib_infra::util::timestamp; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::{broadcast, RwLock}; -use tracing::{error, event, instrument, trace, warn}; +use tracing::{debug, error, event, instrument, trace, warn}; #[derive(Clone)] pub struct DatabaseEditor { @@ -66,7 +66,7 @@ impl DatabaseEditor { // observe_view_change(&database_id, &database).await; // observe_field_change(&database_id, &database).await; observe_rows_change(&database_id, &database, ¬ification_sender).await; - // observe_block_event(&database_id, &database).await; + observe_block_event(&database_id, &database).await; // Used to cache the view of the database for fast access. let editor_by_view_id = Arc::new(RwLock::new(EditorByViewId::default())); @@ -119,7 +119,7 @@ impl DatabaseEditor { .database .read() .await - .get_database_rows() + .get_all_row_orders() .await .into_iter() .map(|entry| entry.id) @@ -509,16 +509,28 @@ impl DatabaseEditor { .ok_or_else(|| FlowyError::internal().with_context("error while copying row"))?; let (index, row_order) = database.create_row_in_view(view_id, params); - tracing::trace!("duplicated row: {:?} at {}", row_order, index); + trace!( + "duplicate row: {:?} at index:{}, new row:{:?}", + row_id, + index, + row_order + ); let row_detail = database.get_row_detail(&row_order.id).await; - (row_detail, index) }; - if let Some(row_detail) = row_detail { - for view in self.database_views.editors().await { - view.v_did_create_row(&row_detail, index).await; - } + match row_detail { + None => { + error!( + "Failed to duplicate row: {:?}. Row is not exist before duplicating", + row_id + ); + }, + Some(row_detail) => { + for view in self.database_views.editors().await { + view.v_did_create_row(&row_detail, index).await; + } + }, } Ok(()) @@ -654,9 +666,9 @@ impl DatabaseEditor { Ok(()) } - pub async fn get_rows(&self, view_id: &str) -> FlowyResult>> { + pub async fn get_row_details(&self, view_id: &str) -> FlowyResult>> { let view_editor = self.database_views.get_view_editor(view_id).await?; - Ok(view_editor.v_get_rows().await) + Ok(view_editor.v_get_row_details().await) } pub async fn get_row(&self, view_id: &str, row_id: &RowId) -> Option { @@ -669,11 +681,22 @@ impl DatabaseEditor { } pub async fn init_database_row(&self, row_id: &RowId) -> FlowyResult<()> { + if self + .database + .read() + .await + .get_database_row(row_id) + .is_some() + { + return Ok(()); + } + + debug!("Init database row: {}", row_id); let database_row = self .database .read() .await - .get_row_collab(row_id) + .create_database_row(row_id) .ok_or_else(|| { FlowyError::record_not_found() .with_context(format!("The row:{} in database not found", row_id)) @@ -1143,7 +1166,7 @@ impl DatabaseEditor { let to_row = if to_row.is_some() { to_row } else { - let row_details = self.get_rows(view_id).await?; + let row_details = self.get_row_details(view_id).await?; row_details .last() .map(|row_detail| row_detail.row.id.clone()) @@ -1275,7 +1298,8 @@ impl DatabaseEditor { .v_get_view() .await .ok_or_else(FlowyError::record_not_found)?; - let rows = database_view.v_get_rows().await; + + let row_orders = self.database.read().await.get_row_orders_for_view(&view_id); let (database_id, fields, is_linked) = { let database = self.database.read().await; let database_id = database.get_database_id(); @@ -1288,9 +1312,15 @@ impl DatabaseEditor { (database_id, fields, is_linked) }; - let rows = rows + let rows = row_orders .into_iter() - .map(|row_detail| RowMetaPB::from(row_detail.as_ref())) + .map(|row_order| RowMetaPB { + id: row_order.id.to_string(), + document_id: "".to_string(), + icon: None, + cover: None, + is_document_empty: false, + }) .collect::>(); Ok(DatabasePB { id: database_id, @@ -1374,7 +1404,7 @@ impl DatabaseEditor { .ok_or(FlowyError::internal())?; let row_data = { - let mut rows = database.get_database_rows().await; + let mut rows = database.get_all_rows().await; if let Some(row_ids) = row_ids { rows.retain(|row| row_ids.contains(&row.id)); } @@ -1504,7 +1534,7 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { self.database.read().await.index_of_row(view_id, row_id) } - async fn get_row(&self, view_id: &str, row_id: &RowId) -> Option<(usize, Arc)> { + async fn get_row_detail(&self, view_id: &str, row_id: &RowId) -> Option<(usize, Arc)> { let database = self.database.read().await; let index = database.index_of_row(view_id, row_id); let row_detail = database.get_row_detail(row_id).await; @@ -1514,7 +1544,7 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { } } - async fn get_rows(&self, view_id: &str) -> Vec> { + async fn get_row_details(&self, view_id: &str) -> Vec> { let view_id = view_id.to_string(); let row_orders = self.database.read().await.get_row_orders_for_view(&view_id); trace!("total row orders: {}", row_orders.len()); diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs index b25d365ab0..164c7c30fb 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs @@ -136,7 +136,6 @@ pub(crate) async fn observe_view_change(database_id: &str, database: &Arc>) { let database_id = database_id.to_string(); let weak_database = Arc::downgrade(database); diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index aafc78e10e..f9377c1868 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -314,8 +314,8 @@ impl DatabaseViewEditor { } #[instrument(level = "info", skip(self))] - pub async fn v_get_rows(&self) -> Vec> { - let mut rows = self.delegate.get_rows(&self.view_id).await; + pub async fn v_get_row_details(&self) -> Vec> { + let mut rows = self.delegate.get_row_details(&self.view_id).await; self.v_filter_rows(&mut rows).await; self.v_sort_rows(&mut rows).await; rows @@ -937,7 +937,7 @@ impl DatabaseViewEditor { .timestamp .unwrap_or_default(); - let (_, row_detail) = self.delegate.get_row(&self.view_id, &row_id).await?; + let (_, row_detail) = self.delegate.get_row_detail(&self.view_id, &row_id).await?; Some(CalendarEventPB { row_meta: RowMetaPB::from(row_detail.as_ref()), date_field_id: date_field.id.clone(), @@ -1000,7 +1000,7 @@ impl DatabaseViewEditor { .unwrap_or_default() .into(); - let (_, row_detail) = self.delegate.get_row(&self.view_id, &row_id).await?; + let (_, row_detail) = self.delegate.get_row_detail(&self.view_id, &row_id).await?; let event = CalendarEventPB { row_meta: RowMetaPB::from(row_detail.as_ref()), date_field_id: calendar_setting.field_id.clone(), diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs index 994041aea9..33d4a40e87 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs @@ -53,11 +53,11 @@ impl FilterDelegate for DatabaseViewFilterDelegateImpl { } async fn get_rows(&self, view_id: &str) -> Vec> { - self.0.get_rows(view_id).await + self.0.get_row_details(view_id).await } async fn get_row(&self, view_id: &str, rows_id: &RowId) -> Option<(usize, Arc)> { - self.0.get_row(view_id, rows_id).await + self.0.get_row_detail(view_id, rows_id).await } async fn get_all_filters(&self, view_id: &str) -> Vec { diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs index b180904d6e..56c81eb183 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs @@ -97,7 +97,7 @@ impl GroupControllerDelegate for GroupControllerDelegateImpl { } async fn get_all_rows(&self, view_id: &str) -> Vec> { - let mut row_details = self.delegate.get_rows(view_id).await; + let mut row_details = self.delegate.get_row_details(view_id).await; self.filter_controller.filter_rows(&mut row_details).await; row_details } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs index 4681566ebd..95c092702e 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs @@ -53,10 +53,10 @@ pub trait DatabaseViewOperation: Send + Sync + 'static { async fn index_of_row(&self, view_id: &str, row_id: &RowId) -> Option; /// Returns the `index` and `RowRevision` with row_id - async fn get_row(&self, view_id: &str, row_id: &RowId) -> Option<(usize, Arc)>; + async fn get_row_detail(&self, view_id: &str, row_id: &RowId) -> Option<(usize, Arc)>; /// Returns all the rows in the view - async fn get_rows(&self, view_id: &str) -> Vec>; + async fn get_row_details(&self, view_id: &str) -> Vec>; async fn remove_row(&self, row_id: &RowId) -> Option; diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs index a719590e09..5491997eb4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs @@ -61,7 +61,7 @@ impl SortDelegate for DatabaseViewSortDelegateImpl { async fn get_rows(&self, view_id: &str) -> Vec> { let view_id = view_id.to_string(); - let mut row_details = self.delegate.get_rows(&view_id).await; + let mut row_details = self.delegate.get_row_details(&view_id).await; self.filter_controller.filter_rows(&mut row_details).await; row_details } diff --git a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs index c18fef66a2..5086ce451c 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs @@ -86,7 +86,7 @@ impl DatabaseEditorTest { .map(Arc::new) .collect(); let rows = editor - .get_rows(&test.child_view.id) + .get_row_details(&test.child_view.id) .await .unwrap() .into_iter() @@ -109,7 +109,7 @@ impl DatabaseEditorTest { } pub async fn get_rows(&self) -> Vec> { - self.editor.get_rows(&self.view_id).await.unwrap() + self.editor.get_row_details(&self.view_id).await.unwrap() } pub async fn get_field(&self, field_id: &str, field_type: FieldType) -> Field { diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs index bf93c130f8..f422cbdc72 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs @@ -121,7 +121,7 @@ impl DatabaseFieldTest { } => { let field = self.editor.get_field(&field_id).await.unwrap(); - let rows = self.editor.get_rows(&self.view_id()).await.unwrap(); + let rows = self.editor.get_row_details(&self.view_id()).await.unwrap(); let row_detail = rows.get(row_index).unwrap(); let cell = row_detail.row.cells.get(&field_id).unwrap().clone(); diff --git a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs index 6b524fdf15..f8f7d2f6ca 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs @@ -86,7 +86,7 @@ impl DatabasePreFillRowCellTest { .await .unwrap(), PreFillRowCellTestScript::AssertRowCount(expected_row_count) => { - let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + let rows = self.editor.get_row_details(&self.view_id).await.unwrap(); assert_eq!(expected_row_count, rows.len()); }, PreFillRowCellTestScript::AssertCellExistence { @@ -94,7 +94,7 @@ impl DatabasePreFillRowCellTest { row_index, exists, } => { - let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + let rows = self.editor.get_row_details(&self.view_id).await.unwrap(); let row_detail = rows.get(row_index).unwrap(); let cell = row_detail.row.cells.get(&field_id).cloned(); @@ -108,7 +108,7 @@ impl DatabasePreFillRowCellTest { } => { let field = self.editor.get_field(&field_id).await.unwrap(); - let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + let rows = self.editor.get_row_details(&self.view_id).await.unwrap(); let row_detail = rows.get(row_index).unwrap(); let cell = row_detail @@ -125,7 +125,7 @@ impl DatabasePreFillRowCellTest { row_index, expected_content, } => { - let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + let rows = self.editor.get_row_details(&self.view_id).await.unwrap(); let row_detail = rows.get(row_index).unwrap(); let cell = row_detail diff --git a/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs b/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs index 02f4f135ca..c23abb7d1d 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs @@ -33,7 +33,7 @@ async fn export_and_then_import_meta_csv_test() { let database = test.get_database(&result.database_id).await.unwrap(); let fields = database.get_fields(&result.view_id, None).await; - let rows = database.get_rows(&result.view_id).await.unwrap(); + let rows = database.get_row_details(&result.view_id).await.unwrap(); assert_eq!(fields[0].field_type, 0); assert_eq!(fields[1].field_type, 1); assert_eq!(fields[2].field_type, 2); @@ -112,7 +112,7 @@ async fn history_database_import_test() { let database = test.get_database(&result.database_id).await.unwrap(); let fields = database.get_fields(&result.view_id, None).await; - let rows = database.get_rows(&result.view_id).await.unwrap(); + let rows = database.get_row_details(&result.view_id).await.unwrap(); assert_eq!(fields[0].field_type, 0); assert_eq!(fields[1].field_type, 1); assert_eq!(fields[2].field_type, 2); diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs index e95deaa187..956d62d727 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs @@ -117,7 +117,7 @@ impl DatabaseSortTest { }, SortScript::AssertCellContentOrder { field_id, orders } => { let mut cells = vec![]; - let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + let rows = self.editor.get_row_details(&self.view_id).await.unwrap(); let field = self.editor.get_field(&field_id).await.unwrap(); for row_detail in rows { if let Some(cell) = row_detail.row.cells.get(&field_id) { From d0ce65f711fadd2030f4edf7aa24bacfe2b060cb Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Mon, 19 Aug 2024 09:47:37 +0800 Subject: [PATCH 15/26] chore: fix database filter (#6005) * chore: fix filter --- .../database/application/view/view_cache.dart | 13 +++ .../application/view/view_listener.dart | 105 +++++++++--------- frontend/appflowy_tauri/src-tauri/Cargo.lock | 15 +-- frontend/appflowy_tauri/src-tauri/Cargo.toml | 14 +-- .../appflowy_web_app/src-tauri/Cargo.lock | 15 +-- .../appflowy_web_app/src-tauri/Cargo.toml | 14 +-- frontend/rust-lib/Cargo.lock | 15 +-- frontend/rust-lib/Cargo.toml | 14 +-- frontend/rust-lib/flowy-database2/Cargo.toml | 1 + .../src/entities/row_entities.rs | 18 +-- .../flowy-database2/src/event_handler.rs | 14 +++ .../rust-lib/flowy-database2/src/event_map.rs | 4 + .../rust-lib/flowy-database2/src/manager.rs | 17 ++- .../flowy-database2/src/notification.rs | 1 + .../src/services/database/database_editor.rs | 34 +++--- .../src/services/database/database_observe.rs | 36 +++++- .../src/services/database_view/view_editor.rs | 20 ++-- .../src/services/filter/controller.rs | 15 ++- 18 files changed, 214 insertions(+), 151 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/view/view_cache.dart b/frontend/appflowy_flutter/lib/plugins/database/application/view/view_cache.dart index 77670fb0bb..7ddd3faf11 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/view/view_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/view/view_cache.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'dart:collection'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import '../defines.dart'; import '../field/field_controller.dart'; @@ -91,6 +93,17 @@ class DatabaseViewCache { (reorderRow) => _rowCache.reorderSingleRow(reorderRow), (err) => Log.error(err), ), + onReloadRows: () { + final payload = DatabaseViewIdPB(value: viewId); + DatabaseEventGetAllRows(payload).send().then((result) { + result.fold( + (rows) { + _rowCache.setInitialRows(rows.items); + }, + (err) => Log.error(err), + ); + }); + }, ); _rowCache.onRowsChanged( diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/view/view_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/application/view/view_listener.dart index 6a41e2f173..1aecbb2767 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/view/view_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/view/view_listener.dart @@ -7,85 +7,96 @@ import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart' import 'package:appflowy_backend/protobuf/flowy-database2/view_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flowy_infra/notifier.dart'; -typedef RowsVisibilityNotifierValue - = FlowyResult; - -typedef NumberOfRowsNotifierValue = FlowyResult; -typedef ReorderAllRowsNotifierValue = FlowyResult, FlowyError>; -typedef SingleRowNotifierValue = FlowyResult; +typedef RowsVisibilityCallback = void Function( + FlowyResult, +); +typedef NumberOfRowsCallback = void Function( + FlowyResult, +); +typedef ReorderAllRowsCallback = void Function( + FlowyResult, FlowyError>, +); +typedef SingleRowCallback = void Function( + FlowyResult, +); class DatabaseViewListener { DatabaseViewListener({required this.viewId}); final String viewId; - - PublishNotifier? _rowsNotifier = PublishNotifier(); - PublishNotifier? _reorderAllRows = - PublishNotifier(); - PublishNotifier? _reorderSingleRow = - PublishNotifier(); - PublishNotifier? _rowsVisibility = - PublishNotifier(); - DatabaseNotificationListener? _listener; void start({ - required void Function(NumberOfRowsNotifierValue) onRowsChanged, - required void Function(ReorderAllRowsNotifierValue) onReorderAllRows, - required void Function(SingleRowNotifierValue) onReorderSingleRow, - required void Function(RowsVisibilityNotifierValue) onRowsVisibilityChanged, + required NumberOfRowsCallback onRowsChanged, + required ReorderAllRowsCallback onReorderAllRows, + required SingleRowCallback onReorderSingleRow, + required RowsVisibilityCallback onRowsVisibilityChanged, + required void Function() onReloadRows, }) { - if (_listener != null) { - _listener?.stop(); - } + // Stop any existing listener + _listener?.stop(); + // Initialize the notification listener _listener = DatabaseNotificationListener( objectId: viewId, - handler: _handler, + handler: (ty, result) => _handler( + ty, + result, + onRowsChanged, + onReorderAllRows, + onReorderSingleRow, + onRowsVisibilityChanged, + onReloadRows, + ), ); - - _rowsNotifier?.addPublishListener(onRowsChanged); - _rowsVisibility?.addPublishListener(onRowsVisibilityChanged); - _reorderAllRows?.addPublishListener(onReorderAllRows); - _reorderSingleRow?.addPublishListener(onReorderSingleRow); } void _handler( DatabaseNotification ty, FlowyResult result, + NumberOfRowsCallback onRowsChanged, + ReorderAllRowsCallback onReorderAllRows, + SingleRowCallback onReorderSingleRow, + RowsVisibilityCallback onRowsVisibilityChanged, + void Function() onReloadRows, ) { switch (ty) { case DatabaseNotification.DidUpdateViewRowsVisibility: result.fold( - (payload) => _rowsVisibility?.value = - FlowyResult.success(RowsVisibilityChangePB.fromBuffer(payload)), - (error) => _rowsVisibility?.value = FlowyResult.failure(error), + (payload) => onRowsVisibilityChanged( + FlowyResult.success(RowsVisibilityChangePB.fromBuffer(payload)), + ), + (error) => onRowsVisibilityChanged(FlowyResult.failure(error)), ); break; case DatabaseNotification.DidUpdateRow: result.fold( - (payload) => _rowsNotifier?.value = - FlowyResult.success(RowsChangePB.fromBuffer(payload)), - (error) => _rowsNotifier?.value = FlowyResult.failure(error), + (payload) => onRowsChanged( + FlowyResult.success(RowsChangePB.fromBuffer(payload)), + ), + (error) => onRowsChanged(FlowyResult.failure(error)), ); break; case DatabaseNotification.DidReorderRows: result.fold( - (payload) => _reorderAllRows?.value = FlowyResult.success( - ReorderAllRowsPB.fromBuffer(payload).rowOrders, + (payload) => onReorderAllRows( + FlowyResult.success(ReorderAllRowsPB.fromBuffer(payload).rowOrders), ), - (error) => _reorderAllRows?.value = FlowyResult.failure(error), + (error) => onReorderAllRows(FlowyResult.failure(error)), ); break; case DatabaseNotification.DidReorderSingleRow: result.fold( - (payload) => _reorderSingleRow?.value = - FlowyResult.success(ReorderSingleRowPB.fromBuffer(payload)), - (error) => _reorderSingleRow?.value = FlowyResult.failure(error), + (payload) => onReorderSingleRow( + FlowyResult.success(ReorderSingleRowPB.fromBuffer(payload)), + ), + (error) => onReorderSingleRow(FlowyResult.failure(error)), ); break; + case DatabaseNotification.ReloadRows: + onReloadRows(); + break; default: break; } @@ -93,16 +104,6 @@ class DatabaseViewListener { Future stop() async { await _listener?.stop(); - _rowsVisibility?.dispose(); - _rowsVisibility = null; - - _rowsNotifier?.dispose(); - _rowsNotifier = null; - - _reorderAllRows?.dispose(); - _reorderAllRows = null; - - _reorderSingleRow?.dispose(); - _reorderSingleRow = null; + _listener = null; } } diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 0beeb29db6..61421256b0 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -963,7 +963,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "arc-swap", @@ -988,7 +988,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "async-trait", @@ -1018,7 +1018,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "arc-swap", @@ -1038,7 +1038,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "bytes", @@ -1057,7 +1057,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "arc-swap", @@ -1100,7 +1100,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "async-stream", @@ -1180,7 +1180,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "collab", @@ -2176,6 +2176,7 @@ dependencies = [ "strum", "strum_macros 0.25.2", "tokio", + "tokio-util", "tracing", "url", "validator", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 5f8c9e4117..eb8d21befc 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -116,13 +116,13 @@ custom-protocol = ["tauri/custom-protocol"] # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index ddb1a8fd54..149735e800 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -946,7 +946,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "arc-swap", @@ -971,7 +971,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "async-trait", @@ -1001,7 +1001,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "arc-swap", @@ -1021,7 +1021,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "bytes", @@ -1040,7 +1040,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "arc-swap", @@ -1083,7 +1083,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "async-stream", @@ -1163,7 +1163,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "collab", @@ -2206,6 +2206,7 @@ dependencies = [ "strum", "strum_macros 0.25.3", "tokio", + "tokio-util", "tracing", "url", "validator", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index 66de0bbd84..566213747f 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -116,13 +116,13 @@ custom-protocol = ["tauri/custom-protocol"] # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 95748a558c..45bebc1af8 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -824,7 +824,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "arc-swap", @@ -849,7 +849,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "async-trait", @@ -879,7 +879,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "arc-swap", @@ -899,7 +899,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "bytes", @@ -918,7 +918,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "arc-swap", @@ -961,7 +961,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "async-stream", @@ -1041,7 +1041,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0af7f361d52611842a862b982b2c72e4fa12cda1#0af7f361d52611842a862b982b2c72e4fa12cda1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" dependencies = [ "anyhow", "collab", @@ -2002,6 +2002,7 @@ dependencies = [ "strum", "strum_macros 0.25.2", "tokio", + "tokio-util", "tracing", "url", "validator", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 163aafd05e..fdabfe8379 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -136,13 +136,13 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0af7f361d52611842a862b982b2c72e4fa12cda1" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/flowy-database2/Cargo.toml b/frontend/rust-lib/flowy-database2/Cargo.toml index ee05a8d73f..1412ddd58b 100644 --- a/frontend/rust-lib/flowy-database2/Cargo.toml +++ b/frontend/rust-lib/flowy-database2/Cargo.toml @@ -49,6 +49,7 @@ csv = "1.1.6" strum = "0.25" strum_macros = "0.25" validator = { workspace = true, features = ["derive"] } +tokio-util.workspace = true [dev-dependencies] event-integration-test = { path = "../event-integration-test", default-features = false } diff --git a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs index 5c31d11b0d..6d949e2e1d 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs @@ -68,6 +68,12 @@ pub struct RowMetaPB { pub is_document_empty: bool, } +#[derive(Debug, Default, ProtoBuf)] +pub struct RepeatedRowMetaPB { + #[pb(index = 1)] + pub items: Vec, +} + impl std::convert::From<&RowDetail> for RowMetaPB { fn from(row_detail: &RowDetail) -> Self { Self { @@ -213,18 +219,6 @@ pub struct OptionalRowPB { pub row: Option, } -#[derive(Debug, Default, ProtoBuf)] -pub struct RepeatedRowPB { - #[pb(index = 1)] - pub items: Vec, -} - -impl std::convert::From> for RepeatedRowPB { - fn from(items: Vec) -> Self { - Self { items } - } -} - #[derive(Debug, Clone, Default, ProtoBuf)] pub struct InsertedRowPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 5f4f7456af..84564473f2 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -47,6 +47,20 @@ pub(crate) async fn get_database_data_handler( data_result_ok(data) } +#[tracing::instrument(level = "trace", skip_all, err)] +pub(crate) async fn get_all_rows_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let manager = upgrade_manager(manager)?; + let view_id: DatabaseViewIdPB = data.into_inner(); + let database_id = manager + .get_database_id_with_view_id(view_id.as_ref()) + .await?; + let database_editor = manager.get_database_editor(&database_id).await?; + let data = database_editor.get_all_rows(view_id.as_ref()).await?; + data_result_ok(data) +} #[tracing::instrument(level = "trace", skip_all, err)] pub(crate) async fn open_database_handler( data: AFPluginData, diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index 03f263d16d..5b0db9d9ed 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -14,6 +14,7 @@ pub fn init(database_manager: Weak) -> AFPlugin { .state(database_manager); plugin .event(DatabaseEvent::GetDatabase, get_database_data_handler) + .event(DatabaseEvent::GetAllRows, get_all_rows_handler) .event(DatabaseEvent::GetDatabaseData, get_database_data_handler) .event(DatabaseEvent::GetDatabaseId, get_database_id_handler) .event(DatabaseEvent::GetDatabaseSetting, get_database_setting_handler) @@ -381,4 +382,7 @@ pub enum DatabaseEvent { #[event(input = "RowIdPB")] InitRow = 176, + + #[event(input = "DatabaseViewIdPB", output = "RepeatedRowMetaPB")] + GetAllRows = 177, } diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 7bce35e053..5a83316ea6 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -290,15 +290,14 @@ impl DatabaseManager { .await .ok_or_else(|| FlowyError::collab_not_sync().with_context("open database error"))?; - let editor = Arc::new( - DatabaseEditor::new( - self.user.clone(), - database, - self.task_scheduler.clone(), - self.collab_builder.clone(), - ) - .await?, - ); + let editor = DatabaseEditor::new( + self.user.clone(), + database, + self.task_scheduler.clone(), + self.collab_builder.clone(), + ) + .await?; + self .editors .lock() diff --git a/frontend/rust-lib/flowy-database2/src/notification.rs b/frontend/rust-lib/flowy-database2/src/notification.rs index eadaa7e031..fb81dac6d5 100644 --- a/frontend/rust-lib/flowy-database2/src/notification.rs +++ b/frontend/rust-lib/flowy-database2/src/notification.rs @@ -52,6 +52,7 @@ pub enum DatabaseNotification { DidUpdateFieldSettings = 86, // Trigger when Calculation changed DidUpdateCalculation = 87, + ReloadRows = 88, } impl std::convert::From for i32 { diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 35a82e3578..c260a0fe14 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -43,7 +43,7 @@ use tracing::{debug, error, event, instrument, trace, warn}; pub struct DatabaseEditor { pub(crate) database: Arc>, pub cell_cache: CellCache, - database_views: Arc, + pub(crate) database_views: Arc, #[allow(dead_code)] /// Used to send notification to the frontend. notification_sender: Arc, @@ -57,7 +57,7 @@ impl DatabaseEditor { database: Arc>, task_scheduler: Arc>, collab_builder: Arc, - ) -> FlowyResult { + ) -> FlowyResult> { let notification_sender = Arc::new(DebounceNotificationSender::new(200)); let cell_cache = AnyTypeCache::::new(); let database_id = database.read().await.get_database_id(); @@ -66,7 +66,6 @@ impl DatabaseEditor { // observe_view_change(&database_id, &database).await; // observe_field_change(&database_id, &database).await; observe_rows_change(&database_id, &database, ¬ification_sender).await; - observe_block_event(&database_id, &database).await; // Used to cache the view of the database for fast access. let editor_by_view_id = Arc::new(RwLock::new(EditorByViewId::default())); @@ -99,15 +98,16 @@ impl DatabaseEditor { CollabBuilderConfig::default(), database.clone(), )?; - - Ok(Self { + let this = Arc::new(Self { user, database, cell_cache, database_views, notification_sender, collab_builder, - }) + }); + observe_block_event(&database_id, &this).await; + Ok(this) } pub async fn close_view(&self, view_id: &str) { @@ -1299,7 +1299,7 @@ impl DatabaseEditor { .await .ok_or_else(FlowyError::record_not_found)?; - let row_orders = self.database.read().await.get_row_orders_for_view(&view_id); + let row_details = database_view.v_get_row_details().await; let (database_id, fields, is_linked) = { let database = self.database.read().await; let database_id = database.get_database_id(); @@ -1312,15 +1312,9 @@ impl DatabaseEditor { (database_id, fields, is_linked) }; - let rows = row_orders + let rows = row_details .into_iter() - .map(|row_order| RowMetaPB { - id: row_order.id.to_string(), - document_id: "".to_string(), - icon: None, - cover: None, - is_document_empty: false, - }) + .map(|detail| RowMetaPB::from(detail.as_ref())) .collect::>(); Ok(DatabasePB { id: database_id, @@ -1331,6 +1325,16 @@ impl DatabaseEditor { }) } + pub async fn get_all_rows(&self, view_id: &str) -> FlowyResult { + let database_view = self.database_views.get_view_editor(view_id).await?; + let row_details = database_view.v_get_row_details().await; + let rows = row_details + .into_iter() + .map(|detail| RowMetaPB::from(detail.as_ref())) + .collect::>(); + Ok(RepeatedRowMetaPB { items: rows }) + } + pub async fn export_csv(&self, style: CSVFormat) -> FlowyResult { let database = self.database.clone(); let database_guard = database.read().await; diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs index 164c7c30fb..251cf98373 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs @@ -1,6 +1,6 @@ use crate::entities::{DatabaseSyncStatePB, DidFetchRowPB, RowsChangePB}; use crate::notification::{send_notification, DatabaseNotification, DATABASE_OBSERVABLE_SOURCE}; -use crate::services::database::UpdatedRow; +use crate::services::database::{DatabaseEditor, UpdatedRow}; use collab_database::blocks::BlockEvent; use collab_database::database::Database; use collab_database::fields::FieldChange; @@ -10,7 +10,9 @@ use flowy_notification::{DebounceNotificationSender, NotificationBuilder}; use futures::StreamExt; use lib_dispatch::prelude::af_spawn; use std::sync::Arc; +use std::time::Duration; use tokio::sync::RwLock; +use tokio_util::sync::CancellationToken; use tracing::{trace, warn}; pub(crate) async fn observe_sync_state(database_id: &str, database: &Arc>) { @@ -136,13 +138,18 @@ pub(crate) async fn observe_view_change(database_id: &str, database: &Arc>) { +pub(crate) async fn observe_block_event(database_id: &str, database_editor: &Arc) { let database_id = database_id.to_string(); - let weak_database = Arc::downgrade(database); - let mut block_event_rx = database.read().await.subscribe_block_event(); + let mut block_event_rx = database_editor + .database + .read() + .await + .subscribe_block_event(); + let database_editor = Arc::downgrade(database_editor); af_spawn(async move { + let token = CancellationToken::new(); while let Ok(event) = block_event_rx.recv().await { - if weak_database.upgrade().is_none() { + if database_editor.upgrade().is_none() { break; } @@ -155,12 +162,31 @@ pub(crate) async fn observe_block_event(database_id: &str, database: &Arc { for row_detail in row_details { trace!("Did fetch row: {:?}", row_detail.row.id); + let row_id = row_detail.row.id.clone(); let pb = DidFetchRowPB::from(row_detail); send_notification(&row_id, DatabaseNotification::DidFetchRow) .payload(pb) .send(); } + + let cloned_token = token.clone(); + let cloned_database_editor = database_editor.clone(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(2)).await; + if cloned_token.is_cancelled() { + return; + } + if let Some(database_editor) = cloned_database_editor.upgrade() { + for view_editor in database_editor.database_views.editors().await { + send_notification( + &view_editor.view_id.clone(), + DatabaseNotification::ReloadRows, + ) + .send(); + } + } + }); }, } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index f9377c1868..47dc49c516 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -293,11 +293,9 @@ impl DatabaseViewEditor { // Each row update will trigger a calculations, filter and sort operation. We don't want // to block the main thread, so we spawn a new task to do the work. - if let Some(field_id) = field_id { - self - .gen_did_update_row_view_tasks(row_detail.row.id.clone(), field_id) - .await; - } + self + .gen_did_update_row_view_tasks(row_detail.row.id.clone(), field_id) + .await; } pub async fn v_filter_rows(&self, row_details: &mut Vec>) { @@ -682,7 +680,6 @@ impl DatabaseViewEditor { #[tracing::instrument(level = "trace", skip(self), err)] pub async fn v_modify_filters(&self, changeset: FilterChangeset) -> FlowyResult<()> { let notification = self.filter_controller.apply_changeset(changeset).await; - notify_did_update_filter(notification).await; let group_controller_read_guard = self.group_controller.read().await; @@ -1100,7 +1097,7 @@ impl DatabaseViewEditor { } } - async fn gen_did_update_row_view_tasks(&self, row_id: RowId, field_id: String) { + async fn gen_did_update_row_view_tasks(&self, row_id: RowId, field_id: Option) { let weak_filter_controller = Arc::downgrade(&self.filter_controller); let weak_sort_controller = Arc::downgrade(&self.sort_controller); let weak_calculations_controller = Arc::downgrade(&self.calculations_controller); @@ -1117,10 +1114,13 @@ impl DatabaseViewEditor { .did_receive_row_changed(row_id.clone()) .await; } + if let Some(calculations_controller) = weak_calculations_controller.upgrade() { - calculations_controller - .did_receive_cell_changed(field_id) - .await; + if let Some(field_id) = field_id { + calculations_controller + .did_receive_cell_changed(field_id) + .await; + } } }); } diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs index 51443c09a2..3fdc60b53e 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs @@ -7,11 +7,11 @@ use collab_database::database::gen_database_filter_id; use collab_database::fields::Field; use collab_database::rows::{Cell, Cells, Row, RowDetail, RowId}; use dashmap::DashMap; -use serde::{Deserialize, Serialize}; -use tokio::sync::RwLock; - use flowy_error::FlowyResult; use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatcher}; +use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; +use tracing::error; use crate::entities::filter_entities::*; use crate::entities::{FieldType, InsertedRowPB, RowMetaPB}; @@ -185,8 +185,9 @@ impl FilterController { .iter_mut() .find_map(|filter| filter.find_filter(&parent_filter_id)) { - // TODO(RS): error handling for inserting filters - let _result = parent_filter.insert_filter(new_filter); + if let Err(err) = parent_filter.insert_filter(new_filter) { + error!("error while inserting filter: {}", err); + } } }, None => { @@ -214,7 +215,9 @@ impl FilterController { .find_map(|filter| filter.find_filter(&filter_id)) { // TODO(RS): error handling for updating filter data - let _result = filter.update_filter_data(data); + if let Err(error) = filter.update_filter_data(data) { + error!("error while updating filter data: {}", error); + } } }, FilterChangeset::Delete { From e460120a1c904c992fe5ba1537d3cb540507b7ac Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 19 Aug 2024 09:50:42 +0800 Subject: [PATCH 16/26] feat: add ai bubble button on mobile home page (#5992) * chore: skip check list test if the task is not found * feat: add ai bubble button in home page * feat: only show the ai bubble button for the cloud user * chore: add border color to ai bubble button * Revert "chore: skip check list test if the task is not found" This reverts commit 961f594a31906c52384c09915dce8f9db7fbd5bc. * fix: only display ai bubble button on home page --- .../presentation/home/mobile_home_page.dart | 149 ++++++++++-------- .../home/tab/ai_bubble_button.dart | 81 ++++++++++ .../home/tab/mobile_space_tab.dart | 41 ++++- .../presentation/chat_welcome_page.dart | 1 + 4 files changed, 197 insertions(+), 75 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index dd9512b0ef..4fe84524a9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -134,75 +134,7 @@ class _MobileHomePageState extends State { value: getIt()..add(const ReminderEvent.started()), ), ], - child: BlocConsumer( - buildWhen: (previous, current) => - previous.currentWorkspace?.workspaceId != - current.currentWorkspace?.workspaceId, - listener: (context, state) { - getIt().reset(); - mCurrentWorkspace.value = state.currentWorkspace; - }, - builder: (context, state) { - if (state.currentWorkspace == null) { - return const SizedBox.shrink(); - } - - final workspaceId = state.currentWorkspace!.workspaceId; - - return Column( - children: [ - // Header - Padding( - padding: EdgeInsets.only( - left: HomeSpaceViewSizes.mHorizontalPadding, - right: 8.0, - top: Platform.isAndroid ? 8.0 : 0.0, - ), - child: MobileHomePageHeader( - userProfile: widget.userProfile, - ), - ), - - Expanded( - child: MultiBlocProvider( - providers: [ - BlocProvider( - create: (_) => SpaceOrderBloc() - ..add(const SpaceOrderEvent.initial()), - ), - BlocProvider( - create: (_) => SidebarSectionsBloc() - ..add( - SidebarSectionsEvent.initial( - widget.userProfile, - workspaceId, - ), - ), - ), - BlocProvider( - create: (_) => - FavoriteBloc()..add(const FavoriteEvent.initial()), - ), - BlocProvider( - create: (_) => SpaceBloc() - ..add( - SpaceEvent.initial( - widget.userProfile, - workspaceId, - openFirstPage: false, - ), - ), - ), - ], - child: MobileSpaceTab( - userProfile: widget.userProfile, - ), - ), - ), - ], - ); - }, - ), + child: _HomePage(userProfile: widget.userProfile), ); } @@ -214,3 +146,82 @@ class _MobileHomePageState extends State { await FolderEventSetLatestView(ViewIdPB(value: id)).send(); } } + +class _HomePage extends StatelessWidget { + const _HomePage({required this.userProfile}); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + return BlocConsumer( + buildWhen: (previous, current) => + previous.currentWorkspace?.workspaceId != + current.currentWorkspace?.workspaceId, + listener: (context, state) { + getIt().reset(); + mCurrentWorkspace.value = state.currentWorkspace; + }, + builder: (context, state) { + if (state.currentWorkspace == null) { + return const SizedBox.shrink(); + } + + final workspaceId = state.currentWorkspace!.workspaceId; + + return Column( + children: [ + // Header + Padding( + padding: EdgeInsets.only( + left: HomeSpaceViewSizes.mHorizontalPadding, + right: 8.0, + top: Platform.isAndroid ? 8.0 : 0.0, + ), + child: MobileHomePageHeader( + userProfile: userProfile, + ), + ), + + Expanded( + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => + SpaceOrderBloc()..add(const SpaceOrderEvent.initial()), + ), + BlocProvider( + create: (_) => SidebarSectionsBloc() + ..add( + SidebarSectionsEvent.initial( + userProfile, + workspaceId, + ), + ), + ), + BlocProvider( + create: (_) => + FavoriteBloc()..add(const FavoriteEvent.initial()), + ), + BlocProvider( + create: (_) => SpaceBloc() + ..add( + SpaceEvent.initial( + userProfile, + workspaceId, + openFirstPage: false, + ), + ), + ), + ], + child: MobileSpaceTab( + userProfile: userProfile, + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart new file mode 100644 index 0000000000..8ecd70f7e5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart @@ -0,0 +1,81 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/base/gesture.dart'; +import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart'; +import 'package:appflowy/util/theme_extension.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class FloatingAIEntry extends StatelessWidget { + const FloatingAIEntry({super.key}); + + @override + Widget build(BuildContext context) { + return AnimatedGestureDetector( + scaleFactor: 0.99, + onTapUp: () => mobileCreateNewAIChatNotifier.value = + mobileCreateNewAIChatNotifier.value + 1, + child: DecoratedBox( + decoration: _buildShadowDecoration(context), + child: Container( + decoration: _buildWrapperDecoration(context), + height: 48, + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 18), + child: _buildHintText(context), + ), + ), + ), + ); + } + + BoxDecoration _buildShadowDecoration(BuildContext context) { + return BoxDecoration( + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + blurRadius: 20, + spreadRadius: 1, + offset: const Offset(0, 4), + color: Colors.black.withOpacity(0.05), + ), + ], + ); + } + + BoxDecoration _buildWrapperDecoration(BuildContext context) { + final outlineColor = Theme.of(context).colorScheme.outline; + final borderColor = Theme.of(context).isLightMode + ? outlineColor.withOpacity(0.7) + : outlineColor.withOpacity(0.3); + return BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Theme.of(context).colorScheme.surface, + border: Border.fromBorderSide( + BorderSide( + color: borderColor, + ), + ), + ); + } + + Widget _buildHintText(BuildContext context) { + return Row( + children: [ + FlowySvg( + FlowySvgs.toolbar_item_ai_s, + size: const Size.square(16.0), + color: Theme.of(context).hintColor, + opacity: 0.7, + ), + const HSpace(8), + FlowyText( + LocaleKeys.chat_inputMessageHint.tr(), + color: Theme.of(context).hintColor, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart index 77c26005c4..1c0f5933fb 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart @@ -20,6 +20,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; +import 'ai_bubble_button.dart'; + +final ValueNotifier mobileCreateNewAIChatNotifier = ValueNotifier(0); + class MobileSpaceTab extends StatefulWidget { const MobileSpaceTab({ super.key, @@ -40,7 +44,8 @@ class _MobileSpaceTabState extends State void initState() { super.initState(); - mobileCreateNewPageNotifier.addListener(_createNewPage); + mobileCreateNewPageNotifier.addListener(_createNewDocument); + mobileCreateNewAIChatNotifier.addListener(_createNewAIChat); mobileLeaveWorkspaceNotifier.addListener(_leaveWorkspace); } @@ -48,7 +53,9 @@ class _MobileSpaceTabState extends State void dispose() { tabController?.removeListener(_onTabChange); tabController?.dispose(); - mobileCreateNewPageNotifier.removeListener(_createNewPage); + + mobileCreateNewPageNotifier.removeListener(_createNewDocument); + mobileCreateNewAIChatNotifier.removeListener(_createNewAIChat); mobileLeaveWorkspaceNotifier.removeListener(_leaveWorkspace); super.dispose(); @@ -145,7 +152,20 @@ class _MobileSpaceTabState extends State case MobileSpaceTabType.recent: return const MobileRecentSpace(); case MobileSpaceTabType.spaces: - return MobileHomeSpace(userProfile: widget.userProfile); + return Stack( + children: [ + MobileHomeSpace(userProfile: widget.userProfile), + // only show ai chat button for cloud user + if (widget.userProfile.authenticator == + AuthenticatorPB.AppFlowyCloud) + Positioned( + bottom: MediaQuery.of(context).padding.bottom + 16, + left: 20, + right: 20, + child: const FloatingAIEntry(), + ), + ], + ); case MobileSpaceTabType.favorites: return MobileFavoriteSpace(userProfile: widget.userProfile); default: @@ -155,15 +175,24 @@ class _MobileSpaceTabState extends State } // quick create new page when clicking the add button in navigation bar - void _createNewPage() { + void _createNewDocument() { + _createNewPage(ViewLayoutPB.Document); + } + + void _createNewAIChat() { + _createNewPage(ViewLayoutPB.Chat); + } + + void _createNewPage(ViewLayoutPB layout) { if (context.read().state.spaces.isNotEmpty) { context.read().add( SpaceEvent.createPage( name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - layout: ViewLayoutPB.Document, + layout: layout, ), ); - } else { + } else if (layout == ViewLayoutPB.Document) { + // only support create document in section context.read().add( SidebarSectionsEvent.createRootViewInSection( name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart index f1ec5d2a7d..5524f1ffbe 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart @@ -73,6 +73,7 @@ class ChatWelcomePage extends StatelessWidget { const VSpace(8), Wrap( direction: Axis.vertical, + spacing: isMobile ? 12.0 : 0.0, children: items .map( (i) => WelcomeQuestionWidget( From 711326980250de616e8470dd411d24058850b12c Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 19 Aug 2024 11:06:34 +0800 Subject: [PATCH 17/26] chore: optimize row card page UI (#5995) * chore: adjust buttons padding in row record page * fix: disable more button in row page * fix: upload image button ui on mobile * fix: embed link button ui on mobile * fix: add missing border for ai text field and ai translate field * fix: delete AI can make mistakes on mobile * chore: disable sentry * fix: invite error toast * fix: add member limit hint text in invite member screen * feat: show toast after opening workspace on mobile * chore: remove sentry * chore: filter row page in recent views * feat: support display field name as row page title * chore: remove scroll bar on home page * chore: remove legacy code * chore: optimize mobile speed * Revert "chore: remove sentry" This reverts commit 73b45e2590655a992cec409503c0693df845914e. * fix: reduce document page rebuild time * chore: improve tooltip style --- .../lib/mobile/application/mobile_router.dart | 13 ++- .../application/recent/recent_view_bloc.dart | 13 --- .../presentation/base/mobile_view_page.dart | 32 +++++-- .../mobile_card_detail_screen.dart | 27 +++++- .../widgets/mobile_create_field_button.dart | 4 +- .../card_detail/widgets/row_page_button.dart | 89 ++++++++++++------- .../editor/mobile_editor_screen.dart | 8 ++ .../home/favorite_folder/favorite_space.dart | 54 ++++++----- .../home/home_space/home_space.dart | 24 +++-- .../presentation/home/mobile_folders.dart | 3 + .../presentation/home/mobile_home_page.dart | 85 ++++++++++++++++-- .../home/recent_folder/recent_space.dart | 54 ++++++----- .../setting/support_setting_group.dart | 11 ++- .../workspace/invite_members_screen.dart | 17 ++++ .../lib/plugins/ai_chat/chat_page.dart | 13 +-- .../grid/presentation/layout/sizes.dart | 1 + .../grid/presentation/widgets/row/row.dart | 10 ++- .../mobile_row_detail_summary_cell.dart | 80 ++++++++++------- .../mobile_row_detail_translate_cell.dart | 80 ++++++++++------- .../database/widgets/row/row_property.dart | 7 +- .../lib/plugins/document/document.dart | 4 + .../lib/plugins/document/document_page.dart | 34 ++++++- .../actions/block_action_add_button.dart | 4 + .../actions/block_action_button.dart | 1 - .../actions/block_action_option_button.dart | 10 ++- .../cover/document_immersive_cover.dart | 14 +++ .../upload_image_menu/upload_image_menu.dart | 56 ++++++------ .../widgets/embed_image_url_widget.dart | 33 +++++-- .../widgets/upload_image_file_widget.dart | 13 ++- .../openai/widgets/loading.dart | 12 +++ .../lib/startup/tasks/generate_router.dart | 13 ++- .../recent/cached_recent_service.dart | 7 +- .../application/sidebar/space/space_bloc.dart | 5 +- .../settings/pages/fix_data_widget.dart | 13 ++- .../presentation/widgets/dialogs.dart | 43 ++++----- .../lib/style_widget/button.dart | 10 ++- .../freezed/generate_freezed.sh | 15 +++- frontend/scripts/code_generation/generate.sh | 2 +- 38 files changed, 619 insertions(+), 295 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart index 7fc1f0824b..153ed451be 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart @@ -18,14 +18,25 @@ extension MobileRouter on BuildContext { ViewPB view, { Map? arguments, bool addInRecent = true, + bool showMoreButton = true, + String? fixedTitle, }) async { // set the current view before pushing the new view getIt().latestOpenView = view; unawaited(getIt().updateRecentViews([view.id], true)); + final queryParameters = view.queryParameters(arguments); + + if (view.layout == ViewLayoutPB.Document) { + queryParameters[MobileDocumentScreen.viewShowMoreButton] = + showMoreButton.toString(); + if (fixedTitle != null) { + queryParameters[MobileDocumentScreen.viewFixedTitle] = fixedTitle; + } + } final uri = Uri( path: view.routeName, - queryParameters: view.queryParameters(arguments), + queryParameters: queryParameters, ).toString(); await push(uri); } diff --git a/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart index 547c81f00b..99098f930d 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart @@ -1,7 +1,5 @@ import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; -import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; import 'package:appflowy/plugins/document/application/document_listener.dart'; -import 'package:appflowy/plugins/document/application/document_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; @@ -113,7 +111,6 @@ class RecentViewBloc extends Bloc { ); } - final _service = DocumentService(); final ViewPB view; final DocumentListener _documentListener; final ViewListener _viewListener; @@ -124,16 +121,6 @@ class RecentViewBloc extends Bloc { // for the version under 0.5.5 Future<(CoverType, String?)> getCoverV1() async { - final result = await _service.getDocument(documentId: view.id); - final document = result.fold((s) => s.toDocument(), (f) => null); - if (document != null) { - final coverType = CoverType.fromString( - document.root.attributes[DocumentHeaderBlockKeys.coverType], - ); - final coverValue = document - .root.attributes[DocumentHeaderBlockKeys.coverDetails] as String?; - return (coverType, coverValue); - } return (CoverType.none, null); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index 55d67ad6af..569cdd5fe6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -3,6 +3,7 @@ import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart'; +import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; import 'package:appflowy/plugins/document/presentation/document_collaborators.dart'; import 'package:appflowy/shared/feature_flags.dart'; @@ -26,6 +27,8 @@ class MobileViewPage extends StatefulWidget { required this.viewLayout, this.title, this.arguments, + this.fixedTitle, + this.showMoreButton = true, }); /// view id @@ -33,6 +36,10 @@ class MobileViewPage extends StatefulWidget { final ViewLayoutPB viewLayout; final String? title; final Map? arguments; + final bool showMoreButton; + + // only used in row page + final String? fixedTitle; @override State createState() => _MobileViewPageState(); @@ -163,6 +170,9 @@ class _MobileViewPageState extends State { return plugin.widgetBuilder.buildWidget( shrinkWrap: false, context: PluginContext(userProfile: state.userProfilePB), + data: { + MobileDocumentScreen.viewFixedTitle: widget.fixedTitle, + }, ); }, (error) { @@ -215,13 +225,19 @@ class _MobileViewPageState extends State { ]); } - actions.addAll([ - MobileViewPageMoreButton( - view: view, - isImmersiveMode: isImmersiveMode, - appBarOpacity: _appBarOpacity, - ), - ]); + if (widget.showMoreButton) { + actions.addAll([ + MobileViewPageMoreButton( + view: view, + isImmersiveMode: isImmersiveMode, + appBarOpacity: _appBarOpacity, + ), + ]); + } else { + actions.addAll([ + const HSpace(18.0), + ]); + } return actions; } @@ -241,7 +257,7 @@ class _MobileViewPageState extends State { ], Expanded( child: FlowyText.medium( - view?.name ?? widget.title ?? '', + widget.fixedTitle ?? view?.name ?? widget.title ?? '', fontSize: 15.0, overflow: TextOverflow.ellipsis, figmaLineHeight: 18.0, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart index 17c97b1f85..6a54646301 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart @@ -295,6 +295,7 @@ class MobileRowDetailPageContentState RowCache get rowCache => widget.databaseController.rowCache; FieldController get fieldController => widget.databaseController.fieldController; + ValueNotifier primaryFieldId = ValueNotifier(''); @override void initState() { @@ -327,7 +328,13 @@ class MobileRowDetailPageContentState fieldController: fieldController, rowMeta: rowController.rowMeta, )..add(const RowBannerEvent.initial()), - child: BlocBuilder( + child: BlocConsumer( + listener: (context, state) { + if (state.primaryField == null) { + return; + } + primaryFieldId.value = state.primaryField!.id; + }, builder: (context, state) { if (state.primaryField == null) { return const SizedBox.shrink(); @@ -367,8 +374,22 @@ class MobileRowDetailPageContentState if (rowDetailState.numHiddenFields != 0) ...[ const ToggleHiddenFieldsVisibilityButton(), ], - OpenRowPageButton( - documentId: rowController.rowMeta.documentId, + const VSpace(8.0), + ValueListenableBuilder( + valueListenable: primaryFieldId, + builder: (context, primaryFieldId, child) { + if (primaryFieldId.isEmpty) { + return const SizedBox.shrink(); + } + return OpenRowPageButton( + databaseController: widget.databaseController, + cellContext: CellContext( + rowId: rowController.rowId, + fieldId: primaryFieldId, + ), + documentId: rowController.rowMeta.documentId, + ); + }, ), MobileRowDetailCreateFieldButton( viewId: viewId, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart index d683a9b72d..1d3d3efcf5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart @@ -22,7 +22,7 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget { return ConstrainedBox( constraints: BoxConstraints( minWidth: double.infinity, - minHeight: GridSize.headerHeight, + maxHeight: GridSize.headerHeight, ), child: TextButton.icon( style: Theme.of(context).textButtonTheme.style?.copyWith( @@ -37,7 +37,7 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget { alignment: AlignmentDirectional.centerStart, splashFactory: NoSplash.splashFactory, padding: const WidgetStatePropertyAll( - EdgeInsets.symmetric(vertical: 14, horizontal: 6), + EdgeInsets.symmetric(horizontal: 6, vertical: 2), ), ), label: FlowyText.medium( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart index c90205e85a..49f95887ab 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart @@ -3,6 +3,10 @@ import 'dart:async'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; +import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; +import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; @@ -11,20 +15,33 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class OpenRowPageButton extends StatefulWidget { const OpenRowPageButton({ super.key, required this.documentId, + required this.databaseController, + required this.cellContext, }); final String documentId; + final DatabaseController databaseController; + final CellContext cellContext; + @override State createState() => _OpenRowPageButtonState(); } class _OpenRowPageButtonState extends State { + late final cellBloc = TextCellBloc( + cellController: makeCellController( + widget.databaseController, + widget.cellContext, + ).as(), + ); + ViewPB? view; @override @@ -36,44 +53,52 @@ class _OpenRowPageButtonState extends State { @override Widget build(BuildContext context) { - return ConstrainedBox( - constraints: BoxConstraints( - minWidth: double.infinity, - minHeight: GridSize.headerHeight, - ), - child: TextButton.icon( - style: Theme.of(context).textButtonTheme.style?.copyWith( - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), + return BlocBuilder( + bloc: cellBloc, + builder: (context, state) { + return ConstrainedBox( + constraints: BoxConstraints( + minWidth: double.infinity, + maxHeight: GridSize.buttonHeight, + ), + child: TextButton.icon( + style: Theme.of(context).textButtonTheme.style?.copyWith( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + ), + overlayColor: WidgetStateProperty.all( + Theme.of(context).hoverColor, + ), + alignment: AlignmentDirectional.centerStart, + splashFactory: NoSplash.splashFactory, + padding: const WidgetStatePropertyAll( + EdgeInsets.symmetric(horizontal: 6), + ), ), - ), - overlayColor: WidgetStateProperty.all( - Theme.of(context).hoverColor, - ), - alignment: AlignmentDirectional.centerStart, - splashFactory: NoSplash.splashFactory, - padding: const WidgetStatePropertyAll( - EdgeInsets.symmetric(vertical: 14, horizontal: 6), + label: FlowyText.medium( + LocaleKeys.grid_field_openRowDocument.tr(), + fontSize: 15, + ), + icon: const Padding( + padding: EdgeInsets.all(4.0), + child: FlowySvg( + FlowySvgs.full_view_s, + size: Size.square(16.0), ), ), - label: FlowyText.medium( - LocaleKeys.grid_field_openRowDocument.tr(), - fontSize: 15, - ), - icon: const Padding( - padding: EdgeInsets.all(4.0), - child: FlowySvg( - FlowySvgs.full_view_s, - size: Size.square(16.0), + onPressed: () { + final name = state.content; + _openRowPage(context, name); + }, ), - ), - onPressed: () => _openRowPage(context), - ), + ); + }, ); } - Future _openRowPage(BuildContext context) async { + Future _openRowPage(BuildContext context, String fieldName) async { Log.info('Open row page(${widget.documentId})'); if (view == null) { @@ -89,6 +114,8 @@ class _OpenRowPageButtonState extends State { await context.pushView( view!, addInRecent: false, + showMoreButton: false, + fixedTitle: fieldName, ); } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart index 14c4e022ae..aacc055e74 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart @@ -7,15 +7,21 @@ class MobileDocumentScreen extends StatelessWidget { super.key, required this.id, this.title, + this.showMoreButton = true, + this.fixedTitle, }); /// view id final String id; final String? title; + final bool showMoreButton; + final String? fixedTitle; static const routeName = '/docs'; static const viewId = 'id'; static const viewTitle = 'title'; + static const viewShowMoreButton = 'show_more_button'; + static const viewFixedTitle = 'fixed_title'; @override Widget build(BuildContext context) { @@ -23,6 +29,8 @@ class MobileDocumentScreen extends StatelessWidget { id: id, title: title, viewLayout: ViewLayoutPB.Document, + showMoreButton: showMoreButton, + fixedTitle: fixedTitle, ); } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart index 36e6c57e85..6282421109 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart @@ -96,36 +96,34 @@ class _FavoriteViews extends StatelessWidget { final borderColor = Theme.of(context).isLightMode ? const Color(0xFFE9E9EC) : const Color(0x1AFFFFFF); - return Scrollbar( - child: ListView.separated( - key: const PageStorageKey('favorite_views_page_storage_key'), - padding: EdgeInsets.only( - bottom: HomeSpaceViewSizes.mVerticalPadding + - MediaQuery.of(context).padding.bottom, - ), - itemBuilder: (context, index) { - final view = favoriteViews[index]; - return Container( - padding: const EdgeInsets.symmetric(vertical: 24.0), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: borderColor, - width: 0.5, - ), + return ListView.separated( + key: const PageStorageKey('favorite_views_page_storage_key'), + padding: EdgeInsets.only( + bottom: HomeSpaceViewSizes.mVerticalPadding + + MediaQuery.of(context).padding.bottom, + ), + itemBuilder: (context, index) { + final view = favoriteViews[index]; + return Container( + padding: const EdgeInsets.symmetric(vertical: 24.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: borderColor, + width: 0.5, ), ), - child: MobileViewPage( - key: ValueKey(view.item.id), - view: view.item, - timestamp: view.timestamp, - type: MobilePageCardType.favorite, - ), - ); - }, - separatorBuilder: (context, index) => const HSpace(8), - itemCount: favoriteViews.length, - ), + ), + child: MobileViewPage( + key: ValueKey(view.item.id), + view: view.item, + timestamp: view.timestamp, + type: MobilePageCardType.favorite, + ), + ); + }, + separatorBuilder: (context, index) => const HSpace(8), + itemCount: favoriteViews.length, ); } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart index 02e5fce9ab..5651379522 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart @@ -25,19 +25,17 @@ class _MobileHomeSpaceState extends State final workspaceId = context.read().state.currentWorkspace?.workspaceId ?? ''; - return Scrollbar( - child: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.only( - top: HomeSpaceViewSizes.mVerticalPadding, - bottom: HomeSpaceViewSizes.mVerticalPadding + - MediaQuery.of(context).padding.bottom, - ), - child: MobileFolders( - user: widget.userProfile, - workspaceId: workspaceId, - showFavorite: false, - ), + return SingleChildScrollView( + child: Padding( + padding: EdgeInsets.only( + top: HomeSpaceViewSizes.mVerticalPadding, + bottom: HomeSpaceViewSizes.mVerticalPadding + + MediaQuery.of(context).padding.bottom, + ), + child: MobileFolders( + user: widget.userProfile, + workspaceId: workspaceId, + showFavorite: false, ), ), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart index a5f4c210d7..dc9602ca13 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart @@ -35,6 +35,9 @@ class MobileFolders extends StatelessWidget { context.read().state.currentWorkspace?.workspaceId ?? ''; return BlocListener( + listenWhen: (previous, current) => + previous.currentWorkspace?.workspaceId != + current.currentWorkspace?.workspaceId, listener: (context, state) { context.read().add( SidebarSectionsEvent.initial( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index 4fe84524a9..d3a0a5ed23 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -1,8 +1,10 @@ import 'dart:io'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart'; import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart'; import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; @@ -14,15 +16,19 @@ import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import 'package:sentry/sentry.dart'; +import 'package:toastification/toastification.dart'; class MobileHomeScreen extends StatelessWidget { const MobileHomeScreen({super.key}); @@ -103,6 +109,8 @@ class MobileHomePage extends StatefulWidget { } class _MobileHomePageState extends State { + Loading? loadingIndicator; + @override void initState() { super.initState(); @@ -147,11 +155,18 @@ class _MobileHomePageState extends State { } } -class _HomePage extends StatelessWidget { +class _HomePage extends StatefulWidget { const _HomePage({required this.userProfile}); final UserProfilePB userProfile; + @override + State<_HomePage> createState() => _HomePageState(); +} + +class _HomePageState extends State<_HomePage> { + Loading? loadingIndicator; + @override Widget build(BuildContext context) { return BlocConsumer( @@ -161,6 +176,8 @@ class _HomePage extends StatelessWidget { listener: (context, state) { getIt().reset(); mCurrentWorkspace.value = state.currentWorkspace; + + _showResultDialog(context, state); }, builder: (context, state) { if (state.currentWorkspace == null) { @@ -170,6 +187,7 @@ class _HomePage extends StatelessWidget { final workspaceId = state.currentWorkspace!.workspaceId; return Column( + key: ValueKey('mobile_home_page_$workspaceId'), children: [ // Header Padding( @@ -179,7 +197,7 @@ class _HomePage extends StatelessWidget { top: Platform.isAndroid ? 8.0 : 0.0, ), child: MobileHomePageHeader( - userProfile: userProfile, + userProfile: widget.userProfile, ), ), @@ -194,7 +212,7 @@ class _HomePage extends StatelessWidget { create: (_) => SidebarSectionsBloc() ..add( SidebarSectionsEvent.initial( - userProfile, + widget.userProfile, workspaceId, ), ), @@ -207,7 +225,7 @@ class _HomePage extends StatelessWidget { create: (_) => SpaceBloc() ..add( SpaceEvent.initial( - userProfile, + widget.userProfile, workspaceId, openFirstPage: false, ), @@ -215,7 +233,7 @@ class _HomePage extends StatelessWidget { ), ], child: MobileSpaceTab( - userProfile: userProfile, + userProfile: widget.userProfile, ), ), ), @@ -224,4 +242,59 @@ class _HomePage extends StatelessWidget { }, ); } + + void _showResultDialog(BuildContext context, UserWorkspaceState state) { + final actionResult = state.actionResult; + if (actionResult == null) { + return; + } + + final actionType = actionResult.actionType; + final result = actionResult.result; + final isLoading = actionResult.isLoading; + + if (isLoading) { + loadingIndicator ??= Loading(context)..start(); + return; + } else { + loadingIndicator?.stop(); + loadingIndicator = null; + } + + if (result == null) { + return; + } + + result.onFailure((f) { + Log.error( + '[Workspace] Failed to perform ${actionType.toString()} action: $f', + ); + }); + + final String? message; + ToastificationType toastType = ToastificationType.success; + switch (actionType) { + case UserWorkspaceActionType.open: + message = result.fold( + (s) { + toastType = ToastificationType.success; + return LocaleKeys.workspace_openSuccess.tr(); + }, + (e) { + toastType = ToastificationType.error; + return '${LocaleKeys.workspace_openFailed.tr()}: ${e.msg}'; + }, + ); + break; + + default: + message = null; + toastType = ToastificationType.error; + break; + } + + if (message != null) { + showToastNotification(context, message: message, type: toastType); + } + } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart index e06506936c..c0baa641d9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart @@ -68,36 +68,34 @@ class _RecentViews extends StatelessWidget { ? const Color(0xFFE9E9EC) : const Color(0x1AFFFFFF); return SlidableAutoCloseBehavior( - child: Scrollbar( - child: ListView.separated( - key: const PageStorageKey('recent_views_page_storage_key'), - padding: EdgeInsets.only( - bottom: HomeSpaceViewSizes.mVerticalPadding + - MediaQuery.of(context).padding.bottom, - ), - itemBuilder: (context, index) { - final sectionView = recentViews[index]; - return Container( - padding: const EdgeInsets.symmetric(vertical: 24.0), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: borderColor, - width: 0.5, - ), + child: ListView.separated( + key: const PageStorageKey('recent_views_page_storage_key'), + padding: EdgeInsets.only( + bottom: HomeSpaceViewSizes.mVerticalPadding + + MediaQuery.of(context).padding.bottom, + ), + itemBuilder: (context, index) { + final sectionView = recentViews[index]; + return Container( + padding: const EdgeInsets.symmetric(vertical: 24.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: borderColor, + width: 0.5, ), ), - child: MobileViewPage( - key: ValueKey(sectionView.item.id), - view: sectionView.item, - timestamp: sectionView.timestamp, - type: MobilePageCardType.recent, - ), - ); - }, - separatorBuilder: (context, index) => const HSpace(8), - itemCount: recentViews.length, - ), + ), + child: MobileViewPage( + key: ValueKey(sectionView.item.id), + view: sectionView.item, + timestamp: sectionView.timestamp, + type: MobilePageCardType.recent, + ), + ); + }, + separatorBuilder: (context, index) => const HSpace(8), + itemCount: recentViews.length, ), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart index 5222a05b8f..584b867736 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart @@ -7,7 +7,8 @@ import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/shared/appflowy_cache_manager.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/share_log_files.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/fix_data_widget.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -74,10 +75,14 @@ class SupportSettingGroup extends StatelessWidget { actionButtonTitle: LocaleKeys.button_yes.tr(), onActionButtonPressed: () async { await getIt().clearAllCache(); + // check the workspace and space health + await WorkspaceDataManager.checkViewHealth( + dryRun: false, + ); if (context.mounted) { - showSnackBarMessage( + showToastNotification( context, - LocaleKeys.settings_files_clearCacheSuccess.tr(), + message: LocaleKeys.settings_files_clearCacheSuccess.tr(), ); } }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart index 775669b970..1aa088d963 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart @@ -48,6 +48,7 @@ class _InviteMemberPage extends StatefulWidget { class _InviteMemberPageState extends State<_InviteMemberPage> { final emailController = TextEditingController(); late final Future userProfile; + bool exceededLimit = false; @override void initState() { @@ -131,6 +132,15 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { ), ), const VSpace(16), + if (exceededLimit) ...[ + FlowyText.regular( + LocaleKeys.settings_appearance_members_inviteFailedMemberLimit.tr(), + fontSize: 14.0, + maxLines: 3, + color: Theme.of(context).colorScheme.error, + ), + const VSpace(16), + ], SizedBox( width: double.infinity, child: ElevatedButton( @@ -197,6 +207,9 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded ? LocaleKeys.settings_appearance_members_memberLimitExceeded.tr() : LocaleKeys.settings_appearance_members_failedToAddMember.tr(); + setState(() { + exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded; + }); showToastNotification( context, type: ToastificationType.error, @@ -220,6 +233,9 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { .tr() : LocaleKeys.settings_appearance_members_failedToInviteMember .tr(); + setState(() { + exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded; + }); showToastNotification( context, type: ToastificationType.error, @@ -255,6 +271,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { if (!isEmail(email)) { return showToastNotification( context, + type: ToastificationType.error, message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index fced505f20..bb2c3af521 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -425,13 +425,14 @@ class _ChatContentPageState extends State<_ChatContentPage> { }, ), const VSpace(6), - Opacity( - opacity: 0.6, - child: FlowyText( - LocaleKeys.chat_aiMistakePrompt.tr(), - fontSize: 12, + if (PlatformExtension.isDesktop) + Opacity( + opacity: 0.6, + child: FlowyText( + LocaleKeys.chat_aiMistakePrompt.tr(), + fontSize: 12, + ), ), - ), ], ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart index 2b1e558569..88facd39a7 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart @@ -6,6 +6,7 @@ class GridSize { static double get scrollBarSize => 8 * scale; static double get headerHeight => 40 * scale; + static double get buttonHeight => 38 * scale; static double get footerHeight => 40 * scale; static double get horizontalHeaderPadding => PlatformExtension.isDesktop ? 40 * scale : 16 * scale; diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart index fd52ec727e..19a5da2438 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart @@ -188,8 +188,14 @@ class _RowMenuButtonState extends State { richTooltipText: widget.isDragEnabled ? TextSpan( children: [ - TextSpan(text: '${LocaleKeys.tooltip_dragRow.tr()}\n'), - TextSpan(text: LocaleKeys.tooltip_openMenu.tr()), + TextSpan( + text: '${LocaleKeys.tooltip_dragRow.tr()}\n', + style: context.tooltipTextStyle(), + ), + TextSpan( + text: LocaleKeys.tooltip_openMenu.tr(), + style: context.tooltipTextStyle(), + ), ], ) : null, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart index a900cf62fb..1e709bdeb9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart @@ -13,42 +13,54 @@ class MobileRowDetailSummaryCellSkin extends IEditableSummaryCellSkin { FocusNode focusNode, TextEditingController textEditingController, ) { - return Column( - children: [ - TextField( - controller: textEditingController, - readOnly: true, - focusNode: focusNode, - onEditingComplete: () => focusNode.unfocus(), - onSubmitted: (_) => focusNode.unfocus(), - style: Theme.of(context).textTheme.bodyMedium, - textInputAction: TextInputAction.done, - maxLines: null, - minLines: 1, - decoration: InputDecoration( - contentPadding: GridSize.cellContentInsets, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - isDense: true, - ), + return Container( + decoration: BoxDecoration( + border: Border.fromBorderSide( + BorderSide(color: Theme.of(context).colorScheme.outline), ), - Row( - children: [ - const Spacer(), - Padding( - padding: const EdgeInsets.all(8.0), - child: SummaryCellAccessory( - viewId: bloc.cellController.viewId, - fieldId: bloc.cellController.fieldId, - rowId: bloc.cellController.rowId, - ), + borderRadius: const BorderRadius.all(Radius.circular(14)), + ), + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 2, + ), + child: Column( + children: [ + TextField( + controller: textEditingController, + readOnly: true, + focusNode: focusNode, + onEditingComplete: () => focusNode.unfocus(), + onSubmitted: (_) => focusNode.unfocus(), + style: Theme.of(context).textTheme.bodyMedium, + textInputAction: TextInputAction.done, + maxLines: null, + minLines: 1, + decoration: InputDecoration( + contentPadding: GridSize.cellContentInsets, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, ), - ], - ), - ], + ), + Row( + children: [ + const Spacer(), + Padding( + padding: const EdgeInsets.all(8.0), + child: SummaryCellAccessory( + viewId: bloc.cellController.viewId, + fieldId: bloc.cellController.fieldId, + rowId: bloc.cellController.rowId, + ), + ), + ], + ), + ], + ), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart index 84af6c7062..a1e4b4bf29 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart @@ -13,42 +13,54 @@ class MobileRowDetailTranslateCellSkin extends IEditableTranslateCellSkin { FocusNode focusNode, TextEditingController textEditingController, ) { - return Column( - children: [ - TextField( - readOnly: true, - controller: textEditingController, - focusNode: focusNode, - onEditingComplete: () => focusNode.unfocus(), - onSubmitted: (_) => focusNode.unfocus(), - style: Theme.of(context).textTheme.bodyMedium, - textInputAction: TextInputAction.done, - maxLines: null, - minLines: 1, - decoration: InputDecoration( - contentPadding: GridSize.cellContentInsets, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - isDense: true, - ), + return Container( + decoration: BoxDecoration( + border: Border.fromBorderSide( + BorderSide(color: Theme.of(context).colorScheme.outline), ), - Row( - children: [ - const Spacer(), - Padding( - padding: const EdgeInsets.all(8.0), - child: TranslateCellAccessory( - viewId: bloc.cellController.viewId, - fieldId: bloc.cellController.fieldId, - rowId: bloc.cellController.rowId, - ), + borderRadius: const BorderRadius.all(Radius.circular(14)), + ), + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 2, + ), + child: Column( + children: [ + TextField( + readOnly: true, + controller: textEditingController, + focusNode: focusNode, + onEditingComplete: () => focusNode.unfocus(), + onSubmitted: (_) => focusNode.unfocus(), + style: Theme.of(context).textTheme.bodyMedium, + textInputAction: TextInputAction.done, + maxLines: null, + minLines: 1, + decoration: InputDecoration( + contentPadding: GridSize.cellContentInsets, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, ), - ], - ), - ], + ), + Row( + children: [ + const Spacer(), + Padding( + padding: const EdgeInsets.all(8.0), + child: TranslateCellAccessory( + viewId: bloc.cellController.viewId, + fieldId: bloc.cellController.fieldId, + rowId: bloc.cellController.rowId, + ), + ), + ], + ), + ], + ), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart index fe08b53ab0..60dc940cea 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart @@ -1,8 +1,5 @@ import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; @@ -21,10 +18,11 @@ import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../cell/editable_cell_builder.dart'; - import 'accessory/cell_accessory.dart'; /// Display the row properties in a list. Only used in [RowDetailPage]. @@ -165,6 +163,7 @@ class _PropertyCellState extends State<_PropertyCell> { svg: FlowySvgs.drag_element_s, richMessage: TextSpan( text: LocaleKeys.grid_rowPage_fieldDragElementTooltip.tr(), + style: context.tooltipTextStyle(), ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/document.dart b/frontend/appflowy_flutter/lib/plugins/document/document.dart index aa0154bf92..ec4dde94ae 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document.dart @@ -2,6 +2,7 @@ library document_plugin; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/document_page.dart'; import 'package:appflowy/plugins/document/presentation/document_collaborators.dart'; @@ -118,6 +119,8 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder } }); + final fixedTitle = data?[MobileDocumentScreen.viewFixedTitle]; + return BlocProvider.value( value: bloc, child: BlocBuilder( @@ -126,6 +129,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder view: view, onDeleted: () => context.onDeleted?.call(view, deletedViewIndex), initialSelection: initialSelection, + fixedTitle: fixedTitle, ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 292b399731..362ae352d6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; @@ -26,6 +24,7 @@ import 'package:cross_file/cross_file.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; @@ -42,11 +41,13 @@ class DocumentPage extends StatefulWidget { required this.view, required this.onDeleted, this.initialSelection, + this.fixedTitle, }); final ViewPB view; final VoidCallback onDeleted; final Selection? initialSelection; + final String? fixedTitle; @override State createState() => _DocumentPageState(); @@ -103,6 +104,7 @@ class _DocumentPageState extends State BlocProvider.value(value: documentBloc), ], child: BlocBuilder( + buildWhen: _shouldRebuildDocument, builder: (context, state) { if (state.isLoading) { return const Center(child: CircularProgressIndicator.adaptive()); @@ -261,6 +263,7 @@ class _DocumentPageState extends State if (PlatformExtension.isMobile) { return DocumentImmersiveCover( + fixedTitle: widget.fixedTitle, view: widget.view, userProfilePB: userProfilePB, ); @@ -308,4 +311,31 @@ class _DocumentPageState extends State } } } + + bool _shouldRebuildDocument(DocumentState previous, DocumentState current) { + // only rebuild the document page when the below fields are changed + // this is to prevent unnecessary rebuilds + // + // If you confirm the newly added fields should be rebuilt, please update + // this function. + if (previous.editorState != current.editorState) { + return true; + } + + if (previous.forceClose != current.forceClose || + previous.isDeleted != current.isDeleted) { + return true; + } + + if (previous.userProfilePB != current.userProfilePB) { + return true; + } + + if (previous.isLoading != current.isLoading || + previous.error != current.error) { + return true; + } + + return false; + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart index b58b0a5646..6d01ed5f1b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart @@ -5,6 +5,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -31,16 +32,19 @@ class BlockAddButton extends StatelessWidget { children: [ TextSpan( text: LocaleKeys.blockActions_addBelowTooltip.tr(), + style: context.tooltipTextStyle(), ), const TextSpan(text: '\n'), TextSpan( text: Platform.isMacOS ? LocaleKeys.blockActions_addAboveMacCmd.tr() : LocaleKeys.blockActions_addAboveCmd.tr(), + style: context.tooltipTextStyle(), ), const TextSpan(text: ' '), TextSpan( text: LocaleKeys.blockActions_addAboveTooltip.tr(), + style: context.tooltipTextStyle(), ), ], ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart index e6a88bc4a8..0822d04db7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart @@ -21,7 +21,6 @@ class BlockActionButton extends StatelessWidget { Widget build(BuildContext context) { return Align( child: FlowyTooltip( - preferBelow: false, richMessage: richMessage, child: MouseRegion( cursor: Platform.isWindows diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart index a5617a5558..205725b81a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart @@ -7,6 +7,7 @@ import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -67,11 +68,14 @@ class BlockOptionButton extends StatelessWidget { controller.close(); } }, - buildChild: (controller) => _buildOptionButton(controller), + buildChild: (controller) => _buildOptionButton(context, controller), ); } - Widget _buildOptionButton(PopoverController controller) { + Widget _buildOptionButton( + BuildContext context, + PopoverController controller, + ) { return BlockActionButton( svg: FlowySvgs.drag_element_s, richMessage: TextSpan( @@ -79,9 +83,11 @@ class BlockOptionButton extends StatelessWidget { TextSpan( // todo: customize the color to highlight the text. text: LocaleKeys.document_plugins_optionAction_click.tr(), + style: context.tooltipTextStyle(), ), TextSpan( text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(), + style: context.tooltipTextStyle(), ), ], ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart index d1b550eded..3c04ce4301 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart @@ -34,10 +34,12 @@ class DocumentImmersiveCover extends StatefulWidget { super.key, required this.view, required this.userProfilePB, + this.fixedTitle, }); final ViewPB view; final UserProfilePB userProfilePB; + final String? fixedTitle; @override State createState() => _DocumentImmersiveCoverState(); @@ -143,6 +145,18 @@ class _DocumentImmersiveCoverState extends State { fontFamily = getGoogleFontSafely(documentFontFamily).fontFamily; } + if (widget.fixedTitle != null) { + return FlowyText( + widget.fixedTitle!, + fontSize: 28.0, + fontWeight: FontWeight.w700, + fontFamily: fontFamily, + color: + state.cover.isNone || state.cover.isPresets ? null : Colors.white, + overflow: TextOverflow.ellipsis, + ); + } + return AutoSizeTextField( controller: textEditingController, focusNode: focusNode, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart index 70b54e0da1..6c4a8dcfd3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart @@ -121,38 +121,36 @@ class _UploadImageMenuState extends State { final type = values[currentTabIndex]; switch (type) { case UploadImageType.local: - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Container( - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Theme.of(context).colorScheme.outline, - ), - ), - constraints: constraints, - child: Column( - children: [ - UploadImageFileWidget( - allowMultipleImages: widget.allowMultipleImages, - onPickFiles: widget.onSelectedLocalImages, - ), - ], + Widget child = UploadImageFileWidget( + allowMultipleImages: widget.allowMultipleImages, + onPickFiles: widget.onSelectedLocalImages, + ); + if (PlatformExtension.isDesktop) { + child = Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Theme.of(context).colorScheme.outline, ), ), + constraints: constraints, + child: child, ), - // if (widget.limitMaximumImageSize) ...[ - // FlowyText( - // LocaleKeys.document_imageBlock_maximumImageSize.tr(), - // fontSize: 10.0, - // color: Theme.of(context).hintColor, - // ), - // ], - ], - ); + ); + } else { + child = Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 12.0, + ), + child: child, + ); + } + return child; + case UploadImageType.url: return Container( padding: const EdgeInsets.all(8.0), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart index d84dcab318..4fa4ec7319 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/patterns/common_patterns.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -22,14 +23,27 @@ class _EmbedImageUrlWidgetState extends State { @override Widget build(BuildContext context) { + final textField = FlowyTextField( + hintText: LocaleKeys.document_imageBlock_embedLink_placeholder.tr(), + onChanged: (value) => inputText = value, + onEditingComplete: submit, + textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 14, + ), + hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).hintColor, + fontSize: 14, + ), + ); return Column( children: [ const VSpace(12), - FlowyTextField( - hintText: LocaleKeys.document_imageBlock_embedLink_placeholder.tr(), - onChanged: (value) => inputText = value, - onEditingComplete: submit, - ), + PlatformExtension.isDesktop + ? textField + : SizedBox( + height: 42, + child: textField, + ), if (!isUrlValid) ...[ const VSpace(12), FlowyText( @@ -39,18 +53,23 @@ class _EmbedImageUrlWidgetState extends State { ], const VSpace(20), SizedBox( - height: 32, + height: PlatformExtension.isMobile ? 36 : 32, width: 300, child: FlowyButton( backgroundColor: Theme.of(context).colorScheme.primary, hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.9), showDefaultBoxDecorationOnMobile: true, + radius: + PlatformExtension.isMobile ? BorderRadius.circular(8) : null, margin: const EdgeInsets.all(5), text: FlowyText( LocaleKeys.document_imageBlock_embedLink_label.tr(), lineHeight: 1, textAlign: TextAlign.center, - color: Theme.of(context).colorScheme.onPrimary, + color: PlatformExtension.isMobile + ? null + : Theme.of(context).colorScheme.onPrimary, + fontSize: PlatformExtension.isMobile ? 14 : null, ), onTap: submit, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/upload_image_file_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/upload_image_file_widget.dart index e9a6ea677d..991a7cb0b9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/upload_image_file_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/upload_image_file_widget.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/permission/permission_checker.dart'; import 'package:appflowy/startup/startup.dart'; @@ -9,6 +7,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; class UploadImageFileWidget extends StatelessWidget { @@ -25,8 +24,9 @@ class UploadImageFileWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final child = FlowyButton( + Widget child = FlowyButton( showDefaultBoxDecorationOnMobile: true, + radius: PlatformExtension.isMobile ? BorderRadius.circular(8.0) : null, text: Container( margin: const EdgeInsets.all(4.0), alignment: Alignment.center, @@ -38,7 +38,12 @@ class UploadImageFileWidget extends StatelessWidget { ); if (PlatformExtension.isDesktopOrWeb) { - return FlowyHover(child: child); + child = FlowyHover(child: child); + } else { + child = Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: child, + ); } return child; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart index 66ce0bef5f..1215a66dae 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart @@ -8,12 +8,22 @@ class Loading { BuildContext? loadingContext; final BuildContext context; + bool hasStopped = false; + void start() => unawaited( showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { loadingContext = context; + + if (hasStopped) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(loadingContext!).pop(); + loadingContext = null; + }); + } + return const SimpleDialog( elevation: 0.0, backgroundColor: @@ -33,6 +43,8 @@ class Loading { Navigator.of(loadingContext!).pop(); loadingContext = null; } + + hasStopped = true; } } diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index 765081be21..eebb8df1cd 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -493,9 +493,20 @@ GoRoute _mobileEditorScreenRoute() { pageBuilder: (context, state) { final id = state.uri.queryParameters[MobileDocumentScreen.viewId]!; final title = state.uri.queryParameters[MobileDocumentScreen.viewTitle]; + final showMoreButton = bool.tryParse( + state.uri.queryParameters[MobileDocumentScreen.viewShowMoreButton] ?? + 'true', + ); + final fixedTitle = + state.uri.queryParameters[MobileDocumentScreen.viewFixedTitle]; return MaterialExtendedPage( - child: MobileDocumentScreen(id: id, title: title), + child: MobileDocumentScreen( + id: id, + title: title, + showMoreButton: showMoreButton ?? true, + fixedTitle: fixedTitle, + ), ); }, ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart b/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart index 7361ab6da2..a5381ce17f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart @@ -33,7 +33,7 @@ class CachedRecentService { final _listener = RecentViewsListener(); Future> recentViews() async { - if (_isInitialized) return _recentViews; + if (_isInitialized || _completer.isCompleted) return _recentViews; _isInitialized = true; @@ -76,7 +76,10 @@ class CachedRecentService { (recentViews) { return FlowyResult.success( RepeatedRecentViewPB( - items: recentViews.items.where((e) => !e.item.isSpace), + // filter the space view and the orphan view + items: recentViews.items.where( + (e) => !e.item.isSpace && e.item.id != e.item.parentViewId, + ), ), ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart index fb96d53d0a..feef136206 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart @@ -68,6 +68,8 @@ class SpaceBloc extends Bloc { (event, emit) async { await event.when( initial: (userProfile, workspaceId, openFirstPage) async { + this.openFirstPage = openFirstPage; + _initial(userProfile, workspaceId); final (spaces, publicViews, privateViews) = await _getSpaces(); @@ -305,7 +307,7 @@ class SpaceBloc extends Bloc { SpaceEvent.initial( userProfile, workspaceId, - openFirstPage: true, + openFirstPage: openFirstPage, ), ); }, @@ -353,6 +355,7 @@ class SpaceBloc extends Bloc { String? _workspaceId; late UserProfilePB userProfile; WorkspaceSectionsListener? _listener; + bool openFirstPage = false; @override Future close() async { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/fix_data_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/fix_data_widget.dart index 6f556ec5b6..2cecb25b30 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/fix_data_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/fix_data_widget.dart @@ -118,6 +118,8 @@ class WorkspaceDataManager { final List unlistedChildViews = []; // Views whose parent is not in allViews final List orphanViews = []; + // Row pages + final List rowPageViews = []; try { if (workspace == null || allViews == null) { @@ -145,6 +147,11 @@ class WorkspaceDataManager { continue; } + if (parentView.id == view.id) { + rowPageViews.add(view); + continue; + } + final childViewsOfParent = await ViewBackendService.getChildViews(viewId: parentView.id) .getOrThrow(); @@ -165,7 +172,11 @@ class WorkspaceDataManager { } for (final view in orphanViews) { - Log.debug('[workspace] orphanViews: ${view.toProto3Json()}'); + Log.info('[workspace] orphanViews: ${view.toProto3Json()}'); + } + + for (final view in rowPageViews) { + Log.info('[workspace] rowPageViews: ${view.toProto3Json()}'); } if (!dryRun && unlistedChildViews.isNotEmpty) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 20ca5a3605..80beca1200 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -353,34 +353,37 @@ class _MToast extends StatelessWidget { @override Widget build(BuildContext context) { + final hintText = FlowyText.regular( + message, + fontSize: 16.0, + figmaLineHeight: 18.0, + color: Colors.white, + maxLines: 10, + ); return Container( alignment: Alignment.bottomCenter, - padding: const EdgeInsets.only(bottom: 100), + padding: const EdgeInsets.only(bottom: 100, left: 16, right: 16), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 13.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12.0), color: const Color(0xE5171717), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (type == ToastificationType.success) ...[ - const FlowySvg( - FlowySvgs.success_s, - blendMode: null, - ), - const HSpace(8.0), - ], - FlowyText.regular( - message, - fontSize: 16.0, - figmaLineHeight: 18.0, - color: Colors.white, - maxLines: 3, - ), - ], - ), + child: type == ToastificationType.success + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (type == ToastificationType.success) ...[ + const FlowySvg( + FlowySvgs.success_s, + blendMode: null, + ), + const HSpace(8.0), + ], + hintText, + ], + ) + : hintText, ), ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart index af5370dbfa..be69e14375 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -263,10 +263,12 @@ class FlowyButton extends StatelessWidget { (Platform.isIOS || Platform.isAndroid) ? BoxDecoration( border: Border.all( - color: borderColor ?? - Theme.of(context).colorScheme.surfaceContainerHighest, - width: 1.0, - )) + color: borderColor ?? + Theme.of(context).colorScheme.outline, + width: 1.0, + ), + borderRadius: radius, + ) : null); return Container( diff --git a/frontend/scripts/code_generation/freezed/generate_freezed.sh b/frontend/scripts/code_generation/freezed/generate_freezed.sh index abc0126325..fb7dbffb20 100755 --- a/frontend/scripts/code_generation/freezed/generate_freezed.sh +++ b/frontend/scripts/code_generation/freezed/generate_freezed.sh @@ -8,6 +8,7 @@ skip_pub_get=false skip_pub_packages_get=false verbose=false exclude_packages=false +show_loading=false # Parse command line arguments while [[ $# -gt 0 ]]; do @@ -28,6 +29,10 @@ while [[ $# -gt 0 ]]; do exclude_packages=true shift ;; + --show-loading) + show_loading=true + shift + ;; *) echo "Unknown option: $1" exit 1 @@ -111,11 +116,13 @@ fi # Get the PID of the background process build_pid=$! -# Start the loading animation -display_loading $build_pid & +if [ "$show_loading" = true ]; then + # Start the loading animation + display_loading $build_pid & -# Get the PID of the loading animation -loading_pid=$! + # Get the PID of the loading animation + loading_pid=$! +fi # Wait for the build_runner to finish wait $build_pid diff --git a/frontend/scripts/code_generation/generate.sh b/frontend/scripts/code_generation/generate.sh index afbf981a4a..e08bc873fd 100755 --- a/frontend/scripts/code_generation/generate.sh +++ b/frontend/scripts/code_generation/generate.sh @@ -64,7 +64,7 @@ cd .. cd freezed # Allow execution permissions on CI chmod +x ./generate_freezed.sh -./generate_freezed.sh "${args[@]}" +./generate_freezed.sh "${args[@]}" --show-loading # Return to the original directory cd "$original_dir" From 58b17a939cb23970947d37b9b0c5316d4673879b Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Mon, 19 Aug 2024 22:08:10 +0800 Subject: [PATCH 18/26] chore: fix lib dispatch (#6008) * chore: replace rc with arc * chore: fix dispatch * chore: fix test * chore: fix dispatch * chore: fix test * chore: remove afconcurrent * chore: fix runtime block_on runtime --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 14 +- frontend/appflowy_tauri/src-tauri/Cargo.toml | 14 +- frontend/appflowy_tauri/src-tauri/src/init.rs | 7 +- .../appflowy_web_app/src-tauri/Cargo.lock | 14 +- .../appflowy_web_app/src-tauri/Cargo.toml | 14 +- .../appflowy_web_app/src-tauri/src/init.rs | 8 +- frontend/rust-lib/Cargo.lock | 15 +- frontend/rust-lib/Cargo.toml | 14 +- .../collab-integrate/src/collab_builder.rs | 21 +- frontend/rust-lib/dart-ffi/Cargo.toml | 1 + frontend/rust-lib/dart-ffi/src/lib.rs | 208 +++++++------- .../src/event_builder.rs | 8 +- .../event-integration-test/src/lib.rs | 2 +- .../event-integration-test/src/user_event.rs | 12 +- frontend/rust-lib/flowy-core/src/lib.rs | 11 +- .../src/services/database/database_editor.rs | 20 +- frontend/rust-lib/flowy-folder/src/manager.rs | 4 +- .../rust-lib/lib-dispatch/src/dispatcher.rs | 258 ++++++++++-------- .../lib-dispatch/src/module/container.rs | 2 +- .../rust-lib/lib-dispatch/src/module/data.rs | 2 +- .../lib-dispatch/src/module/module.rs | 19 +- .../lib-dispatch/src/request/request.rs | 4 +- frontend/rust-lib/lib-dispatch/src/runtime.rs | 44 +-- .../rust-lib/lib-dispatch/tests/api/module.rs | 23 +- 24 files changed, 370 insertions(+), 369 deletions(-) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 61421256b0..625ccdecf3 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -963,7 +963,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "arc-swap", @@ -988,7 +988,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "async-trait", @@ -1018,7 +1018,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "arc-swap", @@ -1038,7 +1038,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "bytes", @@ -1057,7 +1057,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "arc-swap", @@ -1100,7 +1100,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "async-stream", @@ -1180,7 +1180,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "collab", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index eb8d21befc..450e065ce7 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -116,13 +116,13 @@ custom-protocol = ["tauri/custom-protocol"] # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/appflowy_tauri/src-tauri/src/init.rs b/frontend/appflowy_tauri/src-tauri/src/init.rs index 72e60b4a41..4903e1fe34 100644 --- a/frontend/appflowy_tauri/src-tauri/src/init.rs +++ b/frontend/appflowy_tauri/src-tauri/src/init.rs @@ -2,7 +2,6 @@ use dotenv::dotenv; use flowy_core::config::AppFlowyCoreConfig; use flowy_core::{AppFlowyCore, DEFAULT_NAME}; use lib_dispatch::runtime::AFPluginRuntime; -use std::rc::Rc; use std::sync::Mutex; pub fn read_env() { @@ -61,18 +60,18 @@ pub(crate) fn init_appflowy_core() -> MutexAppFlowyCore { ) .log_filter("trace", vec!["appflowy_tauri".to_string()]); - let runtime = Rc::new(AFPluginRuntime::new().unwrap()); + let runtime = Arc::new(AFPluginRuntime::new().unwrap()); let cloned_runtime = runtime.clone(); runtime.block_on(async move { MutexAppFlowyCore::new(AppFlowyCore::new(config, cloned_runtime, None).await) }) } -pub struct MutexAppFlowyCore(pub Rc>); +pub struct MutexAppFlowyCore(pub Arc>); impl MutexAppFlowyCore { fn new(appflowy_core: AppFlowyCore) -> Self { - Self(Rc::new(Mutex::new(appflowy_core))) + Self(Arc::new(Mutex::new(appflowy_core))) } } unsafe impl Sync for MutexAppFlowyCore {} diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index 149735e800..abef0c2e05 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -946,7 +946,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "arc-swap", @@ -971,7 +971,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "async-trait", @@ -1001,7 +1001,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "arc-swap", @@ -1021,7 +1021,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "bytes", @@ -1040,7 +1040,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "arc-swap", @@ -1083,7 +1083,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "async-stream", @@ -1163,7 +1163,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "collab", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index 566213747f..5abf8ea0fd 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -116,13 +116,13 @@ custom-protocol = ["tauri/custom-protocol"] # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/appflowy_web_app/src-tauri/src/init.rs b/frontend/appflowy_web_app/src-tauri/src/init.rs index b4c771b1b5..7af31af362 100644 --- a/frontend/appflowy_web_app/src-tauri/src/init.rs +++ b/frontend/appflowy_web_app/src-tauri/src/init.rs @@ -3,7 +3,7 @@ use flowy_core::config::AppFlowyCoreConfig; use flowy_core::{AppFlowyCore, DEFAULT_NAME}; use lib_dispatch::runtime::AFPluginRuntime; use std::rc::Rc; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; pub fn read_env() { dotenv().ok(); @@ -61,18 +61,18 @@ pub fn init_appflowy_core() -> MutexAppFlowyCore { ) .log_filter("trace", vec!["appflowy_tauri".to_string()]); - let runtime = Rc::new(AFPluginRuntime::new().unwrap()); + let runtime = Arc::new(AFPluginRuntime::new().unwrap()); let cloned_runtime = runtime.clone(); runtime.block_on(async move { MutexAppFlowyCore::new(AppFlowyCore::new(config, cloned_runtime, None).await) }) } -pub struct MutexAppFlowyCore(pub Rc>); +pub struct MutexAppFlowyCore(pub Arc>); impl MutexAppFlowyCore { pub(crate) fn new(appflowy_core: AppFlowyCore) -> Self { - Self(Rc::new(Mutex::new(appflowy_core))) + Self(Arc::new(Mutex::new(appflowy_core))) } } unsafe impl Sync for MutexAppFlowyCore {} diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 45bebc1af8..a53b2cc8ca 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -824,7 +824,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "arc-swap", @@ -849,7 +849,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "async-trait", @@ -879,7 +879,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "arc-swap", @@ -899,7 +899,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "bytes", @@ -918,7 +918,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "arc-swap", @@ -961,7 +961,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "async-stream", @@ -1041,7 +1041,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ba00c1e430f6157a2b6cbda89992d3b154ea6fb#2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" dependencies = [ "anyhow", "collab", @@ -1325,6 +1325,7 @@ dependencies = [ "flowy-server", "flowy-server-pub", "flowy-user", + "futures", "lazy_static", "lib-dispatch", "lib-log", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index fdabfe8379..0016e596af 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -136,13 +136,13 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ba00c1e430f6157a2b6cbda89992d3b154ea6fb" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/collab-integrate/src/collab_builder.rs b/frontend/rust-lib/collab-integrate/src/collab_builder.rs index 3742b2fc72..f539cca182 100644 --- a/frontend/rust-lib/collab-integrate/src/collab_builder.rs +++ b/frontend/rust-lib/collab-integrate/src/collab_builder.rs @@ -257,6 +257,7 @@ impl AppFlowyCollabBuilder { { let mut write_collab = collab.try_write()?; if !write_collab.borrow().get_state().is_uninitialized() { + warn!("{} is already initialized", object); drop(write_collab); return Ok(collab); } @@ -285,10 +286,7 @@ impl AppFlowyCollabBuilder { } } - if build_config.auto_initialize { - // at the moment when we get the lock, the collab object is not yet exposed outside - (*write_collab).borrow_mut().initialize(); - } + (*write_collab).borrow_mut().initialize(); drop(write_collab); Ok(collab) } @@ -296,19 +294,11 @@ impl AppFlowyCollabBuilder { pub struct CollabBuilderConfig { pub sync_enable: bool, - /// If auto_initialize is false, the collab object will not be initialized automatically. - /// You need to call collab.initialize() manually. - /// - /// Default is true. - pub auto_initialize: bool, } impl Default for CollabBuilderConfig { fn default() -> Self { - Self { - sync_enable: true, - auto_initialize: true, - } + Self { sync_enable: true } } } @@ -317,11 +307,6 @@ impl CollabBuilderConfig { self.sync_enable = sync_enable; self } - - pub fn auto_initialize(mut self, auto_initialize: bool) -> Self { - self.auto_initialize = auto_initialize; - self - } } pub struct KVDBCollabPersistenceImpl { diff --git a/frontend/rust-lib/dart-ffi/Cargo.toml b/frontend/rust-lib/dart-ffi/Cargo.toml index c60c09e0c9..91ed0d9bf6 100644 --- a/frontend/rust-lib/dart-ffi/Cargo.toml +++ b/frontend/rust-lib/dart-ffi/Cargo.toml @@ -45,6 +45,7 @@ collab-integrate = { workspace = true } flowy-derive.workspace = true serde_yaml = "0.9.27" flowy-error = { workspace = true, features = ["impl_from_sqlite", "impl_from_dispatch_error", "impl_from_appflowy_cloud", "impl_from_reqwest", "impl_from_serde", "dart"] } +futures = "0.3.26" [features] default = ["dart"] diff --git a/frontend/rust-lib/dart-ffi/src/lib.rs b/frontend/rust-lib/dart-ffi/src/lib.rs index 85281c8cb0..134290e76e 100644 --- a/frontend/rust-lib/dart-ffi/src/lib.rs +++ b/frontend/rust-lib/dart-ffi/src/lib.rs @@ -3,9 +3,10 @@ use allo_isolate::Isolate; use lazy_static::lazy_static; use semver::Version; -use std::rc::Rc; -use std::sync::{Arc, Mutex}; +use std::sync::{mpsc, Arc, RwLock}; use std::{ffi::CStr, os::raw::c_char}; +use tokio::runtime::Builder; +use tokio::task::LocalSet; use tracing::{debug, error, info, trace, warn}; use flowy_core::config::AppFlowyCoreConfig; @@ -33,34 +34,77 @@ mod notification; mod protobuf; lazy_static! { - static ref APPFLOWY_CORE: MutexAppFlowyCore = MutexAppFlowyCore::new(); - static ref LOG_STREAM_ISOLATE: Mutex> = Mutex::new(None); + static ref DART_APPFLOWY_CORE: DartAppFlowyCore = DartAppFlowyCore::new(); + static ref LOG_STREAM_ISOLATE: RwLock> = RwLock::new(None); } -unsafe impl Send for MutexAppFlowyCore {} -unsafe impl Sync for MutexAppFlowyCore {} +pub struct Task { + dispatcher: Arc, + request: AFPluginRequest, + port: i64, + ret: Option>>, +} -///FIXME: I'm pretty sure that there's a better way to do this -struct MutexAppFlowyCore(Rc>>); +unsafe impl Send for Task {} +unsafe impl Sync for DartAppFlowyCore {} -impl MutexAppFlowyCore { +struct DartAppFlowyCore { + core: Arc>>, + handle: RwLock>>, + sender: RwLock>>, +} + +impl DartAppFlowyCore { fn new() -> Self { - Self(Rc::new(Mutex::new(None))) + Self { + core: Arc::new(RwLock::new(None)), + handle: RwLock::new(None), + sender: RwLock::new(None), + } } - fn dispatcher(&self) -> Option> { - let binding = self.0.lock().unwrap(); + fn dispatcher(&self) -> Option> { + let binding = self + .core + .read() + .expect("Failed to acquire read lock for core"); let core = binding.as_ref(); core.map(|core| core.event_dispatcher.clone()) } + + fn dispatch( + &self, + request: AFPluginRequest, + port: i64, + ret: Option>>, + ) { + if let Ok(sender_guard) = self.sender.read() { + if let Err(e) = sender_guard.as_ref().unwrap().send(Task { + dispatcher: self.dispatcher().unwrap(), + request, + port, + ret, + }) { + error!("Failed to send task: {}", e); + } + } else { + warn!("Failed to acquire read lock for sender"); + return; + } + } } #[no_mangle] pub extern "C" fn init_sdk(_port: i64, data: *mut c_char) -> i64 { - // and sent it the `Rust's` result - // no need to convert anything :) - let c_str = unsafe { CStr::from_ptr(data) }; - let serde_str = c_str.to_str().unwrap(); + let c_str = unsafe { + if data.is_null() { + return -1; + } + CStr::from_ptr(data) + }; + let serde_str = c_str + .to_str() + .expect("Failed to convert C string to Rust string"); let configuration = AppFlowyDartConfiguration::from_str(serde_str); configuration.write_env(); @@ -85,26 +129,49 @@ pub extern "C" fn init_sdk(_port: i64, data: *mut c_char) -> i64 { DEFAULT_NAME.to_string(), ); - // Ensure that the database is closed before initialization. Also, verify that the init_sdk function can be called - // multiple times (is reentrant). Currently, only the database resource is exclusive. - if let Some(core) = &*APPFLOWY_CORE.0.lock().unwrap() { + if let Some(core) = &*DART_APPFLOWY_CORE.core.write().unwrap() { core.close_db(); } - let runtime = Rc::new(AFPluginRuntime::new().unwrap()); - let cloned_runtime = runtime.clone(); - let log_stream = LOG_STREAM_ISOLATE - .lock() + .write() .unwrap() .take() .map(|isolate| Arc::new(LogStreamSenderImpl { isolate }) as Arc); + let (sender, task_rx) = mpsc::channel::(); + let handle = std::thread::spawn(move || { + let local_set = LocalSet::new(); + while let Ok(task) = task_rx.recv() { + let Task { + dispatcher, + request, + port, + ret, + } = task; - // let isolate = allo_isolate::Isolate::new(port); - *APPFLOWY_CORE.0.lock().unwrap() = runtime.block_on(async move { - Some(AppFlowyCore::new(config, cloned_runtime, log_stream).await) - // isolate.post("".to_string()); + let resp = AFPluginDispatcher::boxed_async_send_with_callback( + dispatcher.as_ref(), + request, + move |resp: AFPluginEventResponse| { + #[cfg(feature = "sync_verbose_log")] + trace!("[FFI]: Post data to dart through {} port", port); + Box::pin(post_to_flutter(resp, port)) + }, + &local_set, + ); + + if let Some(ret) = ret { + let _ = ret.send(resp); + } + } }); + + *DART_APPFLOWY_CORE.sender.write().unwrap() = Some(sender); + *DART_APPFLOWY_CORE.handle.write().unwrap() = Some(handle); + let runtime = Arc::new(AFPluginRuntime::new().unwrap()); + let cloned_runtime = runtime.clone(); + *DART_APPFLOWY_CORE.core.write().unwrap() = runtime + .block_on(async move { Some(AppFlowyCore::new(config, cloned_runtime, log_stream).await) }); 0 } @@ -120,40 +187,13 @@ pub extern "C" fn async_event(port: i64, input: *const u8, len: usize) { port ); - let dispatcher = match APPFLOWY_CORE.dispatcher() { - None => { - error!("sdk not init yet."); - return; - }, - Some(dispatcher) => dispatcher, - }; - AFPluginDispatcher::boxed_async_send_with_callback( - dispatcher.as_ref(), - request, - move |resp: AFPluginEventResponse| { - #[cfg(feature = "sync_verbose_log")] - trace!("[FFI]: Post data to dart through {} port", port); - Box::pin(post_to_flutter(resp, port)) - }, - ); + DART_APPFLOWY_CORE.dispatch(request, port, None); } #[no_mangle] -pub extern "C" fn sync_event(input: *const u8, len: usize) -> *const u8 { - let request: AFPluginRequest = FFIRequest::from_u8_pointer(input, len).into(); - #[cfg(feature = "sync_verbose_log")] - trace!("[FFI]: {} Sync Event: {:?}", &request.id, &request.event,); +pub extern "C" fn sync_event(_input: *const u8, _len: usize) -> *const u8 { + error!("unimplemented sync_event"); - let dispatcher = match APPFLOWY_CORE.dispatcher() { - None => { - error!("sdk not init yet."); - return forget_rust(Vec::default()); - }, - Some(dispatcher) => dispatcher, - }; - let _response = AFPluginDispatcher::sync_send(dispatcher, request); - - // FFIResponse { } let response_bytes = vec![]; let result = extend_front_four_bytes_into_bytes(&response_bytes); forget_rust(result) @@ -161,7 +201,6 @@ pub extern "C" fn sync_event(input: *const u8, len: usize) -> *const u8 { #[no_mangle] pub extern "C" fn set_stream_port(notification_port: i64) -> i32 { - // Make sure hot reload won't register the notification sender twice unregister_all_notification_sender(); register_notification_sender(DartNotificationSender::new(notification_port)); 0 @@ -169,8 +208,7 @@ pub extern "C" fn set_stream_port(notification_port: i64) -> i32 { #[no_mangle] pub extern "C" fn set_log_stream_port(port: i64) -> i32 { - *LOG_STREAM_ISOLATE.lock().unwrap() = Some(Isolate::new(port)); - + *LOG_STREAM_ISOLATE.write().unwrap() = Some(Isolate::new(port)); 0 } @@ -181,31 +219,22 @@ pub extern "C" fn link_me_please() {} #[inline(always)] async fn post_to_flutter(response: AFPluginEventResponse, port: i64) { let isolate = allo_isolate::Isolate::new(port); - #[allow(clippy::blocks_in_conditions)] - match isolate + if let Ok(_) = isolate .catch_unwind(async { let ffi_resp = FFIResponse::from(response); ffi_resp.into_bytes().unwrap().to_vec() }) .await { - Ok(_success) => { - #[cfg(feature = "sync_verbose_log")] - trace!("[FFI]: Post data to dart success"); - }, - Err(e) => { - if let Some(msg) = e.downcast_ref::<&str>() { - error!("[FFI]: {:?}", msg); - } else { - error!("[FFI]: allo_isolate post panic"); - } - }, + #[cfg(feature = "sync_verbose_log")] + trace!("[FFI]: Post data to dart success"); + } else { + error!("[FFI]: allo_isolate post panic"); } } #[no_mangle] pub extern "C" fn rust_log(level: i64, data: *const c_char) { - // Check if the data pointer is not null if data.is_null() { error!("[flutter error]: null pointer provided to backend_log"); return; @@ -213,7 +242,6 @@ pub extern "C" fn rust_log(level: i64, data: *const c_char) { let log_result = unsafe { CStr::from_ptr(data) }.to_str(); - // Handle potential UTF-8 conversion error let log_str = match log_result { Ok(str) => str, Err(e) => { @@ -225,29 +253,13 @@ pub extern "C" fn rust_log(level: i64, data: *const c_char) { }, }; - // Simplify logging by determining the log level outside of the match - let log_level = match level { - 0 => "info", - 1 => "debug", - 2 => "trace", - 3 => "warn", - 4 => "error", - _ => { - warn!("[flutter error]: Unsupported log level: {}", level); - return; - }, - }; - - // Log the message at the appropriate level - match log_level { - "info" => info!("[Flutter]: {}", log_str), - "debug" => debug!("[Flutter]: {}", log_str), - "trace" => trace!("[Flutter]: {}", log_str), - "warn" => warn!("[Flutter]: {}", log_str), - "error" => error!("[Flutter]: {}", log_str), - _ => { - warn!("[flutter error]: Unsupported log level: {}", log_level); - }, + match level { + 0 => info!("[Flutter]: {}", log_str), + 1 => debug!("[Flutter]: {}", log_str), + 2 => trace!("[Flutter]: {}", log_str), + 3 => warn!("[Flutter]: {}", log_str), + 4 => error!("[Flutter]: {}", log_str), + _ => warn!("[flutter error]: Unsupported log level: {}", level), } } diff --git a/frontend/rust-lib/event-integration-test/src/event_builder.rs b/frontend/rust-lib/event-integration-test/src/event_builder.rs index c4149378e5..a50f3aa314 100644 --- a/frontend/rust-lib/event-integration-test/src/event_builder.rs +++ b/frontend/rust-lib/event-integration-test/src/event_builder.rs @@ -3,12 +3,13 @@ use flowy_user::errors::{internal_error, FlowyError}; use lib_dispatch::prelude::{ AFPluginDispatcher, AFPluginEventResponse, AFPluginFromBytes, AFPluginRequest, ToBytes, *, }; -use std::rc::Rc; +use std::sync::Arc; use std::{ convert::TryFrom, fmt::{Debug, Display}, hash::Hash, }; +use tokio::task::LocalSet; #[derive(Clone)] pub struct EventBuilder { @@ -47,8 +48,9 @@ impl EventBuilder { } pub async fn async_send(mut self) -> Self { + let local_set = LocalSet::new(); let request = self.get_request(); - let resp = AFPluginDispatcher::async_send(self.dispatch().as_ref(), request).await; + let resp = AFPluginDispatcher::async_send(self.dispatch().as_ref(), request, &local_set).await; self.context.response = Some(resp); self } @@ -84,7 +86,7 @@ impl EventBuilder { .map(|data| data.into_inner()) } - fn dispatch(&self) -> Rc { + fn dispatch(&self) -> Arc { self.context.sdk.dispatcher() } diff --git a/frontend/rust-lib/event-integration-test/src/lib.rs b/frontend/rust-lib/event-integration-test/src/lib.rs index 88100b2f85..03e93bd90e 100644 --- a/frontend/rust-lib/event-integration-test/src/lib.rs +++ b/frontend/rust-lib/event-integration-test/src/lib.rs @@ -164,7 +164,7 @@ pub fn document_from_document_doc_state(doc_id: &str, doc_state: Vec) -> Doc } async fn init_core(config: AppFlowyCoreConfig) -> AppFlowyCore { - let runtime = Rc::new(AFPluginRuntime::new().unwrap()); + let runtime = Arc::new(AFPluginRuntime::new().unwrap()); let cloned_runtime = runtime.clone(); AppFlowyCore::new(config, cloned_runtime, None).await } diff --git a/frontend/rust-lib/event-integration-test/src/user_event.rs b/frontend/rust-lib/event-integration-test/src/user_event.rs index d4de053426..b2b3ac33e5 100644 --- a/frontend/rust-lib/event-integration-test/src/user_event.rs +++ b/frontend/rust-lib/event-integration-test/src/user_event.rs @@ -9,6 +9,7 @@ use flowy_folder::entities::{RepeatedViewPB, WorkspacePB}; use protobuf::ProtobufError; use tokio::sync::broadcast::{channel, Sender}; +use tokio::task::LocalSet; use tracing::error; use uuid::Uuid; @@ -73,11 +74,12 @@ impl EventIntegrationTest { .unwrap(); let request = AFPluginRequest::new(UserEvent::SignUp).payload(payload); - let user_profile = AFPluginDispatcher::async_send(&self.appflowy_core.dispatcher(), request) - .await - .parse::() - .unwrap() - .unwrap(); + let user_profile = + AFPluginDispatcher::async_send(&self.appflowy_core.dispatcher(), request, &LocalSet::new()) + .await + .parse::() + .unwrap() + .unwrap(); // let _ = create_default_workspace_if_need(dispatch.clone(), &user_profile.id); SignUpContext { diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 55b4753c66..a5d60ae703 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -2,7 +2,6 @@ use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_search::services::manager::SearchManager; -use std::rc::Rc; use std::sync::{Arc, Weak}; use std::time::Duration; use sysinfo::System; @@ -55,7 +54,7 @@ pub struct AppFlowyCore { pub document_manager: Arc, pub folder_manager: Arc, pub database_manager: Arc, - pub event_dispatcher: Rc, + pub event_dispatcher: Arc, pub server_provider: Arc, pub task_dispatcher: Arc>, pub store_preference: Arc, @@ -67,7 +66,7 @@ pub struct AppFlowyCore { impl AppFlowyCore { pub async fn new( config: AppFlowyCoreConfig, - runtime: Rc, + runtime: Arc, stream_log_sender: Option>, ) -> Self { let platform = OperatingSystem::from(&config.platform); @@ -103,7 +102,7 @@ impl AppFlowyCore { } #[instrument(skip(config, runtime))] - async fn init(config: AppFlowyCoreConfig, runtime: Rc) -> Self { + async fn init(config: AppFlowyCoreConfig, runtime: Arc) -> Self { // Init the key value database let store_preference = Arc::new(KVStorePreferences::new(&config.storage_path).unwrap()); info!("🔥{:?}", &config); @@ -262,7 +261,7 @@ impl AppFlowyCore { error!("Init user failed: {}", err) } } - let event_dispatcher = Rc::new(AFPluginDispatcher::new( + let event_dispatcher = Arc::new(AFPluginDispatcher::new( runtime, make_plugins( Arc::downgrade(&folder_manager), @@ -291,7 +290,7 @@ impl AppFlowyCore { } /// Only expose the dispatcher in test - pub fn dispatcher(&self) -> Rc { + pub fn dispatcher(&self) -> Arc { self.event_dispatcher.clone() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index c260a0fe14..2f084537cd 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -681,14 +681,16 @@ impl DatabaseEditor { } pub async fn init_database_row(&self, row_id: &RowId) -> FlowyResult<()> { - if self - .database - .read() - .await - .get_database_row(row_id) - .is_some() - { - return Ok(()); + if let Some(database_row) = self.database.read().await.get_database_row(row_id) { + if !database_row + .read() + .await + .collab + .get_state() + .is_uninitialized() + { + return Ok(()); + } } debug!("Init database row: {}", row_id); @@ -696,7 +698,7 @@ impl DatabaseEditor { .database .read() .await - .create_database_row(row_id) + .init_database_row(row_id) .ok_or_else(|| { FlowyError::record_not_found() .with_context(format!("The row:{} in database not found", row_id)) diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index bc691ab7dc..7a6966d306 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -157,9 +157,7 @@ impl FolderManager { ) -> Result>, FlowyError> { let folder_notifier = folder_notifier.into(); // only need the check the workspace id when the doc state is not from the disk. - let config = CollabBuilderConfig::default() - .sync_enable(true) - .auto_initialize(true); + let config = CollabBuilderConfig::default().sync_enable(true); let data_source = data_source .unwrap_or_else(|| KVDBCollabPersistenceImpl::new(collab_db.clone(), uid).into_data_source()); diff --git a/frontend/rust-lib/lib-dispatch/src/dispatcher.rs b/frontend/rust-lib/lib-dispatch/src/dispatcher.rs index e3e72ff2be..920a9f1e2a 100644 --- a/frontend/rust-lib/lib-dispatch/src/dispatcher.rs +++ b/frontend/rust-lib/lib-dispatch/src/dispatcher.rs @@ -3,7 +3,7 @@ use pin_project::pin_project; use std::any::Any; use std::future::Future; use std::pin::Pin; -use std::rc::Rc; +use std::sync::Arc; use std::task::{Context, Poll}; use tracing::event; @@ -17,10 +17,10 @@ use crate::{ }; #[cfg(feature = "local_set")] -pub trait AFConcurrent {} +pub trait AFConcurrent: Send {} #[cfg(feature = "local_set")] -impl AFConcurrent for T where T: ?Sized {} +impl AFConcurrent for T where T: Send + ?Sized {} #[cfg(not(feature = "local_set"))] pub trait AFConcurrent: Send + Sync {} @@ -47,7 +47,7 @@ pub(crate) fn downcast_owned(boxed: AFBox) -> Option; +pub(crate) type AFBox = Box; #[cfg(not(feature = "local_set"))] pub(crate) type AFBox = Box; @@ -70,11 +70,12 @@ where pub struct AFPluginDispatcher { plugins: AFPluginMap, - runtime: Rc, + #[allow(dead_code)] + runtime: Arc, } impl AFPluginDispatcher { - pub fn new(runtime: Rc, plugins: Vec) -> AFPluginDispatcher { + pub fn new(runtime: Arc, plugins: Vec) -> AFPluginDispatcher { tracing::trace!("{}", plugin_info(&plugins)); AFPluginDispatcher { plugins: plugin_map_or_crash(plugins), @@ -82,13 +83,111 @@ impl AFPluginDispatcher { } } - pub async fn async_send(dispatch: &AFPluginDispatcher, request: Req) -> AFPluginEventResponse + #[cfg(feature = "local_set")] + pub async fn async_send( + dispatch: &AFPluginDispatcher, + request: Req, + local_set: &tokio::task::LocalSet, + ) -> AFPluginEventResponse where Req: Into, { - AFPluginDispatcher::async_send_with_callback(dispatch, request, |_| Box::pin(async {})).await + AFPluginDispatcher::async_send_with_callback( + dispatch, + request, + |_| Box::pin(async {}), + local_set, + ) + .await + } + #[cfg(feature = "local_set")] + pub async fn async_send_with_callback( + dispatch: &AFPluginDispatcher, + request: Req, + callback: Callback, + local_set: &tokio::task::LocalSet, + ) -> AFPluginEventResponse + where + Req: Into, + Callback: FnOnce(AFPluginEventResponse) -> AFBoxFuture<'static, ()> + AFConcurrent + 'static, + { + let request: AFPluginRequest = request.into(); + let plugins = dispatch.plugins.clone(); + let service = Box::new(DispatchService { plugins }); + tracing::trace!("Async event: {:?}", &request.event); + let service_ctx = DispatchContext { + request, + callback: Some(Box::new(callback)), + }; + + let handle = local_set.spawn_local(async move { + service.call(service_ctx).await.unwrap_or_else(|e| { + tracing::error!("Dispatch runtime error: {:?}", e); + InternalError::Other(format!("{:?}", e)).as_response() + }) + }); + // let handle = tokio::spawn(async move { + // service.call(service_ctx).await.unwrap_or_else(|e| { + // tracing::error!("Dispatch runtime error: {:?}", e); + // InternalError::Other(format!("{:?}", e)).as_response() + // }) + // }); + + let result: Result = local_set + .run_until(handle) + .await + .map_err(|e| e.to_string().into()); + + result.unwrap_or_else(|e| { + let msg = format!("EVENT_DISPATCH join error: {:?}", e); + tracing::error!("{}", msg); + let error = InternalError::JoinError(msg); + error.as_response() + }) } + #[cfg(feature = "local_set")] + pub fn boxed_async_send_with_callback( + dispatch: &AFPluginDispatcher, + request: Req, + callback: Callback, + local_set: &tokio::task::LocalSet, + ) -> DispatchFuture + where + Req: Into + 'static, + Callback: FnOnce(AFPluginEventResponse) -> AFBoxFuture<'static, ()> + AFConcurrent + 'static, + { + let request: AFPluginRequest = request.into(); + let plugins = dispatch.plugins.clone(); + let service = Box::new(DispatchService { plugins }); + tracing::trace!("[dispatch]: Async event: {:?}", &request.event); + let service_ctx = DispatchContext { + request, + callback: Some(Box::new(callback)), + }; + + let handle = local_set.spawn_local(async move { + service.call(service_ctx).await.unwrap_or_else(|e| { + tracing::error!("Dispatch runtime error: {:?}", e); + InternalError::Other(format!("{:?}", e)).as_response() + }) + }); + + let fut = local_set.run_until(handle); + let result = local_set.block_on(&dispatch.runtime.inner, fut); + DispatchFuture { + fut: Box::pin(async move { + result.unwrap_or_else(|e| { + let msg = format!("EVENT_DISPATCH join error: {:?}", e); + tracing::error!("{}", msg); + let error = InternalError::JoinError(msg); + error.as_response() + }) + }), + } + } + + #[cfg(not(feature = "local_set"))] pub async fn async_send_with_callback( dispatch: &AFPluginDispatcher, request: Req, @@ -107,65 +206,25 @@ impl AFPluginDispatcher { callback: Some(Box::new(callback)), }; - // Spawns a future onto the runtime. - // - // This spawns the given future onto the runtime's executor, usually a - // thread pool. The thread pool is then responsible for polling the future - // until it completes. - // - // The provided future will start running in the background immediately - // when `spawn` is called, even if you don't await the returned - // `JoinHandle`. - let result: Result; - #[cfg(feature = "local_set")] - { - let handle = dispatch.runtime.local.spawn_local(async move { + dispatch + .runtime + .spawn(async move { service.call(service_ctx).await.unwrap_or_else(|e| { tracing::error!("Dispatch runtime error: {:?}", e); InternalError::Other(format!("{:?}", e)).as_response() }) - }); - - result = dispatch - .runtime - .local - .run_until(handle) - .await - .map_err(|e| e.to_string().into()) - } - - #[cfg(not(feature = "local_set"))] - { - result = dispatch - .runtime - .spawn(async move { - service.call(service_ctx).await.unwrap_or_else(|e| { - tracing::error!("Dispatch runtime error: {:?}", e); - InternalError::Other(format!("{:?}", e)).as_response() - }) - }) - .await; - } - - result.unwrap_or_else(|e| { - let msg = format!("EVENT_DISPATCH join error: {:?}", e); - tracing::error!("{}", msg); - let error = InternalError::JoinError(msg); - error.as_response() - }) + }) + .await + .unwrap_or_else(|e| { + let msg = format!("EVENT_DISPATCH join error: {:?}", e); + tracing::error!("{}", msg); + let error = InternalError::JoinError(msg); + error.as_response() + }) } - pub fn box_async_send( - dispatch: &AFPluginDispatcher, - request: Req, - ) -> DispatchFuture - where - Req: Into + 'static, - { - AFPluginDispatcher::boxed_async_send_with_callback(dispatch, request, |_| Box::pin(async {})) - } - - pub fn boxed_async_send_with_callback( + #[cfg(not(feature = "local_set"))] + pub async fn boxed_async_send_with_callback( dispatch: &AFPluginDispatcher, request: Req, callback: Callback, @@ -183,76 +242,39 @@ impl AFPluginDispatcher { callback: Some(Box::new(callback)), }; - #[cfg(feature = "local_set")] - { - let handle = dispatch.runtime.local.spawn_local(async move { - service.call(service_ctx).await.unwrap_or_else(|e| { - tracing::error!("Dispatch runtime error: {:?}", e); - InternalError::Other(format!("{:?}", e)).as_response() + let handle = dispatch.runtime.spawn(async move { + service.call(service_ctx).await.unwrap_or_else(|e| { + tracing::error!("[dispatch]: runtime error: {:?}", e); + InternalError::Other(format!("{:?}", e)).as_response() + }) + }); + + let runtime = dispatch.runtime.clone(); + DispatchFuture { + fut: Box::pin(async move { + let result = runtime.spawn(handle).await.unwrap(); + result.unwrap_or_else(|e| { + let msg = format!("EVENT_DISPATCH join error: {:?}", e); + tracing::error!("{}", msg); + let error = InternalError::JoinError(msg); + error.as_response() }) - }); - - let fut = dispatch.runtime.local.run_until(handle); - let result = dispatch.runtime.block_on(fut); - DispatchFuture { - fut: Box::pin(async move { - result.unwrap_or_else(|e| { - let msg = format!("EVENT_DISPATCH join error: {:?}", e); - tracing::error!("{}", msg); - let error = InternalError::JoinError(msg); - error.as_response() - }) - }), - } - } - - #[cfg(not(feature = "local_set"))] - { - let handle = dispatch.runtime.spawn(async move { - service - .call(crate::service::service::Service) - .await - .unwrap_or_else(|e| { - tracing::error!("[dispatch]: runtime error: {:?}", e); - InternalError::Other(format!("{:?}", e)).as_response() - }) - }); - - let runtime = dispatch.runtime.clone(); - DispatchFuture { - fut: Box::pin(async move { - let result = runtime.run_until(handle).await; - result.unwrap_or_else(|e| { - let msg = format!("EVENT_DISPATCH join error: {:?}", e); - tracing::error!("{}", msg); - let error = InternalError::JoinError(msg); - error.as_response() - }) - }), - } + }), } } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(feature = "local_set")] pub fn sync_send( - dispatch: Rc, + dispatch: Arc, request: AFPluginRequest, ) -> AFPluginEventResponse { futures::executor::block_on(AFPluginDispatcher::async_send_with_callback( dispatch.as_ref(), request, |_| Box::pin(async {}), + &tokio::task::LocalSet::new(), )) } - - #[track_caller] - pub fn spawn(&self, future: F) -> tokio::task::JoinHandle - where - F: Future + Send + 'static, - ::Output: Send + 'static, - { - self.runtime.spawn(future) - } } #[derive(Derivative)] diff --git a/frontend/rust-lib/lib-dispatch/src/module/container.rs b/frontend/rust-lib/lib-dispatch/src/module/container.rs index d6fdf24d67..4082590345 100644 --- a/frontend/rust-lib/lib-dispatch/src/module/container.rs +++ b/frontend/rust-lib/lib-dispatch/src/module/container.rs @@ -13,7 +13,7 @@ impl AFPluginStateMap { pub fn insert(&mut self, val: T) -> Option where - T: 'static + AFConcurrent, + T: 'static + Send + Sync, { self .0 diff --git a/frontend/rust-lib/lib-dispatch/src/module/data.rs b/frontend/rust-lib/lib-dispatch/src/module/data.rs index 520c3e2494..3cf8f23d51 100644 --- a/frontend/rust-lib/lib-dispatch/src/module/data.rs +++ b/frontend/rust-lib/lib-dispatch/src/module/data.rs @@ -53,7 +53,7 @@ where impl FromAFPluginRequest for AFPluginState where - T: ?Sized + AFConcurrent + 'static, + T: ?Sized + Send + Sync + 'static, { type Error = DispatchError; type Future = Ready>; diff --git a/frontend/rust-lib/lib-dispatch/src/module/module.rs b/frontend/rust-lib/lib-dispatch/src/module/module.rs index 883225c1b0..12cbb0c150 100644 --- a/frontend/rust-lib/lib-dispatch/src/module/module.rs +++ b/frontend/rust-lib/lib-dispatch/src/module/module.rs @@ -13,7 +13,6 @@ use crate::{ use futures_core::ready; use nanoid::nanoid; use pin_project::pin_project; -use std::rc::Rc; use std::sync::Arc; use std::{ collections::HashMap, @@ -25,12 +24,12 @@ use std::{ task::{Context, Poll}, }; -pub type AFPluginMap = Rc>>; +pub type AFPluginMap = Arc>>; pub(crate) fn plugin_map_or_crash(plugins: Vec) -> AFPluginMap { - let mut plugin_map: HashMap> = HashMap::new(); + let mut plugin_map: HashMap> = HashMap::new(); plugins.into_iter().for_each(|m| { let events = m.events(); - let plugins = Rc::new(m); + let plugins = Arc::new(m); events.into_iter().for_each(|e| { if plugin_map.contains_key(&e) { let plugin_name = plugin_map.get(&e).map(|p| &p.name); @@ -39,7 +38,7 @@ pub(crate) fn plugin_map_or_crash(plugins: Vec) -> AFPluginMap { plugin_map.insert(e, plugins.clone()); }); }); - Rc::new(plugin_map) + Arc::new(plugin_map) } #[derive(PartialEq, Eq, Hash, Debug, Clone)] @@ -66,7 +65,7 @@ pub struct AFPlugin { /// Contains a list of factories that are used to generate the services used to handle the passed-in /// `ServiceRequest`. /// - event_service_factory: Rc< + event_service_factory: Arc< HashMap>, >, } @@ -76,7 +75,7 @@ impl std::default::Default for AFPlugin { Self { name: "".to_owned(), states: Default::default(), - event_service_factory: Rc::new(HashMap::new()), + event_service_factory: Arc::new(HashMap::new()), } } } @@ -91,7 +90,7 @@ impl AFPlugin { self } - pub fn state(mut self, data: D) -> Self { + pub fn state(mut self, data: D) -> Self { Arc::get_mut(&mut self.states) .unwrap() .insert(crate::module::AFPluginState::new(data)); @@ -112,7 +111,7 @@ impl AFPlugin { if self.event_service_factory.contains_key(&event) { panic!("Register duplicate Event: {:?}", &event); } else { - Rc::get_mut(&mut self.event_service_factory) + Arc::get_mut(&mut self.event_service_factory) .unwrap() .insert(event, factory(AFPluginHandlerService::new(handler))); } @@ -184,7 +183,7 @@ impl AFPluginServiceFactory for AFPlugin { } pub struct AFPluginService { - services: Rc< + services: Arc< HashMap>, >, states: AFStateMap, diff --git a/frontend/rust-lib/lib-dispatch/src/request/request.rs b/frontend/rust-lib/lib-dispatch/src/request/request.rs index c62950f65d..68aab764d4 100644 --- a/frontend/rust-lib/lib-dispatch/src/request/request.rs +++ b/frontend/rust-lib/lib-dispatch/src/request/request.rs @@ -8,7 +8,7 @@ use std::{ use derivative::*; use futures_core::ready; -use crate::prelude::{AFConcurrent, AFStateMap}; +use crate::prelude::AFStateMap; use crate::{ errors::{DispatchError, InternalError}, module::AFPluginEvent, @@ -39,7 +39,7 @@ impl AFPluginEventRequest { pub fn get_state(&self) -> Option where - T: AFConcurrent + 'static + Clone, + T: Send + Sync + 'static + Clone, { if let Some(data) = self.states.get::() { return Some(data.clone()); diff --git a/frontend/rust-lib/lib-dispatch/src/runtime.rs b/frontend/rust-lib/lib-dispatch/src/runtime.rs index eaa3223a20..e2f5cd56c3 100644 --- a/frontend/rust-lib/lib-dispatch/src/runtime.rs +++ b/frontend/rust-lib/lib-dispatch/src/runtime.rs @@ -7,17 +7,15 @@ use tokio::runtime::Runtime; use tokio::task::JoinHandle; pub struct AFPluginRuntime { - inner: Runtime, - #[cfg(feature = "local_set")] - pub(crate) local: tokio::task::LocalSet, + pub(crate) inner: Runtime, } impl Display for AFPluginRuntime { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if cfg!(any(target_arch = "wasm32", feature = "local_set")) { - write!(f, "Runtime(current_thread)") + write!(f, "Runtime(local_set)") } else { - write!(f, "Runtime(multi_thread)") + write!(f, "Runtime") } } } @@ -25,11 +23,7 @@ impl Display for AFPluginRuntime { impl AFPluginRuntime { pub fn new() -> io::Result { let inner = default_tokio_runtime()?; - Ok(Self { - inner, - #[cfg(feature = "local_set")] - local: tokio::task::LocalSet::new(), - }) + Ok(Self { inner }) } #[track_caller] @@ -41,16 +35,6 @@ impl AFPluginRuntime { self.inner.spawn(future) } - #[cfg(feature = "local_set")] - #[track_caller] - pub fn block_on(&self, f: F) -> F::Output - where - F: Future, - { - self.local.block_on(&self.inner, f) - } - - #[cfg(not(feature = "local_set"))] #[track_caller] pub fn block_on(&self, f: F) -> F::Output where @@ -62,21 +46,11 @@ impl AFPluginRuntime { #[cfg(feature = "local_set")] pub fn default_tokio_runtime() -> io::Result { - #[cfg(not(target_arch = "wasm32"))] - { - runtime::Builder::new_multi_thread() - .enable_io() - .enable_time() - .thread_name("dispatch-rt-st") - .build() - } - - #[cfg(target_arch = "wasm32")] - { - runtime::Builder::new_current_thread() - .thread_name("dispatch-rt-st") - .build() - } + runtime::Builder::new_multi_thread() + .enable_io() + .enable_time() + .thread_name("dispatch-rt-st") + .build() } #[cfg(not(feature = "local_set"))] diff --git a/frontend/rust-lib/lib-dispatch/tests/api/module.rs b/frontend/rust-lib/lib-dispatch/tests/api/module.rs index fed8d75720..f7c4a9f591 100644 --- a/frontend/rust-lib/lib-dispatch/tests/api/module.rs +++ b/frontend/rust-lib/lib-dispatch/tests/api/module.rs @@ -1,7 +1,7 @@ -use std::rc::Rc; - use lib_dispatch::prelude::*; use lib_dispatch::runtime::AFPluginRuntime; +use std::sync::Arc; +use tokio::task::LocalSet; pub async fn hello() -> String { "say hello".to_string() @@ -10,17 +10,22 @@ pub async fn hello() -> String { #[tokio::test] async fn test() { let event = "1"; - let runtime = Rc::new(AFPluginRuntime::new().unwrap()); - let dispatch = Rc::new(AFPluginDispatcher::new( + let runtime = Arc::new(AFPluginRuntime::new().unwrap()); + let dispatch = Arc::new(AFPluginDispatcher::new( runtime, vec![AFPlugin::new().event(event, hello)], )); let request = AFPluginRequest::new(event); - let _ = AFPluginDispatcher::async_send_with_callback(dispatch.as_ref(), request, |resp| { - Box::pin(async move { - dbg!(&resp); - }) - }) + let _ = AFPluginDispatcher::async_send_with_callback( + dispatch.as_ref(), + request, + |resp| { + Box::pin(async move { + dbg!(&resp); + }) + }, + &LocalSet::new(), + ) .await; std::mem::forget(dispatch); From faf1e98d1511f153b6abfe78d27baee2366f954f Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 19 Aug 2024 22:09:31 +0800 Subject: [PATCH 19/26] chore: update appflowy collab --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 14 +++++++------- frontend/appflowy_tauri/src-tauri/Cargo.toml | 14 +++++++------- frontend/appflowy_web_app/src-tauri/Cargo.lock | 14 +++++++------- frontend/appflowy_web_app/src-tauri/Cargo.toml | 14 +++++++------- frontend/rust-lib/Cargo.lock | 14 +++++++------- frontend/rust-lib/Cargo.toml | 14 +++++++------- 6 files changed, 42 insertions(+), 42 deletions(-) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 625ccdecf3..c7247f61da 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -963,7 +963,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "arc-swap", @@ -988,7 +988,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "async-trait", @@ -1018,7 +1018,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "arc-swap", @@ -1038,7 +1038,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "bytes", @@ -1057,7 +1057,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "arc-swap", @@ -1100,7 +1100,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "async-stream", @@ -1180,7 +1180,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "collab", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 450e065ce7..eaf9c45240 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -116,13 +116,13 @@ custom-protocol = ["tauri/custom-protocol"] # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index abef0c2e05..67a03fc4b2 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -946,7 +946,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "arc-swap", @@ -971,7 +971,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "async-trait", @@ -1001,7 +1001,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "arc-swap", @@ -1021,7 +1021,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "bytes", @@ -1040,7 +1040,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "arc-swap", @@ -1083,7 +1083,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "async-stream", @@ -1163,7 +1163,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "collab", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index 5abf8ea0fd..9939037c67 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -116,13 +116,13 @@ custom-protocol = ["tauri/custom-protocol"] # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index a53b2cc8ca..ccf8f27506 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -824,7 +824,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "arc-swap", @@ -849,7 +849,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "async-trait", @@ -879,7 +879,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "arc-swap", @@ -899,7 +899,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "bytes", @@ -918,7 +918,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "arc-swap", @@ -961,7 +961,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "async-stream", @@ -1041,7 +1041,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b0cd69ec92a42a319a1fdb2e184151162db52cd6#b0cd69ec92a42a319a1fdb2e184151162db52cd6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" dependencies = [ "anyhow", "collab", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 0016e596af..86c73438be 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -136,13 +136,13 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b0cd69ec92a42a319a1fdb2e184151162db52cd6" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } # Working directory: frontend # To update the commit ID, run: From 6d09c337820c1d49ed08c004ad6c5a615c4bbcdb Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:16:24 +0800 Subject: [PATCH 20/26] chore: spawn task on local set (#6012) * chore: spawn local * chore: using multiple thread runtime * chore: fix test --- frontend/rust-lib/dart-ffi/src/lib.rs | 82 ++++++++++++------- .../src/event_builder.rs | 13 ++- .../event-integration-test/src/lib.rs | 4 +- .../event-integration-test/src/user_event.rs | 19 +++-- .../flowy-database2/src/event_handler.rs | 3 +- .../src/services/database/database_editor.rs | 9 +- .../rust-lib/lib-dispatch/src/dispatcher.rs | 82 ++++--------------- .../rust-lib/lib-dispatch/tests/api/module.rs | 23 +++--- 8 files changed, 114 insertions(+), 121 deletions(-) diff --git a/frontend/rust-lib/dart-ffi/src/lib.rs b/frontend/rust-lib/dart-ffi/src/lib.rs index 134290e76e..b9aca0fc90 100644 --- a/frontend/rust-lib/dart-ffi/src/lib.rs +++ b/frontend/rust-lib/dart-ffi/src/lib.rs @@ -1,11 +1,16 @@ #![allow(clippy::not_unsafe_ptr_arg_deref)] use allo_isolate::Isolate; +use futures::ready; use lazy_static::lazy_static; use semver::Version; -use std::sync::{mpsc, Arc, RwLock}; +use std::future::Future; +use std::pin::Pin; +use std::sync::{Arc, RwLock}; +use std::task::{Context, Poll}; use std::{ffi::CStr, os::raw::c_char}; use tokio::runtime::Builder; +use tokio::sync::mpsc; use tokio::task::LocalSet; use tracing::{debug, error, info, trace, warn}; @@ -42,7 +47,7 @@ pub struct Task { dispatcher: Arc, request: AFPluginRequest, port: i64, - ret: Option>>, + ret: Option>, } unsafe impl Send for Task {} @@ -51,7 +56,7 @@ unsafe impl Sync for DartAppFlowyCore {} struct DartAppFlowyCore { core: Arc>>, handle: RwLock>>, - sender: RwLock>>, + sender: RwLock>>, } impl DartAppFlowyCore { @@ -76,7 +81,7 @@ impl DartAppFlowyCore { &self, request: AFPluginRequest, port: i64, - ret: Option>>, + ret: Option>, ) { if let Ok(sender_guard) = self.sender.read() { if let Err(e) = sender_guard.as_ref().unwrap().send(Task { @@ -138,32 +143,11 @@ pub extern "C" fn init_sdk(_port: i64, data: *mut c_char) -> i64 { .unwrap() .take() .map(|isolate| Arc::new(LogStreamSenderImpl { isolate }) as Arc); - let (sender, task_rx) = mpsc::channel::(); + let (sender, task_rx) = mpsc::unbounded_channel::(); let handle = std::thread::spawn(move || { + let runtime = Builder::new_multi_thread().enable_all().build().unwrap(); let local_set = LocalSet::new(); - while let Ok(task) = task_rx.recv() { - let Task { - dispatcher, - request, - port, - ret, - } = task; - - let resp = AFPluginDispatcher::boxed_async_send_with_callback( - dispatcher.as_ref(), - request, - move |resp: AFPluginEventResponse| { - #[cfg(feature = "sync_verbose_log")] - trace!("[FFI]: Post data to dart through {} port", port); - Box::pin(post_to_flutter(resp, port)) - }, - &local_set, - ); - - if let Some(ret) = ret { - let _ = ret.send(resp); - } - } + runtime.block_on(local_set.run_until(Runner { rx: task_rx })); }); *DART_APPFLOWY_CORE.sender.write().unwrap() = Some(sender); @@ -190,6 +174,48 @@ pub extern "C" fn async_event(port: i64, input: *const u8, len: usize) { DART_APPFLOWY_CORE.dispatch(request, port, None); } +/// A persistent future that processes [Arbiter] commands. +struct Runner { + rx: mpsc::UnboundedReceiver, +} + +impl Future for Runner { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + loop { + match ready!(self.rx.poll_recv(cx)) { + None => return Poll::Ready(()), + Some(task) => { + let Task { + dispatcher, + request, + port, + ret, + } = task; + + tokio::task::spawn_local(async move { + let resp = AFPluginDispatcher::boxed_async_send_with_callback( + dispatcher.as_ref(), + request, + move |resp: AFPluginEventResponse| { + #[cfg(feature = "sync_verbose_log")] + trace!("[FFI]: Post data to dart through {} port", port); + Box::pin(post_to_flutter(resp, port)) + }, + ) + .await; + + if let Some(ret) = ret { + let _ = ret.send(resp); + } + }); + }, + } + } + } +} + #[no_mangle] pub extern "C" fn sync_event(_input: *const u8, _len: usize) -> *const u8 { error!("unimplemented sync_event"); diff --git a/frontend/rust-lib/event-integration-test/src/event_builder.rs b/frontend/rust-lib/event-integration-test/src/event_builder.rs index a50f3aa314..b3d4a313f0 100644 --- a/frontend/rust-lib/event-integration-test/src/event_builder.rs +++ b/frontend/rust-lib/event-integration-test/src/event_builder.rs @@ -11,15 +11,17 @@ use std::{ }; use tokio::task::LocalSet; -#[derive(Clone)] +// #[derive(Clone)] pub struct EventBuilder { context: TestContext, + local_set: LocalSet, } impl EventBuilder { pub fn new(sdk: EventIntegrationTest) -> Self { Self { context: TestContext::new(sdk), + local_set: Default::default(), } } @@ -48,9 +50,14 @@ impl EventBuilder { } pub async fn async_send(mut self) -> Self { - let local_set = LocalSet::new(); let request = self.get_request(); - let resp = AFPluginDispatcher::async_send(self.dispatch().as_ref(), request, &local_set).await; + let resp = self + .local_set + .run_until(AFPluginDispatcher::async_send( + self.dispatch().as_ref(), + request, + )) + .await; self.context.response = Some(resp); self } diff --git a/frontend/rust-lib/event-integration-test/src/lib.rs b/frontend/rust-lib/event-integration-test/src/lib.rs index 03e93bd90e..1aaf4a57db 100644 --- a/frontend/rust-lib/event-integration-test/src/lib.rs +++ b/frontend/rust-lib/event-integration-test/src/lib.rs @@ -5,7 +5,6 @@ use collab_document::document::Document; use collab_entity::CollabType; use std::env::temp_dir; use std::path::PathBuf; -use std::rc::Rc; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -13,6 +12,7 @@ use std::time::Duration; use nanoid::nanoid; use semver::Version; use tokio::select; +use tokio::task::LocalSet; use tokio::time::sleep; use flowy_core::config::AppFlowyCoreConfig; @@ -40,6 +40,7 @@ pub struct EventIntegrationTest { #[allow(dead_code)] cleaner: Arc, pub notification_sender: TestNotificationSender, + local_set: Arc, } impl EventIntegrationTest { @@ -67,6 +68,7 @@ impl EventIntegrationTest { authenticator, notification_sender, cleaner: Arc::new(Cleaner::new(PathBuf::from(clean_path))), + local_set: Arc::new(Default::default()), } } diff --git a/frontend/rust-lib/event-integration-test/src/user_event.rs b/frontend/rust-lib/event-integration-test/src/user_event.rs index b2b3ac33e5..e11f645aec 100644 --- a/frontend/rust-lib/event-integration-test/src/user_event.rs +++ b/frontend/rust-lib/event-integration-test/src/user_event.rs @@ -4,12 +4,9 @@ use std::sync::atomic::Ordering; use std::sync::Arc; use bytes::Bytes; - use flowy_folder::entities::{RepeatedViewPB, WorkspacePB}; - use protobuf::ProtobufError; use tokio::sync::broadcast::{channel, Sender}; -use tokio::task::LocalSet; use tracing::error; use uuid::Uuid; @@ -74,12 +71,16 @@ impl EventIntegrationTest { .unwrap(); let request = AFPluginRequest::new(UserEvent::SignUp).payload(payload); - let user_profile = - AFPluginDispatcher::async_send(&self.appflowy_core.dispatcher(), request, &LocalSet::new()) - .await - .parse::() - .unwrap() - .unwrap(); + let user_profile = self + .local_set + .run_until(AFPluginDispatcher::async_send( + &self.appflowy_core.dispatcher(), + request, + )) + .await + .parse::() + .unwrap() + .unwrap(); // let _ = create_default_workspace_if_need(dispatch.clone(), &user_profile.id); SignUpContext { diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 84564473f2..89aef3a89a 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -1,7 +1,6 @@ -use std::sync::{Arc, Weak}; - use collab_database::rows::RowId; use lib_infra::box_any::BoxAny; +use std::sync::{Arc, Weak}; use tokio::sync::oneshot; use tracing::{error, trace}; diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 2f084537cd..fcd6c253a1 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -1309,7 +1309,7 @@ impl DatabaseEditor { .get_all_field_orders() .into_iter() .map(FieldIdPB::from) - .collect(); + .collect::>(); let is_linked = database.is_inline_view(view_id); (database_id, fields, is_linked) }; @@ -1318,6 +1318,13 @@ impl DatabaseEditor { .into_iter() .map(|detail| RowMetaPB::from(detail.as_ref())) .collect::>(); + + trace!( + "database: {}, num fields: {}, num row: {}", + database_id, + fields.len(), + rows.len() + ); Ok(DatabasePB { id: database_id, fields, diff --git a/frontend/rust-lib/lib-dispatch/src/dispatcher.rs b/frontend/rust-lib/lib-dispatch/src/dispatcher.rs index 920a9f1e2a..8a1f1d0e31 100644 --- a/frontend/rust-lib/lib-dispatch/src/dispatcher.rs +++ b/frontend/rust-lib/lib-dispatch/src/dispatcher.rs @@ -84,75 +84,31 @@ impl AFPluginDispatcher { } #[cfg(feature = "local_set")] - pub async fn async_send( - dispatch: &AFPluginDispatcher, - request: Req, - local_set: &tokio::task::LocalSet, - ) -> AFPluginEventResponse + pub async fn async_send(dispatch: &AFPluginDispatcher, request: Req) -> AFPluginEventResponse where - Req: Into, + Req: Into + 'static, { - AFPluginDispatcher::async_send_with_callback( - dispatch, - request, - |_| Box::pin(async {}), - local_set, - ) - .await + AFPluginDispatcher::async_send_with_callback(dispatch, request, |_| Box::pin(async {})).await } #[cfg(feature = "local_set")] pub async fn async_send_with_callback( dispatch: &AFPluginDispatcher, request: Req, callback: Callback, - local_set: &tokio::task::LocalSet, ) -> AFPluginEventResponse where - Req: Into, + Req: Into + 'static, Callback: FnOnce(AFPluginEventResponse) -> AFBoxFuture<'static, ()> + AFConcurrent + 'static, { - let request: AFPluginRequest = request.into(); - let plugins = dispatch.plugins.clone(); - let service = Box::new(DispatchService { plugins }); - tracing::trace!("Async event: {:?}", &request.event); - let service_ctx = DispatchContext { - request, - callback: Some(Box::new(callback)), - }; - - let handle = local_set.spawn_local(async move { - service.call(service_ctx).await.unwrap_or_else(|e| { - tracing::error!("Dispatch runtime error: {:?}", e); - InternalError::Other(format!("{:?}", e)).as_response() - }) - }); - // let handle = tokio::spawn(async move { - // service.call(service_ctx).await.unwrap_or_else(|e| { - // tracing::error!("Dispatch runtime error: {:?}", e); - // InternalError::Other(format!("{:?}", e)).as_response() - // }) - // }); - - let result: Result = local_set - .run_until(handle) - .await - .map_err(|e| e.to_string().into()); - - result.unwrap_or_else(|e| { - let msg = format!("EVENT_DISPATCH join error: {:?}", e); - tracing::error!("{}", msg); - let error = InternalError::JoinError(msg); - error.as_response() - }) + Self::boxed_async_send_with_callback(dispatch, request, callback).await } #[cfg(feature = "local_set")] - pub fn boxed_async_send_with_callback( + pub async fn boxed_async_send_with_callback( dispatch: &AFPluginDispatcher, request: Req, callback: Callback, - local_set: &tokio::task::LocalSet, - ) -> DispatchFuture + ) -> AFPluginEventResponse where Req: Into + 'static, Callback: FnOnce(AFPluginEventResponse) -> AFBoxFuture<'static, ()> + AFConcurrent + 'static, @@ -166,25 +122,20 @@ impl AFPluginDispatcher { callback: Some(Box::new(callback)), }; - let handle = local_set.spawn_local(async move { + let result = tokio::task::spawn_local(async move { service.call(service_ctx).await.unwrap_or_else(|e| { tracing::error!("Dispatch runtime error: {:?}", e); InternalError::Other(format!("{:?}", e)).as_response() }) - }); + }) + .await; - let fut = local_set.run_until(handle); - let result = local_set.block_on(&dispatch.runtime.inner, fut); - DispatchFuture { - fut: Box::pin(async move { - result.unwrap_or_else(|e| { - let msg = format!("EVENT_DISPATCH join error: {:?}", e); - tracing::error!("{}", msg); - let error = InternalError::JoinError(msg); - error.as_response() - }) - }), - } + result.unwrap_or_else(|e| { + let msg = format!("EVENT_DISPATCH join error: {:?}", e); + tracing::error!("{}", msg); + let error = InternalError::JoinError(msg); + error.as_response() + }) } #[cfg(not(feature = "local_set"))] @@ -272,7 +223,6 @@ impl AFPluginDispatcher { dispatch.as_ref(), request, |_| Box::pin(async {}), - &tokio::task::LocalSet::new(), )) } } diff --git a/frontend/rust-lib/lib-dispatch/tests/api/module.rs b/frontend/rust-lib/lib-dispatch/tests/api/module.rs index f7c4a9f591..27ee94a9ee 100644 --- a/frontend/rust-lib/lib-dispatch/tests/api/module.rs +++ b/frontend/rust-lib/lib-dispatch/tests/api/module.rs @@ -16,17 +16,18 @@ async fn test() { vec![AFPlugin::new().event(event, hello)], )); let request = AFPluginRequest::new(event); - let _ = AFPluginDispatcher::async_send_with_callback( - dispatch.as_ref(), - request, - |resp| { - Box::pin(async move { - dbg!(&resp); - }) - }, - &LocalSet::new(), - ) - .await; + let local_set = LocalSet::new(); + local_set + .run_until(AFPluginDispatcher::async_send_with_callback( + dispatch.as_ref(), + request, + |resp| { + Box::pin(async move { + dbg!(&resp); + }) + }, + )) + .await; std::mem::forget(dispatch); } From 6a0650e6d5a1bc1f58c277e22a5ed8b8f494dddc Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:18:57 +0800 Subject: [PATCH 21/26] chore: fix file upload test (#6016) * chore: fix file upload test --- .../af_cloud_test/file_upload_test.rs | 31 +++-- .../flowy-document/tests/document/util.rs | 8 +- .../down.sql | 2 + .../up.sql | 2 + frontend/rust-lib/flowy-sqlite/src/schema.rs | 1 + .../flowy-storage-pub/src/chunked_byte.rs | 8 +- .../rust-lib/flowy-storage-pub/src/storage.rs | 5 +- .../rust-lib/flowy-storage/src/manager.rs | 112 +++++++++--------- .../rust-lib/flowy-storage/src/sqlite_sql.rs | 71 +++++++---- .../rust-lib/flowy-storage/src/uploader.rs | 6 +- .../tests/multiple_part_upload_test.rs | 1 + 11 files changed, 153 insertions(+), 94 deletions(-) create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2024-08-20-061727_file_upload_finish/down.sql create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2024-08-20-061727_file_upload_finish/up.sql diff --git a/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/file_upload_test.rs b/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/file_upload_test.rs index a2ab2d1245..7376ff238e 100644 --- a/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/file_upload_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/file_upload_test.rs @@ -2,6 +2,7 @@ use crate::document::generate_random_bytes; use event_integration_test::user_event::user_localhost_af_cloud; use event_integration_test::EventIntegrationTest; use flowy_storage_pub::storage::FileUploadState; +use lib_infra::util::md5; use std::env::temp_dir; use std::sync::Arc; use std::time::Duration; @@ -9,19 +10,21 @@ use tokio::fs; use tokio::fs::File; use tokio::io::AsyncWriteExt; use tokio::sync::Mutex; +use tokio::time::timeout; + #[tokio::test] async fn af_cloud_upload_big_file_test() { user_localhost_af_cloud().await; let mut test = EventIntegrationTest::new().await; test.af_cloud_sign_up().await; tokio::time::sleep(Duration::from_secs(6)).await; - + let parent_dir = "temp_test"; let workspace_id = test.get_current_workspace().await.id; let (file_path, upload_data) = generate_file_with_bytes_len(15 * 1024 * 1024).await; let (created_upload, rx) = test .storage_manager .storage_service - .create_upload(&workspace_id, "temp_test", &file_path, false) + .create_upload(&workspace_id, parent_dir, &file_path, false) .await .unwrap(); @@ -42,15 +45,22 @@ async fn af_cloud_upload_big_file_test() { // Restart the test. It will load unfinished uploads let test = EventIntegrationTest::new_with_config(config).await; - let mut rx = test + if let Some(mut rx) = test .storage_manager - .subscribe_file_state(&created_upload.file_id) + .subscribe_file_state(parent_dir, &created_upload.file_id) .await - .unwrap(); - - while let Some(state) = rx.recv().await { - if let FileUploadState::Finished { .. } = state { - break; + .unwrap() + { + let timeout_duration = Duration::from_secs(180); + while let Some(state) = match timeout(timeout_duration, rx.recv()).await { + Ok(result) => result, + Err(_) => { + panic!("Timed out waiting for file upload completion"); + }, + } { + if let FileUploadState::Finished { .. } = state { + break; + } } } @@ -62,8 +72,7 @@ async fn af_cloud_upload_big_file_test() { .file_storage() .unwrap(); let file = file_service.get_object(created_upload.url).await.unwrap(); - assert_eq!(file.raw.to_vec(), upload_data); - + assert_eq!(md5(file.raw), md5(upload_data)); let _ = fs::remove_file(file_path).await; } diff --git a/frontend/rust-lib/flowy-document/tests/document/util.rs b/frontend/rust-lib/flowy-document/tests/document/util.rs index 2bc2f9d7bb..2d3fa202a9 100644 --- a/frontend/rust-lib/flowy-document/tests/document/util.rs +++ b/frontend/rust-lib/flowy-document/tests/document/util.rs @@ -195,7 +195,7 @@ impl StorageService for DocumentTestFileStorageService { todo!() } - async fn start_upload(&self, _chunks: &ChunkedBytes, _record: &BoxAny) -> Result<(), FlowyError> { + async fn start_upload(&self, _chunks: ChunkedBytes, _record: &BoxAny) -> Result<(), FlowyError> { todo!() } @@ -208,7 +208,11 @@ impl StorageService for DocumentTestFileStorageService { todo!() } - async fn subscribe_file_progress(&self, _url: &str) -> Result { + async fn subscribe_file_progress( + &self, + _parent_idr: &str, + _url: &str, + ) -> Result, FlowyError> { todo!() } } diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-08-20-061727_file_upload_finish/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-08-20-061727_file_upload_finish/down.sql new file mode 100644 index 0000000000..8c072ae1ce --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2024-08-20-061727_file_upload_finish/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE upload_file_table DROP COLUMN is_finish; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-08-20-061727_file_upload_finish/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-08-20-061727_file_upload_finish/up.sql new file mode 100644 index 0000000000..088564dca4 --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2024-08-20-061727_file_upload_finish/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE upload_file_table ADD COLUMN is_finish BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index 28d278c6a4..ed2290dd6f 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -64,6 +64,7 @@ diesel::table! { num_chunk -> Integer, upload_id -> Text, created_at -> BigInt, + is_finish -> Bool, } } diff --git a/frontend/rust-lib/flowy-storage-pub/src/chunked_byte.rs b/frontend/rust-lib/flowy-storage-pub/src/chunked_byte.rs index d1210ec8b0..8614fb4489 100644 --- a/frontend/rust-lib/flowy-storage-pub/src/chunked_byte.rs +++ b/frontend/rust-lib/flowy-storage-pub/src/chunked_byte.rs @@ -9,6 +9,7 @@ use tokio::io::AsyncReadExt; /// In Amazon S3, the minimum chunk size for multipart uploads is 5 MB,except for the last part, /// which can be smaller.(https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html) pub const MIN_CHUNK_SIZE: usize = 5 * 1024 * 1024; // Minimum Chunk Size 5 MB +#[derive(Debug, Clone)] pub struct ChunkedBytes { pub data: Bytes, pub chunk_size: i32, @@ -28,8 +29,11 @@ impl Display for ChunkedBytes { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "ChunkedBytes: chunk_size: {}, offsets: {:?}, current_offset: {}", - self.chunk_size, self.offsets, self.current_offset + "data:{}, chunk_size:{}, num chunk:{}, offset:{}", + self.data.len(), + self.chunk_size, + self.offsets.len(), + self.current_offset ) } } diff --git a/frontend/rust-lib/flowy-storage-pub/src/storage.rs b/frontend/rust-lib/flowy-storage-pub/src/storage.rs index 30a9231dab..12124504b9 100644 --- a/frontend/rust-lib/flowy-storage-pub/src/storage.rs +++ b/frontend/rust-lib/flowy-storage-pub/src/storage.rs @@ -21,7 +21,7 @@ pub trait StorageService: Send + Sync { upload_immediately: bool, ) -> Result<(CreatedUpload, Option), FlowyError>; - async fn start_upload(&self, chunks: &ChunkedBytes, record: &BoxAny) -> Result<(), FlowyError>; + async fn start_upload(&self, chunks: ChunkedBytes, record: &BoxAny) -> Result<(), FlowyError>; async fn resume_upload( &self, @@ -32,8 +32,9 @@ pub trait StorageService: Send + Sync { async fn subscribe_file_progress( &self, + parent_idr: &str, file_id: &str, - ) -> Result; + ) -> Result, FlowyError>; } pub struct FileProgressReceiver { diff --git a/frontend/rust-lib/flowy-storage/src/manager.rs b/frontend/rust-lib/flowy-storage/src/manager.rs index 69455666e8..d3772b43b0 100644 --- a/frontend/rust-lib/flowy-storage/src/manager.rs +++ b/frontend/rust-lib/flowy-storage/src/manager.rs @@ -2,8 +2,8 @@ use crate::file_cache::FileTempStorage; use crate::notification::{make_notification, StorageNotification}; use crate::sqlite_sql::{ batch_select_upload_file, delete_upload_file, insert_upload_file, insert_upload_part, - select_upload_file, select_upload_parts, update_upload_file_upload_id, UploadFilePartTable, - UploadFileTable, + is_upload_completed, select_upload_file, select_upload_parts, update_upload_file_completed, + update_upload_file_upload_id, UploadFilePartTable, UploadFileTable, }; use crate::uploader::{FileUploader, FileUploaderRunner, Signal, UploadTask, UploadTaskQueue}; use async_trait::async_trait; @@ -117,9 +117,13 @@ impl StorageManager { pub async fn subscribe_file_state( &self, + parent_dir: &str, file_id: &str, - ) -> Result { - self.storage_service.subscribe_file_progress(file_id).await + ) -> Result, FlowyError> { + self + .storage_service + .subscribe_file_progress(parent_dir, file_id) + .await } pub async fn get_file_state(&self, file_id: &str) -> Option { @@ -313,7 +317,7 @@ impl StorageService for StorageServiceImpl { } } - async fn start_upload(&self, chunks: &ChunkedBytes, record: &BoxAny) -> Result<(), FlowyError> { + async fn start_upload(&self, chunks: ChunkedBytes, record: &BoxAny) -> Result<(), FlowyError> { let file_record = record.downcast_ref::().ok_or_else(|| { FlowyError::internal().with_context("failed to downcast record to UploadFileTable") })?; @@ -340,34 +344,16 @@ impl StorageService for StorageServiceImpl { file_id: &str, ) -> Result<(), FlowyError> { // Gathering the upload record and parts from the sqlite database. - let record = { - let mut conn = self - .user_service - .sqlite_connection(self.user_service.user_id()?)?; - conn.immediate_transaction(|conn| { - Ok::<_, FlowyError>( - // When resuming an upload, check if the upload_id is empty. - // If the upload_id is empty, the upload has likely not been created yet. - // If the upload_id is not empty, verify which parts have already been uploaded. - select_upload_file(conn, workspace_id, parent_dir, file_id)?.map(|record| { - if record.upload_id.is_empty() { - (record, vec![]) - } else { - let parts = select_upload_parts(conn, &record.upload_id).unwrap_or_default(); - (record, parts) - } - }), - ) - })? - }; + let mut conn = self + .user_service + .sqlite_connection(self.user_service.user_id()?)?; - if let Some((upload_file, parts)) = record { + if let Some(upload_file) = select_upload_file(&mut conn, workspace_id, parent_dir, file_id)? { resume_upload( &self.cloud_service, &self.user_service, &self.temp_storage, upload_file, - parts, self.progress_notifiers.clone(), ) .await?; @@ -379,18 +365,32 @@ impl StorageService for StorageServiceImpl { async fn subscribe_file_progress( &self, + parent_idr: &str, file_id: &str, - ) -> Result { + ) -> Result, FlowyError> { trace!("[File]: subscribe file progress: {}", file_id); + + let is_completed = { + let mut conn = self + .user_service + .sqlite_connection(self.user_service.user_id()?)?; + let workspace_id = self.user_service.workspace_id()?; + is_upload_completed(&mut conn, &workspace_id, parent_idr, file_id).unwrap_or(false) + }; + if is_completed { + return Ok(None); + } + let (notifier, receiver) = ProgressNotifier::new(); let receiver = FileProgressReceiver { rx: receiver, file_id: file_id.to_string(), }; + self .progress_notifiers .insert(file_id.to_string(), notifier); - Ok(receiver) + Ok(Some(receiver)) } } @@ -420,6 +420,7 @@ async fn create_upload_record( chunk_size: chunked_bytes.chunk_size, num_chunk: chunked_bytes.offsets.len() as i32, created_at: timestamp(), + is_finish: false, }; Ok((chunked_bytes, record)) } @@ -429,10 +430,29 @@ async fn start_upload( cloud_service: &Arc, user_service: &Arc, temp_storage: &Arc, - chunked_bytes: &ChunkedBytes, + mut chunked_bytes: ChunkedBytes, upload_file: &UploadFileTable, progress_notifiers: Arc>, ) -> FlowyResult<()> { + // 4. gather existing completed parts + let mut conn = user_service.sqlite_connection(user_service.user_id()?)?; + let mut completed_parts = select_upload_parts(&mut conn, &upload_file.upload_id) + .unwrap_or_default() + .into_iter() + .map(|part| CompletedPartRequest { + e_tag: part.e_tag, + part_number: part.part_num, + }) + .collect::>(); + + let upload_offset = completed_parts.len() as i32; + chunked_bytes.set_current_offset(upload_offset); + + info!( + "[File] start upload: workspace: {}, parent_dir: {}, file_id: {}, chunk: {}", + upload_file.workspace_id, upload_file.parent_dir, upload_file.file_id, chunked_bytes, + ); + let mut upload_file = upload_file.clone(); if upload_file.upload_id.is_empty() { // 1. create upload @@ -488,25 +508,10 @@ async fn start_upload( let total_parts = chunked_bytes.iter().count(); let iter = chunked_bytes.iter().enumerate(); - let mut conn = user_service.sqlite_connection(user_service.user_id()?)?; - - // 4. gather existing completed parts - let mut completed_parts = select_upload_parts(&mut conn, &upload_file.upload_id) - .unwrap_or_default() - .into_iter() - .map(|part| CompletedPartRequest { - e_tag: part.e_tag, - part_number: part.part_num, - }) - .collect::>(); - - // when there are any existing parts, skip those parts by setting the current offset. - let offset = completed_parts.len(); - for (index, chunk_bytes) in iter { - let part_number = offset + index + 1; + let part_number = upload_offset + index as i32 + 1; trace!( - "[File] {} uploading part: {}, len:{}KB", + "[File] {} uploading {}th part, size:{}KB", upload_file.file_id, part_number, chunk_bytes.len() / 1000, @@ -584,7 +589,6 @@ async fn resume_upload( user_service: &Arc, temp_storage: &Arc, upload_file: UploadFileTable, - parts: Vec, progress_notifiers: Arc>, ) -> FlowyResult<()> { trace!( @@ -596,14 +600,13 @@ async fn resume_upload( ); match ChunkedBytes::from_file(&upload_file.local_file_path, MIN_CHUNK_SIZE as i32).await { - Ok(mut chunked_bytes) => { + Ok(chunked_bytes) => { // When there were any parts already uploaded, skip those parts by setting the current offset. - chunked_bytes.set_current_offset(parts.len() as i32); start_upload( cloud_service, user_service, temp_storage, - &chunked_bytes, + chunked_bytes, &upload_file, progress_notifiers, ) @@ -675,7 +678,7 @@ async fn complete_upload( progress_notifiers: &Arc>, ) -> Result<(), FlowyError> { trace!( - "[File]: completing file upload: {}, part: {}", + "[File]: completing file upload: {}, num parts: {}", upload_file.file_id, parts.len() ); @@ -692,7 +695,7 @@ async fn complete_upload( Ok(_) => { info!("[File] completed upload file: {}", upload_file.file_id); if let Some(mut notifier) = progress_notifiers.get_mut(&upload_file.file_id) { - trace!("[File]: notify upload finished"); + info!("[File]: notify upload:{} finished", upload_file.file_id); notifier .notify(FileUploadState::Finished { file_id: upload_file.file_id.clone(), @@ -700,9 +703,8 @@ async fn complete_upload( .await; } - trace!("[File] delete upload record from sqlite"); let conn = user_service.sqlite_connection(user_service.user_id()?)?; - delete_upload_file(conn, &upload_file.upload_id)?; + update_upload_file_completed(conn, &upload_file.upload_id)?; if let Err(err) = temp_storage .delete_temp_file(&upload_file.local_file_path) .await diff --git a/frontend/rust-lib/flowy-storage/src/sqlite_sql.rs b/frontend/rust-lib/flowy-storage/src/sqlite_sql.rs index c05800341f..52487e6de2 100644 --- a/frontend/rust-lib/flowy-storage/src/sqlite_sql.rs +++ b/frontend/rust-lib/flowy-storage/src/sqlite_sql.rs @@ -21,6 +21,7 @@ pub struct UploadFileTable { pub num_chunk: i32, pub upload_id: String, pub created_at: i64, + pub is_finish: bool, } #[derive(Queryable, Insertable, AsChangeset, Identifiable, Debug)] @@ -87,6 +88,55 @@ pub fn update_upload_file_upload_id( Ok(()) } +pub fn update_upload_file_completed(mut conn: DBConnection, upload_id: &str) -> FlowyResult<()> { + diesel::update( + upload_file_table::dsl::upload_file_table.filter(upload_file_table::upload_id.eq(upload_id)), + ) + .set(upload_file_table::is_finish.eq(true)) + .execute(&mut *conn)?; + Ok(()) +} + +pub fn is_upload_completed( + conn: &mut SqliteConnection, + workspace_id: &str, + parent_dir: &str, + file_id: &str, +) -> FlowyResult { + let result = upload_file_table::dsl::upload_file_table + .filter( + upload_file_table::workspace_id + .eq(workspace_id) + .and(upload_file_table::parent_dir.eq(parent_dir)) + .and(upload_file_table::file_id.eq(file_id)) + .and(upload_file_table::is_finish.eq(true)), + ) + .first::(conn) + .optional()?; + Ok(result.is_some()) +} + +pub fn delete_upload_file(mut conn: DBConnection, upload_id: &str) -> FlowyResult<()> { + conn.immediate_transaction(|conn| { + diesel::delete( + upload_file_table::dsl::upload_file_table.filter(upload_file_table::upload_id.eq(upload_id)), + ) + .execute(&mut *conn)?; + + if let Err(err) = diesel::delete( + upload_file_part::dsl::upload_file_part.filter(upload_file_part::upload_id.eq(upload_id)), + ) + .execute(&mut *conn) + { + warn!("Failed to delete upload parts: {:?}", err) + } + + Ok::<_, FlowyError>(()) + })?; + + Ok(()) +} + pub fn insert_upload_part( mut conn: DBConnection, upload_part: &UploadFilePartTable, @@ -147,24 +197,3 @@ pub fn select_upload_file( .optional()?; Ok(result) } - -pub fn delete_upload_file(mut conn: DBConnection, upload_id: &str) -> FlowyResult<()> { - conn.immediate_transaction(|conn| { - diesel::delete( - upload_file_table::dsl::upload_file_table.filter(upload_file_table::upload_id.eq(upload_id)), - ) - .execute(&mut *conn)?; - - if let Err(err) = diesel::delete( - upload_file_part::dsl::upload_file_part.filter(upload_file_part::upload_id.eq(upload_id)), - ) - .execute(&mut *conn) - { - warn!("Failed to delete upload parts: {:?}", err) - } - - Ok::<_, FlowyError>(()) - })?; - - Ok(()) -} diff --git a/frontend/rust-lib/flowy-storage/src/uploader.rs b/frontend/rust-lib/flowy-storage/src/uploader.rs index 7a92f24e03..2ebe3dcf69 100644 --- a/frontend/rust-lib/flowy-storage/src/uploader.rs +++ b/frontend/rust-lib/flowy-storage/src/uploader.rs @@ -164,7 +164,11 @@ impl FileUploader { mut retry_count, } => { let record = BoxAny::new(record); - if let Err(err) = self.storage_service.start_upload(&chunks, &record).await { + if let Err(err) = self + .storage_service + .start_upload(chunks.clone(), &record) + .await + { if err.is_file_limit_exceeded() { error!("Failed to upload file: {}", err); self.disable_storage_write(); diff --git a/frontend/rust-lib/flowy-storage/tests/multiple_part_upload_test.rs b/frontend/rust-lib/flowy-storage/tests/multiple_part_upload_test.rs index 64ab83b076..5b6b02e0aa 100644 --- a/frontend/rust-lib/flowy-storage/tests/multiple_part_upload_test.rs +++ b/frontend/rust-lib/flowy-storage/tests/multiple_part_upload_test.rs @@ -175,5 +175,6 @@ pub async fn create_upload_file_record( chunk_size: MIN_CHUNK_SIZE as i32, num_chunk: chunked_bytes.offsets.len() as i32, created_at: chrono::Utc::now().timestamp(), + is_finish: false, } } From 70e96c01b35ccba7e957498efa932abab984d89d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 20 Aug 2024 16:55:53 +0800 Subject: [PATCH 22/26] fix: ignore case sensitive of image name when dragging image to document (#6017) --- .../lib/plugins/document/document_page.dart | 10 ++++++---- .../copy_and_paste/paste_from_image.dart | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 362ae352d6..d835a7c00b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -197,13 +197,15 @@ class _DocumentPageState extends State final isLocalMode = context.read().isLocalMode; final List imageFiles = []; - final List otherfiles = []; + final List otherFiles = []; + for (final file in details.files) { + final fileName = file.name.toLowerCase(); if (file.mimeType?.startsWith('image/') ?? - false || imgExtensionRegex.hasMatch(file.name)) { + false || imgExtensionRegex.hasMatch(fileName)) { imageFiles.add(file); } else { - otherfiles.add(file); + otherFiles.add(file); } } @@ -215,7 +217,7 @@ class _DocumentPageState extends State ); await editorState!.dropFiles( data.dropTarget!, - otherfiles, + otherFiles, widget.view.id, isLocalMode, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart index 4ce4f6c405..57ebe69fc6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart @@ -35,7 +35,7 @@ extension PasteFromImage on EditorState { final imageFiles = files.where( (file) => file.mimeType?.startsWith('image/') ?? - false || imgExtensionRegex.hasMatch(file.name), + false || imgExtensionRegex.hasMatch(file.name.toLowerCase()), ); for (final file in imageFiles) { From 70d6351a6cc065b0c8748f5904162a4dd56e85e8 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Tue, 20 Aug 2024 17:07:54 +0800 Subject: [PATCH 23/26] chore: calm clippy wanring when using non send with Arc (#6018) --- frontend/rust-lib/dart-ffi/src/lib.rs | 18 +++++++++++------- .../rust-lib/event-integration-test/src/lib.rs | 1 + frontend/rust-lib/flowy-core/src/lib.rs | 1 + .../rust-lib/lib-dispatch/src/module/module.rs | 3 +++ .../rust-lib/lib-dispatch/tests/api/module.rs | 1 + 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/frontend/rust-lib/dart-ffi/src/lib.rs b/frontend/rust-lib/dart-ffi/src/lib.rs index b9aca0fc90..984b77697f 100644 --- a/frontend/rust-lib/dart-ffi/src/lib.rs +++ b/frontend/rust-lib/dart-ffi/src/lib.rs @@ -62,6 +62,7 @@ struct DartAppFlowyCore { impl DartAppFlowyCore { fn new() -> Self { Self { + #[allow(clippy::arc_with_non_send_sync)] core: Arc::new(RwLock::new(None)), handle: RwLock::new(None), sender: RwLock::new(None), @@ -94,7 +95,6 @@ impl DartAppFlowyCore { } } else { warn!("Failed to acquire read lock for sender"); - return; } } } @@ -207,7 +207,7 @@ impl Future for Runner { .await; if let Some(ret) = ret { - let _ = ret.send(resp); + let _ = ret.send(resp).await; } }); }, @@ -243,19 +243,23 @@ pub extern "C" fn set_log_stream_port(port: i64) -> i32 { pub extern "C" fn link_me_please() {} #[inline(always)] +#[allow(clippy::blocks_in_conditions)] async fn post_to_flutter(response: AFPluginEventResponse, port: i64) { let isolate = allo_isolate::Isolate::new(port); - if let Ok(_) = isolate + match isolate .catch_unwind(async { let ffi_resp = FFIResponse::from(response); ffi_resp.into_bytes().unwrap().to_vec() }) .await { - #[cfg(feature = "sync_verbose_log")] - trace!("[FFI]: Post data to dart success"); - } else { - error!("[FFI]: allo_isolate post panic"); + Ok(_) => { + #[cfg(feature = "sync_verbose_log")] + trace!("[FFI]: Post data to dart success"); + }, + Err(err) => { + error!("[FFI]: allo_isolate post failed: {:?}", err); + }, } } diff --git a/frontend/rust-lib/event-integration-test/src/lib.rs b/frontend/rust-lib/event-integration-test/src/lib.rs index 1aaf4a57db..758d035841 100644 --- a/frontend/rust-lib/event-integration-test/src/lib.rs +++ b/frontend/rust-lib/event-integration-test/src/lib.rs @@ -68,6 +68,7 @@ impl EventIntegrationTest { authenticator, notification_sender, cleaner: Arc::new(Cleaner::new(PathBuf::from(clean_path))), + #[allow(clippy::arc_with_non_send_sync)] local_set: Arc::new(Default::default()), } } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index a5d60ae703..cca75a6f54 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -261,6 +261,7 @@ impl AppFlowyCore { error!("Init user failed: {}", err) } } + #[allow(clippy::arc_with_non_send_sync)] let event_dispatcher = Arc::new(AFPluginDispatcher::new( runtime, make_plugins( diff --git a/frontend/rust-lib/lib-dispatch/src/module/module.rs b/frontend/rust-lib/lib-dispatch/src/module/module.rs index 12cbb0c150..54f8babad9 100644 --- a/frontend/rust-lib/lib-dispatch/src/module/module.rs +++ b/frontend/rust-lib/lib-dispatch/src/module/module.rs @@ -29,6 +29,7 @@ pub(crate) fn plugin_map_or_crash(plugins: Vec) -> AFPluginMap { let mut plugin_map: HashMap> = HashMap::new(); plugins.into_iter().for_each(|m| { let events = m.events(); + #[allow(clippy::arc_with_non_send_sync)] let plugins = Arc::new(m); events.into_iter().for_each(|e| { if plugin_map.contains_key(&e) { @@ -38,6 +39,7 @@ pub(crate) fn plugin_map_or_crash(plugins: Vec) -> AFPluginMap { plugin_map.insert(e, plugins.clone()); }); }); + #[allow(clippy::arc_with_non_send_sync)] Arc::new(plugin_map) } @@ -75,6 +77,7 @@ impl std::default::Default for AFPlugin { Self { name: "".to_owned(), states: Default::default(), + #[allow(clippy::arc_with_non_send_sync)] event_service_factory: Arc::new(HashMap::new()), } } diff --git a/frontend/rust-lib/lib-dispatch/tests/api/module.rs b/frontend/rust-lib/lib-dispatch/tests/api/module.rs index 27ee94a9ee..214eda32fa 100644 --- a/frontend/rust-lib/lib-dispatch/tests/api/module.rs +++ b/frontend/rust-lib/lib-dispatch/tests/api/module.rs @@ -11,6 +11,7 @@ pub async fn hello() -> String { async fn test() { let event = "1"; let runtime = Arc::new(AFPluginRuntime::new().unwrap()); + #[allow(clippy::arc_with_non_send_sync)] let dispatch = Arc::new(AFPluginDispatcher::new( runtime, vec![AFPlugin::new().event(event, hello)], From 93a110d37d2de50342829a344895978e4378c8ba Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:38:29 +0800 Subject: [PATCH 24/26] chore: explicit using any int64 (#6020) * chore: explicit using any int64 * chore: update commit id of appflowy collab --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 14 +++++++------- frontend/appflowy_tauri/src-tauri/Cargo.toml | 14 +++++++------- frontend/appflowy_web_app/src-tauri/Cargo.lock | 14 +++++++------- frontend/appflowy_web_app/src-tauri/Cargo.toml | 14 +++++++------- frontend/rust-lib/Cargo.lock | 14 +++++++------- frontend/rust-lib/Cargo.toml | 14 +++++++------- frontend/rust-lib/flowy-core/Cargo.toml | 2 +- .../src/services/calculations/entities.rs | 2 +- .../date_type_option/date_type_option.rs | 5 +++-- .../number_type_option/number_type_option.rs | 4 ++-- .../timestamp_type_option.rs | 15 +++++++++++---- .../translate_type_option/translate.rs | 2 +- .../src/services/field_settings/entities.rs | 7 ++++--- .../field_settings/field_settings_builder.rs | 6 +++--- .../src/services/filter/entities.rs | 6 +++--- .../src/services/group/entities.rs | 2 +- .../src/services/setting/entities.rs | 11 +++++++---- .../flowy-database2/src/services/sort/entities.rs | 3 ++- 18 files changed, 81 insertions(+), 68 deletions(-) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index c7247f61da..695b36fe1e 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -963,7 +963,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "arc-swap", @@ -988,7 +988,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "async-trait", @@ -1018,7 +1018,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "arc-swap", @@ -1038,7 +1038,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "bytes", @@ -1057,7 +1057,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "arc-swap", @@ -1100,7 +1100,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "async-stream", @@ -1180,7 +1180,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "collab", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index eaf9c45240..2aac8f12c9 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -116,13 +116,13 @@ custom-protocol = ["tauri/custom-protocol"] # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index 67a03fc4b2..3c6481f07e 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -946,7 +946,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "arc-swap", @@ -971,7 +971,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "async-trait", @@ -1001,7 +1001,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "arc-swap", @@ -1021,7 +1021,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "bytes", @@ -1040,7 +1040,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "arc-swap", @@ -1083,7 +1083,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "async-stream", @@ -1163,7 +1163,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "collab", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index 9939037c67..05e3b89617 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -116,13 +116,13 @@ custom-protocol = ["tauri/custom-protocol"] # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index ccf8f27506..3831b53f05 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -824,7 +824,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "arc-swap", @@ -849,7 +849,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "async-trait", @@ -879,7 +879,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "arc-swap", @@ -899,7 +899,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "bytes", @@ -918,7 +918,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "arc-swap", @@ -961,7 +961,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "async-stream", @@ -1041,7 +1041,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f29a7f182da0f5a84bdc6cd0c7862934faab4afe#f29a7f182da0f5a84bdc6cd0c7862934faab4afe" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" dependencies = [ "anyhow", "collab", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 86c73438be..f6c2073a83 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -136,13 +136,13 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f29a7f182da0f5a84bdc6cd0c7862934faab4afe" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index d6cd0c6635..8ec17fa5f6 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -26,7 +26,7 @@ collab-integrate = { workspace = true } flowy-search = { workspace = true } flowy-search-pub = { workspace = true } collab-entity = { workspace = true } -collab-plugins = { workspace = true } +collab-plugins = { workspace = true, features = ["verbose_log"] } collab = { workspace = true } diesel.workspace = true uuid.workspace = true diff --git a/frontend/rust-lib/flowy-database2/src/services/calculations/entities.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/entities.rs index 2a2613230d..42f2b60ca1 100644 --- a/frontend/rust-lib/flowy-database2/src/services/calculations/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/entities.rs @@ -25,7 +25,7 @@ impl From for CalculationMap { CalculationMapBuilder::from([ (CALCULATION_ID.into(), data.id.into()), (FIELD_ID.into(), data.field_id.into()), - (CALCULATION_TYPE.into(), data.calculation_type.into()), + (CALCULATION_TYPE.into(), Any::BigInt(data.calculation_type)), (CALCULATION_VALUE.into(), data.value.into()), ]) } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs index 2a7a713b61..16a626fbcb 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, NaiveTime, Offset, TimeZone}; use chrono_tz::Tz; +use collab::preclude::Any; use collab::util::AnyMapExt; use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; @@ -55,8 +56,8 @@ impl From for DateTypeOption { impl From for TypeOptionData { fn from(data: DateTypeOption) -> Self { TypeOptionDataBuilder::from([ - ("date_format".into(), data.date_format.value().into()), - ("time_format".into(), data.time_format.value().into()), + ("date_format".into(), Any::BigInt(data.date_format.value())), + ("time_format".into(), Any::BigInt(data.time_format.value())), ("timezone_id".into(), data.timezone_id.into()), ]) } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs index eee660af86..c13b007fa6 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs @@ -96,8 +96,8 @@ impl From for NumberTypeOption { impl From for TypeOptionData { fn from(data: NumberTypeOption) -> Self { TypeOptionDataBuilder::from([ - ("format".into(), data.format.value().into()), - ("scale".into(), data.scale.into()), + ("format".into(), Any::BigInt(data.format.value())), + ("scale".into(), Any::BigInt(data.scale as i64)), ("name".into(), data.name.into()), ("symbol".into(), data.symbol.into()), ]) diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs index 116bd1a0a4..0e1416ab89 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs @@ -1,6 +1,7 @@ use std::cmp::Ordering; use chrono::{DateTime, Local, Offset}; +use collab::preclude::Any; use collab::util::AnyMapExt; use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; @@ -68,10 +69,16 @@ impl From for TimestampTypeOption { impl From for TypeOptionData { fn from(option: TimestampTypeOption) -> Self { TypeOptionDataBuilder::from([ - ("date_format".into(), option.date_format.value().into()), - ("time_format".into(), option.time_format.value().into()), - ("include_time".into(), option.include_time.into()), - ("field_type".into(), option.field_type.value().into()), + ( + "date_format".into(), + Any::BigInt(option.date_format.value()), + ), + ( + "time_format".into(), + Any::BigInt(option.time_format.value()), + ), + ("include_time".into(), Any::Bool(option.include_time)), + ("field_type".into(), Any::BigInt(option.field_type.value())), ]) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate.rs index ff84213a15..fedfd9b4f8 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate.rs @@ -60,7 +60,7 @@ impl From for TypeOptionData { fn from(value: TranslateTypeOption) -> Self { TypeOptionDataBuilder::from([ ("auto_fill".into(), value.auto_fill.into()), - ("language".into(), value.language_type.into()), + ("language".into(), Any::BigInt(value.language_type)), ]) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs b/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs index 1fb7cde207..65d58441bc 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs @@ -1,3 +1,4 @@ +use collab::preclude::Any; use collab::util::AnyMapExt; use collab_database::views::{DatabaseLayout, FieldSettingsMap, FieldSettingsMapBuilder}; @@ -45,12 +46,12 @@ impl From for FieldSettingsMap { FieldSettingsMapBuilder::from([ ( VISIBILITY.into(), - i64::from(field_settings.visibility).into(), + Any::BigInt(i64::from(field_settings.visibility)), ), - (WIDTH.into(), field_settings.width.into()), + (WIDTH.into(), Any::BigInt(field_settings.width as i64)), ( WRAP_CELL_CONTENT.into(), - field_settings.wrap_cell_content.into(), + Any::Bool(field_settings.wrap_cell_content), ), ]) } diff --git a/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs b/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs index 7f9ed6b2c0..95d70184c2 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs @@ -1,9 +1,9 @@ -use std::collections::HashMap; - +use collab::preclude::Any; use collab_database::fields::Field; use collab_database::views::{ DatabaseLayout, FieldSettingsByFieldIdMap, FieldSettingsMap, FieldSettingsMapBuilder, }; +use std::collections::HashMap; use strum::IntoEnumIterator; use crate::entities::FieldVisibility; @@ -87,7 +87,7 @@ pub fn default_field_settings_by_layout_map() -> HashMap From<&'a Filter> for FilterMap { fn from(filter: &'a Filter) -> Self { let mut builder = FilterMapBuilder::from([ (FILTER_ID.into(), filter.id.as_str().into()), - (FILTER_TYPE.into(), filter.inner.get_int_repr().into()), + (FILTER_TYPE.into(), Any::BigInt(filter.inner.get_int_repr())), ]); builder = match &filter.inner { @@ -397,8 +397,8 @@ impl<'a> From<&'a Filter> for FilterMap { }); builder.insert(FIELD_ID.into(), field_id.as_str().into()); - builder.insert(FIELD_TYPE.into(), i64::from(field_type).into()); - builder.insert(FILTER_CONDITION.into(), (condition as i64).into()); + builder.insert(FIELD_TYPE.into(), Any::BigInt(i64::from(field_type))); + builder.insert(FILTER_CONDITION.into(), Any::BigInt(condition as i64)); builder.insert(FILTER_CONTENT.into(), content.into()); builder }, diff --git a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs index cfb5de588e..d14ec235f9 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs @@ -58,7 +58,7 @@ impl From for GroupSettingMap { GroupSettingBuilder::from([ (GROUP_ID.into(), setting.id.into()), (FIELD_ID.into(), setting.field_id.into()), - (FIELD_TYPE.into(), setting.field_type.into()), + (FIELD_TYPE.into(), Any::BigInt(setting.field_type)), (GROUPS.into(), groups), (CONTENT.into(), setting.content.into()), ]) diff --git a/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs b/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs index 5a71b58127..8535f46024 100644 --- a/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs @@ -27,13 +27,16 @@ impl From for CalendarLayoutSetting { impl From for LayoutSetting { fn from(setting: CalendarLayoutSetting) -> Self { LayoutSettingBuilder::from([ - ("layout_ty".into(), setting.layout_ty.value().into()), + ("layout_ty".into(), Any::BigInt(setting.layout_ty.value())), ( "first_day_of_week".into(), - (setting.first_day_of_week as i64).into(), + Any::BigInt(setting.first_day_of_week as i64), ), - ("show_week_numbers".into(), setting.show_week_numbers.into()), - ("show_weekends".into(), setting.show_weekends.into()), + ( + "show_week_numbers".into(), + Any::Bool(setting.show_week_numbers), + ), + ("show_weekends".into(), Any::Bool(setting.show_weekends)), ("field_id".into(), setting.field_id.into()), ]) } diff --git a/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs b/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs index 9b5608761a..cc4908cda9 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs @@ -1,6 +1,7 @@ use std::cmp::Ordering; use anyhow::bail; +use collab::preclude::Any; use collab::util::AnyMapExt; use collab_database::rows::{RowDetail, RowId}; use collab_database::views::{SortMap, SortMapBuilder}; @@ -47,7 +48,7 @@ impl From for SortMap { SortMapBuilder::from([ (SORT_ID.into(), data.id.into()), (FIELD_ID.into(), data.field_id.into()), - (SORT_CONDITION.into(), data.condition.value().into()), + (SORT_CONDITION.into(), Any::BigInt(data.condition.value())), ]) } } From b9a34f6fc20d19cb848a9a265b83b7732fa25ade Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 20 Aug 2024 21:45:14 +0800 Subject: [PATCH 25/26] chore: turn off verbose log --- frontend/rust-lib/flowy-core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index 8ec17fa5f6..d6cd0c6635 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -26,7 +26,7 @@ collab-integrate = { workspace = true } flowy-search = { workspace = true } flowy-search-pub = { workspace = true } collab-entity = { workspace = true } -collab-plugins = { workspace = true, features = ["verbose_log"] } +collab-plugins = { workspace = true } collab = { workspace = true } diesel.workspace = true uuid.workspace = true From 0ce43ca5fa5b5f53f81b94680a2d5815b87828f0 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Wed, 21 Aug 2024 14:04:53 +0800 Subject: [PATCH 26/26] chore: write collab to disk if it's not exist (#6023) * chore: write collab to disk if it's not exist * chore: write collab if it's not exit * chore: fix test --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 15 ++-- frontend/appflowy_tauri/src-tauri/Cargo.toml | 14 ++-- .../appflowy_web_app/src-tauri/Cargo.lock | 15 ++-- .../appflowy_web_app/src-tauri/Cargo.toml | 14 ++-- frontend/rust-lib/Cargo.lock | 15 ++-- frontend/rust-lib/Cargo.toml | 14 ++-- .../collab-integrate/src/collab_builder.rs | 82 ++++++++++++++++++- .../tests/user/migration_test/version_test.rs | 3 + .../rust-lib/flowy-database2/src/manager.rs | 14 ++-- .../rust-lib/flowy-document/src/manager.rs | 2 +- frontend/rust-lib/flowy-server/Cargo.toml | 1 + .../src/local_server/impls/database.rs | 34 +------- .../src/migrations/document_empty_content.rs | 7 +- .../migrations/workspace_and_favorite_v1.rs | 6 +- .../src/migrations/workspace_trash_v1.rs | 6 +- 15 files changed, 151 insertions(+), 91 deletions(-) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 695b36fe1e..99f7541e17 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -963,7 +963,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "arc-swap", @@ -988,7 +988,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "async-trait", @@ -1018,7 +1018,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "arc-swap", @@ -1038,7 +1038,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "bytes", @@ -1057,7 +1057,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "arc-swap", @@ -1100,7 +1100,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "async-stream", @@ -1180,7 +1180,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "collab", @@ -2429,6 +2429,7 @@ dependencies = [ "chrono", "client-api", "collab", + "collab-database", "collab-document", "collab-entity", "collab-folder", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 2aac8f12c9..23e30f4bfd 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -116,13 +116,13 @@ custom-protocol = ["tauri/custom-protocol"] # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index 3c6481f07e..56ffd76575 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -946,7 +946,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "arc-swap", @@ -971,7 +971,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "async-trait", @@ -1001,7 +1001,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "arc-swap", @@ -1021,7 +1021,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "bytes", @@ -1040,7 +1040,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "arc-swap", @@ -1083,7 +1083,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "async-stream", @@ -1163,7 +1163,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "collab", @@ -2459,6 +2459,7 @@ dependencies = [ "chrono", "client-api", "collab", + "collab-database", "collab-document", "collab-entity", "collab-folder", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index 05e3b89617..10142f4acc 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -116,13 +116,13 @@ custom-protocol = ["tauri/custom-protocol"] # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 3831b53f05..01f2ca464a 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -824,7 +824,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "arc-swap", @@ -849,7 +849,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "async-trait", @@ -879,7 +879,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "arc-swap", @@ -899,7 +899,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "bytes", @@ -918,7 +918,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "arc-swap", @@ -961,7 +961,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "async-stream", @@ -1041,7 +1041,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2cc23e05010a7ed72e5c63c59dfa3db7d789620c#2cc23e05010a7ed72e5c63c59dfa3db7d789620c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6a5e7e49c159fcf782df84208cdb26c212c28ede#6a5e7e49c159fcf782df84208cdb26c212c28ede" dependencies = [ "anyhow", "collab", @@ -2260,6 +2260,7 @@ dependencies = [ "chrono", "client-api", "collab", + "collab-database", "collab-document", "collab-entity", "collab-folder", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index f6c2073a83..cb09e65d6b 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -136,13 +136,13 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2cc23e05010a7ed72e5c63c59dfa3db7d789620c" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6a5e7e49c159fcf782df84208cdb26c212c28ede" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/collab-integrate/src/collab_builder.rs b/frontend/rust-lib/collab-integrate/src/collab_builder.rs index f539cca182..9800e1a130 100644 --- a/frontend/rust-lib/collab-integrate/src/collab_builder.rs +++ b/frontend/rust-lib/collab-integrate/src/collab_builder.rs @@ -15,6 +15,7 @@ use collab_entity::{CollabObject, CollabType}; use collab_folder::{Folder, FolderData, FolderNotify}; use collab_plugins::connect_state::{CollabConnectReachability, CollabConnectState}; use collab_plugins::local_storage::kv::snapshot::SnapshotPersistence; + if_native! { use collab_plugins::local_storage::rocksdb::rocksdb_plugin::{RocksdbBackup, RocksdbDiskPlugin}; } @@ -155,9 +156,19 @@ impl AppFlowyCollabBuilder { builder_config: CollabBuilderConfig, data: Option, ) -> Result>, Error> { - assert_eq!(object.collab_type, CollabType::Document); + let expected_collab_type = CollabType::Document; + assert_eq!(object.collab_type, expected_collab_type); let collab = self.build_collab(&object, &collab_db, data_source)?; let document = Document::open_with(collab, data)?; + + self.flush_collab_if_not_exist( + object.uid, + &object.object_id, + collab_db.clone(), + &expected_collab_type, + &document, + )?; + let document = Arc::new(RwLock::new(document)); self.finalize(object, builder_config, document) } @@ -176,9 +187,19 @@ impl AppFlowyCollabBuilder { folder_notifier: Option, folder_data: Option, ) -> Result>, Error> { - assert_eq!(object.collab_type, CollabType::Folder); + let expected_collab_type = CollabType::Folder; + assert_eq!(object.collab_type, expected_collab_type); let collab = self.build_collab(&object, &collab_db, doc_state)?; let folder = Folder::open_with(object.uid, collab, folder_notifier, folder_data); + + self.flush_collab_if_not_exist( + object.uid, + &object.object_id, + collab_db.clone(), + &expected_collab_type, + &folder, + )?; + let folder = Arc::new(RwLock::new(folder)); self.finalize(object, builder_config, folder) } @@ -196,9 +217,19 @@ impl AppFlowyCollabBuilder { builder_config: CollabBuilderConfig, notifier: Option, ) -> Result>, Error> { - assert_eq!(object.collab_type, CollabType::UserAwareness); + let expected_collab_type = CollabType::UserAwareness; + assert_eq!(object.collab_type, expected_collab_type); let collab = self.build_collab(&object, &collab_db, doc_state)?; let user_awareness = UserAwareness::open(collab, notifier); + + self.flush_collab_if_not_exist( + object.uid, + &object.object_id, + collab_db.clone(), + &expected_collab_type, + &user_awareness, + )?; + let user_awareness = Arc::new(RwLock::new(user_awareness)); self.finalize(object, builder_config, user_awareness) } @@ -216,9 +247,19 @@ impl AppFlowyCollabBuilder { builder_config: CollabBuilderConfig, collab_service: impl DatabaseCollabService, ) -> Result>, Error> { - assert_eq!(object.collab_type, CollabType::WorkspaceDatabase); + let expected_collab_type = CollabType::WorkspaceDatabase; + assert_eq!(object.collab_type, expected_collab_type); let collab = self.build_collab(&object, &collab_db, doc_state)?; let workspace = WorkspaceDatabase::open(object.uid, collab, collab_db.clone(), collab_service); + + self.flush_collab_if_not_exist( + object.uid, + &object.object_id, + collab_db.clone(), + &expected_collab_type, + &workspace, + )?; + let workspace = Arc::new(RwLock::new(workspace)); self.finalize(object, builder_config, workspace) } @@ -290,6 +331,39 @@ impl AppFlowyCollabBuilder { drop(write_collab); Ok(collab) } + + /// Remove all updates in disk and write the final state vector to disk. + pub fn flush_collab_if_not_exist( + &self, + uid: i64, + object_id: &str, + collab_db: Weak, + collab_type: &CollabType, + collab: &T, + ) -> Result<(), Error> + where + T: BorrowMut + Send + Sync + 'static, + { + if let Some(collab_db) = collab_db.upgrade() { + let write_txn = collab_db.write_txn(); + let is_not_exist_on_disk = !write_txn.is_exist(uid, object_id); + if is_not_exist_on_disk { + trace!("flush collab:{}-{} to disk", collab_type, object_id); + let collab: &Collab = collab.borrow(); + let encode_collab = + collab.encode_collab_v1(|collab| collab_type.validate_require_data(collab))?; + + write_txn.flush_doc( + uid, + object_id, + encode_collab.state_vector.to_vec(), + encode_collab.doc_state.to_vec(), + )?; + } + } + + Ok(()) + } } pub struct CollabBuilderConfig { diff --git a/frontend/rust-lib/event-integration-test/tests/user/migration_test/version_test.rs b/frontend/rust-lib/event-integration-test/tests/user/migration_test/version_test.rs index 3e925ba0ec..5edbb64caf 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/migration_test/version_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/migration_test/version_test.rs @@ -135,6 +135,9 @@ async fn collab_db_backup_test() { EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; let uid = test.get_user_profile().await.unwrap().id; + // sleep a bit to make sure the backup is generated + + tokio::time::sleep(Duration::from_secs(10)).await; let backups = test.user_manager.get_collab_backup_list(uid); assert_eq!(backups.len(), 1); diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 5a83316ea6..f4f95b8d39 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -109,10 +109,11 @@ impl DatabaseManager { let workspace_id = self.user.workspace_id()?; let workspace_database_object_id = self.user.workspace_database_object_id()?; - let mut workspace_database_doc_state = + let mut workspace_database_data_source = KVDBCollabPersistenceImpl::new(collab_db.clone(), uid).into_data_source(); - // If the workspace database not exist in disk, try to fetch from remote. - if !self.is_collab_exist(uid, &collab_db, &workspace_database_object_id) { + let is_exist_in_disk = self.is_collab_exist(uid, &collab_db, &workspace_database_object_id); + // 4.If the workspace database not exist in disk, try to fetch from remote. + if !is_exist_in_disk { trace!("workspace database not exist, try to fetch from remote"); match self .cloud_service @@ -125,7 +126,7 @@ impl DatabaseManager { { Ok(value) => { if let Some(encode_collab) = value { - workspace_database_doc_state = DataSource::from(encode_collab); + workspace_database_data_source = DataSource::from(encode_collab); } }, Err(err) => { @@ -140,7 +141,7 @@ impl DatabaseManager { // Construct the workspace database. event!( tracing::Level::INFO, - "open aggregate database views object: {}", + "create workspace database object: {}", &workspace_database_object_id ); @@ -154,9 +155,10 @@ impl DatabaseManager { &workspace_database_object_id, CollabType::WorkspaceDatabase, )?; + let workspace_database = self.collab_builder.create_workspace_database( collab_object, - workspace_database_doc_state, + workspace_database_data_source, collab_db, CollabBuilderConfig::default().sync_enable(true), collab_builder, diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index bf503b0cfc..8eadf2b55f 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -310,8 +310,8 @@ impl DocumentManager { // clear the awareness state when close the document let mut lock = document.write().await; lock.clean_awareness_local_state(); - lock.flush(); } + let clone_doc_id = doc_id.clone(); trace!("move document to removing_documents: {}", doc_id); self.removing_documents.insert(doc_id, document); diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index e746aca35a..0ae7da2093 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -31,6 +31,7 @@ collab-plugins = { workspace = true } collab-document = { workspace = true } collab-entity = { workspace = true } collab-folder = { workspace = true } +collab-database = { workspace = true } hex = "0.4.3" postgrest = "1.0" lib-infra = { workspace = true } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs index 6d2ad4deab..fc42709b93 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs @@ -1,13 +1,7 @@ use anyhow::Error; -use collab::core::transaction::DocTransactionExtension; use collab::entity::EncodedCollab; -use collab::preclude::Collab; -use collab_entity::define::{DATABASE, DATABASE_ROW_DATA, WORKSPACE_DATABASES}; use collab_entity::CollabType; -use yrs::{ArrayPrelim, Map, MapPrelim}; - use flowy_database_pub::cloud::{DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid}; - use lib_infra::async_trait::async_trait; pub(crate) struct LocalServerDatabaseCloudServiceImpl(); @@ -16,33 +10,11 @@ pub(crate) struct LocalServerDatabaseCloudServiceImpl(); impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { async fn get_database_encode_collab( &self, - object_id: &str, - collab_type: CollabType, + _object_id: &str, + _collab_type: CollabType, _workspace_id: &str, ) -> Result, Error> { - let object_id = object_id.to_string(); - // create the minimal required data for the given collab type - - let mut collab = Collab::new(1, object_id, collab_type.clone(), vec![], false); - let mut txn = collab.context.transact_mut(); - match collab_type { - CollabType::Database => { - collab.data.insert(&mut txn, DATABASE, MapPrelim::default()); - }, - CollabType::WorkspaceDatabase => { - collab - .data - .insert(&mut txn, WORKSPACE_DATABASES, ArrayPrelim::default()); - }, - CollabType::DatabaseRow => { - collab - .data - .insert(&mut txn, DATABASE_ROW_DATA, MapPrelim::default()); - }, - _ => { /* do nothing */ }, - }; - - Ok(Some(txn.get_encoded_collab_v1())) + Ok(None) } async fn batch_get_database_encode_collab( diff --git a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs index 8b0b0694b5..7d64647fd4 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs @@ -89,7 +89,12 @@ where let collab = Collab::new_with_origin(origin.clone(), &view.id, vec![], false); let document = Document::open_with(collab, Some(default_document_data(&view.id)))?; let encode = document.encode_collab_v1(|_| Ok::<(), PersistenceError>(()))?; - write_txn.flush_doc_with(user_id, &view.id, &encode.doc_state, &encode.state_vector)?; + write_txn.flush_doc( + user_id, + &view.id, + encode.state_vector.to_vec(), + encode.doc_state.to_vec(), + )?; event!( tracing::Level::INFO, "Did migrate empty document {}", diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs index e15bc5109c..2aaf3b8063 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs @@ -55,11 +55,11 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { let encode = folder .encode_collab() .map_err(|err| PersistenceError::Internal(err.into()))?; - write_txn.flush_doc_with( + write_txn.flush_doc( session.user_id, &session.user_workspace.id, - &encode.doc_state, - &encode.state_vector, + encode.state_vector.to_vec(), + encode.doc_state.to_vec(), )?; } Ok(()) diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs index 168c7e2510..47cecc35a0 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs @@ -49,11 +49,11 @@ impl UserDataMigration for WorkspaceTrashMapToSectionMigration { let encode = folder .encode_collab() .map_err(|err| PersistenceError::Internal(err.into()))?; - write_txn.flush_doc_with( + write_txn.flush_doc( session.user_id, &session.user_workspace.id, - &encode.doc_state, - &encode.state_vector, + encode.state_vector.to_vec(), + encode.doc_state.to_vec(), )?; } Ok(())

~%bdQ4)bDY-;#t%myHIDQ5Fp)ZZ;l4*c)|^! zVP~%c_hjI#gzQ`A`D@b?UFiK*St|X!5nx9nl zr!N2FZJKMil{?4Vo|OYBBd?H+n)^Qbtr)=sscW0J1Wg#sDrGjPw*{yCQ#Q6tN|A*B6xB|Jl!gii2p04bsA8zxxoHII+K!NHNuAu{V3p_W=tu?(-ZjxQevD;a7(lR6D z6S|rY7&wfdIA-Tf=$fld@Vu78Wc3nR;{&HC-KLQ6_x<=RLoS5v2@e3QE^wV*e36$^ z=gihT1*=w(Q)g4s$G2O^0Q@~dU>P0 qJW%kHnfL#F{eREl|9S|{tvzZR?K8>qm%!r!+|kfiuT`}P{r> + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..ba42ab6878 --- /dev/null +++ b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..036d09bc5f --- /dev/null +++ b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index b00c03fd17463e53fc05715126573785b8ddd976..911ee844c765f892df52a1385d77183c281534ad 100644 GIT binary patch delta 2312 zcmV+j3HSDuFOm|FBYz0lNklhQc9E*>L#u6S3uS7l*gCO7+p!KBt}S$2I-0f%*|ct1N;l#JLfn93Na6P7$YMiS(>y?q<2H5EaHe!u?>mMj+q zv6?8Dp#7}YR0I`yz1}y&$ztCi2QUS-w6q*xwWca)4@;Kgf)27;QxfFyc=obnxh$x; zx%q8YYhDmfUeFli1&u*=3sN;5y+H+60xEo>%&vhu=YJN-JVC;chVw^cI9`$P!7n9L zt(I`|EeXDEHR`;|ZW;GG6wLfc$7hcinDZYEUq3EkeX9iLbs3s|TV!R9Ai<}>eMrWi zOI7SzqGP*#N65-Owfm;vnN*2D8&s1esoqrub7gg2^3_E;DSTM z-UpMDV+y*@z`3f>GOPE|GsDTiiJ9WN!8$sZf|-4z-7_;$L95u{UbRWA%-`~1W`CzkhqK)X}K zENJv9xbxo{3XbwZteAUJhifP_1CwN81a-KA*!6(K2}s25r3P*Pq5czw-S@mm+j=2w z^M8r@P$WM-7oB4aCPq-z`T=aSM{nhcV7(*j(2{4G$hyl3HernKX+o29use z=@%sU73_LwfRhjpc2DGZNrDuJ-(YL5DVXKZI2l$HQN!NfoAfM7zo1JW2e9@2K~6+0 zWp)L7z8S#jieV@MKXW2s4g2NuGA7jOD1f?O2?*u0>hB=8No%&uaK zeHedOEauAyqUcp9tG@F-o;D7My ztLQr~ru+0Vh{G{p?{ow$dod2 zwBRO2O%bH71KgN6q+gKS+6(z}&vSBaVtX3mx3{M#$$%RC6ms*=pgU$@#6ePOiDRR% z*EIzgZu_;xSWD6`NPOD~rRW)6Oq8UjAUyF7^x@=p997%jJqg`US~dUdXc& z^q>fW#I^6e3C%BZc^Hb%0qxA)yo3Z8M<>JQ*vRFIL;3|V1lp-$Wq(*96Me<2F}U5q z)ZC3g=`Kjc)?2?47DV(cdV z-RoXnMl6kmQ2#L_N>0`ZVyNCA{I4E?pEYBeDNIUOkWdg>Vt-6f_pEhLeZ%G$`RZ=y zr%QM#j8isxIQ!2`X{qsoSqKDH3OPsHsp$V5ck+2ys?01 z|8mIPr}+1`CVztjMW87vgfm?jdbbgS+Z^!itVQ5x8+3{Pe-t&_PyfN`kyzZ_dsjRudC+ z_UzfzXHb8Ce_5`7psum8aWAViB|%Q7^YL&pBgOxzXMZBB7WR?MU@(aC^73D>TAP}h zieo**4TL`6I*GWeqod?1EvzQD+uckx-ot7gtvUIqsHk9qN>;5}wW6h^ zrFm#*2&$@@xBuA;TULI*A1;@xrL?s4Tdc+n8#W}rydq=e%9T^-a5u5EtgLK(b#--l zU0vN@Ykz8L4%gP!Rz~7*IHF$1ex2+&|Bsxb$HVN}Sq-cfRuijj`SRscqpKcY*p@9@ zb_X3lPke>Ak64uDuo_q`cP?47WEvgEx3sK+Bo)(aDxu?SN;)^2VKt1_l6+GhM=;yD iKtA%3k9=fB{1^DM+GSwbzh?jd002ovPDHLkU;%;}&RDzv literal 6037 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 P00000NkvXXu0mjf+AM@C diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..1b466c0eb266a02ddbcf521ed0d4ecefd18f75f9 GIT binary patch literal 3710 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 DKE7Ig literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..56ea852799b4d93276604615bb73ef3b4e0ef438 GIT binary patch literal 4548 zcmV;#5j*aQP)Px`en~_@RCr#^TnVro#d+@TId*1eX77FP?UQf{5`0JjJE6b_Ca8!*iZfL zqW}SH;)GDCBnFH`NXk(hOD+^#1$MA0;xhQCvT;HXc0?=)V5307QN&J}67TIh_TJfZ zcDqx5@APWkvio*+->kw^NvqP%bpQSJ*Z)0>(QGb^5c2+kSr|fyx}Sb$CGI%g5f8u| z;vO+J%oErJ69jb$VPU*c3xe-pL2$u>HoM?HaS!}Et!Y|VRaMDxoMxlZs3C-kLk5th z*ocvG1Od>nT{aBEIE68`oiSzrV2rVKS(ab3ZF|j?2A+f69=78k7c&gwbTh2&mSpu@ zgxGeBv4t@vl+q?fs7MjYw%g8sW6YjzHti#}-G0ThQUFS@c4`uA1t7r~!IpH)vMvmT zwTmPooka*)LR*fGsZN=MxPptdrQC= zx4K>WT&vyucD>&GcC*=h-5q~RKurq}Ov-@?g+gzSTGk)Rs&W;{?iB{I4Ybm;#1f4ZOKIZHA-n1VdNl&;MrladV%HR6M_*#xZG~FAE}qi zU#dCI3m%|0JNiReHYq?t@lwMu<_%rHRn?Sh8ATBn6d}N%f) znpiA$wP_f88KWn8fDyXL-`v0qgp`isd~0nzf4Rr%b)$yyAuASroKh-SUl(RF=$G#dS)B$1CWzPYFayETITeiqe{2$3sx<%)9I ze%RGnPAC-msLzI&WtQSxGD;TeR`uem1M?=c%C2L}pcEHi|E zUaQr9w_Gm&#NFIY_0l+iILcAeyxfXL9-x%!?vC^o?zG6b&xs?nvYcJni4ZCX7A2F( zyR?w{my9uBDaaAT#m6Fql&f~_vU0f$mUL4lZ3Yl$PBdzM#4;n_q?E!g0@^`=0}B=a zH44Sjj%Ks@3n8#+nmc3B$n%s^a23XFxYz5)m_Y1sXn+60g%0%UD<&UI_CAXQ>oN0gX#6$ z`*IEgh!#w*zfP~_unT5GWF(wzdnm#BUFbEP?(w#2-m$kLE3kP(mVO={Iu!0=Mpe`mvs#fh!RVtNx1{5X- zC>#!-0ZgWp3U?w%eZ)b+gvfTeaz)wZu14SAgD<_4@uL+Ehcoe5^hL&4+~d4Wq6Gck z%L6PzaJf({@2J=7Sx?9z#^4~$&86;DWaau9u?UVi#u&f8w44RE0*-k7p&0C6GG>EwP@QExs0x~D@R#HiZ8U#V2SKFY*;=*CkPnasiy1fyMy1@4c* zR9()ZM(x%@p}bG%LlND=&ZV|3W?p8DrDo*w^wx`!gArERPWvnC>-pV&+>>4wy-#;S?9|s3x#c4T*)^fRv zT_Hwg6$cl@S)NEFZVBt!K1ykjAxQt25khb-UpS}PZ2p+1O~mfZ&(A+95e$A0=!aRH zaWLsJbJ|ZVujM}MaSHt=+55q9W?}viA!HY0Y?6G@I0p4hKE`sR+1gtu6mAFda2;eA zXg(Iryvi7hdEz5K1Nyl~#6GC)I1j9?t>1tUYWUDQ!^RfI#!7n25(7oiKwp4jebdfMkM6v0_(mEtjkJd(8B>s>Z1C`B96-tZPie zx}Q=Sq*ddL#2Ec-X_<#U4GvHu7Q0Ok>w5yUUVI|R4|lrsN3~l0#%i_tswdy$_gHNT z(tJAo4Ox+|4A5s2!ETU2^IdJ>FCxBJsl zt$K5{+IY&NBc?r4o3h9>>Z>$iKP~|5nn!!jfT(*wRvV^)x6HUoP28er|5CUV2^UX%{uXBaM0oU;hv^MU8 z03uvc)9Lghsv=)Z8SCK5t1aANC4{hIseDGQR)c~gcN9@No%)8VC|9^TXfr3VU(LW6 zGpFr5*m9iDmCNP-4ZwW+z+&Vrunt&YXfB=pa!6IK?)J)3{#^h!1nM}Q`&QT2uN8h8 z2PhVc-E11+{UhA>ei05~+$(c3^jN7{z1z0!Cq2h$($Z4l>GmM+`uLMg)X@;dHt`s8*{FdK9pTi-PiN0>p?9)i)59 zWt|rT6K|LXcW_-oK3u%WskD97ok>-u}6QR@OmX~b!F z-k_b%QEc1K7kiZ3U^RkX(G12LY}85`hVdR%Ro|tDLaA1()$BOV|8y8TQmfTgM(NPO zob)JX&Rf+Or0ERS7U+<|=#1-ngLMh^^h2RRr3E*L0eQk`lZ$8he6-R9y4o!aCe#VY z4Qum~0h~+7FTE*790m}7P}lXd4I_N1gvkzuSOgPnQ%Zlg{&>v0SbINqf<+#64+oae5hsv0#S7UsO~DqU1MC4AzWN zw&HZ!-zt}@2ixuT%cIWkIH^y-_ZxuGH0@;541YpZLRS%jPZjGEpFL~lJiM~Hex+;4 zr~pVLqJAZrh<_hr{0`S?x`z8dLy(48Olr28_vG_M=%3^%*Ivmd zncT02)SG>P;~lpTlq5;zQn~W+YPI_P|3{GetcgS-SDA)!C&ow|B|qJ8JTzg5J(bO_ zp55~X5ki`#{Zcxac)3gIv;{+vcXsSb^~zGU`kfODQorZgG|hJzM)+?OS-!N_1Iptu zzx|WFoEAc_EoE0A{nJaiCgbrNHC?-Biy1|(uLvQHO4UBEQmH@--O!#Jze>dCeS$S4 z9*;vw=r)9qHD#M%PbXn?Y`K?{E^&Ys=F$&Hviy5KqWN($_o8Qver_+XoK-=nv`IJp zxUt54-)9YU zLt(`CqOTCA5{W<2w9uUtAp`a5_I{7ku%Sl)lujN|6!ilE0R$8Co|{^&zPnT^eI-DB z)c0$k(IOFxeMyp~^C9YzdE#t0^MIiEkX+G@QPfIjN$ zvsldfq!Bj%4j~kGW!?&FymF6XQ2HMgMZP*ffO>iW%0J!ik)_qOT~odF3~p6PrpV{+&_^?Z`p5ybSK>P=G}7ZBIxI z-4?JO+F<%y?bhC0j<=PJ3u;AtsAMwpKO~rdh4o*dn4bSb0TMfPaxxMBFUDBd9e-E_ zVA`qqQ6q$Oily><-I~*8UUsi9F_E}a)3w`Y&@45Xk`_CfNhI#YTIhP$;R@0^aH|Fm zWT)FXyt=x6o-5(76fXzPRMj2vRPv`C##E@R1pL0n!~k)(9nO0)5r-aWcoBCNLUF5R z*Xq}lN~N!kIBJ|d^Ye3Gmn7-qGuo_)0fGm(V98|cvsy^s8(^={pcw_=m)v(VAXbCR zTc?6{WO$(tHVR(9f-A`5zL}aB?gx2<;1+E$llc#X*t@4_X7Hn<_)%-N+IQvid5FBd z3cII!QmH=;h19=g!FG@Nk!@3eU;uvpBaz7aBBuEyW&9nnpxk&NAZH7~q?|7nPir(9 zOCtX(=3jt@Qi)s;Kz6|oDB~T)fl;{GPb3n59M-ij%*cYcuOYNQpUdU{%w>_N?Q7e& zZF`B*?kO{3@YF|4JlKzz)9Lhms;cZ}TW?Gn6pFJ5W1L%B&O)mjwEpwsWt!$(G-83f z0bX;^p8YsRjXh#skhm3FoKHW5B^g>{{N!}U3E=&nTeMo;zNuKO92D=WLWI7!xcD++ zbm6TGkk@iZ>ACcMAw{{`Z4(x{Y7#Jp-RrjgvnR7Fs}Svr3?bUKnE44~3|>tVWu~pp z<@b5t4DvFGo8fda`Pq=B?(N01fy6}o_+X6d`9g80`$8|2fSqJIez&ITe?Eik!Z@HN z21qaojvH9Fi!94HfEhZ49&b$AipLG%2PqdzmsIO@C>QY>IXJ9yiTF{6u_!*VESd@+ zfjU>B7D6dKmD;O>lxyKfM|^Z}ZMJK?P}gX-{-97OKx!5&D6h4}Bay2N%Y2B^E<*@| z;9J>H>H1kl#QF+Go(avM6JHw|0xZiqCmhzVmkGWI zV-lms8y-Z}b>tivVF?qw^v2RM2-APd&y|QftufQwr5VQOWm!6N;HP&c7JRb@Kx`9PNPq@`7t9jO5TD+DqiK4p ih0L>%9c0|V+aNzs@=L-s! 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 Cv>i_X literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..63fa775f58c4e9019329e94127781ca607b05d7a GIT binary patch literal 2876 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 literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..8efe0ff281f5de28262564cce8fb1aa1413bfc3a GIT binary patch literal 6430 zcmV+(8R6!MP)K1C~9sR))@6!GenRRP(= z1w>HxJqrX#LfALMl8}WYlgT!jIp@socfa@KKjdW244IiTU?P6c^FFhj^Kb9>|KIYT zb7q<~`%b=-hJ=VLj0Az^4M0235?s%B5}^nJuMub-d^}PS3eoCG2tNLu2*GR>WVN^y z;e4zOqYI-KBZ)Cc0Ppb~j^2F{lnXae9Exu3D=S7XZUo$XCPR#?}N`ZC`Gh| zHqrKV;rjQjk>G*x`@W2+jME`V*Ax7;p1>G^Iv`rT>i0TjpbzLv-*EkHvO>bn>OS{l z%wbe(#0!GFf~0s5-Vii}piQ+sp$PN|ee0*|ufabx83`i36Jr{~t|MME7gvJxa6sD^ zO_fjt`iQ=E3N@BSFPJfm7P`;BX5{LKH`wMz0&PNQia?)#qZvi0@Z6H5TCx(GEai}7cust_7w}YO8D=xa3-;q0&Daik( z!^+DIHEe|vJRVP0QBhH6Jr`F@voMM2z*iuwa2^iZ5?;M7kK58~^YsF+1VF^Y3dPD}t)4t3Pg57ad)` zJVDbihhx}hM<*axBl#MjgaA8bDlILYs^=)~Lc-fK`9A^qBF={dcj{_`8X2)apuD`i zNAz69O-MKxNs_v9V}*@1&iDBIBzt}2@c0=%k{E9Oj_riu+<1by@PiXJMMasJnLYHJ znHH+uT!VeHa62n7`!6T?ZkLBjzmn;6vP37>yXe$rnewwFa#p+0uHk$D#HU+j8h^-1 zKS*=Xt;ud0lDGvHwmXqE*<)m%JGF8d$uyw=%Y#e+rMXQ-B^{x!Tn>o=m$#E|( z9*7XQFQ?~nA*yxY7qZ@7mR-tD1JA64k!C#;AHZ@sd9 ze}GmO$aLd&4<&5!Q?reMT5Mu$36N!bfLiSEQ>UF?8nN3&Q;s|7K)Fj@DZ%|P33by* zmU;4=K0Qv)UCctlp;T2>b>Jm-R+yf+ZY5V1xoG#h5-qvkMRNxFX?9W|Eau(jqb+}z zNO9CD!R_(VuX%-BBwj-bV-I7W4(S0(Nb^&VG&j9?RH8FgilIMdpqZk|%F5gg8#Y|8 z=P)J_g)Kb$UlXP$Zsf}~UdmqX0N``lr2FW}!!mtU?Kbo^7Jws6 zR$c|wyLRmw2o6)l=3P`G?3aaO+?ny3{UEFOq(q-QCe!Qzkn~5Sw%%fCHsN_;PSwM1Ci$jn(cSFTpk1 zoSi&*@(pkqu`eL8myw#9+Kwwa8IQ=v%aO|37btzILUF<{O>A07R|S+9+9CT$?c z{ue{g-9~~VL5mDOJ(DBh>X%_0(Lm#@q@*Ng>eQ*)wsph%M4Ih)I2=D0vmb~#?)Q29 zbYw#n%^s{6h>R7B?{ksuoU2|t@GlF_O4M$P@C67%Ll7JZT4(xbL#bpcgGNLO{+LI~ z%F6mJIBn1uP+j(xz~<-Ucx*XT6e+a)m*oZ$W5t|-0op!UBDZV|9pMXpvR8@O59slu zqY4$v9zz>Z!iSBzGiT0x&YY@uWD9;ni?&4AZY6Tz3uc`!X65chHhOoUY#=gLEV{=- zWoN=CP`j-Pb=vBw^#kcfVk1S1eLh-TC>iRH23kmJwOUKwfB*g7;C9ujRpEUB)$Zb1 zIEH;Z9_FXIOs2&@v(c=0I9|s4w|gmTkwku9c+;SGee{oG!XcY5L%_X96smE>z6$ha zXwRNKzX!KGx$E5~Ur(skQzyu>JkRXx2aay8q*=FD8;Fbrb2#?jKG{i9Rl|f7I$Sh7 zg-y{mBdO8ghAa=Ime(eHCJ^v&w$8*a0k?uSm3O5)7&2y(99(4^%zNw1hbNSwD4Yq(&spM<{O>vr67BJH1&i;&6E8G zVxz$m$DQOd5t3?tz-F`AKl$X7VcCLHkLO=UD~Kt8>j7MH5Wdq$$2o5#U7$B^&8K+}<CGf7y`5xd)Tpl@{LQ3N zdOfL-{>{h#os>s2M&!}vX(d#iZ#wEQpvLW`zn^iEB_le2pgQ4P*;Ga>06K~NYZ`N` zI$|B?7e82@c;bn6Znygg3Yk#=kiFW*Ca9L&THw9@b1kM1%cBp+71EhK)#O&-Pjkh)Qg0KO|pFO!3SMA1kKYIiWPYW ztLcrw^~u)b^+9vhxzRDk>^??z!h~0@wBYfTn;izx=WvpKBF`0>KqU1I6K?rBAVm8K5Jc-$N-M{34&y z7ul$W9VGe|T=sGTgl1Go^z-8qb!0^Z6A#6-=T$P}yi>ikCAfSn0GCieX=$nL_19k? z98LiVJ9g|C$S0f50cy+hk~`#}Pybv*?>}5fi+@>2YbO=ckc5oe10b90g8OjA5 zTA|>IVu9lHP*sUS&MG%~<2a~z0S>mt?IA1sp9)rr`!e)VQvfGpPFBFEa0+O?VZ(;o z0&!M=2CwWQ`E&`%S?5SPT0riyYF-8LA8f3EDfGCiNv=G?ecMT{iv8qu*Ijip1HA%f z&z^l3xDKg+)vH$zikAt};OE&T?>$S-Uo0ePz-uJ;c!lIXuaY$CEpq&A6(2i8UWfTe zBUbplZgShdBInuJq-5Ps?%jRJn|eKYcJ(Cb_d@CzGeyUqeE7^c=2b>p$OogEsptSWEDcxQ+kRA+o z!gyXC{~~$qA@aE!-flJnObWNNh@_m?$i1sK`F9$L*Wgc1AnC*e^83v730y(}=g*%n z;9RRexK7pID@3)4Si+z|gF1629G67Q=X8)=F8y$;N1Z@1fbnB9s1 z*BL8Nm&;K=(vd%qH>G_Y>8Xr##y&MTOgLfc)T#BhA}ss`mP9*t6iefw0Q;&;@B`SlNDHG zs52HIl938NCS~t1@~5!5Nn>L73ZkHYmc$vH^wxh8X9rJdyJbzcr3UD=lmMZ?RoV-pGOCPpgE+gqU*SY(96xb61>A_%S zRi@m}N;IVaY)5}BNfL3Mg&TI;guEalzHF7Ap8gx5003gBG!odC?}=FfKy{l5R6cVj z`D4fxeC#ykl653!kDx$C2x22enuRL2J-`YuwL_~$K-JaNv~=mxKZ0Yn|HcdRRC|@C z<*Zq=MsO%viE_18$WUQKaHi*wn`d$`v5`Vy6nC3M&O`Zzx-SKB{@aN)v*>gS(-ehfGcxdMbu!hstc4o9|DIJm+{;V!NuDS{9* z9Ndg@MhPcyZd;9^{!s9`?Ih>DM81R98pw0m1>}VGUZaV8LI(E&G91!MtA- zksP#s^2sM}RTQOAR1yfTI8u1*5;=eHu7T8OP{AnRRaE@=Lh{yxCc+~Zk#hVl1Bo$% z&5!qsb|la3X;uNM{XcW&O!0#cK8S7T)*{>wFAqKR5LQ8#{QUeCq7qX#=Su4jtI1U` zv5^91sHj=3DXRQvpCLiRXQ^m5o1SZoM8=Br1X31sBYE~sF)KjC|2~+Xl$4aU%xPD_ z?WKh^gb=CY+O=!P=>{8{8De;=coqEjM=|>X7o&txz_>8x9a8db_3HPysz^EYBO{5i zBFjRa)$LXCO)5Y&Kjr1+G;`+6zkt(A+J8-i8EVtBXU~3YyH1MAOl8GNK{-kN|7jpM zQrLt73aD%6mObDunN(LiJ@ z@TIgQ*TP!UV@3gD_6O{CJAMB7=d;13;50H3W~lwRapUgfoWN=}Bn7a|Y@eS-Qm5$s zfkUT&0`>(JpJrZ+h@93h$#cMb^yA;toLoz~)+6790yO7qd*qQv?gf|Q$B%E={OClz zM~Gj$A3S()zIa{=VrVQH_}pHqUYSMq?vvC+E12kTkn{>*l>BNDc^tK*xT^P?HbK7B zYss}Z(LjDI3J}gWfM){OkA4Ua1()HUj0q(|W1XIQ>Z$MZQYwqbDgb;QFFCdzr;2;t zCh3~Uwki+_=@n3LE1MzqE2vM+esdlt|A82`9oXS|x3(o^VRr-ZCKRB~{?n&Vm-FoZ zAvk>c>8HcT{+AHT=+UF`)9yWX@810m9y|`zilNb{!LvlpD62 z>aBo*uki|~xL5}UE?4g5WNZZa4m3BAeG%Cf@};#T_nJ4Bf zB}oK~#Af?!uU@^7cy*2__P>N!co{*1H!fPV=r2yE`ll9X2ol_QY~T*#t_bJ;Tq>Ko zo=WceUn)*~nTp#yPZyd$ONACD9AM=j48^uY<$_(tYsY?E^~xbwhgL zE1RqGxhdqp_H>v%Aq{Re8A{Dra#d!LV%tYbF~1h>j}FC0$s^khb%%m5J$_8k4J_bF za28qI-+)-gjvb2_HgVXnVRz)`=jVysAg-p!06#9G`^e8u+t1OH`WlBDW#bKv3%njW zbSQtwkRf-2tNZS|FZ#&m5|VJwJ@?=b4Q`q~efra!UaLP`8jq-`sY5e8d3kxhi4!M2 z1C9h&b$30)Nm>jYIz*sS6Jo&hF>(&(JVkkHf+=RZ|7)NQ&jz;wdEn2h~mYtow?`l$l zMm|=5a&mGOb1(v22u|W|_d|*LH0AK&!*Af%-fh~n8IhTpnW32@A%>2B8z5|cKs@ui zzjf=@cY*`Kh5DRzJ*FrcFh$hUlUac?0m2Uu@Z(XTej?NPS(8mp(2qnH_6qY&3fV6@C%6E`Sa%^)Et7b ziZOqqf}5gRN|et3f~OF@~(Htn(8lOhA$~2xG)p zF=jFLMj@&aI(6#Qp*IV^A9%|xx7@RFig4Ld<%EIqIq{Dk`Ei zYu2p4`R1E(Rcj!|){zBc6=QBxf({`JYum41zn=VhGh^tqY11YgIB+1RqM`yy&=k?A zk~j1PeL~+b>(N)9^{1y!o%$lifU$@%wH0G*bmZ#=iAbwHefo6m(xpp3et&D{&YkaC zzI^%I6DLks5h+0tEKmR@+&AJQ#BArc0X#lb`!Ak$JMQ7ME?KhVJ@grU#~3gcj7f~G zNr-Ba7}xdf+ZT`KV>JvJGiJ<#D_5>u%8Dq(EW@>CRTH)U#iFkM%c%@OdTqwYZK7>_ zhtLvzTCrlqveBbQKZL%b&!X?R9?~dHPD7%Zq*jTEi8u7>)e8C++-*LwsKSAHnNAwka zM&HF4njHChLFbVrw&msAm7Ozk1}I_(r}1}Ao;>-vwQJXYdGzSf0uX|W8rsUx1jLDu z7L!*mnm?)gg3rOZda@yWNL4iQ6mBaYZK7@N1D;&Rj88(}&`0!D^tnkBA1tt+Sjuf- zQ$@4{(=>=lxGgCu>4#IMOqsxm&d2-q?K_CO3anPEdVw3W70+0KL~W&n5*))hh`R6# zRj}zcn=Nqm>{(kzM#e$ZjW*C0+CJ+5o*GiJmCfXJ;J+_d)^~48@<|EW+5@2)T^l(oqf5yYi z_}O=mH59w9*mZ=t;N$m>969pG_uqg2SeBmwc#rS+*chB6&c${CY&z;dUHwrv+7R{~ zmkiN%leh6^1%t=TgUtmYZFzQf!UN5SyD1e(@HQ+Oj;hcUbgR-KI zMAV78MH^@fZC=&HhZDU*u-DR>(H2=Y?lkD$z5DgTha~EO?{Ey~;9QguWl_g>gqskM srYQwbf7hZ}Evb5F-)rY-sI352HR<`x}I(pp&EuRvqG2 zqP5Dzh?LEYDDv_m-OX`26wGmdBf=j6t;yo3)4_@YEHkCs!l#y#Wu13(W}iDXTf&7- zO95Z@bB?8xKB>1n@ec8ByAIkn1OSwysks!xiD|h1=VY`3Cs`dx^CMIz!ZVpMCs%|? z2Aa=8CG}U7<|E646p)04J}A~jEn#VA(Q0qk_j2hz}A^=o14So-1-H zC=FeMsQTm4B5rU{khe~I>TGivSZ8$0FPTN?Sc!H8V z=|LeJ5p1rqB|=|Z{|8j36z0-Ku7;#cntghs4HAlkOjRFYXwMn=F~VvJPU(qUri7~Y zj_R`l%T$*FmNRa>8O3ifeOY|AyL1Fhs}{ch#77nu z%$xY6Ceptvx$w;=Gux2TYqffm(p-ck@u(V#IGi=~8Wu~w7&HZjr(|v^De|&OT#tCx z5J@NcN3vSjX;>hl7RN_kBN4CCKJVRr`DmU6)`{=Lmkd^%-l8q1wMQ zQd=M;$#SkxMg3Jf<5#!Q$V%Z`4zLld=4n4`pPrFBxyXi_)ffta#`qBpaRxvx==04| zA|eURi%kxCA-3h`abu(dYAk(4PK9NVM+gIocWZt2?wLZ|mGtVR$_PisnA(+0Jj9-8 zg6tGATybfu)VVEmm?Ndy>(ew)HQ;s3(PHYRWJs0=T}Gt48GVh+hXjwzaL1UB|D>d? zc3rSN^D+2w>mGF{Qmsf#2Tqmk3$g0FZW}erE&bhBD*XZ_iO!KG(v`le*D6}zb0(dg zY<+S9;&bm9ia7gQI_-F7(Vz>2Sk`;?l28yMs9=)T1UNvL zrjP2ze1DgaU7oW*)jK=5!GZQ1cM>_8c3Ki%SiqhAH7o48cre6O0IJs#Th=yRPRkG0 ztYAnbvnYA#HNMXXS$W0T%fv7xzY&dDbfkHO43f2Q;ZEKYJ)nlzYM{q|@p4ZKsRV3Q zk4F(vHI3x;xJt#D!NqzKDy_a0_uKB)PT~U`#^WG@)W<;)p{}j~sM36UDMmZtm zDF9>K7Y2<73*E7qh8zKf_RLEBgr_!faln&bc4DNpJwghfv_W}!I&llP&^hAn{m#FW zFHFS795w4FzhDN#I9grJXa}-G-t9w~S)9nu!a6n30<~yve?xv(e8jd`AgB)4t*{9~ z(g0Wp8uA|ugTU-sjh(Sn+FIa~1$mc+k=jrUb)Vj9=k+<#TF=SL z$Z9~9gkqg7HEMZn!{~8BfYL)(fvX?q%Zo|;dX3qK|2(1Q+n8V{dU)GJ7pHHi ze65#CN;Mr4qA6NYMB0AoU*ShM1vw zx+B0J+;YCZ?b71O>Riw{eQjNeS4maboo%aCeVL#MdRwkb>{+7IGqTIr79LzT$arC++GBIEqA>3a0Ih|vqZzlQ_^L%U>3FU5+p8bRfA8NHLinXPk>mfy}fQDVTrD6(> z7ud|ph76nWKgg0wFO@ehXcGr)UvIb*%sbvkqO69wt0I*p4N!1@1T9c51gg<*6i=n{ z`$&mj2-1eQS(Xmo`5xRoJ61(T`3t|F2O~<%@R1G|>==H5#Yt* zj0Ou%z(2Q@nunL3>-^Gd;ma|WmE#XDf^$4ndTG_(oIF0rTysdsxg=Z=Nm;6f6*Be@ zTXJsv63;4ibyH1;xw{JXZ1=j{ZfWY5?=x_BY>8JL)rMOeKoGw%QmeC)IUwFH_MW7_ z_XiYf(VNvfa8p(SbGKwM4OLmyJe=`jNwj<6N~~I7T!p)!A-4jv_D`V=d-vzoF~Qhm zdF9|-1nubvdq}SRmObzD2xb>JALH{&9@|j* zwkGNDco@8YC{}Cvd};ff`x8mrviV=OmW#=BOJKr7%gP6XrnsLk*!l?CqH@NmRDDPT z%@9iinr9)xxmP%HZeB%o7(k!JYWS*d8TvfacJaVq~~qJ$lLsyFqX$c zi37oOV=1O1KJo7*%7}(sHGlwZ2kDn+$yn`SYXWzkYIC|-o^!X7JTxFjbI9mg#XZrcNRS>1e|q4?6())S~1a4#JM5JtQ2r!?0c)4PlF zC@;y<>Ik0ZUmRu{Ewih=1IyizOnhue0nJ15QB-v>X7a8pOMVgjF+mp2V&SI)qm*KM zNzdv#%6cuiE0-f*eIq++B`Rs#azS+I8ZmMuT@p7giC_$(wiaEbg?_cJY|D?xX^liDL|+d>&8EOEO4NV*a@_ArWVYS9w#b^rIX?5+doeRSk1GCv z=oT!TW7v_dFDIL@=P|yMgb=igrG>4v3@ z(io6#|L!@WR@D-mw`%}rRm`JAYx^f5+!Da?+GRCal8fyu5%MdaCiV(~*Ii#pj&3`t zDcTm2WaP_^3}1g$&)Zo{0oN5BtNi=0?&!`&1O^1C3uynT@TEE1r!=h>JHxzD=Q9S+ z`q2ma#gyynx$?wXlP-%8{3WY$LNOk(&{A67{MLucE6h z(#;Jad1(X|e70Z2?r^cg5e{bS@%}}4+GGp?C~FGqO<_$Z+~%hD0*V@sFx#GJHfG2X zFpJiWej+en$}0S7k~ZR=QQ(xk-%y`oPnRFh-*)L;Qn+ij?C_E%=A)`cV!q7JDP@PU zD>68@%Bzo5EyZ=?nb{@4engJq$evf(D6zdToqD2 zqpQ~}Cwsctz-aszT%1sD$eUFLC3(-=KaebQ(&-Mp4Lh|5`wg-sSL6U((}-_VBz~QM zRQwc}E3>WoP?pMK$8;xl;|7p%OTQ2_ul=JQB~vQLVM%FxHL1v1wuIkr8ih86S-(hY z3V#m{clqVqr`8X_+RtUNZFkK6{867JYY^1BM=(8B76CTuIzIvBnz?O#&rdr@1;6<* zt|xo!$v7C8sarZ2QGCGd*(v-zBbD4`9j8r3F^@mbaF#2<%;7%Ee|FF!PTBlz%tbHW zVlzCci{6;|Cec4WJp{zaZwKhGb3xOfKDnVDH&*K0HPpL>8XVm)dVhDN)VT8dYaEl2 z|IN`PnRxh0YY1X-FWgB|7h*{4cp#WQC+Nk?rOeqW?jbUPcn%pde&=j|g8v&yY`dxg z6K0uHzL%z3eyc~pa@HO^FSJRcxz_fDMst{{X>HrB<&ue3=}YulnjNE7k7r?14_Rtf(c%kTBLencCPa{niqgd0SCL9q1ZUB zW&eMYHPSbzaeJuu49$@gCH~$#{Y0(dgtz7f!Htt59drmNZqZRD;JdP0AP-GEO#0*u9}yjLdxbGO7sUFPSCvX z`AnL@OMI%2ygPjyZ!f3zox#8&5idCr0=M)P4ZH@Jr8`o|))z?2SXq$ohCL{HZ)fo=eBK?Qv&@G4^cCtT*@`g`Tz30GO;Z732J5}Sf;gY@<)G2M>uK#1B* zyL`dbj~64(*tYpenAU^|w%zov3@U0Lemx=#obVnwx1^H2BXlt3LAzq;`+M6$n+3{K z;(e-|SU=U>kz-qRH_c18{u*)OJMsM8TX)JIJ=ypvy=;}AF?@wn;xOTJHr zor(m@Z7OVk^`DMr@-{vDZv*@PE$%(Ss|oyex`IvV$oJkJ^P+JFKRn02Lycz=EAwrs zM&;c5eTh^V5w4L44QL($0oaXTuw?9&4BDyg#ekG0cp+J7+EF>Qd^)U~hrZ4a;2`ZV zIxoi|k3C9O5EPl7E}rHoW!;G{LaCPol(K#zb>=PgQBsEZ>&)~|90Ci*{%qZ8Z|C_Y zZ;ADzBd|_trw1ESJxko>m7CPr;P7w=7fdaedJ{KvVLuq|EY4Cw?-%B&@mz~z$i<|h z01%@TMF~N#z6m3wXo6=|jX#KKUdE&MlEppp{e072?=mYuM9%m$)YEUbZu)F(E^=K; zpo4Eq&S&S>z!u1NYpgIS1BtvOQpGzVObiuKhqAlokdcvzzMcBQgz5r|G{O@Q=|ZOMjVvJ8((9o& zoW-Iv(P3Ck5x=18(?1g7i3J-&AEkLDBpMLu5l};Ui@8xk;H1i3ywI&z}qko&^Gz;K<& zA-_l8{qO!?#Ky*Yz1o?uv$x+XE=|>uT#?Nkg`+H(`mBEzRgS#2ZT!%hh7lNvpa0bo zaBdbYsuW4y_^0)HFW~j|#Am9PQBOAUi?lx+Ew!94NMB~Xt!Rw%c$V(jB}uxrT?*a< zMTwTnb-cE{$&MvSz+=ipAfDBR+}%o??ag2FUR@Qv*ACT*^z4vao`WTVjNvHve;Ji> z9mL2@n-$VfmOpFHziBBZ7Y5u=Te$o8594^5`0t?>7 SU+!y^I2tNC$`y*QL;nM(o*V=K literal 13071 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 diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..95a312fbc55cf1acbfef625bda8076d561097861 GIT binary patch literal 10339 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 literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..a63acece7032816ef5472650bf20f48a8cb01e42 GIT binary patch literal 10194 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